//! SQLite schema migrations for the MNote control-plane database. //! //! Each migration is a named step that runs inside a transaction. //! The `migrations` table tracks which steps have been applied. use rusqlite::{Connection, Result as SqlResult}; const MIGRATIONS: &[(&str, &str)] = &[ ( "v1-initial-schema", include_str!("../migrations/001-initial-schema.sql"), ), ( "v2-ai-runtime-store", include_str!("../migrations/002-ai-runtime-store.sql"), ), ]; /// Create the `_migrations` meta-table if it does not exist. fn ensure_meta_table(conn: &Connection) -> SqlResult<()> { conn.execute_batch( "CREATE TABLE IF NOT EXISTS _migrations ( name TEXT PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')) );", ) } /// Run all pending migrations inside a single transaction. pub fn run_migrations(conn: &Connection) -> SqlResult<()> { ensure_meta_table(conn)?; let already_applied: Vec = conn .prepare("SELECT name FROM _migrations")? .query_map([], |row| row.get(0))? .filter_map(|r| r.ok()) .collect(); for (name, sql) in MIGRATIONS { if already_applied.contains(&name.to_string()) { continue; } tracing::info!("Running migration: {name}"); conn.execute_batch(sql)?; conn.execute("INSERT INTO _migrations (name) VALUES (?1)", [name])?; } Ok(()) } #[cfg(test)] mod tests { use super::*; /// Create a fresh in-memory SQLite database and run migrations. fn fresh_db() -> Connection { let conn = Connection::open_in_memory().expect("open in-memory db"); conn.execute_batch( "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA foreign_keys=ON;", ) .expect("pragmas"); conn } fn table_exists(conn: &Connection, name: &str) -> bool { conn.query_row( "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?1", [name], |row| row.get::<_, i64>(0), ) .unwrap_or(0) > 0 } #[test] fn migration_is_idempotent() { let conn = fresh_db(); run_migrations(&conn).expect("first run"); run_migrations(&conn).expect("second run (idempotent)"); // All expected tables exist for table in &[ "users", "auth_identities", "auth_sessions", "workspaces", "workspace_members", "directory_grants", "share_links", "sync_state", "ai_policies", "audit_log", "outbox_events", "legacy_id_map", "ai_runtime_runs", "ai_runtime_events", ] { assert!(table_exists(&conn, table), "table {table} should exist"); } } #[test] fn migration_runs_fresh_db_ok() { let conn = fresh_db(); run_migrations(&conn).expect("fresh migration ok"); assert!(table_exists(&conn, "users")); assert!(table_exists(&conn, "_migrations")); } }