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)
58 lines
2.6 KiB
Rust
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)
|
|
}
|