miden_node_db/migration/
builder.rs1use anyhow::{Context, Result};
2use rusqlite::Connection;
3
4use super::entry::{CodeMigrationFn, Migration, SqlMigration, apply_migration};
5use super::{Migrator, SchemaHash};
6
7pub struct MigratorBuilder {
50 reference: Connection,
52 migrator: Migrator,
54}
55
56impl MigratorBuilder {
57 pub(super) fn new() -> Result<Self> {
58 let reference = Connection::open_in_memory()
59 .context("failed to create in-memory migration database")?;
60
61 Ok(Self { reference, migrator: Migrator::empty() })
62 }
63
64 pub fn push_retired(mut self, name: &'static str, sql: &'static str) -> Result<Self> {
69 let version = self.migrator.next_version();
70 let migration = SqlMigration::new(name, sql);
71 let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration)
72 .with_context(|| format!("failed to apply retired migration {version} \"{name}\""))?;
73
74 self.migrator.push_retired_unchecked(migration, hash);
75 Ok(self)
76 }
77
78 pub fn push_sql(mut self, name: &'static str, sql: &'static str) -> Result<Self> {
80 let version = self.migrator.next_version();
81 let migration = Migration::sql(name, sql);
82 let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration)
83 .with_context(|| format!("failed to apply SQL migration {version} \"{name}\""))?;
84
85 self.migrator.push_active_unchecked(migration, hash);
86 Ok(self)
87 }
88
89 pub fn push_code(mut self, name: &'static str, apply: CodeMigrationFn) -> Result<Self> {
91 let version = self.migrator.next_version();
92 let migration = Migration::code(name, apply);
93 let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration)
94 .with_context(|| format!("failed to apply code migration {version} \"{name}\""))?;
95
96 self.migrator.push_active_unchecked(migration, hash);
97 Ok(self)
98 }
99
100 pub fn build(self) -> Result<Migrator> {
102 self.migrator.validate()?;
103 Ok(self.migrator)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use anyhow::Result;
110 use rusqlite::{Connection, Transaction};
111
112 use super::super::{Migrator, SchemaHash};
113 use crate::migration::SchemaHashes;
114
115 fn add_item_height(tx: &Transaction<'_>) -> Result<()> {
116 tx.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?;
117 Ok(())
118 }
119
120 #[test]
121 fn empty_builder_returns_error() -> Result<()> {
122 let err = Migrator::builder()?.build().expect_err("empty builder should fail");
123 assert!(err.to_string().contains("cannot build migrator without migrations"));
124 Ok(())
125 }
126
127 #[test]
128 #[should_panic(expected = "cannot add retired migration after active migrations have started")]
129 fn panics_when_adding_retired_after_active_migration() {
130 let _builder = Migrator::builder()
131 .expect("builder should be created")
132 .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")
133 .expect("SQL migration should be added")
134 .push_retired("add notes", "CREATE TABLE notes (id INTEGER PRIMARY KEY);");
135 }
136
137 #[test]
138 fn exposes_schema_hashes() -> Result<()> {
139 let reference = Connection::open_in_memory()?;
140 reference.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?;
141 let retired_hash = SchemaHash::new(&reference)?;
142 reference.execute_batch("CREATE INDEX idx_items_value ON items(value);")?;
143 let sql_hash = SchemaHash::new(&reference)?;
144 reference.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?;
145 let final_hash = SchemaHash::new(&reference)?;
146
147 let migrator = Migrator::builder()?
148 .push_retired(
149 "create items",
150 "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);",
151 )?
152 .push_sql("index item values", "CREATE INDEX idx_items_value ON items(value);")?
153 .push_code("add item height", add_item_height)?
154 .build()?;
155
156 assert_eq!(migrator.schema_hashes(), SchemaHashes(&[retired_hash, sql_hash, final_hash]));
157 Ok(())
158 }
159}