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.