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.
- Open
Options - 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§
- Database
Error - Errors from a
Databaseoperation. - Engine
- The compiled SQL engine backend a
Databaseroutes through. - Value
- The five SQLite storage classes — this crate’s currency for both statement parameters and result-row values.
Traits§
- Into
Params - Types that can be passed as SQL statement parameters to
Database::execute/Database::query/Transaction::execute/Transaction::query.