Follow-through on OQ-06/ADR-011: pin the fork's structural decisions (alkstore-substrate as a vendored path-dep crate, contract-blind API boundary with contract formulas computed engine-side and pinned equivalent by the contract suite, keep-the-kept-half API fidelity for cheap cherry-picks, the W-1/W-2/dead-man's-switch/W-4 port deltas decided per item, bootstrap re-keying off error-string matching, no rename migration, deliberate upstream tracking). Consistency sweep across the doc set for the fork: annotate ADR-003/ 005/009/010 and core-contract for superseded ownership facts, fix schedule-storage table naming (ADR-009 §5, queues.md), re-key ADR-010 §6's notifications hygiene to the at-attach cap the fork scope realizes, add OQ-11 (scaffold-time residue), and complete both ADR indexes. Independent review: 0 critical, warnings addressed.
5.3 KiB
ADR-003: SQLite engine — rusqlite + published honker-core, bridged at the trait seam
Status
Accepted
Context
The SQLite engine's options were never a single "rusqlite vs sqlx" axis (OQ-ST-03). Three distinct postures existed:
- honker-core on our own rusqlite connection — we own
connection/schema/watcher wiring; honker supplies the SQL-function
machinery (
attach_notify,attach_honker_functions,bootstrap_honker_schema). - honker-rs as the SQLite substrate (
Database::open— honker holds its own connections; sync-only; transactions pin the connection mutex). - raw SQL over sqlx-sqlite with the honker loadable extension
(
.sobuilt from published source, loaded per pool connection).
POC #1 (docs/research/poc-sqlite-posture-findings.md, ran 2026-10-04;
POC #2 is its pg twin) measured postures 1
and 3 end-to-end and settled the choice. Constraint facts that outlive
the comparison:
- rusqlite 0.40.x (what honker-core 0.5.0 pins) needs rustc ≥ 1.99
(
cfg_select!) — a deployment note for any binary linking it. - sqlx's
libsqlite3-sysrange collides with rusqlite 0.40's — mixed rusqlite+sqlx binaries need a vendored one-line patch today. The per-engine-crate split ([ADR-001]) keeps engine binaries single-driver, dissolving this. - The async-facing-trait + sync-bridge posture is family-standard (alktty REQ-TTY-01; alkblobs store-api.md): bridge-at-the-seam is a supported posture, not a workaround — which removes native-async's main differentiator before any measurement.
Decision
The SQLite engine uses posture 1: published honker-core = 0.5
as a library dependency over the crate's own rusqlite connections
(bundled-sqlite for hermetic builds). (Ownership superseded
2026-10-05 by ADR-011: the substrate
is the forked alkstore-substrate lineage crate, not the published
dependency — the driver, seam, and watcher architecture below are
unchanged.)
- Driver: rusqlite, riding honker-core's pinned version. (The pin is ours to move deliberately since the fork — [ADR-011], per [ADR-005]'s annotated rusqlite row.)
- Async seam: bridged. The writer slot (
Writer) + reader pool (Readers) + every engine call inspawn_blocking. A dedicated std-thread bridge (one thread owning the writer conn, ops over mpsc) is the recorded optimization path if seam throughput ever demands it — not the default. - Wake: honker-core's
SharedUpdateWatcherinherited (p50 ≈ 1.4 ms at the default 1 msPRAGMA data_versioncadence, with battle-tested failure handling — subscriber senders close on watcher death, never silent-hang). A self-built thin watcher was benchmarked only to confirm honker-core's is tighter (p50 1.40 vs 2.15 ms; max 29 vs 172 ms); no hybrid is warranted. - Transactions: the engine opens
BEGIN IMMEDIATEon the writer slot and hands back a caller-held tx handle ([ADR-007]). The slot lease models WAL's single-writer honestly: a long transaction parks the writer — that is SQLite's shape, not an emulation artifact. - No
.soruntime artifact, no vendored patches in the engine crate.
Rejected alternatives, briefly, for the record: posture 3 is viable
but pays ~2× p50 seam cost, a runtime dlopen artifact, wider
dependency tree, and sqlx 0.9 ergonomic warts (unsafe extension(),
SqlSafeStr) for no compensating advantage. Posture 2 (honker-rs)
was never measured because posture 1 dominates it on control with
comparable reuse.
Consequences
Positive
- ~0.35 ms p50 transactional seam (measured), ~1.4 ms wake latency, and honker-core's watcher failure-handling inherited for free.
- Static linkage: "open a path, get a store" needs no runtime artifacts.
- The engine's job shrinks to wiring + the SQL surfaces honker doesn't cover (queue semantics depth, OQ-ST-05's SQLite side rides honker's machinery directly) + the async bridge. (Ownership since resolved: the substrate is forked — ADR-011 — and the queue ops re-derived on contract v1 within it.)
Negative
- honker-core pins rusqlite ^0.40.1; version movement in honker-core moves our rusqlite. A honker-core quality read is the recorded fork trigger ([ADR-005], OQ-06) — (resolved 2026-10-05: the trigger fired, ADR-011 forks the substrate; its rusqlite pin is then ours to move deliberately.)
- rustc ≥ 1.99 required by rusqlite 0.40.x — binaries linking this engine carry that toolchain floor (deployment-matrix row, deployment.md).
- The sync bridge means engine ops pay a
spawn_blockinghop — mostly invisible next to SQLite write costs, but it shapes the tx-handle design ([ADR-007]).
References
docs/research/poc-sqlite-posture-findings.md— the measurements.- OQ-ST-03 (
docs/research/phase-0.md) — resolution record. - ADR-001 — why the engine crate is single-driver.
- ADR-004 — the Postgres counterpart.
- ADR-007 — the tx-handle shape both engines implement.
- engine-sqlite.md — the engine spec this decision defines. [ADR-001]: 001-crate-split.md [ADR-005]: 005-dependency-ownership.md [ADR-007]: 007-transactional-seam.md