Skip to main content

miden_node_db/migration/
entry.rs

1use std::fmt;
2
3use anyhow::{Context, Result, ensure};
4use rusqlite::{Connection, Transaction};
5
6use super::schema::{self, SchemaHash};
7
8/// A migration entry that can be executed inside a SQLite transaction.
9pub(super) trait MigrationEntry {
10    /// Returns the migration name used in diagnostics.
11    fn name(&self) -> &'static str;
12
13    /// Executes the migration body inside `tx`.
14    fn execute_migration(&self, tx: &Transaction<'_>) -> Result<()>;
15}
16
17/// A pure SQL migration.
18pub(super) struct SqlMigration {
19    name: &'static str,
20    sql: &'static str,
21}
22
23impl SqlMigration {
24    pub(super) fn new(name: &'static str, sql: &'static str) -> Self {
25        Self { name, sql }
26    }
27}
28
29impl MigrationEntry for SqlMigration {
30    fn name(&self) -> &'static str {
31        self.name
32    }
33
34    fn execute_migration(&self, tx: &Transaction<'_>) -> Result<()> {
35        tx.execute_batch(self.sql).map_err(Into::into)
36    }
37}
38
39impl fmt::Debug for SqlMigration {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        f.debug_struct("SqlMigration").field("name", &self.name).finish_non_exhaustive()
42    }
43}
44
45/// A Rust migration function.
46pub(super) struct CodeMigration {
47    name: &'static str,
48    apply: CodeMigrationFn,
49}
50
51impl CodeMigration {
52    pub(super) fn new(name: &'static str, apply: CodeMigrationFn) -> Self {
53        Self { name, apply }
54    }
55}
56
57impl MigrationEntry for CodeMigration {
58    fn name(&self) -> &'static str {
59        self.name
60    }
61
62    fn execute_migration(&self, tx: &Transaction<'_>) -> Result<()> {
63        (self.apply)(tx)
64    }
65}
66
67impl fmt::Debug for CodeMigration {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        f.debug_struct("CodeMigration")
70            .field("name", &self.name)
71            .finish_non_exhaustive()
72    }
73}
74
75/// An active migration that remains supported for existing databases.
76pub(super) enum Migration {
77    Sql(SqlMigration),
78    Code(CodeMigration),
79}
80
81impl Migration {
82    pub(super) fn sql(name: &'static str, sql: &'static str) -> Self {
83        Self::Sql(SqlMigration::new(name, sql))
84    }
85
86    pub(super) fn code(name: &'static str, apply: CodeMigrationFn) -> Self {
87        Self::Code(CodeMigration::new(name, apply))
88    }
89}
90
91impl MigrationEntry for Migration {
92    fn name(&self) -> &'static str {
93        match self {
94            Self::Sql(migration) => migration.name(),
95            Self::Code(migration) => migration.name(),
96        }
97    }
98
99    fn execute_migration(&self, tx: &Transaction<'_>) -> Result<()> {
100        match self {
101            Self::Sql(migration) => migration.execute_migration(tx),
102            Self::Code(migration) => migration.execute_migration(tx),
103        }
104    }
105}
106
107impl fmt::Debug for Migration {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        match self {
110            Self::Sql(migration) => fmt::Debug::fmt(migration, f),
111            Self::Code(migration) => fmt::Debug::fmt(migration, f),
112        }
113    }
114}
115
116/// Applies `migration`, sets `user_version`, commits, and returns the resulting schema hash.
117pub(super) fn apply_migration(
118    conn: &mut Connection,
119    version: usize,
120    migration: &impl MigrationEntry,
121) -> Result<SchemaHash> {
122    apply_migration_transaction(conn, version, migration, Ok::<SchemaHash, anyhow::Error>)
123}
124
125/// Applies `migration`, verifies the resulting schema hash, sets `user_version`, and commits.
126pub(super) fn apply_migration_and_verify_schema(
127    conn: &mut Connection,
128    version: usize,
129    migration: &impl MigrationEntry,
130    expected: SchemaHash,
131) -> Result<()> {
132    apply_migration_transaction(conn, version, migration, |actual| {
133        ensure!(actual == expected, "schema hash mismatch: expected {expected}, got {actual}");
134        Ok(())
135    })
136}
137
138fn apply_migration_transaction<T>(
139    conn: &mut Connection,
140    version: usize,
141    migration: &impl MigrationEntry,
142    verify_hash: impl FnOnce(SchemaHash) -> Result<T>,
143) -> Result<T> {
144    let tx = conn.transaction().context("failed to start transaction")?;
145
146    migration.execute_migration(&tx).context("failed to execute migration")?;
147    let schema_hash = SchemaHash::new(&tx).context("failed to compute schema hash")?;
148    let result = verify_hash(schema_hash)?;
149    schema::set_version(&tx, version).context("failed to update user_version")?;
150    tx.commit().context("failed to commit transaction")?;
151
152    Ok(result)
153}
154
155/// A Rust migration function executed inside a SQLite transaction.
156pub type CodeMigrationFn = for<'conn> fn(&Transaction<'conn>) -> Result<()>;