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

+220
View File
@@ -0,0 +1,220 @@
//! The SQLite engine's store: the `open` constructor, the connection
//! architecture (engine-sqlite.md's "Connection architecture"), and
//! the [`Store`] trait wiring.
//!
//! Connection architecture: one writer connection behind the
//! substrate's `Writer` slot (WAL single-writer modeled honestly), a
//! pool of reader connections (fresh opens on demand up to
//! [`SqliteOpts::max_readers`]), and one `SharedUpdateWatcher` poll
//! thread over the db path. Every connection — writer and pooled
//! readers alike — runs the substrate's full bootstrap at open:
//! pragmas (WAL, `synchronous=NORMAL`, busy timeout), the notify
//! machinery, the substrate's SQL function set, and the schema
//! bootstrap.
//!
//! Watcher spawn is fallible in the substrate (ADR-012 §4's W-2); the
//! failure maps into [`Error::Database`](alkstore::Error::Database)
//! engine-side and `open` fails if the watcher cannot start. On
//! watcher death the substrate's `WatcherDeathGuard` closes every
//! subscriber (ADR-006) — the death-guard plumbing exists from open
//! onward; the consumer-visible receiver bridging arrives with
//! `listen`.
//!
//! Drop/close: the watcher's poll thread is joined (releasing its
//! connection and the db file handle) and the writer slot + reader
//! pool are closed; [`Drop`](SqliteStore::drop) delegates to
//! [`SqliteStore::close`].
//!
//! Every engine call round-trips the `spawn_blocking` seam
//! ([`crate::seam`]); rusqlite connections are not
//! `Send`-across-await. Single-host by nature: file-backed, one
//! machine; NFS two-writers-unsupported (the lineage's honesty
//! posture, inherited — ADR-016).
use alkstore::Store;
use std::path::PathBuf;
use std::sync::Arc;
use crate::opts::SqliteOpts;
use crate::seam::{database_error, sqlite_error};
use crate::substrate::{Readers, SharedUpdateWatcher, Writer};
/// The SQLite engine's store handle. Consumers hold
/// [`alkstore::Store`] — the trait is the contract surface; the type
/// exists so the engine can own its machinery and so tests can reach
/// the close path directly.
pub struct SqliteStore {
writer: Arc<Writer>,
readers: Arc<Readers>,
watcher: Arc<SharedUpdateWatcher>,
db_path: PathBuf,
}
impl std::fmt::Debug for SqliteStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SqliteStore")
.field("db_path", &self.db_path)
.finish_non_exhaustive()
}
}
impl SqliteStore {
/// Explicit close: stops the watcher, disconnects every subscriber
/// (the death-guard close — receivers see them close), closes the
/// reader pool, and closes the writer slot. Idempotent. [`Drop`]
/// delegates here.
pub fn close(&self) {
let _ = self.watcher.close();
self.readers.close();
self.writer.close();
}
}
impl Drop for SqliteStore {
fn drop(&mut self) {
self.close();
}
}
/// Open a SQLite-backed store at `path`.
///
/// The path is a **plain filesystem path** (ADR-023 §3): no URI
/// interpretation — a `?name=value` suffix is part of the literal
/// filename (`:memory:` works without URI mode). The full connection
/// architecture boots: the writer slot, the reader pool, and the
/// update watcher (whose spawn is fallible — open fails with
/// [`Error::Database`](alkstore::Error::Database) if it cannot start).
pub fn open(path: &str, opts: SqliteOpts) -> alkstore::Result<Box<dyn Store>> {
Ok(Box::new(open_store(path, opts)?))
}
fn open_store(path: &str, opts: SqliteOpts) -> alkstore::Result<SqliteStore> {
let config = opts.watcher_config()?;
let writer_conn = crate::substrate::open_conn_bootstrapped(path)
.map_err(|e| match e {
crate::substrate::SchemaError::Sqlite(inner) => sqlite_error(inner),
})
.map_err(|e| database_error(format!("failed to open/bootstrap writer connection: {e}")))?;
let watcher = SharedUpdateWatcher::new_with_config(PathBuf::from(path), config)
.map_err(|e| database_error(format!("failed to spawn update watcher: {e}")))?;
Ok(SqliteStore {
writer: Arc::new(Writer::new(writer_conn)),
readers: Arc::new(Readers::new(path.to_string(), opts.max_readers)),
watcher: Arc::new(watcher),
db_path: PathBuf::from(path),
})
}
impl Store for SqliteStore {
fn begin_tx(
&self,
) -> alkstore::BoxedFuture<'_, alkstore::Result<Box<dyn alkstore::TxHandle + Send>>> {
Box::pin(async { Err(database_error("begin_tx wiring lands with the seam task")) })
}
fn notify<'a>(
&'a self,
_channel: &str,
_payload: serde_json::Value,
) -> alkstore::BoxedFuture<'a, alkstore::Result<()>> {
Box::pin(async {
Err(database_error(
"notify wiring lands with the mechanism tasks",
))
})
}
fn listen<'a>(
&'a self,
_channel: &str,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::WakeReceiver>>> {
Box::pin(async {
Err(database_error(
"listen wiring lands with the mechanism tasks",
))
})
}
fn stream<'a>(
&'a self,
_name: &str,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::StreamHandle>>> {
Box::pin(async {
Err(database_error(
"stream wiring lands with the mechanism tasks",
))
})
}
fn queue<'a>(
&'a self,
_name: &str,
_opts: alkstore::QueueOpts,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::Queue>>> {
Box::pin(async {
Err(database_error(
"queue wiring lands with the mechanism tasks",
))
})
}
fn outbox<'a>(
&'a self,
_name: &str,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Box<dyn alkstore::Outbox>>> {
Box::pin(async {
Err(database_error(
"outbox wiring lands with the mechanism tasks",
))
})
}
fn try_lock<'a>(
&'a self,
_name: &str,
_owner: &str,
_ttl: i64,
) -> alkstore::BoxedFuture<'a, alkstore::Result<Option<Box<dyn alkstore::Lock>>>> {
Box::pin(async { Err(database_error("lock wiring lands with the mechanism tasks")) })
}
fn schedule<'a>(
&'a self,
_name: &str,
_spec: &str,
_queue: &str,
_payload: serde_json::Value,
_opts: alkstore::ScheduleOpts,
) -> alkstore::BoxedFuture<'a, alkstore::Result<alkstore::Schedule>> {
Box::pin(async {
Err(database_error(
"schedule wiring lands with the mechanism tasks",
))
})
}
fn unschedule<'a>(&'a self, _name: &str) -> alkstore::BoxedFuture<'a, alkstore::Result<bool>> {
Box::pin(async {
Err(database_error(
"unschedule wiring lands with the mechanism tasks",
))
})
}
fn run_schedules<'a>(
&'a self,
_stop: alkstore::StopToken,
) -> alkstore::BoxedFuture<'a, alkstore::Result<()>> {
Box::pin(async {
Err(database_error(
"scheduler wiring lands with the mechanism tasks",
))
})
}
}
#[cfg(test)]
mod open_tests;