Skip to main content

Crate frust_database

Crate frust_database 

Source
Expand description

frust-database: a platform-independent, synchronous local SQL API — one public surface over an engine-swappable SQLite-compatible store.

§Charter: a pure-Rust plugin, no frust-plugin

Unlike every other crate under plugins/ (see docs/ARCHITECTURE.md’s Module Structure / docs/CODE_STANDARDS.md’s Plugin Conventions), this crate depends on no frust-plugin — it needs no JNI/platform handle. Both SQL engines it can route to (rusqlite’s bundled SQLite today; a future turso async engine — see Engines below) reach on-disk storage directly through their own FFI/bindings, never through an OS capability API a platform handle would gate. The crate’s sole frust-* dependency is frust_paths, for Database::open’s default <data_dir>/databases/<name>.db path resolution. An app adds this crate to its own Cargo.toml alongside frust, the same way every other plugin does; the frust facade does not depend on or re-export it.

§Engines

Engine names the compiled backend a Database routes through: Engine::Sqlite (this crate’s default — the engine-sqlite feature, rusqlite’s bundled, synchronous, in-process SQLite) and Engine::Turso (the engine-turso feature — an async engine bridged onto this crate’s synchronous API, see turso.rs; not doc-linkable here since the variant only exists when that feature is compiled). OpenOptions::engine picks one explicitly; Database::open/Database::open_in_memory/Database::open_at resolve a default instead — Sqlite when compiled, else Turso, else (neither compiled) a DatabaseError::Storage naming the missing feature, since Engine itself has no values to report in DatabaseError::EngineUnavailable once both its variants are compiled out (each is individually feature-gated — see the type’s own doc).

§UI-thread discipline: pair every call with spawn_blocking

Every Database operation is a blocking synchronous call — a rusqlite statement runs on the calling thread, and the turso backend bridges its async engine onto this same blocking call shape (see DatabaseError::AsyncContext). Like every other plugin’s blocking/gated call (docs/PLUGINS_ARCHITECTURE.md’s Layer Dependencies), an app must never call Database::execute / Database::query / Database::transaction directly from the UI thread — route it through frust::spawn_blocking:

ⓘ
let db = frust_database::Database::open("app")?;
let inserted = frust::spawn_blocking(move || {
    db.execute("INSERT INTO notes (body) VALUES (?1)", ["hello"])
})
.await??;

Unlike secure-storage’s biometric gate or camera’s permission/ capture calls, this crate has no code-level UI-thread guard — there is no shared guard helper in this codebase, and adding one here would need an FFI dependency this pure-Rust crate deliberately carries none of (see Charter above). UI-thread discipline is docs-only here, matching secure-storage’s own precedent for calls it can’t cheaply guard in code.

§Threading model: one serialized connection per handle

A Database wraps exactly one engine connection behind a crate-private Mutex<Box<dyn engine::EngineConn>>, so every call through one handle is serialized — Database is Send + Sync and cheap to share (e.g. behind an Arc), but two concurrent calls on the same handle queue rather than run in parallel. Every real backend opens its file in WAL journal mode (see Interop discipline below), which supports concurrent readers alongside one writer, but only across separate connections — an app that wants read parallelism opens more than one Database handle onto the same file rather than sharing one handle across threads expecting internal parallelism.

“Open multiple handles for concurrent readers” is an engine-sqlite guarantee, not a cross-engine one. rusqlite‘s bundled SQLite really does run separate connections’ reads on separate OS threads, in parallel. engine-turso cannot: every Database handle, however many an app opens onto the same file, routes its calls through one process-wide, single-threaded bridge (see turso.rs‘s module doc, The bridge). Additional turso handles still buy correctness and cross-handle write visibility — each is its own connection, seeing the others’ commits — but not wall-clock parallelism: their calls serialize/interleave on that one bridge thread exactly as if issued through a single handle.

§Interop discipline (enforced by backends, not this module)

Every real backend opens its connection in WAL journal mode, and neither engine turns on a session-extension-style MVCC layer or on-disk encryption (no SQLCipher/equivalent) — a frust-database file is a plain, unencrypted WAL SQLite file any standard sqlite3 tool can open. This crate’s public API doesn’t enforce that directly; each backend module’s own doc comment documents how it configures its connection.

§v1 scope

Positional parameters only (Value / IntoParams); no prepared-statement cache, no streaming cursors, no migrations — a plain execute/query/transaction surface. Future enhancements (not v1): named parameters, a statement cache, streaming query results, and a migration runner.

Structs§

Database
A handle onto one SQL database.
OpenOptions
Builder for Database::open_with.
Row
One result row: column names paired with their Values, in statement column order.
Transaction
A handle to one open transaction, passed to Database::transaction’s closure.

Enums§

DatabaseError
Errors from a Database operation.
Engine
The compiled SQL engine backend a Database routes through.
Value
The five SQLite storage classes — this crate’s currency for both statement parameters and result-row values.

Traits§

IntoParams
Types that can be passed as SQL statement parameters to Database::execute / Database::query / Transaction::execute / Transaction::query.