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_unique_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_unique_versions(migrations: &[Migration]) -> Result<(), DbError> {
45    let mut seen = BTreeSet::new();
46    for migration in migrations {
47        if !seen.insert(migration.version) {
48            return Err(DbError::DuplicateMigrationVersion(migration.version));
49        }
50    }
51    Ok(())
52}
53
54fn sqlite_version(version: u64) -> Result<i64, DbError> {
55    i64::try_from(version).map_err(|_| DbError::MigrationVersionOutOfRange(version))
56}