Docs
What it’s for
sqlite is an embedded SQL database for Mojo: a Database, prepared Statements, Row snapshots and a Transaction guard over libsqlite3. It’s the store behind the SQL catalog in iceberg, the one PyIceberg calls SqlCatalog, and the way most local Iceberg setups start. Orca and Dista depend on it too. It’s a fork of ehsanmok/sqlite with the ORM removed, so it depends on nothing but conda-forge’s libsqlite.
Install
A new project needs Modular’s channel, pixi’s build preview and the compiler before the tin:
pixi init app -c https://conda.modular.com/max -c conda-forge && cd app
sed -i.bak 's/^\[workspace\]/[workspace]\npreview = ["pixi-build"]/' pixi.toml
pixi add "mojo-compiler==1.2.0"
pixi shelf add sqlite-mojo # no other tins; brings libsqlite from conda-forge
The package is sqlite-mojo, because conda-forge owns sqlite; you import it as sqlite. libsqlite3 is found in $CONDA_PREFIX/lib, next to the executable, or through the binary’s rpath, so a program built with pixi run mojo build also runs from a plain shell.
Quick start
Create a table, insert two rows in one transaction and read them back:
from sqlite import Database
def main() raises:
var db = Database(":memory:") # or a file path
db.execute("CREATE TABLE people (name TEXT, age INTEGER, score REAL)")
with db.transaction(): # COMMIT here, ROLLBACK on raise
var ins = db.prepare("INSERT INTO people VALUES (?, ?, ?)")
ins.bind_text(1, "Ada") # parameters are 1-based
ins.bind_int(2, 36)
ins.bind_float(3, 9.5)
_ = ins.step()
ins.reset() # reuse the statement
ins.bind_text(1, "Grace")
ins.bind_int(2, 45)
ins.bind_null(3)
_ = ins.step()
var q = db.prepare("SELECT name, age, score FROM people ORDER BY age")
while True:
var row = q.step() # None once the rows run out
if not row:
break
ref r = row.value() # columns are 0-based
var score = "NULL" if r.is_null(2) else String(r.float_val(2))
print(r.text_val(0), r.int_val(1), score)
API
Everything is exported from sqlite.
Database(path: String) raises: a connection to a file, or to ":memory:". It closes when it’s destroyed.
execute(self, sql: String) raises: runs SQL with no parameters and no rows, such as DDL or a script.prepare(self, sql: String) raises -> Statement: compiles one statement.transaction(self) raises -> Transaction: issuesBEGINand returns the guard.changes(self) raises -> Int,total_changes(self) raises -> Intandlast_insert_rowid(self) raises -> Int: what the last statements did.last_error(self) raises -> String: SQLite’s message for the last failure.
Statement: a prepared statement, finalized when it’s destroyed. Parameters are 1-based.
bind_int(self, idx: Int, val: Int) raises,bind_float(self, idx: Int, val: Float64) raises,bind_text(self, idx: Int, val: String) raisesandbind_null(self, idx: Int) raises.step(self) raises -> Optional[Row]: the next row, or None when the statement is done.reset(self) raises: rewinds the statement so you can bind new values and run it again. Bindings you don’t replace keep their old values.
Row: a snapshot of one result row, copied out when it’s produced, so it outlives its Statement. Columns are 0-based: num_cols(self) -> Int, int_val(self, col: Int) -> Int, float_val(self, col: Int) -> Float64, text_val(self, col: Int) -> String and is_null(self, col: Int) -> Bool.
Transaction: commit(mut self) raises and rollback(mut self) raises. Use it as with db.transaction(): to commit when the block ends and roll back (and re-raise) when it raises. Keep it as var tx = db.transaction() to decide yourself; a guard destroyed without commit() rolls back.
Errors and limits
- Failures raise with SQLite’s code and message:
sqlite3_prepare_v2 failed: <sql> -- <message> (sqlite3 rc=1), and the same shape forsqlite3_execandsqlite3_open('<path>').stepraisessqlite3_step failed (rc=19): <message>for a constraint violation. - A library that can’t be loaded raises from the first call that needs it, naming every place searched and saying to set
CONDA_PREFIX. - The value accessors don’t raise.
int_valandfloat_valon a NULL or a column of another type give 0, andtext_valgives an empty string, so checkis_nullfirst when NULL matters. - BLOB columns aren’t exposed: a BLOB cell reads as empty. There is no bind-by-name; named placeholders in SQL work, but you bind them by index.
with db.transaction() as tx:bindstxto None, because__enter__can’t consume the guard; use thevar txform when you need the guard. NestedBEGINisn’t supported; useSAVEPOINTin SQL.- There is no connection pool, no async and no query builder.
Tests
pixi run test runs the unit suite (every bind and column variant, NULLs, statement reuse, every transaction path) and 13,500 property trials: arbitrary bytes executed as SQL never crash, binds round-trip exactly, and no bind_text payload escapes its placeholder. A cross-check has the sqlite3 shell read back what this tin wrote, under PRAGMA integrity_check, and the tin read back what the shell wrote. A last test loads libsqlite3 with CONDA_PREFIX unset. CI runs it all on Linux and macOS, on the stable and nightly toolchains.
The full reference is the README on GitHub.