use anyhow::{Context, Result};
use rusqlite::Connection;
use super::entry::{CodeMigrationFn, Migration, SqlMigration, apply_migration};
use super::{Migrator, SchemaHash};
pub struct MigratorBuilder {
reference: Connection,
migrator: Migrator,
}
impl MigratorBuilder {
pub(super) fn new() -> Result<Self> {
let reference = Connection::open_in_memory()
.context("failed to create in-memory migration database")?;
Ok(Self { reference, migrator: Migrator::empty() })
}
pub fn push_retired(mut self, name: &'static str, sql: &'static str) -> Result<Self> {
let version = self.migrator.next_version();
let migration = SqlMigration::new(name, sql);
let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration)
.with_context(|| format!("failed to apply retired migration {version} \"{name}\""))?;
self.migrator.push_retired_unchecked(migration, hash);
Ok(self)
}
pub fn push_sql(mut self, name: &'static str, sql: &'static str) -> Result<Self> {
let version = self.migrator.next_version();
let migration = Migration::sql(name, sql);
let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration)
.with_context(|| format!("failed to apply SQL migration {version} \"{name}\""))?;
self.migrator.push_active_unchecked(migration, hash);
Ok(self)
}
pub fn push_code(mut self, name: &'static str, apply: CodeMigrationFn) -> Result<Self> {
let version = self.migrator.next_version();
let migration = Migration::code(name, apply);
let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration)
.with_context(|| format!("failed to apply code migration {version} \"{name}\""))?;
self.migrator.push_active_unchecked(migration, hash);
Ok(self)
}
pub fn build(self) -> Result<Migrator> {
self.migrator.validate()?;
Ok(self.migrator)
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use rusqlite::{Connection, Transaction};
use super::super::{Migrator, SchemaHash};
use crate::migration::SchemaHashes;
fn add_item_height(tx: &Transaction<'_>) -> Result<()> {
tx.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?;
Ok(())
}
#[test]
fn empty_builder_returns_error() -> Result<()> {
let err = Migrator::builder()?.build().expect_err("empty builder should fail");
assert!(err.to_string().contains("cannot build migrator without migrations"));
Ok(())
}
#[test]
#[should_panic(expected = "cannot add retired migration after active migrations have started")]
fn panics_when_adding_retired_after_active_migration() {
let _builder = Migrator::builder()
.expect("builder should be created")
.push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")
.expect("SQL migration should be added")
.push_retired("add notes", "CREATE TABLE notes (id INTEGER PRIMARY KEY);");
}
#[test]
fn exposes_schema_hashes() -> Result<()> {
let reference = Connection::open_in_memory()?;
reference.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?;
let retired_hash = SchemaHash::new(&reference)?;
reference.execute_batch("CREATE INDEX idx_items_value ON items(value);")?;
let sql_hash = SchemaHash::new(&reference)?;
reference.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?;
let final_hash = SchemaHash::new(&reference)?;
let migrator = Migrator::builder()?
.push_retired(
"create items",
"CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);",
)?
.push_sql("index item values", "CREATE INDEX idx_items_value ON items(value);")?
.push_code("add item height", add_item_height)?
.build()?;
assert_eq!(migrator.schema_hashes(), SchemaHashes(&[retired_hash, sql_hash, final_hash]));
Ok(())
}
}