Files
alkstore/tasks/sqlite-engine-open-opts.md
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

8.6 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
sqlite-engine-open-opts SQLite engine — `open` constructor, `SqliteOpts`, connection architecture completed
core-engine-value-constructors
moderate medium component implementation
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

  • SqliteOpts exists with poll_interval: Option<Duration> (None = 1 ms default) and reader-pool sizing; documented
  • open(path, opts) boots the full connection architecture: writer slot, reader pool, watcher (fallible spawn mapped to Database), substrate bootstrap on every connection
  • 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)
  • 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)
  • Watcher death handling wired: subscribers' receivers close (mechanism lands with listen, but the death-guard plumbing exists here)
  • 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.