SqliteCrdtTable class final

A CrdtTable backed by SQLite (package:sqlite3).

Records live in a single crdt_records table keyed by (collection, key); one SQLite database can host every collection. Values are stored as JSON text. Ordering and sync filtering use hlc_key, a fixed-width sortable encoding of the record's Hybrid Logical Clock, so WHERE hlc_key > ? is a correct causal comparison.

Construct it with a path; the database is opened on the first init (which Collection calls, since isPersistent is true). On native targets pass an encryptionKey to use SQLCipher. The open fails closed: init throws SqlCipherUnavailableException when a key is given but the linked native library is not a SQLCipher build, so data is never silently written in plaintext.

Wrong key versus corruption

An existing encrypted file that does not decrypt under encryptionKey makes init throw WrongPassphraseException and leaves the file exactly as it was — retrying with the right key succeeds. An encrypted file whose damage makes it unreadable answers exactly like a mis-keyed one, so the snapshot arbitrates: when <path>.snapshot opens cleanly under the same key, the key is proven right, the file is treated as corrupt, and recovery proceeds. Without a snapshot the ambiguity resolves to WrongPassphraseException — the file is never destroyed on a guess, at the price that an encrypted database without snapshots recovers from header damage only by the user confirming the passphrase and deleting the file. Corruption past the header (a damaged page under a valid key) fails PRAGMA quick_check directly and recovers without arbitration.

Corruption recovery

init runs PRAGMA quick_check on open. If a corrupt file does not pass, it is moved aside to <path>.corrupt-<timestamp> and the store is rebuilt: from <path>.snapshot if a readable one exists, otherwise empty (and recoveries emits so the sync layer can pull the data back from a peer). Set snapshotInterval (or PassnoteConfig.snapshotInterval) to keep those snapshots current.

Implemented types

Constructors

SqliteCrdtTable(String _path, {List<int>? encryptionKey, Duration snapshotInterval = Duration.zero})
Creates a table for the database at path, which is opened on the first init. Pass ":memory:" for a transient database; pass an encryptionKey to use SQLCipher on a SQLCipher build of the native library. snapshotInterval sets how often a recovery snapshot is written (Duration.zero, the default, disables it).

Properties

hashCode → int
The hash code for this object.
no setterinherited
isPersistent → bool
Whether this table owns its own durability.
no setteroverride
nodeId → String
The node id persisted in crdt_meta, stable across restarts. Created on the first init of a fresh database. Throws StateError before init.
no setteroverride
recoveries → Stream<CrdtRecovery>
Emits once if init found the local store unreadable and rebuilt it (from an on-disk snapshot, or empty).
no setteroverride
recovery → CrdtRecovery?
Non-null once init has run if it had to rebuild an unreadable store.
no setter
runtimeType → Type
A representation of the runtime type of the object.
no setterinherited
snapshotInterval ← Duration
How often to write a snapshot of the store. Duration.zero disables snapshots. Set before CrdtTable.init; later changes may not take effect.
no getteroverride

Methods

changeset(String table, {String? sinceHlc, String? untilHlc}) → Future<CrdtChangeset>
Returns records in table in CrdtChangeset format.
override
close() → Future<void>
Closes the database and every watch stream. Safe to call more than once.
override
compact(String table, {required Duration horizon}) → Future<int>
Permanently removes tombstoned records whose wall-clock time is older than horizon and returns the number of reclaimed records.
override
docFields(String table, Iterable<String> docIds) → Future<Map<String, Map<String, Object?>>>
One indexed lookup per chunk of ids (doc_id IN (...)), never a table scan.
override
ensureQueryIndex(String table, String fieldPath) → Future<void>
Creates a small partial index for fieldPath in table: it covers only that field's live rows and indexes their extracted value, so an equality predicate on the path is an index search rather than a table scan.
override
explainQuery(String table, QuerySpec spec) → String
The EXPLAIN QUERY PLAN output for the SQL that queryDocIds would run for spec — one plan line per row. A diagnostic: use it to confirm a predicate on an indexed path searches its index instead of scanning.
init() → Future<void>
Initializes the table from storage (called by Collection during startup when isPersistent is true).
override
liveFields(String table, String prefix) → Future<Map<String, Object?>>
Returns all live (non-tombstoned) records in table whose key starts with prefix, as a flat map of key → value.
override
liveKeys(String table, String prefix) → Set<String>
All live (non-tombstoned) keys in table whose key starts with prefix.
override
maxHlc(String table) → String
The highest HLC seen in table as a string, or the empty string when the table is empty.
override
merge(CrdtChangeset changeset) → Future<void>
Merges a CrdtChangeset received from a peer or loaded from storage.
override
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
put(String table, String key, Object? value, {bool isDeleted = false, CrdtType crdtType = const LwwRegister()}) → Future<void>
Writes or tombstones the record identified by table/key.
override
queryDocIds(String table, QuerySpec spec) → Future<List<QueryRow>>
Returns the ids of the live documents in table that satisfy spec, in the spec's total order — (order-by value, document id) — windowed by its limit and cursor.
override
rangeDigest(String table, {String? fromHlc, String? toHlc}) → Future<RangeDigest>
One indexed range scan over (collection, hlc_key); O(1) memory.
override
recordHlc(String table, String key) → Future<String?>
The stored HLC string of the record at table/key — tombstoned or live — or null when no record exists.
override
rekey(List<int> newKey) → Future<void>
Re-encrypts the database under newKey with SQLCipher's PRAGMA rekey. All data is preserved. Subsequent opens and recovery snapshots use newKey, and an existing recovery snapshot is rewritten under it at once: a passphrase is usually rotated because the old one leaked, so a copy under the old key must not wait for the next snapshot tick. Quarantined <file>.corrupt-* copies keep the key they had; delete them once inspected.
override
snapshotNow() → void
Writes a consistent copy of the store to <path>.snapshot, replacing any previous one. Called by the periodic timer; also safe to call directly for a manual checkpoint. A no-op for an in-memory database.
toString() → String
A string representation of this object.
inherited
value(String table, String key) → Future<Object?>
Returns the live value for key in table, or null if absent or tombstoned.
override
watch(String table) → Stream<void>
A broadcast stream that emits whenever any record in table changes.
override

Operators

operator ==(Object other) → bool
The equality operator.
inherited

Static Methods

configureWeb({Uri? wasmUri}) → void
Web-only configuration (a no-op on native): points the engine at the sqlite3.wasm module. Call it once before opening a database.
open(String path, {List<int>? encryptionKey}) → Future<SqliteCrdtTable>
Convenience for tests: constructs and inits in one step.