Files
alkstore/tasks/sqlite-engine-open-opts.md
T
glm-5.3-flash 7d400906f5 SQLite engine open: SqliteOpts, connection architecture, spawn_blocking seam posture (task sqlite-engine-open-opts)
- SqliteOpts (poll_interval: Option<Duration>, None = 1 ms shipping
  default ADR-023 §4; max_readers, DEFAULT_MAX_READERS = 8; opts
  exemption from non_exhaustive per ADR-017 §3)
- open(path, opts) -> Box<dyn Store>: writer slot, reader pool,
  SharedUpdateWatcher with fallible spawn mapped W-2-style into
  Error::Database; plain-path posture (ADR-023 §3) pinned by test
- Substrate delta D-30: open_conn_bootstrapped — every connection
  (writer and pooled readers) carries the full bootstrap surface
  (pragmas, notify, alkstore functions, schema) per engine-sqlite.md;
  lineage opened readers pragmas-only. Registered in PROVENANCE.md
- spawn_blocking seam helper + error mappings (string/rusqlite ->
  Database, source chains preserved); helper cfg(test)-gated until
  sqlite-engine-seam-tx wires the trait impls
- Store trait stubbed with Database errors (no panics); close()/Drop
  join the watcher, clear subscribers (death-guard), close pool+writer
- 15 new tests (open boot, watcher fanout/death-close, cadence
  accounting, plain-path literal filename, :memory:, pool bounding,
  drop teardown, seam smoke incl. panic mapping); gates green
2026-10-08 11:25:16 +00:00

160 lines
8.6 KiB
Markdown

---
id: sqlite-engine-open-opts
name: SQLite engine — `open` constructor, `SqliteOpts`, connection architecture
status: completed
depends_on: [core-engine-value-constructors]
scope: moderate
risk: medium
impact: component
level: implementation
tags: [wave-3, sqlite-engine]
---
## Description
Stand up the engine crate's public surface: the `open` constructor,
`SqliteOpts`, and the connection architecture of engine-sqlite.md's
"Connection architecture" section — the first wiring of the ported
substrate.
- `SqliteOpts` (engine-crate type, ADR-008 §6's config split — engine
opts live engine-side): at minimum `poll_interval:
Option<Duration>` (`None` = the substrate's 1 ms default,
ADR-023 §4 — the shipping default stands; the knob is wiring onto
`WatcherConfig::with_poll_interval`, which rejects zero) and
reader-pool size. `#[non_exhaustive]` policy does NOT apply (opts
structs consumers construct — ADR-017 §3's exemption; this is an
engine opts struct, same posture). Derive/`Default` it.
- `open(path: &str, opts: SqliteOpts) -> Result<Box<dyn Store>>` (or
a concrete `SqliteStore` re-exported as the trait — implementer's
choice, but the trait is what consumers hold). The path is a **plain
filesystem path** (ADR-023 §3 — `open_conn` already dropped the URI
flag; do not re-add it; `:memory:` works without URI mode).
- Connection architecture per engine-sqlite.md: one `Writer` slot
(substrate's `Writer`), a `Readers` pool (substrate's `Readers`),
and the `SharedUpdateWatcher` spawned against the db path — watcher
spawn is fallible (W-2): map the substrate's `Result<_, String>`
into `Error::Database` (the wave-2 review's watch-item; the mapping
is engine-layer work). Open fails if the watcher cannot start.
- Each connection runs the substrate's bootstrap at open
(`open_conn` → pragmas, `attach_notify`, `attach_alkstore_functions`,
`bootstrap_schema`) — the writer and each pooled reader.
- The store struct holds the shared machinery (writer slot, readers,
watcher handle, db path) behind the `Store` trait; `Drop`/close
stops the watcher and closes connections.
- `spawn_blocking` seam posture established here for all later tasks:
a private helper the trait methods use (tokio dep added to the
engine crate — dev-dep today; make it a real dep).
This task does NOT implement any trait method beyond what's needed to
compile (the trait impls come in the mechanism tasks) — but the store
struct, opts, and constructor are real and tested (open a temp file,
verify bootstrap ran, verify the watcher is up, verify opts flow).
## Acceptance Criteria
- [x] `SqliteOpts` exists with `poll_interval: Option<Duration>`
(None = 1 ms default) and reader-pool sizing; documented
- [x] `open(path, opts)` boots the full connection architecture:
writer slot, reader pool, watcher (fallible spawn mapped to
`Database`), substrate bootstrap on every connection
- [x] Plain-path posture holds: no URI flag re-introduced anywhere in
the open path (pinning test: a `?name=value`-suffixed path
creates a file with that literal name — ADR-023 §3's backlog row)
- [x] `poll_interval` flows to the watcher config (config-surface
accounting test — assert the watcher's configured cadence, not
wall-clock timing; ADR-023 §4's backlog row)
- [x] Watcher death handling wired: subscribers' receivers close
(mechanism lands with `listen`, but the death-guard plumbing
exists here)
- [x] `cargo test -p alkstore-sqlite`, clippy `-D warnings`, fmt clean
## References
- docs/architecture/engine-sqlite.md (Connection architecture)
- docs/architecture/decisions/023-fourth-review-round.md §3, §4
- docs/architecture/decisions/008-contract-v1-pinning.md §6
- docs/architecture/decisions/007-transactional-seam.md (SQLite arm)
- alkstore-sqlite/src/substrate/schema.rs, watcher.rs
## Notes
- **Concrete type + boxed constructor** (the task's implementer's
choice): `open()` returns `Box<dyn Store>`; the concrete
`SqliteStore` is also re-exported (pub) so tests and the later
mechanism tasks can reach the machinery — consumers hold the trait.
- **Substrate delta D-30** (registered in PROVENANCE.md): the engine
spec's "each connection runs the substrate's bootstrap" obligation
required reader opens to carry the full surface — the lineage
opened readers pragmas+notify only (functions + schema lived on the
writer alone). Added `open_conn_bootstrapped` (pragmas, notify,
`attach_alkstore_functions`, `bootstrap_schema`) and switched
`Readers::acquire`'s fresh-open path to it. Fresh connections only;
the pool never re-bootstraps reused connections (readers stay
read-mostly — writes ride the writer slot; the idempotent bootstrap
would be harmless but pointless per-acquire).
- **Zero `poll_interval` disposition**: `SqliteOpts::poll_interval =
Some(ZERO)` fails at `open` with `Error::Database` whose source
chain carries the substrate's "watcher poll interval must be
positive" (the W-2 mapping posture — engine-layer mapping of the
substrate's string into the taxonomy; no new variant, per ADR-008
§5). Pinned by test at the source-chain level.
- **`max_readers: 0` clamps to 1** (the substrate's existing `max()`
discipline, inherited — not re-decided here).
- **Defaults**: `DEFAULT_MAX_READERS = 8` (the lineage bindings'
default, carried) — re-exported from the crate root like core's
opts constants.
- **Trait stubs**: all twelve `Store` methods return
`Err(Database("… wiring lands with the … task"))` — nothing panics
(family standard); the mechanism tasks replace them. `with_tx` is
core's provided method — it surfaces the `begin_tx` stub naturally.
- **`spawn_blocking` seam posture**: the private `blocking` helper
exists in `seam.rs` (tokio promoted to a real dep, `rt`+`sync`) but
is `#[cfg(test)]`-gated until `sqlite-engine-seam-tx` wires the
trait impls — clippy `-D warnings` rejects dead code, and an
ungated unused helper would trip the gate every intermediate commit.
Its shape is pinned by a smoke test (panics inside bridged closures
map to `Database` — no panics cross the seam) and the seam task
drops the gate. `database_error(msg)` (string→`Database`, io::Error
source) and `sqlite_error(e)` (`rusqlite::Error`→`Database`, source
chain preserved) are the two mappings the mechanism tasks reuse.
- **`SqliteStore` accessors deliberately absent**: the test module
lives under `store::` and reaches the private fields directly;
`close()` is pub (explicit close, idempotent, `Drop` delegates).
`Debug` is hand-implemented (`finish_non_exhaustive`) — no derive.
- **Watcher death plumbing verified at the substrate seam**: the open
tests subscribe through the store's `SharedUpdateWatcher` and
assert receivers disconnect on close (and on drop's teardown); the
file-replacement death path is already pinned substrate-side
(`shared_update_watcher_signals_subscribers_on_watcher_death`).
- **`store/open_tests.rs`**: `open` returns `Box<dyn Store>`, so the
tests use `open_store` (crate-private, returns `SqliteStore`) — the
boxed public constructor itself is exercised by the reopen/close
tests.
## Summary
Stood up `alkstore-sqlite`'s public surface: `SqliteOpts`
(`poll_interval: Option<Duration>` with None = the substrate's 1 ms
shipping default via `WatcherConfig::with_poll_interval`; `max_readers`
with `DEFAULT_MAX_READERS = 8`; not `#[non_exhaustive]` per ADR-017
§3's opts exemption; `Default`+`Clone`), the `open(path, opts) ->
Result<Box<dyn Store>>` constructor booting engine-sqlite.md's full
connection architecture (writer slot, reader pool,
`SharedUpdateWatcher` with fallible spawn mapped W-2-style into
`Error::Database` — open fails if the watcher cannot start), and the
`spawn_blocking` seam's helper + error mappings. Required substrate
delta D-30 (`open_conn_bootstrapped` + reader-pool open path carries
the full per-connection bootstrap — pragmas/notify/functions/schema —
per engine-sqlite.md's "each connection runs the substrate's
bootstrap"; the lineage opened readers pragmas-only), registered in
PROVENANCE.md. Store wiring: `close()`/`Drop` join the watcher,
clear subscribers (death-guard), close pool + writer slot. Trait
methods are `Database`-error stubs pending the mechanism tasks. 14
new engine tests (full-boot, watcher fanout, death-close, cadence
accounting ADR-023 §4, plain-path ADR-023 §3, `:memory:`, pool
bounding + 0-clamp, read visibility, close/reopen, drop teardown,
seam smoke incl. panic mapping, unopenable-path failure) + 1 substrate
test. Verified: `cargo test` (workspace: 25 core + 3 suite + 101
sqlite), `cargo clippy --all-targets -- -D warnings`, `cargo fmt
--check` all clean.