hyperhive/hive-c0re/src/stores/db.rs
müde 0e4b5a1120 refactor(hive-c0re): group src-root files into submodules
stores/ (sqlite-backed host stores + db helper), stats/, agent_config/,
workers/ — pure git-mv moves; crate-root re-exports keep every
crate::<module> path compiling. flake_check stays at root (synchronous
approval-flow validation, not a background worker)
2026-07-06 22:38:47 +02:00

58 lines
2.6 KiB
Rust

//! Shared sqlite connection setup for hive-c0re's host-side stores.
//!
//! Several modules keep their own tables — and their own
//! `Mutex<Connection>` — 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<Connection> {
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)
}