Skip to main content

cel_memory_sqlite/
migrations.rs

1//! Embedded SQL migrations.
2//!
3//! Migrations are bundled into the binary via `include_str!` so the daemon
4//! never reads files from disk to find them. Each migration runs in a
5//! transaction; the schema version is tracked in `_cellar_schema_version`.
6
7use rusqlite::{params, Connection};
8
9use crate::error::SqliteMemoryError;
10
11/// One migration step.
12struct Migration {
13    version: i64,
14    name: &'static str,
15    sql: &'static str,
16}
17
18const MIGRATIONS: &[Migration] = &[Migration {
19    version: 1,
20    name: "001_initial.sql",
21    sql: include_str!("../migrations/001_initial.sql"),
22}];
23
24/// Highest schema version this build can apply.
25pub const LATEST_VERSION: i64 = 1;
26
27/// Run all pending migrations against the connection. Idempotent: a
28/// connection that already at `LATEST_VERSION` becomes a no-op.
29pub fn run(conn: &mut Connection) -> Result<(), SqliteMemoryError> {
30    // Create the schema-tracking table if it doesn't exist. Standalone
31    // statement so we know it executes even on a fresh DB.
32    conn.execute(
33        "CREATE TABLE IF NOT EXISTS _cellar_schema_version (
34            version INTEGER PRIMARY KEY,
35            applied_at INTEGER NOT NULL
36        )",
37        [],
38    )?;
39
40    let current: i64 = conn
41        .query_row(
42            "SELECT COALESCE(MAX(version), 0) FROM _cellar_schema_version",
43            [],
44            |row| row.get(0),
45        )
46        .unwrap_or(0);
47
48    for m in MIGRATIONS {
49        if m.version <= current {
50            continue;
51        }
52        tracing::info!(version = m.version, name = m.name, "applying migration");
53        let tx = conn.transaction()?;
54        tx.execute_batch(m.sql)
55            .map_err(|e| SqliteMemoryError::Migration {
56                name: m.name.to_string(),
57                source: e,
58            })?;
59        tx.execute(
60            "INSERT INTO _cellar_schema_version(version, applied_at) VALUES(?, ?)",
61            params![m.version, chrono::Utc::now().timestamp_millis()],
62        )?;
63        tx.commit()?;
64    }
65    Ok(())
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use rusqlite::Connection;
72
73    fn open_with_vec() -> Connection {
74        // Register the extension BEFORE opening so the new connection
75        // picks it up. Auto-extensions only affect connections opened
76        // after registration; calling multiple times is safe — SQLite
77        // dedupes by function pointer.
78        crate::vec_extension::register();
79        Connection::open_in_memory().unwrap()
80    }
81
82    #[test]
83    fn schema_version_starts_at_zero() {
84        let mut conn = open_with_vec();
85        run(&mut conn).unwrap();
86        let v: i64 = conn
87            .query_row("SELECT MAX(version) FROM _cellar_schema_version", [], |r| {
88                r.get(0)
89            })
90            .unwrap();
91        assert_eq!(v, LATEST_VERSION);
92    }
93
94    #[test]
95    fn running_twice_is_idempotent() {
96        let mut conn = open_with_vec();
97        run(&mut conn).unwrap();
98        run(&mut conn).unwrap();
99        let v: i64 = conn
100            .query_row("SELECT COUNT(*) FROM _cellar_schema_version", [], |r| {
101                r.get(0)
102            })
103            .unwrap();
104        assert_eq!(v, LATEST_VERSION); // each migration recorded once
105    }
106}