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
164 lines
8.7 KiB
Markdown
164 lines
8.7 KiB
Markdown
---
|
|
id: engine-sqlite
|
|
name: sqlite kv engine — the default engine
|
|
status: completed
|
|
depends_on: []
|
|
scope: narrow
|
|
risk: medium
|
|
impact: component
|
|
level: implementation
|
|
tags: [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 `Clone`s. 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`). |