Open SQLite databases, run statements, and decode rows.
A Db is one connection. A string literal is a Query: each ${…} in it
is a Value bound as a parameter, never pasted into the SQL text. A
Decoder turns a result row into a Roc value.
import pf.Sqlite
import pf.Sqlite.Decoder
decode_todo : Decoder({ id : I64, task : Str }, Sqlite.DecodeErr)
decode_todo = { id: Decoder.i64("id"), task: Decoder.str("task") }.Decoder
db = Sqlite.open!("app.db", {})?
todo = db.query_one!("SELECT id, task FROM todos WHERE id = ${Integer(id)}", decode_todo)?
Without the second import the same names are Sqlite.Decoder.i64("id") and
{ … }.Sqlite.Decoder.
Every call opens a new connection, also for a path that is already
open. The path ":memory:" opens a new, private in-memory database,
like [open_in_memory!].
mode is ReadWriteCreate (create the file if it is missing),
ReadWrite (the file must exist) or ReadOnly (the file must exist and
every write fails with Failed({ code: ReadOnly, .. })).
busy_timeout is how long a statement waits for another connection's
lock before failing with Failed({ code: Busy, .. }). Duration.zero
fails at once.
The connection is closed when the last reference to the Db, and to
every unfinished Db.rows stream made from it, is released. [close!]
closes it earlier and reports a failure to do so.
The connection keeps the statements it has prepared, keyed by their SQL
text, so running the same text again does not prepare it again. It keeps
at most 32, dropping the least recently used.
Run one statement that returns no rows, such as an INSERT,
UPDATE, DELETE or CREATE TABLE.
A statement with result columns is rejected before it runs with
ReturnsRows, even if it would return no row; so an
INSERT … RETURNING inserts nothing. Text holding a second
statement is rejected with MultipleStatements; use
[execute_script!] for that. Text holding no statement at all, only
white space or comments, does nothing.
db.execute!("INSERT INTO todos (task) VALUES (${String(task)})")?
Run every statement in sql, in order, stopping at the first one
that fails. The statements take no parameters, and any rows they
return are discarded.
The statements are not wrapped in a transaction: those before a
failing one have taken effect, unless the text itself has a BEGIN
or the call is made inside [transaction!].
db.execute_script!(
\\CREATE TABLE todos (id INTEGER PRIMARY KEY, task TEXT NOT NULL);
\\CREATE INDEX todos_task ON todos (task);
)?
Lazily run a query, decoding one row for each item pulled.
Building the stream does nothing; the first pull prepares the
statement, binds its parameters and resolves the decoder's columns.
The stream ends after the last row.
A BadRow(err) item concerns one row, whose values the decoder
rejected, and the stream continues with the next row. A SqliteErr
or Closed item is the last one. A column the decoder names that
the query does not return is Err(SqliteErr(ColumnNotFound(_))),
the only item.
for! item in db.rows("SELECT id, payload FROM events ORDER BY id", decode_event) {
match item {
Ok(event) if event.id > last => break
Ok(event) => Stdout.line!(event.payload)?
Err(BadRow(_)) => {}
Err(SqliteErr(err)) => { return Err(SqliteErr(err)) }
Err(Closed) => { return Err(Closed) }
}
}
The stream holds its statement, and with it SQLite's read lock, until
it has finished or is released. The connection stays usable
meanwhile, also for another stream of the same query.
A stream left with break is released when the function containing
the loop returns, not at the end of the loop
(roc-lang/roc#12266).
Give the loop a function of its own if the lock has to be gone before
the next statement, such as a DROP TABLE.
A string literal is a Query. Each ${…} in it must be a Value and
becomes a bound parameter (?1, ?2, …), so the value is never parsed
as SQL:
db.execute!("UPDATE todos SET status = ${String(status)} WHERE id = ${Integer(id)}")?
A ${…} inside '…', "…" or a comment would be text, not a
parameter, so such a literal is rejected at compile time; write
name = ${String(name)}, not name = '${String(name)}'.
SQL assembled at run time, with the values for its ? markers in
order. A count that differs from the statement's number of
parameters is WrongParamCount when the query is run.
marks = Str.join_with(ids.map(|_| "?"), ", ")
query = Query.new("SELECT task FROM todos WHERE id IN (${marks})", ids.map(|id| Integer(id)))
A result column, by name or by position counted from zero. A string
literal is a name and a number literal is a position:
Decoder.i64("id"), Decoder.i64(0).
A name must match the column's name in the result exactly; give an
expression a name with AS. Where two columns have the same name, the
name selects the first.
Failed is an error reported by SQLite: its primary result code, its
extended result code and its
message. The other variants mean the call does not fit the statement,
and are found before anything runs:
ColumnNotFound: a decoder reads a column the query does not return.
WrongParamCount: the statement has expected parameters and the Query carries given values.
ReturnsRows: a statement given to [Db.execute!] has result columns.
MultipleStatements: the text holds more than one statement.
A decoder's columns are looked up once per query, before its first row
is read; a column that is missing is SqliteErr(ColumnNotFound(_)).
A value the decoder rejects fails that row only, as BadRow(err).
SQLite stores a value with its own type, whatever the column was
declared as, and the leaf decoders do not convert: i64 accepts only
an integer.
map2 : Decoder(a, err), Decoder(b, err), (a, b -> c) -> Decoder(c, err)
Combine two decoders; this is what the record-builder suffix
{ … }.Decoder calls.
map : Decoder(a, err), (a -> b) -> Decoder(b, err)