Files
alkblobs/tasks/engine-sqlite.md
T
glm-5.3-flash 908fb5d2dd feat(engines): the sqlite kv engine — the default engine, first SQL-backed (wave 3)
Implements the sqlite kv-tier engine per ADR-003/007/010 §1 behind
feature `sqlite` (default-on): the B5 row shape (key BLOB PK / value
BLOB / size INTEGER NOT NULL), INSERT OR IGNORE CAS first-writer-wins
on both put forms, the EXISTS has probe, the materializing cursor body
with len() from the size column, the degenerate in-memory stage with
both hygiene forms, the indexed-column size probe (plan-tested), the
no-tables-yet virgin-store read tolerance (tested against a tableless
database file), WAL + synchronous=NORMAL verified at open, prepare_cached
discipline, and every rusqlite operation in spawn_blocking under the
recorded single-connection posture.

Task: tasks/engine-sqlite.md (status: completed). The engine-state
window family and store construction remain in their own tasks.

Verification:
- cargo test — 58 pass (11 new sqlite in-module tests)
- cargo clippy --all-targets -- -D warnings — clean
- cargo fmt --check — clean
- cargo check --no-default-features — green
2026-10-12 04:31:24 +00:00

8.7 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
engine-sqlite sqlite kv engine — the default engine completed
narrow medium component implementation
wave-3
engines
sqlite

Description

Implement the sqlite kv-tier engine (feature sqlite, default-on; ADR-007; REQ-1) against the wave-1 trait (src/backend.rs) — the first SQL-backed engine and the default build's kv engine.

  • Backend impl (src/sqlite.rs, behind #[cfg(feature = "sqlite")]): the seven methods plus put_staged, opaque byte keys/values, name() -> "sqlite".
    • Schema — the POC #5 finding B5 row shape adapted to sqlite: blobs (key BLOB PRIMARY KEY, value BLOB, size INTEGER NOT NULL). The dedicated size column is what makes the indexed size column probe metadata-only: SELECT size … WHERE key = ? rides the key's index and never loads the value blob (ADR-008's size discipline).
    • WAL posture — journal_mode=WAL + synchronous=NORMAL set at open: the shipped durability tier (ADR-003; the posture honker ships and iroh's store measured). The single-writer ceiling is a documented posture, not a defect (REQ-1's economics — the module doc records it; ~1.2k objects/s under contention is an order above single-node push rates).
    • Prepared statements — rusqlite's prepare_cached discipline per the backends spec's "prepared statements" requirement.
    • has — an EXISTS probe (SELECT EXISTS(…)) — never a value load.
    • get — the materializing cursor body (kv family): the whole value loaded once (bounded by tier policy — the kv tier holds only ≤ threshold content), len() from the row's size column, reads slice the materialized value composing backend.rs's clamped. This body is also what the facade's verification-on-get re-derives over (store.rs's kv design point — no engine change needed).
    • put — CAS first-writer-wins (INSERT OR IGNORE); a re-put of an existing key is a no-op, never an overwrite.
    • put_staged — the degenerate in-memory stage (ADR-010 §1: kv bodies stage in memory and commit through the whole-value row write); append extends the buffer, commit publishes via the same INSERT OR IGNORE clause and self-discards on failure, discard drops the buffer with zero residue.
    • delete — Err(Error::Missing) when absent (rows-affected discipline); list — complete and well-formed (SELECT key); size — the indexed column probe above.
    • Virgin-store no-tables-yet tolerance — read paths treat a missing table as absent/empty, never "table does not exist" errors (POC #1 finding 2; store-api.md §Error model's virgin-store semantics). Schema creation timing (constructor CREATE TABLE IF NOT EXISTS vs lazy) is a recorded design point; the tolerance is unit-tested against a tableless database file either way.
  • Blocking discipline: rusqlite is synchronous — all engine I/O runs in spawn_blocking (AGENTS convention 3; store-api.md §Concurrency posture). The connection posture (the expected shape: a single connection behind a short-held lock, the single-writer posture; a pool is not required solo) is a recorded design point. No blocking I/O on the async path; no unwrap/expect outside tests.
  • Unit tests in-module: the method contract (virgin-store no-ops incl. the no-tables-yet form, CAS re-put, Missing discipline, size exactness + metadata-only), the staged-put happy path and both hygiene forms (discard → zero residue; commit failure → self-discard — a #[cfg(test)] fail-commit knob standing in for media failure, the mem engine's pattern), the pragma posture.

The engine-state window family (EngineWindowState) is engine-sqlite-window (separate task); the constructor wiring is store-sqlite-construction. This task ships the engine and its in-module unit tests only.

Acceptance Criteria

  • src/sqlite.rs implements Backend completely (seven methods + put_staged) behind feature sqlite (default-on), with the materializing cursor body and the degenerate staging body
  • WAL + synchronous=NORMAL set at open; prepared-statement discipline; has is an EXISTS probe; the single-writer ceiling recorded as posture in the module doc
  • size reads the dedicated size column via the key index, never the value blob (metadata-only, unit-tested)
  • CAS first-writer-wins on both put forms; re-put never overwrites
  • Stage hygiene holds in both forms (discard → zero residue; failed commit → self-discard), unit-tested
  • Virgin-store reads are no-ops including the no-tables-yet form (unit-tested against a tableless database file)
  • All engine I/O in spawn_blocking; connection posture recorded
  • cargo test (default — sqlite is default-on) green; cargo clippy --all-targets -- -D warnings and cargo fmt --check green

References

  • docs/architecture/backends-and-dispatch.md (§kv tier — sqlite engine)
  • docs/architecture/decisions/007-two-kv-engines-sqlite-and-postgres.md
  • docs/architecture/decisions/003-backend-contract-and-dispatch.md (the shipped durability tier; the three numbered put rules' kv side)
  • docs/architecture/decisions/010-backend-io-seams-and-fleet-state.md §1
  • docs/architecture/requirements.md (REQ-1's economics)
  • docs/research/poc-postgres-kv-findings.md (finding B5's row shape)
  • docs/research/poc-largeblob-findings.md (finding A4's economics)
  • src/backend.rs (the trait + seam contracts this implements)
  • src/mem.rs (the contract-reference engine's shapes this mirrors)

Notes

Recorded design points landed in the module doc (src/sqlite.rs):

  • Schema-creation timing: the constructor runs CREATE TABLE IF NOT EXISTS at open. The virgin-store tolerance itself is an engine-level read-path property independent of that timing — unit-tested against a genuinely tableless database file via a #[cfg(test)] new_tableless constructor. The tolerance is deliberately read-only: a write against a tableless database surfaces the engine error rather than silently creating schema (tested).
  • WAL engagement is verified at open, not assumed: the constructor probes PRAGMA journal_mode = WAL and errors when WAL cannot be engaged (in-memory databases included — tested). The pragma posture (WAL persistent + synchronous NORMAL per-connection) is asserted in the pragma test on the backend's own single connection.
  • Error mapping: the no-tables-yet discriminant is a stringly check on rusqlite's Display ("no such table") — stringly by nature, the stable surface sqlite itself guarantees. delete maps it to Error::Missing (rows-affected discipline holds for the virgin form); put propagates it as Error::Io.
  • size metadata-only proof: exactness is asserted for large/absent keys and EXPLAIN QUERY PLAN asserts the probe searches the key's index (USING INDEX) — never a scan or value load.
  • Connection posture: one connection behind a short-held parking_lot::Mutex, shared with staging handouts via Arc (a staging commit must see the same table); the engine Clones. All rusqlite I/O in spawn_blocking via a local blocking helper (the local.rs pattern); cursor/staging read bodies are in-memory after materialization and need no blocking.
  • EXPLAIN QUERY PLAN calls must bind the ?1 parameter even though they do not execute — sqlite enforces the bind count at step time.

Summary

The sqlite kv engine shipped complete behind feature sqlite (default-on): the row shape blobs (key BLOB PRIMARY KEY, value BLOB, size INTEGER NOT NULL) (POC #5 B5 adapted), INSERT OR IGNORE CAS first-writer-wins on both put forms, the EXISTS has probe, the materializing cursor body with len() from the size column, the degenerate in-memory stage committed through the whole-value row write with both hygiene forms, the indexed-column size probe, complete list, and the no-tables-yet read tolerance. WAL + synchronous=NORMAL set and verified at open; prepare_cached discipline throughout; every rusqlite operation in spawn_blocking; single-writer posture recorded in the module doc. Eleven in-module tests green (method contract, list completeness, cursor contract, empty entry, staged round-trip incl. both dedup forms, both hygiene forms, pragma posture, WAL-engagement rejection, size/index plan, tableless virgin store). cargo test 58 pass, cargo clippy --all-targets -- -D warnings clean, cargo fmt --check clean, cargo check --no-default-features green. Engine-state window family and store construction remain in their own tasks (engine-sqlite-window, store-sqlite-construction).