- 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
160 lines
8.6 KiB
Markdown
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. |