Skip to main content

scitadel_db/sqlite/
migrations.rs

1use 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
33/// Run all pending migrations, skipping already-applied ones.
34pub 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(); // should not fail
73    }
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}