Guides

Local database

Structured local data with SQLite, written once in the core. The platform owns the file and the thread; the core sends bound statements and gets typed rows back.

Turn it on#

Db is an opt-in port over the SQLite each platform already has: the system library on iOS, android.database.sqlite on Android, a JDBC driver on the JVM, node:sqlite in Node and wa-sqlite in a worker on the web. The core stays small and never blocks: the platform owns the database and its thread. On the web the files live in the origin private file system, which one tab holds at a time: a second tab of the same app gets Unavailable.

core/Cargo.toml
[dependencies]
undra = { version = "0.1", features = ["db"] }

Open and migrate#

Migrations are numbered SQL. The platform runs every one the database has not seen yet, in one transaction: if one fails, none of them happened.

core/src/notes.rs
const MIGRATIONS: &[Migration] = &[
    Migration::new(1, "CREATE TABLE notes (id INTEGER PRIMARY KEY, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0)"),
    Migration::new(2, "CREATE INDEX notes_done ON notes (done)"),
];

let db = Database::open(&ctx, "notes", MIGRATIONS).await?;

Values are parameters#

SQL is a &'static str, so a value can only reach a statement as a bound parameter. Cells come back typed.

rust
db.execute("INSERT INTO notes (title) VALUES (?)", params![title]).await?;
let rows = db.query("SELECT id, title, done FROM notes WHERE done = ?", params![false]).await?;
for row in rows.iter() {
    let id: i64 = row.get("id")?;
    let title: String = row.get("title")?;
}

Transactions#

A transaction commits when its body returns Ok and rolls back on Err, or when its task is dropped half-way.

rust
db.transaction(|tx| async move {
    for title in titles {
        tx.execute("INSERT INTO notes (title) VALUES (?)", params![title]).await?;
    }
    Ok(())
}).await?;

Errors you can match#

ErrorWhen
Constraint { kind, .. }a UNIQUE, NOT NULL, foreign-key or CHECK constraint failed
Busythe database stayed locked past five seconds
Migration { version, .. }a migration failed and everything rolled back, or the file is newer than the app
Sqla syntax error, a missing table, a cell read as the wrong type
Corrupt, Full, Unavailablethe file, the disk, or no database to talk to

In tests#

FakeDb answers by SQL fragment, injects failures and records every statement, migration and transaction, so the core's database logic is tested without a device.

rust
fakes.db.fail_on("INSERT", DbError::Full);
assert_eq!(notes.add("milk".into()).await, Err(DbError::Full));

Design: ADR-048.