scitadel_db/sqlite/
migrations.rs1use rusqlite::Connection;
2
3use crate::error::DbError;
4
5const MIGRATION_001: &str = include_str!("../../migrations/001_initial.sql");
6const MIGRATION_002: &str = include_str!("../../migrations/002_citations.sql");
7const MIGRATION_003: &str = include_str!("../../migrations/003_full_text.sql");
8const MIGRATION_004: &str = include_str!("../../migrations/004_paper_state.sql");
9const MIGRATION_005: &str = include_str!("../../migrations/005_annotations.sql");
10const MIGRATION_006: &str = include_str!("../../migrations/006_search_fts.sql");
11const MIGRATION_007: &str = include_str!("../../migrations/007_paper_download_state.sql");
12const MIGRATION_008: &str = include_str!("../../migrations/008_tui_state.sql");
13const MIGRATION_009: &str = include_str!("../../migrations/009_bibtex_keys.sql");
14const MIGRATION_010: &str = include_str!("../../migrations/010_shortlists.sql");
15const MIGRATION_011: &str = include_str!("../../migrations/011_paper_aliases.sql");
16const MIGRATION_012: &str = include_str!("../../migrations/012_paper_tags.sql");
17
18const MIGRATIONS: &[(i64, &str)] = &[
19 (1, MIGRATION_001),
20 (2, MIGRATION_002),
21 (3, MIGRATION_003),
22 (4, MIGRATION_004),
23 (5, MIGRATION_005),
24 (6, MIGRATION_006),
25 (7, MIGRATION_007),
26 (8, MIGRATION_008),
27 (9, MIGRATION_009),
28 (10, MIGRATION_010),
29 (11, MIGRATION_011),
30 (12, MIGRATION_012),
31];
32
33pub fn run_migrations(conn: &Connection) -> Result<(), DbError> {
35 conn.execute_batch(
36 "CREATE TABLE IF NOT EXISTS schema_version (
37 version INTEGER PRIMARY KEY,
38 applied_at TEXT NOT NULL
39 )",
40 )
41 .map_err(|e| DbError::Migration(e.to_string()))?;
42
43 let applied: Vec<i64> = {
44 let mut stmt = conn
45 .prepare("SELECT version FROM schema_version")
46 .map_err(|e| DbError::Migration(e.to_string()))?;
47 let rows = stmt
48 .query_map([], |row| row.get(0))
49 .map_err(|e| DbError::Migration(e.to_string()))?;
50 rows.filter_map(Result::ok).collect()
51 };
52
53 for &(version, sql) in MIGRATIONS {
54 if applied.contains(&version) {
55 continue;
56 }
57 conn.execute_batch(sql)
58 .map_err(|e| DbError::Migration(format!("migration {version} failed: {e}")))?;
59 }
60
61 Ok(())
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn test_migrations_idempotent() {
70 let conn = Connection::open_in_memory().unwrap();
71 run_migrations(&conn).unwrap();
72 run_migrations(&conn).unwrap(); }
74
75 #[test]
76 fn test_all_tables_created() {
77 let conn = Connection::open_in_memory().unwrap();
78 run_migrations(&conn).unwrap();
79
80 let tables: Vec<String> = {
81 let mut stmt = conn
82 .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
83 .unwrap();
84 stmt.query_map([], |row| row.get(0))
85 .unwrap()
86 .filter_map(Result::ok)
87 .collect()
88 };
89
90 assert!(tables.contains(&"papers".to_string()));
91 assert!(tables.contains(&"searches".to_string()));
92 assert!(tables.contains(&"search_results".to_string()));
93 assert!(tables.contains(&"research_questions".to_string()));
94 assert!(tables.contains(&"search_terms".to_string()));
95 assert!(tables.contains(&"assessments".to_string()));
96 assert!(tables.contains(&"citations".to_string()));
97 assert!(tables.contains(&"snowball_runs".to_string()));
98 assert!(tables.contains(&"paper_state".to_string()));
99 assert!(tables.contains(&"annotations".to_string()));
100 assert!(tables.contains(&"annotation_reads".to_string()));
101 assert!(tables.contains(&"searches_fts".to_string()));
102 assert!(tables.contains(&"paper_aliases".to_string()));
103 assert!(tables.contains(&"paper_tags".to_string()));
104 assert!(tables.contains(&"schema_version".to_string()));
105 }
106}