# 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: 1. **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`). 2. **honker-rs as the SQLite substrate** (`Database::open` — honker holds its own connections; sync-only; transactions pin the connection mutex). 3. **raw SQL over sqlx-sqlite with the honker loadable extension** (`.so` built 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-sys` range 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](011-sqlite-substrate-fork.md): the substrate is the forked honker-core lineage, carried in-tree as the engine crate's substrate module subtree per [ADR-013](013-fold-substrate-into-sqlite.md), 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 in `spawn_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 `SharedUpdateWatcher` inherited (p50 ≈ 1.4 ms at the default 1 ms `PRAGMA data_version` cadence, 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 IMMEDIATE` on 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 `.so` runtime 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](011-sqlite-substrate-fork.md) — 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](011-sqlite-substrate-fork.md) 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](../deployment.md)). - The sync bridge means engine ops pay a `spawn_blocking` hop — 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](001-crate-split.md) — why the engine crate is single-driver. - [ADR-004](004-postgres-driver.md) — the Postgres counterpart. - [ADR-007](007-transactional-seam.md) — the tx-handle shape both engines implement. - [engine-sqlite.md](../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