Files
alkstore/alkstore-sqlite/src/substrate/schema.rs
T
glm-5.3-flash 22f25ffafd sqlite: panic-safety armor for the auto-commit seams (review 004 item 1)
with_writer/with_reader/blocking_read release their slot/pool
connection only on the normal return — a panic inside the closure
wedged the single writer slot forever (Writer::acquire waits on a
condvar) and permanently shrank the reader pool (outstanding never
decremented on abandonment). The seams now ride RAII drop-guards
(ConnLease's idiom): the normal arm defuses and releases unchanged;
the unwind arm drops the unknown-state connection and takes the
established abandonment dispositions — Writer::replenish() (the tick's
poisoned arm as a substrate method, the writer now holding its path)
and Readers::abandon() (outstanding decrement + pool-locked notify).
Three panic-injection tests pin the properties; no reachable panic
path exists in production — this is the defense-in-depth armor the
review scoped. Task: sqlite-fix-seam-panic-safety.
2026-10-11 09:55:45 +00:00

722 lines
25 KiB
Rust

//! Connection open posture, notify machinery, bootstrap, and the
//! writer/reader connection architecture — ported from honker-core
//! `lib.rs` @ `f4e53c6` (upstream names preserved; the table family
//! renamed `_honker_*` → `__alkstore_*` per ADR-011; the bootstrap's
//! duplicate-column race swallow re-keyed to `pragma_table_info`
//! per ADR-012 §5; the at-attach notifications pruning cap per
//! ADR-010 §6 / register D-16).
use parking_lot::{Condvar, Mutex};
use rusqlite::functions::FunctionFlags;
use rusqlite::{Connection, OpenFlags};
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(thiserror::Error, Debug)]
pub(crate) enum Error {
#[error("Database error: {0}")]
Sqlite(#[from] rusqlite::Error),
}
pub(crate) const DEFAULT_PRAGMAS: &str = "PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA cache_size = -32000;
PRAGMA temp_store = MEMORY;
PRAGMA wal_autocheckpoint = 10000;";
pub(crate) fn apply_default_pragmas(conn: &Connection) -> rusqlite::Result<()> {
conn.execute_batch("PRAGMA busy_timeout = 5000;")?;
set_journal_mode_wal(conn)?;
conn.execute_batch(DEFAULT_PRAGMAS)
}
fn set_journal_mode_wal(conn: &Connection) -> rusqlite::Result<()> {
let already_wal = |conn: &Connection| -> rusqlite::Result<bool> {
let mode: String = conn.query_row("PRAGMA journal_mode", [], |row| row.get(0))?;
Ok(mode.eq_ignore_ascii_case("wal"))
};
if already_wal(conn)? {
return Ok(());
}
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(5000);
let mut backoff = std::time::Duration::from_millis(1);
loop {
match conn.execute_batch("PRAGMA journal_mode = WAL;") {
Ok(()) => return Ok(()),
Err(e) => {
if already_wal(conn).unwrap_or(false) {
return Ok(());
}
if std::time::Instant::now() >= deadline {
return Err(e);
}
std::thread::sleep(backoff);
backoff = (backoff * 2).min(std::time::Duration::from_millis(50));
}
}
}
}
/// Upper bound on `__alkstore_notifications` rows before the next
/// attach prunes oldest-first past it (register D-16 — ADR-010 §6's
/// at-attach cap; engine-internal hygiene, not consumer-visible).
///
/// Not upstream. Upstream's `attach_notify` never prunes ("no magic
/// timer" — callers invoke `prune_notifications` explicitly); this
/// substrate's postures pins hygiene at attach so the engine grows
/// nothing unbounded by deferring to a caller who never prunes.
/// Rows are only trimmed while their senders' transactions are
/// committed — pruning runs on a fresh connection before its
/// `notify()` function is registered, so no in-flight wake row can
/// race the DELETE.
pub(crate) const NOTIFY_ATTACH_MAX_ROWS: i64 = 10_000;
/// Install the `__alkstore_notifications` table and the
/// `notify(channel, payload)` SQL scalar function on `conn`.
/// Idempotent.
pub(crate) fn attach_notify(conn: &Connection) -> Result<(), Error> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS __alkstore_notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel TEXT NOT NULL,
payload TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX IF NOT EXISTS __alkstore_notifications_recent
ON __alkstore_notifications(channel, id);",
)?;
conn.execute(
"DELETE FROM __alkstore_notifications
WHERE id IN (
SELECT id FROM __alkstore_notifications
ORDER BY id DESC
LIMIT -1 OFFSET ?1
)",
[NOTIFY_ATTACH_MAX_ROWS],
)?;
conn.create_scalar_function("notify", 2, FunctionFlags::SQLITE_UTF8, |ctx| {
let channel: String = ctx.get(0)?;
let payload: String = ctx.get(1)?;
let db = unsafe { ctx.get_connection() }?;
let mut ins = db.prepare_cached(
"INSERT INTO __alkstore_notifications (channel, payload) VALUES (?1, ?2)",
)?;
let id = ins.insert(rusqlite::params![channel, payload])?;
Ok(id)
})?;
Ok(())
}
pub(crate) const BOOTSTRAP_ALKSTORE_SQL: &str = "
CREATE TABLE IF NOT EXISTS __alkstore_live (
id INTEGER PRIMARY KEY AUTOINCREMENT,
queue TEXT NOT NULL,
payload TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending',
priority INTEGER NOT NULL DEFAULT 0,
run_at INTEGER NOT NULL DEFAULT (unixepoch()),
worker_id TEXT,
claim_expires_at INTEGER,
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 3,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
expires_at INTEGER,
claimed_at INTEGER,
visibility_timeout_s INTEGER NOT NULL DEFAULT 300,
backoff_base_s INTEGER NOT NULL DEFAULT 5,
dead_letter_retention_s INTEGER
);
CREATE INDEX IF NOT EXISTS __alkstore_live_claim
ON __alkstore_live(queue, priority DESC, run_at, id)
WHERE state IN ('pending', 'processing');
CREATE INDEX IF NOT EXISTS __alkstore_live_pending_deadline
ON __alkstore_live(queue, run_at)
WHERE state = 'pending';
CREATE INDEX IF NOT EXISTS __alkstore_live_processing_deadline
ON __alkstore_live(queue, claim_expires_at)
WHERE state = 'processing';
CREATE TABLE IF NOT EXISTS __alkstore_dead (
id INTEGER PRIMARY KEY,
queue TEXT NOT NULL,
payload TEXT NOT NULL,
priority INTEGER NOT NULL DEFAULT 0,
run_at INTEGER NOT NULL DEFAULT 0,
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
died_at INTEGER NOT NULL DEFAULT (unixepoch()),
visibility_timeout_s INTEGER NOT NULL DEFAULT 300,
backoff_base_s INTEGER NOT NULL DEFAULT 5,
dead_letter_retention_s INTEGER,
claimed_at INTEGER,
expires_at INTEGER
);
CREATE INDEX IF NOT EXISTS __alkstore_dead_retention
ON __alkstore_dead(queue, died_at);
CREATE TABLE IF NOT EXISTS __alkstore_locks (
name TEXT PRIMARY KEY,
owner TEXT NOT NULL,
expires_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS __alkstore_scheduler_tasks (
name TEXT PRIMARY KEY,
queue TEXT NOT NULL,
spec TEXT NOT NULL,
payload TEXT NOT NULL,
priority INTEGER NOT NULL DEFAULT 0,
expires_s INTEGER,
next_fire_at INTEGER NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
max_attempts INTEGER NOT NULL DEFAULT 3,
visibility_timeout_s INTEGER NOT NULL DEFAULT 300,
backoff_base_s INTEGER NOT NULL DEFAULT 5,
dead_letter_retention_s INTEGER
);
CREATE TABLE IF NOT EXISTS __alkstore_stream (
offset INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL,
key TEXT,
payload TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX IF NOT EXISTS __alkstore_stream_topic
ON __alkstore_stream(topic, offset);
CREATE TABLE IF NOT EXISTS __alkstore_stream_consumers (
name TEXT NOT NULL,
topic TEXT NOT NULL,
offset INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (name, topic)
);
";
fn column_present(conn: &Connection, table: &str, column: &str) -> bool {
let mut stmt = match conn.prepare(&format!(
"SELECT 1 FROM pragma_table_info('{table}') WHERE name = ?1"
)) {
Ok(stmt) => stmt,
Err(_) => return false,
};
stmt.query_row([column], |_| Ok(true)).unwrap_or(false)
}
fn add_column_if_absent(
conn: &Connection,
table: &str,
column: &str,
ddl: &str,
) -> Result<(), Error> {
if column_present(conn, table, column) {
return Ok(());
}
match conn.execute(&format!("ALTER TABLE {table} ADD COLUMN {ddl}"), []) {
Ok(_) => Ok(()),
Err(e) => {
if column_present(conn, table, column) {
Ok(())
} else {
Err(e.into())
}
}
}
}
/// Install the alkstore queue schema on `conn`. Idempotent.
///
/// Fresh bootstrap only (ADR-012 §5): a file carrying honker's legacy
/// `_honker_*` tables gets inert orphans — never touched, never read,
/// never migrated. The migrations below exist for the same-schema
/// case: a database this bootstrap already created in an earlier
/// revision, missing a column appended since.
pub(crate) fn bootstrap_schema(conn: &Connection) -> Result<(), Error> {
conn.execute_batch(BOOTSTRAP_ALKSTORE_SQL)?;
add_column_if_absent(
conn,
"__alkstore_scheduler_tasks",
"enabled",
"enabled INTEGER NOT NULL DEFAULT 1",
)?;
add_column_if_absent(
conn,
"__alkstore_scheduler_tasks",
"max_attempts",
"max_attempts INTEGER NOT NULL DEFAULT 3",
)?;
add_column_if_absent(
conn,
"__alkstore_scheduler_tasks",
"visibility_timeout_s",
"visibility_timeout_s INTEGER NOT NULL DEFAULT 300",
)?;
add_column_if_absent(
conn,
"__alkstore_scheduler_tasks",
"backoff_base_s",
"backoff_base_s INTEGER NOT NULL DEFAULT 5",
)?;
add_column_if_absent(
conn,
"__alkstore_scheduler_tasks",
"dead_letter_retention_s",
"dead_letter_retention_s INTEGER",
)?;
add_column_if_absent(conn, "__alkstore_live", "claimed_at", "claimed_at INTEGER")?;
add_column_if_absent(
conn,
"__alkstore_live",
"visibility_timeout_s",
"visibility_timeout_s INTEGER NOT NULL DEFAULT 300",
)?;
add_column_if_absent(
conn,
"__alkstore_live",
"backoff_base_s",
"backoff_base_s INTEGER NOT NULL DEFAULT 5",
)?;
add_column_if_absent(
conn,
"__alkstore_live",
"dead_letter_retention_s",
"dead_letter_retention_s INTEGER",
)?;
add_column_if_absent(
conn,
"__alkstore_dead",
"visibility_timeout_s",
"visibility_timeout_s INTEGER NOT NULL DEFAULT 300",
)?;
add_column_if_absent(
conn,
"__alkstore_dead",
"backoff_base_s",
"backoff_base_s INTEGER NOT NULL DEFAULT 5",
)?;
add_column_if_absent(
conn,
"__alkstore_dead",
"dead_letter_retention_s",
"dead_letter_retention_s INTEGER",
)?;
add_column_if_absent(conn, "__alkstore_dead", "claimed_at", "claimed_at INTEGER")?;
add_column_if_absent(conn, "__alkstore_dead", "expires_at", "expires_at INTEGER")?;
Ok(())
}
pub(crate) fn open_conn(path: &str, install_notify: bool) -> Result<Connection, Error> {
// No SQLITE_OPEN_URI (ADR-023 §3 delta): the path argument is a
// plain filesystem path — URI-interpreted `?name=value` suffixes
// would mutate connection semantics outside the engine
// constructor's opts.
let conn = Connection::open_with_flags(
path,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
)?;
apply_default_pragmas(&conn)?;
if install_notify {
attach_notify(&conn)?;
}
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,
closed: AtomicBool,
path: String,
}
impl Writer {
pub(crate) fn new(path: String, conn: Connection) -> Self {
Self {
slot: Mutex::new(Some(conn)),
available: Condvar::new(),
closed: AtomicBool::new(false),
path,
}
}
/// The abandonment arm (the auto-commit seam's panic-safety armor,
/// review 004 item 1): a connection that cannot return to the slot
/// — a panicked op leaves its statement state unknown — is dropped,
/// not released, and the slot is replenished with a fresh
/// bootstrapped open from the writer's own path. The release's slot
/// notify re-opens the waiters. A failed replenish leaves the slot
/// empty (the established arms' tolerance — the tick's
/// poisoned-disposition arm and the tx seam's `ConnLease` drop arm
/// both take `if let Ok(fresh)`), with the failure printed for
/// diagnosability.
pub(crate) fn replenish(&self) {
if self.closed.load(Ordering::Acquire) {
return;
}
match open_conn_bootstrapped(&self.path) {
Ok(fresh) => self.release(fresh),
Err(e) => eprintln!(
"alkstore: error: writer slot replenish after an abandoned \
operation failed to open a fresh connection: {e}"
),
}
}
pub(crate) fn acquire(&self) -> Option<Connection> {
let mut guard = self.slot.lock();
loop {
if self.closed.load(Ordering::Acquire) {
return None;
}
if let Some(c) = guard.take() {
return Some(c);
}
self.available.wait(&mut guard);
}
}
pub(crate) fn release(&self, conn: Connection) {
if self.closed.load(Ordering::Acquire) {
return;
}
let mut guard = self.slot.lock();
*guard = Some(conn);
self.available.notify_one();
}
pub(crate) fn close(&self) {
self.closed.store(true, Ordering::Release);
let mut guard = self.slot.lock();
guard.take();
self.available.notify_all();
}
}
pub(crate) struct Readers {
pool: Mutex<Vec<Connection>>,
outstanding: Mutex<usize>,
available: Condvar,
path: String,
max: usize,
closed: AtomicBool,
}
impl Readers {
pub(crate) fn new(path: String, max: usize) -> Self {
Self {
pool: Mutex::new(Vec::new()),
outstanding: Mutex::new(0),
available: Condvar::new(),
path,
max: max.max(1),
closed: AtomicBool::new(false),
}
}
pub(crate) fn acquire(&self) -> Result<Connection, Error> {
loop {
if self.closed.load(Ordering::Acquire) {
return Err(closed_err());
}
let mut pool = self.pool.lock();
if let Some(c) = pool.pop() {
return Ok(c);
}
let mut out = self.outstanding.lock();
if *out < self.max {
*out += 1;
drop(out);
drop(pool);
match open_conn_bootstrapped(&self.path) {
Ok(conn) => {
if self.closed.load(Ordering::Acquire) {
*self.outstanding.lock() -= 1;
drop(conn);
return Err(closed_err());
}
return Ok(conn);
}
Err(e) => {
*self.outstanding.lock() -= 1;
return Err(e);
}
}
}
drop(out);
self.available.wait(&mut pool);
}
}
pub(crate) fn release(&self, conn: Connection) {
if self.closed.load(Ordering::Acquire) {
return;
}
let mut pool = self.pool.lock();
pool.push(conn);
self.available.notify_one();
}
/// The abandonment arm (the auto-commit seam's panic-safety
/// armor, review 004 item 1): the abandoned connection is dropped,
/// not released. `outstanding` counts connections in existence —
/// every connection carries its fresh-open increment for life, and
/// release/pop never decrement — so the abandoned connection's
/// decrement restores the pool's open-budget, and a waiter wakes
/// under the pool lock (the release's notify discipline) to
/// re-evaluate: pool miss + under `max` ⇒ fresh open.
pub(crate) fn abandon(&self) {
{
let mut out = self.outstanding.lock();
*out = out.saturating_sub(1);
}
let _pool = self.pool.lock();
self.available.notify_one();
}
pub(crate) fn close(&self) {
self.closed.store(true, Ordering::Release);
self.pool.lock().clear();
self.available.notify_all();
}
}
fn closed_err() -> Error {
Error::Sqlite(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_MISUSE),
Some("Database is closed".to_string()),
))
}
#[cfg(test)]
mod writer_reader_tests {
use super::*;
use std::sync::Arc;
fn mem() -> Connection {
Connection::open_in_memory().unwrap()
}
fn temp_db(name: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!(
"alkstore-{name}-{}-{:?}.db",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_file(&p);
let _ = std::fs::remove_file(format!("{}-wal", p.display()));
let _ = std::fs::remove_file(format!("{}-shm", p.display()));
p
}
#[test]
fn writer_close_drops_idle_connection() {
let w = Writer::new(":memory:".to_string(), mem());
w.close();
assert!(w.acquire().is_none());
}
#[test]
fn writer_close_drops_returned_connection() {
let w = Writer::new(":memory:".to_string(), mem());
let conn = w.acquire().unwrap();
w.close();
w.release(conn);
assert!(w.acquire().is_none());
}
#[test]
fn readers_close_returns_closed_err() {
let tmp = temp_db("readers-close");
let _ = std::fs::remove_file(&tmp);
let c = Connection::open(&tmp).unwrap();
c.execute_batch("PRAGMA journal_mode=WAL;").unwrap();
drop(c);
let r = Readers::new(tmp.to_string_lossy().into_owned(), 4);
let c = r.acquire().unwrap();
r.release(c);
r.close();
match r.acquire() {
Err(Error::Sqlite(rusqlite::Error::SqliteFailure(_, Some(msg)))) => {
assert!(msg.contains("Database is closed"));
}
other => panic!("expected closed err, got {other:?}"),
}
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn readers_open_failure_does_not_leak_capacity() {
let r = Arc::new(Readers::new(
"/this/parent/does/not/exist/alkstore-readers-leak.db".into(),
2,
));
for i in 0..5 {
let r = r.clone();
let handle = std::thread::spawn(move || r.acquire());
let result = handle.join().unwrap();
assert!(
result.is_err(),
"attempt {i}: expected open failure, got Ok"
);
}
}
#[test]
fn open_conn_applies_wal_and_default_pragmas() {
let tmp = temp_db("open-pragmas");
let conn = open_conn(tmp.to_str().unwrap(), false).unwrap();
let mode: String = conn
.pragma_query_value(None, "journal_mode", |r| r.get(0))
.unwrap();
assert!(mode.eq_ignore_ascii_case("wal"));
let busy: i64 = conn
.pragma_query_value(None, "busy_timeout", |r| r.get(0))
.unwrap();
assert_eq!(busy, 5000);
drop(conn);
let _ = std::fs::remove_file(&tmp);
let _ = std::fs::remove_file(format!("{}-wal", tmp.display()));
let _ = std::fs::remove_file(format!("{}-shm", tmp.display()));
}
#[test]
fn open_conn_treats_uri_shaped_path_as_plain_filename() {
// ADR-023 §3: no SQLITE_OPEN_URI — a `?name=value` suffix must
// be a literal filename (the file exists with that name), not
// URI interpretation (which for a bare-relative "file"-less
// string with query params would error or mutate semantics).
let dir = std::env::temp_dir().join(format!(
"alkstore-uri-literal-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("plain.db?mode=memory");
let conn = open_conn(path.to_str().unwrap(), false).unwrap();
conn.execute("CREATE TABLE t (x)", []).unwrap();
drop(conn);
let plain = dir.join("plain.db?mode=memory");
assert!(plain.exists(), "literal-named file must exist on disk");
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;
const OPENERS: usize = 16;
for round in 0..ROUNDS {
let dir = std::env::temp_dir().join(format!(
"alkstore-open-race-{}-{round}-{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("pressure.db");
let barrier = Arc::new(std::sync::Barrier::new(OPENERS));
let handles: Vec<_> = (0..OPENERS)
.map(|_| {
let path = path.clone();
let barrier = Arc::clone(&barrier);
std::thread::spawn(move || {
let conn = Connection::open(&path)?;
barrier.wait();
apply_default_pragmas(&conn)?;
let mode: String =
conn.query_row("PRAGMA journal_mode", [], |row| row.get(0))?;
assert!(
mode.eq_ignore_ascii_case("wal"),
"expected WAL after open, got {mode}"
);
Ok::<_, rusqlite::Error>(())
})
})
.collect();
let failures: Vec<String> = handles
.into_iter()
.filter_map(|h| h.join().unwrap().err().map(|e| e.to_string()))
.collect();
std::fs::remove_dir_all(&dir).ok();
assert!(
failures.is_empty(),
"round {round}: concurrent opens failed: {failures:?}"
);
}
}
}