- 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
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 |
|
moderate | medium | component | implementation |
|
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 minimumpoll_interval: Option<Duration>(None= the substrate's 1 ms default, ADR-023 §4 — the shipping default stands; the knob is wiring ontoWatcherConfig::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/Defaultit.open(path: &str, opts: SqliteOpts) -> Result<Box<dyn Store>>(or a concreteSqliteStorere-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_connalready dropped the URI flag; do not re-add it;:memory:works without URI mode).- Connection architecture per engine-sqlite.md: one
Writerslot (substrate'sWriter), aReaderspool (substrate'sReaders), and theSharedUpdateWatcherspawned against the db path — watcher spawn is fallible (W-2): map the substrate'sResult<_, String>intoError::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
Storetrait;Drop/close stops the watcher and closes connections. spawn_blockingseam 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
SqliteOptsexists withpoll_interval: Option<Duration>(None = 1 ms default) and reader-pool sizing; documentedopen(path, opts)boots the full connection architecture: writer slot, reader pool, watcher (fallible spawn mapped toDatabase), 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_intervalflows 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()returnsBox<dyn Store>; the concreteSqliteStoreis 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 switchedReaders::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_intervaldisposition:SqliteOpts::poll_interval = Some(ZERO)fails atopenwithError::Databasewhose 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: 0clamps to 1 (the substrate's existingmax()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
Storemethods returnErr(Database("… wiring lands with the … task"))— nothing panics (family standard); the mechanism tasks replace them.with_txis core's provided method — it surfaces thebegin_txstub naturally. spawn_blockingseam posture: the privateblockinghelper exists inseam.rs(tokio promoted to a real dep,rt+sync) but is#[cfg(test)]-gated untilsqlite-engine-seam-txwires the trait impls — clippy-D warningsrejects 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 toDatabase— no panics cross the seam) and the seam task drops the gate.database_error(msg)(string→Database, io::Error source) andsqlite_error(e)(rusqlite::Error→Database, source chain preserved) are the two mappings the mechanism tasks reuse.SqliteStoreaccessors deliberately absent: the test module lives understore::and reaches the private fields directly;close()is pub (explicit close, idempotent,Dropdelegates).Debugis hand-implemented (finish_non_exhaustive) — no derive.- Watcher death plumbing verified at the substrate seam: the open
tests subscribe through the store's
SharedUpdateWatcherand 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:openreturnsBox<dyn Store>, so the tests useopen_store(crate-private, returnsSqliteStore) — 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.