Use sqlite/sqlite. Connections are valid only inside with_db; statements
remain private. Tokens are never reused, so a connection captured from a finished
scope returns sqlite.invalid_handle. Calls belong to one OS thread; multiple
connections may be used on that thread. Dawn reference counting manages copied
values, while bracket closes SQLite resources on return and panic.
The package does not bundle SQLite or libsimple, expose borrowed column views, or support static linking, other operating systems, or WASI persistence.
use sqlite/sqlite
# Give the result a type so the callback's ? expressions have context.
let answer: Result[sqlite.Rows, ForeignError] = sqlite.with_db(":memory:", db =>
sqlite.query(db, "select ? as title", [sqlite.Text("hello")], [sqlite.ColText]))
Value is Integer, Real, Text or Null. ColInt, ColFloat and ColText
request SQLite conversions; SQL NULL always becomes Null. Rows.names keeps
labels (including duplicates) even for an empty result. Every text and label is
copied, and invalid UTF-8 returns sqlite.invalid_utf8. Bindings preserve embedded
NUL; paths, SQL and extension entry points reject it as sqlite.embedded_nul.
Parameters start at 1 and columns at 0. Counts must match. Empty SQL and multiple
statements are refused; trailing comments are allowed.
execute consumes one statement and discards rows. execute_changes is for DML:
it returns sqlite3_changes, limited to C int range. DDL retains the preceding
DML count in SQLite; use execute for DDL, PRAGMA and transaction statements.
last_id preserves signed 64-bit rowids. busy_timeout accepts 0 to 2147483647 ms.
with_tx(db, Deferred | Immediate, body) rejects nesting. It commits Ok and
rolls back Err, panic and failed commits. in_transaction reads SQLite's real
autocommit flag, including automatic rollbacks. Cleanup errors append to a
primary error's message and preserve its kind. During panic, cleanup failures are reported to stderr and the original panic
propagates (a fault writing the diagnostic is contained). There are no automatic savepoints.
load_extension(db, path, entry) loads an explicitly trusted library using only
SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION and the C API, then disables C loading.
The SQL function stays disabled before, during and after loading. The caller
provides libsimple as a deployment dependency; no extension is bundled. Tests
pin libsimple v0.7.1 and its archive SHA256 independently of production.
Recoverable errors use ForeignError.kind: sqlite.no_library, no_symbol,
version, invalid_handle, embedded_nul, range, statement_state,
empty_sql, multiple_statements, parameter_count, column_count,
invalid_utf8, busy, locked, readonly, cant_open, constraint,
no_memory, extension, nested_transaction, and generic error, all with
the sqlite. prefix. SQLite messages are copied before cleanup.
Run scripts/sqlite-contract/run.sh --native for engine, ownership, transaction
and FTS checks, and --backends for all JVM/WASI refusals and downstream package
consumers. Test assets and SHA256 digests live in fixtures.lock.json; missing
assets fail the run. Sanitizer engines are cached by source digest, compiler
identity, target, macros and sanitizer options; a cold cache builds them once.
API#
sqlite/sqlite
Scoped SQLite resources. Values are copied across the ABI; RC never closes a database. A captured connection is checked again on every native call.
Value
type Value = Integer(value: Int) | Real(value: Float) | Text(value: String) | Null
Bind parameters; Text preserves its byte length, including embedded NUL.
Col
type Col = ColInt | ColFloat | ColText
Requested SQLite conversion for each output column; NULL takes precedence.
Rows
type Rows = { names: List[String], rows: List[List[Value]] }
Copied labels and rows, valid after the statement and connection close.
TxMode
type TxMode = Deferred | Immediate
Begin a deferred or immediate transaction. Nested transactions are refused.
sqlite.with_db
fn with_db[T, !e](path: String, body: fn(Db) -> Result[T, ForeignError] !(e|io), uri: Bool = false) -> Result[T, ForeignError] !(e|io)
Open a path (or, with uri=true, a SQLite URI), run body, and always close.
sqlite.query
fn query(db: Db, sql: String, args: List[Value], cols: List[Col]) -> Result[Rows, ForeignError] !io
Bind exactly one statement and materialize every row using cols conversions. Labels are available even for an empty result and duplicate labels are retained.
sqlite.execute
fn execute(db: Db, sql: String, args: List[Value]) -> Result[Unit, ForeignError] !io
Execute one statement and discard any returned rows. DDL and transaction statements use this entry; it makes no affected-row-count promise.
sqlite.execute_changes
fn execute_changes(db: Db, sql: String, args: List[Value]) -> Result[Int, ForeignError] !io
Execute DML and read sqlite3_changes (C int range). DDL retains the previous DML count in SQLite; use execute for DDL, PRAGMA and transaction statements.
sqlite.last_id
fn last_id(db: Db) -> Result[Int, ForeignError] !io
The most recent inserted rowid on this connection, as a signed 64-bit Int.
sqlite.busy_timeout
fn busy_timeout(db: Db, ms: Int) -> Result[Unit, ForeignError] !io
Set the lock wait in milliseconds, in the range 0 through 2147483647.
sqlite.in_transaction
fn in_transaction(db: Db) -> Result[Bool, ForeignError] !io
Read SQLite's current autocommit state, including automatic rollbacks.
sqlite.with_tx
fn with_tx[T, !e](db: Db, mode: TxMode, body: fn() -> Result[T, ForeignError] !(e|io)) -> Result[T, ForeignError] !(e|io)
Reject nesting, begin, run body, and commit on Ok. Err, panic and failed commits roll back; a cleanup error is appended without replacing the cause.
sqlite.load_extension
fn load_extension(db: Db, path: String, entry: Option[String]) -> Result[Unit, ForeignError] !io
Load an explicitly chosen trusted extension through the C API. SQL load_extension remains disabled throughout, and C loading closes on return.