Skip to main content

ic_sqlite_vfs/db/
migrate.rs

1//! Minimal schema migration runner.
2//!
3//! Versions are stored both in SQLite and the stable superblock. The SQLite table
4//! is the source of truth for applied SQL; the superblock is quick canister state.
5
6use crate::db::connection::Connection;
7use crate::db::DbError;
8use std::collections::BTreeSet;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub struct Migration {
12    pub version: u64,
13    pub sql: &'static str,
14}
15
16pub fn apply(connection: &Connection, migrations: &[Migration]) -> Result<(), DbError> {
17    validate_versions(migrations)?;
18    connection.execute_batch(
19        "CREATE TABLE IF NOT EXISTS __ic_sqlite_migrations (
20            version INTEGER PRIMARY KEY NOT NULL
21        )",
22    )?;
23
24    for migration in migrations {
25        let version = sqlite_version(migration.version)?;
26        let exists = connection.query_scalar::<i64>(
27            &format!(
28                "SELECT EXISTS(SELECT 1 FROM __ic_sqlite_migrations WHERE version = {version})"
29            ),
30            crate::params![],
31        )?;
32        if exists != 0 {
33            continue;
34        }
35        connection.execute_batch(migration.sql)?;
36        connection.execute_batch(&format!(
37            "INSERT INTO __ic_sqlite_migrations(version) VALUES ({version})"
38        ))?;
39    }
40
41    Ok(())
42}
43
44fn validate_versions(migrations: &[Migration]) -> Result<(), DbError> {
45    let mut seen = BTreeSet::new();
46    let mut previous = None;
47    for migration in migrations {
48        if !seen.insert(migration.version) {
49            return Err(DbError::DuplicateMigrationVersion(migration.version));
50        }
51        if let Some(previous) = previous {
52            if migration.version < previous {
53                return Err(DbError::MigrationVersionOutOfOrder {
54                    previous,
55                    next: migration.version,
56                });
57            }
58        }
59        previous = Some(migration.version);
60    }
61    Ok(())
62}
63
64fn sqlite_version(version: u64) -> Result<i64, DbError> {
65    i64::try_from(version).map_err(|_| DbError::MigrationVersionOutOfRange(version))
66}