migrations.rs (3045B)
1 use rusqlite::Connection; 2 3 const MIGRATIONS: &[&str] = &[ 4 include_str!("../migrations/0001_initial_schema.sql"), 5 include_str!("../migrations/0002_email_verification.sql"), 6 include_str!("../migrations/0003_strict_tables.sql"), 7 include_str!("../migrations/0004_sessions.sql"), 8 include_str!("../migrations/0005_epoch_timestamps.sql"), 9 include_str!("../migrations/0006_fts_vocab.sql"), 10 include_str!("../migrations/0007_structured_pks.sql"), 11 ]; 12 13 pub fn run(conn: &Connection) -> Result<(), String> { 14 conn.execute_batch( 15 "CREATE TABLE IF NOT EXISTS migration_version (version INTEGER NOT NULL);", 16 ) 17 .map_err(|e| format!("Failed to create migration_version table: {e}"))?; 18 19 // Seed version row if missing 20 let count: i64 = conn 21 .query_row("SELECT COUNT(*) FROM migration_version", [], |row| row.get(0)) 22 .map_err(|e| format!("Failed to read migration_version: {e}"))?; 23 if count == 0 { 24 conn.execute("INSERT INTO migration_version (version) VALUES (0)", []) 25 .map_err(|e| format!("Failed to seed migration_version: {e}"))?; 26 } 27 28 let current_version: i64 = conn 29 .query_row("SELECT version FROM migration_version", [], |row| row.get(0)) 30 .map_err(|e| format!("Failed to read version: {e}"))?; 31 32 let total = MIGRATIONS.len() as i64; 33 if current_version >= total { 34 return Ok(()); 35 } 36 37 eprintln!( 38 "Running migrations {} through {} (current version: {})", 39 current_version, 40 total - 1, 41 current_version 42 ); 43 44 for i in current_version..total { 45 eprintln!(" Applying migration {}...", i); 46 conn.execute_batch(MIGRATIONS[i as usize]) 47 .map_err(|e| format!("Migration {} failed: {}", i, e))?; 48 } 49 50 eprintln!("Migrations complete. Database is now at version {}", total); 51 Ok(()) 52 } 53 54 #[cfg(test)] 55 mod tests { 56 use super::*; 57 58 #[test] 59 fn each_migration_bumps_version() { 60 let conn = Connection::open_in_memory().unwrap(); 61 run(&conn).unwrap(); 62 63 let version: i64 = conn 64 .query_row("SELECT version FROM migration_version", [], |row| row.get(0)) 65 .unwrap(); 66 67 assert_eq!(version, MIGRATIONS.len() as i64); 68 } 69 70 #[test] 71 fn migrations_are_idempotent() { 72 let conn = Connection::open_in_memory().unwrap(); 73 run(&conn).unwrap(); 74 run(&conn).unwrap(); 75 76 let version: i64 = conn 77 .query_row("SELECT version FROM migration_version", [], |row| row.get(0)) 78 .unwrap(); 79 80 assert_eq!(version, MIGRATIONS.len() as i64); 81 } 82 83 #[test] 84 fn foreign_keys_on_after_migrations() { 85 let conn = Connection::open_in_memory().unwrap(); 86 conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap(); 87 88 run(&conn).unwrap(); 89 90 let fk_enabled: i64 = conn 91 .query_row("PRAGMA foreign_keys", [], |row| row.get(0)) 92 .unwrap(); 93 94 assert_eq!(fk_enabled, 1, "foreign_keys must be ON after migrations"); 95 } 96 }