rightkit-db
Correct-by-default SQLite and SQLCipher connection bootstrap for Right Suite desktop apps. It owns connection pragmas, SQLCipher key ordering, a bounded read pool, checkpointing, optimization, optional integrity checks, online backup and restore, and versioned migrations. It does not own app schemas or queries.
Catalog capability: sqlite-storage. crates.io has 0.3.2; the source is 0.4.1 (see INDEX.md). The 0.4.0 APIs below (ReadPoolOptions, ReadPool::close_and_wait, VersionStorage::VersionTable, MigrateOptions::import_legacy_versions, the statement-cache feature) are not on crates.io yet, and open_writer_recovering_unreadable is unreleased (CHANGELOG Unreleased).
When to use it
- Use it to open a SQLite or SQLCipher database with the suite's journal, busy-timeout and durability rules.
- Use
write_txn()for any transaction that may write. It startsBEGIN IMMEDIATE, so read-then-write work cannot fail with an unretryable stale snapshot. A caller that holds only&Connection(a closure-style helper) useswrite_txn_unchecked(&conn), the sameBEGIN IMMEDIATEthroughTransaction::new_unchecked; SQLite refuses a nested one.
Do not use it to define app tables or queries. Keep those in the app.
Default behaviour
The default writer and legacy reader open select WAL with rollback fallback, a bounded busy wait and journal, memory-backed temporary storage, and the selected durability profile.
DurabilityProfile::Full: for user mutations that cannot be reconstructed.DurabilityProfile::Normal: for rebuildable indexes or telemetry.CacheProfile::LargeWorkingSet: opt-in. It setscache_sizeto -65536 (KiB) (src/lib.rs:576).
Cargo features
No feature is on by default (0.4.0). Name bundled (otherwise the system SQLite is linked) and statement-cache to get what 0.3.x built.
| Feature | Default | Pulls in | Enables |
|---|---|---|---|
bundled |
no | rusqlite/bundled |
Bundled plain SQLite. |
statement-cache |
no | rusqlite/cache |
Prepared-statement cache (prepare_cached) through the rusqlite re-export. |
sqlcipher-libtomcrypt |
no | cipher, rusqlite/sqlcipher, libsqlite3-sys, build-time cc and sqlcipher-src |
SQLCipher built on LibTomCrypt by this crate's build.rs, with no OpenSSL in the graph. Use it on Windows and wherever no system crypto is wanted. Existing OpenSSL-written databases open unchanged (SQLCipher 4's format is provider independent; proven on a real HeardRight database). Replaces the sqlcipher (vendored OpenSSL) feature, removed in 0.5.5. |
sqlcipher-system |
no | cipher, rusqlite/bundled-sqlcipher |
SQLCipher with the system crypto provider: CommonCrypto on Apple, libcrypto elsewhere (so OpenSSL off Apple; Windows apps use sqlcipher-libtomcrypt). Use it where existing encrypted databases were created with system crypto. Switching providers is not a silent refactor. |
cipher |
no | none | Internal gate for keyed open and rekey. Enabled by either SQLCipher feature. Do not name it directly. |
hooks |
no | rusqlite/hooks |
rusqlite's Connection::progress_handler() for cancellation, on the connections this crate returns. The rightkit_db::rusqlite re-export is not gated by it (src/lib.rs:22). |
SQLCipher consumers must turn off default features and choose exactly one provider feature. Both providers expose the same keyed-open and rekey APIs. Enabling both is a compile error. Keep the provider compatible with existing encrypted databases:
= { = "<exact published version from INDEX.md>", = false, = ["sqlcipher-libtomcrypt"] }
rusqlite is a caret requirement (0.40.1), so consumers that resolve 0.40.2 stay compatible.
Usage
Taken from tests/contract.rs and the crate's read-only and migration API. Illustrative as a snippet:
use Duration;
use ;
let mut db = new;
db.busy_timeout = from_millis;
let reader = db.open_read_only?;
let migrations = MigrateOptions ;
Other APIs
open_read_only(path)opens withSQLITE_OPEN_READ_ONLY.open_existing_writer(path)opens read-write without CREATE. Both accept paths orfile:URIs with query parameters, and refuse missing files. The matchingDbOptionsmethods use the path, key, timeout and optional integrity check. They skip journal selection, per-host routing, permission changes,init, optimization and migrations. Read-only main databases still allow SQLite TEMP tables unless callers setquery_only.open_reader()is read-write or create bootstrap, thenquery_onlyas the last setup statement, read back and required to be on. It does not runPRAGMA optimize; the writer runs it when it opens.open_read_pool(cap)andopen_read_pool_with(cap, ReadPoolOptions)return aReadPool. Every reader hasquery_onlyset and read back as on, or its open fails. The default pool (ReadPoolOptions::new()) keeps read-write file handles.ReadPoolOptions::with_read_only(true)opens readers withSQLITE_OPEN_READ_ONLY, so writes fail even afterquery_onlyis turned off. A read-only pool needs an existing database: it never creates the file, changes the journal mode, narrows permissions, or runsoptimize. Each reader runs theinithook, as on the other connections. A rollback journal left by a crash must be recovered by a writer first.ReadPoolOptions::with_reader_cache_kib(kib)sets each reader'scache_sizeto-kib(KiB, 1 to 2147483647, checked when the pool opens). It overridesCacheProfile::LargeWorkingSeton readers only.ReadPool::close()stops checkout and drops idle readers; a checked-out reader closes when it is returned.ReadPool::close_and_wait(timeout)closes, then waits until every reader is returned, so the database files are released (needed on Windows). If readers are still out at the timeout, it returnsDbError::ReadPoolCloseTimeout { outstanding, timeout }. After close,get()returnsDbError::ReadPoolClosed.ReadPool::get_timeout(duration)isget()with a bounded wait:Ok(Some(reader)),Ok(None)if no reader frees up in time (zero never waits), orDbError::ReadPoolClosedif the pool closes first.DbOptions::busy_timeout: Durationdefaults to five seconds. It is installed before any bootstrap SQL, including key verification andinit. It is the wait for a lock, not the time a backup copy may take:restore_database*,replace_database*,EncryptedStore::backup_toandrestore, the pre-migration backup and relocation bound the whole copy by a budget derived from the source size (120 s plus 1 s per 4 MiB, capped at six hours, never less thanbusy_timeout) and usebusy_timeoutonly for a busy or locked source. The publiconline_backup_tokeepsbusy_timeoutas the deadline of the whole copy.DbOptions::optimize_on_open: booldefaults totrue: a writer runsPRAGMA optimizewhen it opens.DbOptions::without_optimize_on_open()turns it off. Readers never run it.QuarantineStamp::Secondsnames are unique per second: a second quarantine in the same second takes<db>.corrupt-<secs>-1,-2, and so on, claimed without overwriting an earlier file.QuarantineStamp::Customnames take the same-1,-2retries.Millisnames still refuse an existing destination.- Unreadable (not corrupt) databases:
open_writer_recovering_unreadable(policy, UnreadablePolicy). The defaultFailreturnsDbError::Unreadablenaming the cause and never moves the file;QuarantineAfter { attempts }renames it only after that many consecutive read failures (permission denied, I/O read error), counted in<db>.unreadable-opens. Disk full, read-only filesystem, device or share not ready, sharing or lock violations, busy/locked and a missing file never count. open_in_memory()returns a private ephemeral connection. The matchingDbOptions::open_in_memory()keeps key, timeout, initializer, cache, foreign-key, durability, optimization and integrity options. Path, filesystem permissions, journal selection and mmap do not apply. The journal mode staysmemory, including withWalStrict. Runmigratewithdb_path: Noneto skip filesystem backup and marker handling.MigrateOptions::version_storagedefaults toVersionStorage::UserVersion.VersionStorage::Table { table, key }useskeyandvaluecolumns compatible with Blueprint'smetatable.VersionStorage::VersionTable { table, version_column, applied_at_column }is the other shape: one row per applied version (src/migrate.rs:67-84). Values are decimal text. Identifiers accept ASCII letters, digits and underscores. Keys are bound parameters. A missing table or key reads as zero. Table creation and version writes share each migration transaction.migration_versionreads the chosen storage, androllback_to_with_storagereverses it.user_versionandrollback_tokeep the default storage.DbOptions::restore_from(backup_path)checks everyPRAGMA integrity_checkrow, including CHECK constraints, before it restores an existing live database through SQLite's backup API. A read transaction holds the validated source snapshot until the copy finishes. A missing or corrupt backup leaves the live database untouched. A missing live database is refused. The backup must use the options' SQLCipher key. The free functionrestore_from(options, destination, backup_path)restores into an existing mutable connection, including in-memory stores. The destination must be writable and outside any transaction. Busy and locked retries are bounded by the options' timeout.replace_database(options, replacement, lock_options)atomically replaces the live database file with a validated copy underStoreLock::exclusive(<db>.store-lock). Processes that keep connections open holdStoreLock::sharedmeanwhile; the lock is advisory and cooperative. Close every connection to the live database first (required on Windows). The replacement must use the options' SQLCipher key.replace_database_lockedis the same step for a caller that already holds the exclusive guard.ReadPool::from_opener(cap, opener)builds a pool whose readers come from a closure, for tests over a connection the test builds. The pool enforcescap,get_timeout, and the close rules;query_onlyis the opener's job.open_writer_migrated_or_degrade(migrations, &MigrateOptions, RecoveryPolicy)isopen_writer_migratedwith recovery. A file that is corrupt, not a database, or fails its integrity check (at open, or as a migration step's SQLite error) is quarantined and recreated under the policy's stamp and sidecar options whenquarantine_corrupt(plain) orquarantine_keyed(keyed) allows it, then migrated again. When the policy does not allow it, or the store still cannot be opened, the connection is a private in-memory store, migrated the same way.OpenOrDegrade::degraded(Degraded { reason, quarantined, quarantine_incomplete, in_memory }) reports either case. Any other error (busy, permission, I/O, a newer database, an invalid migration list) is returned, not degraded.MigrateOptions::backup_name: Option<BackupName>(Arc<dyn Fn(i64, i64) -> String + Send + Sync>) names the pre-migration backup (default<db>.pre-v<target>.bak; a name that is not a plain file name, or is the database, a live sidecar or a recovery file beside it, is refused before the backup is made).MigrateOptions::verify_backup: Option<VerifyBackup>(Arc<dyn Fn(&Path) -> Result<(), String> + Send + Sync>, so an owner can hold a key, a message count or a free-space floor) checks the backup before the first step; a failure stops the migration and keeps the backup.MigrateOptions::backup_retention: Option<usize>keeps only the newestn<db>.pre-*.bakbackups after each backup (best effort; default keeps all).write_pending_marker(db, from, to)writes the<db>.migratingmarker atomically with the pid andstarted_at_unix, the same filemigratewrites.pending_marker_started_at(db)returns its start time (0for a marker written before the time was recorded);PendingMarkerkeeps its three fields.stored_version_is_newer(conn, storage, migrations)reports whether the stored version is above the newest migration, reading only.checkpoint_restart(conn)isPRAGMA wal_checkpoint(RESTART).WalCheckpointJob(withWalCheckpointPolicy) is a caller-driven checkpoint job:note_activityon each use, andtick(conn)from the loop or timer the owner already runs. PASSIVE once the-walreachespassive_above_bytes; RESTART once it reachesrestart_above_bytesand the database has been idle forquiet_for. A tick is single-flight. The crate starts no thread.rightkit_db::Zeroizingre-exportszeroize::Zeroizing, the wrapper of the key fields and of theopen_writer_with_key_candidatesslice, so callers need nozeroizedependency.restore_database(options, replacement, &RestoreOptions, lock_options)isreplace_databasefor a restore that must not lose the old store: the previous file and its sidecars are kept at<db>.pre-restore-<ms>(the path is in the returnedRestoreReceipt), a failed placement orRestoreOptions::verifyputs them back and keeps the bad copy at<db>.restore-failed-<ms>, and a crash mid-quarantine is finished byrecover_interrupted_restore(call it before opening the store; a restore calls it first).restore_database_lockedis the same under a heldStoreLock::exclusive.
Platform support
- Unix: descriptor-relative file handling through
rightkit-fs. - Windows: the same database API. SQLCipher provider choice is the main per-OS difference (see Cargo features).
- Apple:
sqlcipher-systemuses CommonCrypto.
Verification
Tests: tests/contract.rs, tests/e2e.rs, tests/journal_fallback.rs, tests/membrane.rs, tests/read_pool.rs.
Version and changes
Published version: see INDEX.md. Release notes: CHANGELOG.md.
Publish only through the RightKit release command:
right-release publish cargo --crate rightkit-db