refactor(hive-c0re): shared additive-migration helper in db

db::apply_migrations runs ALTER lists and ignores duplicate-column
errors (turn_stats' pattern); approvals, operator_questions, broker
reminders, and scheduled_prompts drop their hand-rolled
pragma_table_info guards. broker's acked_at migration stays bespoke —
its backfill must only run when the column was just created
This commit is contained in:
müde 2026-07-06 21:53:48 +02:00
commit d190420946
7 changed files with 71 additions and 132 deletions

View file

@ -33,41 +33,18 @@ CREATE INDEX IF NOT EXISTS idx_operator_questions_pending
ON operator_questions (id) WHERE answered_at IS NULL;
";
/// Add late-added columns to pre-existing databases. `ALTER TABLE
/// ADD COLUMN` has no `IF NOT EXISTS` form in sqlite, so we check
/// `pragma_table_info` first per column.
fn ensure_columns(conn: &Connection) -> Result<()> {
for (name, sql) in [
(
"multi",
"ALTER TABLE operator_questions ADD COLUMN multi INTEGER NOT NULL DEFAULT 0;",
),
(
"deadline_at",
"ALTER TABLE operator_questions ADD COLUMN deadline_at INTEGER;",
),
// `target` = recipient of the question. NULL = operator
// (back-compat default for rows written before agent-to-agent
// questions existed); a non-null agent name = peer-to-peer
// question. Dashboard's `pending()` filters on `target IS NULL`
// so peer questions never leak into the operator's queue.
(
"target",
"ALTER TABLE operator_questions ADD COLUMN target TEXT;",
),
] {
let has: bool = conn
.prepare(&format!(
"SELECT 1 FROM pragma_table_info('operator_questions') WHERE name = '{name}'"
))?
.exists([])?;
if !has {
conn.execute_batch(sql)
.with_context(|| format!("add operator_questions.{name} column"))?;
}
}
Ok(())
}
/// Additive column migrations for pre-existing databases, applied via
/// `db::apply_migrations` (try-and-ignore-duplicate-column).
const MIGRATIONS: &[&str] = &[
"ALTER TABLE operator_questions ADD COLUMN multi INTEGER NOT NULL DEFAULT 0",
"ALTER TABLE operator_questions ADD COLUMN deadline_at INTEGER",
// `target` = recipient of the question. NULL = operator
// (back-compat default for rows written before agent-to-agent
// questions existed); a non-null agent name = peer-to-peer
// question. Dashboard's `pending()` filters on `target IS NULL`
// so peer questions never leak into the operator's queue.
"ALTER TABLE operator_questions ADD COLUMN target TEXT",
];
#[derive(Debug, Clone, Serialize)]
pub struct OpQuestion {
@ -100,7 +77,7 @@ impl OperatorQuestions {
let conn = crate::db::open(path, "operator_questions")?;
conn.execute_batch(SCHEMA)
.context("apply operator_questions schema")?;
ensure_columns(&conn).context("migrate operator_questions columns")?;
crate::db::apply_migrations(&conn, "operator_questions", MIGRATIONS)?;
Ok(Self {
conn: Mutex::new(conn),
})