--- 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` (`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>` (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` (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`; 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`, 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` 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>` 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.