lode/adapters/sqlite

A SQLite adapter built on sqlight (which wraps the Erlang esqlite NIF).

Like the Postgres adapter, it renders a Query to parameterized SQL via lode/query/sql — using the SQLite dialect (?N placeholders, LIKE, no row locks) — binds Value params as sqlight values, runs the statement, and decodes rows back into Dict(String, Value). Rows come back column-keyed via lode_sqlite_ffi (esqlite returns positional rows, so the FFI reads column_names and zips), the SQLite twin of the Postgres rows_as_map path.

let assert Ok(conn) = sqlight.open(“:memory:”) let repo = repo.new(sqlite.new(conn))

Type fidelity: SQLite has only INTEGER / REAL / TEXT / BLOB / NULL, so Bool binds as 0/1, and Decimal / Uuid / Date / Time / Timestamp / JSON travel as TEXT. The field’s typed load interprets them on read.

Concurrency: SQLite gives the adapter a single connection, so new spawns a per-adapter lock process (see lode_sqlite_ffi) that serializes every operation across BEAM processes — a transaction (or stream) holds the lock for its whole body, a plain statement for its own duration. Concurrent repo.transaction calls queue instead of interleaving BEGIN/COMMIT, and a plain write issued during another process’s transaction waits and runs outside it. If the process holding the lock dies mid-transaction, the lock rolls the connection back and frees itself.

Connection defaults: new fixes SQLite’s legacy per-connection settings (see https://mort.coffee/home/sqlite-editions/) — foreign_keys = ON (SQLite ignores REFERENCES clauses by default, letting dangling keys silently rebind via ROWID reuse), busy_timeout = 5000 (wait for a competing writer instead of failing SQLITE_BUSY immediately), journal_mode = WAL and synchronous = NORMAL (writers don’t block readers, and fewer fsyncs without risking corruption — WAL is a no-op on :memory:, and persistent on a file database). To keep a different setting, re-issue the pragma on the connection after new.

Values

pub fn new(conn: sqlight.Connection) -> adapter.Adapter

Build an adapter over an open sqlight connection.

Sets the connection defaults (foreign_keys = ON, busy_timeout = 5000, journal_mode = WAL, synchronous = NORMAL — see the module doc), then spawns a per-adapter lock process serializing all use of the connection across BEAM processes: transaction holds it for its whole body, plain operations for a single statement. If the holder dies mid-transaction the lock issues a ROLLBACK and frees itself, so the connection is never left inside a dangling BEGIN.

pub fn with_connection(
  repo r: repo.Repo,
  body body: fn(sqlight.Connection) -> a,
) -> a

Run body with the repo’s raw sqlight.Connection, holding the adapter’s connection lock for the duration — the safe escape hatch for engine-specific code the value layer cannot express: compiled query functions (marmot), pragmas, VACUUM. Never keep the connection around after body returns: outside the lock, statements race the adapter’s own (and would silently join another process’s open transaction).

Composes with repo.transaction: on the transaction-scoped repo the callback runs bare on the transaction’s connection (the outer acquire — reentrant per process — already covers it), so its statements join the transaction and roll back with it.

Panics when the repo’s adapter is not this module’s — the connection type would be a lie.

Search Document