use rusqlite::{Connection, Result as SqliteResult};
pub struct Migration {
pub version: u32,
pub description: &'static str,
pub requires_rebuild: bool,
pub up: fn(&Connection) -> SqliteResult<()>,
}
pub fn all_migrations() -> Vec<Migration> {
vec![
Migration {
version: 3,
description: "Add argument, evidence, author columns to critiques",
requires_rebuild: false,
up: |conn| {
conn.execute_batch(
"ALTER TABLE critiques ADD COLUMN argument TEXT DEFAULT '';
ALTER TABLE critiques ADD COLUMN evidence TEXT DEFAULT '';
ALTER TABLE critiques ADD COLUMN author TEXT;",
)?;
let mut stmt = conn.prepare("SELECT id, body FROM critiques WHERE body != ''")?;
let rows: Vec<(String, String)> = stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
.collect::<Result<_, _>>()?;
for (id, body) in rows {
let separator = "\n\n## Evidence\n\n";
let (argument, evidence) = if let Some(idx) = body.find(separator) {
(
body[..idx].to_string(),
body[idx + separator.len()..].to_string(),
)
} else {
(body, String::new())
};
conn.execute(
"UPDATE critiques SET argument = ?1, evidence = ?2 WHERE id = ?3",
rusqlite::params![argument, evidence, id],
)?;
}
conn.execute_batch(
"UPDATE critiques SET author = reviewer WHERE author IS NULL AND reviewer IS NOT NULL;",
)?;
Ok(())
},
},
Migration {
version: 4,
description: "Recreate FTS table without contentless mode (enables SELECT from FTS)",
requires_rebuild: true,
up: |_conn| Ok(()),
},
Migration {
version: 5,
description: "Add GitHub sync columns",
requires_rebuild: false,
up: |conn| {
conn.execute_batch(
"ALTER TABLE problems ADD COLUMN github_issue INTEGER;
ALTER TABLE solutions ADD COLUMN github_pr INTEGER;
ALTER TABLE solutions ADD COLUMN github_branch TEXT;
ALTER TABLE critiques ADD COLUMN github_review_id INTEGER;
CREATE INDEX IF NOT EXISTS idx_problems_github_issue ON problems(github_issue);
CREATE INDEX IF NOT EXISTS idx_solutions_github_pr ON solutions(github_pr);
CREATE INDEX IF NOT EXISTS idx_critiques_github_review_id ON critiques(github_review_id);"
)?;
Ok(())
},
},
Migration {
version: 7,
description: "Add tags column to problems and solutions",
requires_rebuild: false,
up: |conn| {
conn.execute_batch(
"ALTER TABLE problems ADD COLUMN tags TEXT DEFAULT '[]';
ALTER TABLE solutions ADD COLUMN tags TEXT DEFAULT '[]';",
)?;
Ok(())
},
},
Migration {
version: 8,
description: "Add confidence column to problems",
requires_rebuild: false,
up: |conn| {
conn.execute_batch(
"ALTER TABLE problems ADD COLUMN confidence TEXT NOT NULL DEFAULT 'unknown';",
)?;
Ok(())
},
},
Migration {
version: 9,
description: "Remove dead columns: problems.context, solutions.tradeoffs, critiques.body, critiques.evidence",
requires_rebuild: true,
up: |_conn| Ok(()),
},
]
}
pub fn run_migrations(conn: &Connection, current_version: u32) -> SqliteResult<bool> {
let migrations = all_migrations();
for migration in &migrations {
if migration.version <= current_version {
continue;
}
if migration.requires_rebuild {
return Ok(false);
}
(migration.up)(conn)?;
conn.execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?1)",
[migration.version.to_string()],
)?;
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::Database;
#[test]
fn test_no_migrations_needed() {
let db = Database::open_in_memory().expect("Failed to open database");
let conn = db.conn();
let result = run_migrations(conn, crate::db::SCHEMA_VERSION).unwrap();
assert!(result);
}
#[test]
fn test_migrations_registered() {
let migrations = all_migrations();
assert!(!migrations.is_empty());
assert_eq!(migrations[0].version, 3);
assert!(!migrations[0].requires_rebuild);
}
}