//! Shared sqlite connection setup for hive-c0re's host-side stores. //! //! Several modules keep their own tables — and their own //! `Mutex` — in the coordinator DB //! (`db/broker.sqlite`: broker, approvals, operator questions, //! scheduled prompts, agent power) or in a sibling file under the same //! `db/` dir (`build_logs.sqlite`, `audit_log.sqlite`). The open dance //! is identical everywhere: ensure the parent dir exists, open the //! connection, set a busy timeout so concurrent same-process writers //! wait each other out instead of surfacing `SQLITE_BUSY`. This helper //! owns that dance; schema creation + column migrations stay with each //! store (they're per-table concerns). use std::path::Path; use std::time::Duration; use anyhow::{Context, Result}; use rusqlite::Connection; /// How long a write waits on another connection's lock before erroring. /// Generous relative to the stores' tiny transactions — a timeout here /// means something is genuinely wedged, not ordinary contention. const BUSY_TIMEOUT: Duration = Duration::from_secs(5); /// Apply additive, idempotent migrations: run each statement and /// ignore `duplicate column name` errors (sqlite has no /// `ADD COLUMN IF NOT EXISTS`; try-and-ignore is the portable path — /// same pattern as hive-ag3nt's `turn_stats`). Any other error /// propagates. Fits plain `ALTER TABLE ADD COLUMN` (new columns must /// carry a default or tolerate NULL) and `IF NOT EXISTS` index DDL; /// migrations with creation-conditional backfills (broker's /// `acked_at`) stay bespoke in their store. pub fn apply_migrations(conn: &Connection, subsystem: &str, statements: &[&str]) -> Result<()> { for stmt in statements { if let Err(e) = conn.execute_batch(stmt) { if e.to_string().contains("duplicate column name") { continue; } return Err(e).with_context(|| format!("{subsystem} migration failed: {stmt}")); } } Ok(()) } /// Open a connection to the sqlite file at `path`, creating the parent /// directory if needed. `subsystem` labels error contexts (`"broker"`, /// `"approvals"`, …). pub fn open(path: &Path, subsystem: &str) -> Result { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("create {subsystem} db parent {}", parent.display()))?; } let conn = Connection::open(path) .with_context(|| format!("open {subsystem} db {}", path.display()))?; conn.busy_timeout(BUSY_TIMEOUT) .with_context(|| format!("set {subsystem} busy_timeout"))?; Ok(conn) }