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
This commit is contained in:
glm-5.3-flash committed 2026-10-08 11:25:16 +00:00
1 parent 5474141f2b
commit 7d400906f5
11 files changed
+937 -13

No files matched your search

@@ -118,6 +118,7 @@ fixes. Cherry-picks: empty at scaffold, append-only forever.
| D-27 | re-derivation | wave-2 review fix — `scheduler_tick`'s fire enqueue resolved the schedule row's relative `expires_s` into the absolute row `expires_at` (`now + s` at the fire transaction, one clock read) instead of passing the relative value through; regression test pins a fired job's `expires_at = fire-instant + s` and its claimability. The pre-fix pass-through would have produced rows expired in 1970 under any `expires_s` schedule | `queue_ops.rs` (`scheduler_tick`, fire-expiry resolution + test) | ADR-020 §2 (relative `expires` → resolved absolute row value, resolved at enqueue against the single clock) — D-12's landed state carried the lineage's `expires_s` field shape without carrying the lineage's enqueue-side resolution; wave-2 review (task review-wave-2) re-derivation spot-check found it | ours |
| D-28 | hygiene | wave-2 review fix — the cross-mechanism pressure test's lock-churn branch exercised a per-producer lock name (literal `"p{producer}"` had no interpolation, so all producers contended on one name) with its result swallowed via `.unwrap_or(0)`; now interpolates, releases periodically, propagates errors | `ops.rs` (pressure test, lock branch) | test-quality only (no lineage counterpart, no library code touched); `fork-provenance-and-floor`'s ported-surface pressure intent | ours |
| D-29 | port delta | `SQLITE_OPEN_URI` dropped from `open_conn`'s flags (the watcher's own opens never carried it — the inherited posture was internally inconsistent); the path argument is a plain filesystem path, `?name=value` suffixes are literal filenames, no connection-semantics mutation outside the engine constructor's opts; pinned by test (`open_conn_treats_uri_shaped_path_as_plain_filename`) | `schema.rs` (`open_conn`) | ADR-023 §3 (plain-path open — the URI parameter surface is SQLite's, not this engine's; no consumer-inventory row names URI features) | ours |
| D-30 | port delta | `open_conn_bootstrapped` — new function stacking the engine's full per-connection open posture (pragmas, notify, `attach_alkstore_functions`, `bootstrap_schema`), and `Readers::acquire`'s open path switched from bare `open_conn` to it (fresh connections only — the pool never re-bootstraps a reused connection). The lineage's pools opened readers pragmas+notify only, the function set + schema living only on the writer; this engine's reader-slot operations (claim/ack, stream reads, lock ops, scheduler checks, leadership probes) run this substrate's SQL functions on pooled connections, so every pooled connection carries the full surface | `schema.rs` (`open_conn_bootstrapped`, `Readers::acquire` open path) | engine-sqlite.md's connection architecture ("each connection runs the substrate's bootstrap at open — the writer and each pooled reader") — engine-layer obligation of `sqlite-engine-open-opts`; landed with the engine's `open` wiring | ours |
### Cherry-picks
+5 -2
View File
@@ -53,5 +53,8 @@ mod queue_ops;
mod schema;
mod watcher;
pub(crate) use schema::{attach_notify, bootstrap_schema, open_conn};
pub(crate) use watcher::SharedUpdateWatcher;
pub(crate) use schema::{
Error as SchemaError, Readers, Writer, apply_default_pragmas, attach_notify, bootstrap_schema,
open_conn, open_conn_bootstrapped,
};
pub(crate) use watcher::{DEFAULT_WATCHER_POLL_INTERVAL, SharedUpdateWatcher, WatcherConfig};
+71 -1
View File
@@ -324,6 +324,27 @@ pub(crate) fn open_conn(path: &str, install_notify: bool) -> Result<Connection,
Ok(conn)
}
/// Open a connection carrying the engine's full per-connection
/// bootstrap: pragmas, notify machinery, this substrate's function
/// set, and schema bootstrap. The engine's `open` constructor runs
/// this on the writer and each pooled reader (engine-sqlite.md's
/// connection architecture: "each connection runs the substrate's
/// bootstrap at open").
///
/// D-30: the lineage's pools open readers through bare `open_conn`
/// (pragmas + notify only), the function set + schema living only on
/// the writer. This substrate extends the pool open path with
/// `attach_alkstore_functions` + `bootstrap_schema` — the engine's
/// reader-slot operations (claim/ack, stream reads, lock ops,
/// scheduler checks) run this substrate's SQL functions on pooled
/// connections, so every pooled connection must carry them.
pub(crate) fn open_conn_bootstrapped(path: &str) -> Result<Connection, Error> {
let conn = open_conn(path, true)?;
super::ops::attach_alkstore_functions(&conn)?;
bootstrap_schema(&conn)?;
Ok(conn)
}
pub(crate) struct Writer {
slot: Mutex<Option<Connection>>,
available: Condvar,
@@ -411,7 +432,7 @@ impl Readers {
*out += 1;
drop(out);
drop(pool);
match open_conn(&self.path, false) {
match open_conn_bootstrapped(&self.path) {
Ok(conn) => {
if self.closed.load(Ordering::Acquire) {
*self.outstanding.lock() -= 1;
@@ -577,6 +598,55 @@ mod writer_reader_tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn open_conn_bootstrapped_carries_the_full_surface_on_every_connection() {
let dir = std::env::temp_dir().join(format!(
"alkstore-bootstrapped-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("bootstrapped.db");
let first = open_conn_bootstrapped(path.to_str().unwrap()).unwrap();
drop(first);
for i in 0..3 {
let conn = open_conn_bootstrapped(path.to_str().unwrap())
.unwrap_or_else(|e| panic!("bootstrapped open {i} failed: {e}"));
let mode: String = conn
.pragma_query_value(None, "journal_mode", |r| r.get(0))
.unwrap();
assert!(mode.eq_ignore_ascii_case("wal"), "open {i}: not WAL");
let notify_fn: i64 = conn
.query_row("SELECT notify('__alkstore_probe', 'x')", [], |r| r.get(0))
.unwrap_or(0);
assert!(notify_fn >= 1, "open {i}: notify function missing");
let tables: i64 = conn
.query_row(
"SELECT count(*) FROM sqlite_master
WHERE type = 'table' AND name LIKE '__alkstore_%'",
[],
|r| r.get(0),
)
.unwrap();
assert!(
tables >= 7,
"open {i}: bootstrap schema missing (saw {tables} tables)"
);
let publish_ready: i64 = conn
.query_row(
"SELECT alkstore_stream_publish('__alkstore_probe', NULL, '{}')",
[],
|r| r.get(0),
)
.unwrap_or(0);
assert!(publish_ready >= 1, "open {i}: function set missing");
drop(conn);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn concurrent_opens_never_return_database_is_locked() {
const ROUNDS: usize = 8;