use sqlx::{Database, Sqlite, SqlitePool};
use super::{DatabaseOperation, Info, Migrate, Migrator};
use crate::error::Error;
use crate::migration::{AppliedMigrationSqlRow, Migration};
use crate::migrator::Plan;
use crate::operation::Operation;
use crate::sync::Synchronize;
use crate::vec_box;
#[test]
fn table_name() {
assert!(Migrator::<Sqlite>::new().set_table_prefix("abc").is_ok());
assert!(Migrator::<Sqlite>::new().set_table_prefix("aBc").is_err());
assert!(Migrator::<Sqlite>::new().set_table_prefix("ab1").is_ok());
assert!(
Migrator::<Sqlite>::new()
.set_table_prefix("abc___123")
.is_ok()
);
assert!(Migrator::<Sqlite>::new().set_table_prefix("_123").is_ok());
assert!(
Migrator::<Sqlite>::new()
.set_table_prefix("1@_2_3")
.is_err()
);
assert!(Migrator::<Sqlite>::new().set_table_prefix("").is_err());
}
#[test]
fn schema_name() {
assert!(Migrator::<Sqlite>::new().set_schema("abc").is_ok());
assert!(Migrator::<Sqlite>::new().set_schema("aBc").is_err());
assert!(Migrator::<Sqlite>::new().set_schema("1abc").is_err());
assert!(Migrator::<Sqlite>::new().set_schema("ab1").is_ok());
assert!(Migrator::<Sqlite>::new().set_schema("abc___123").is_ok());
assert!(Migrator::<Sqlite>::new().set_schema("_123").is_ok());
assert!(Migrator::<Sqlite>::new().set_schema("1@_2_3").is_err());
assert!(Migrator::<Sqlite>::new().set_schema("").is_err());
assert!(Migrator::<Sqlite>::new().set_schema("!").is_err());
}
#[test]
fn table_name_format() {
assert_eq!(
Migrator::<Sqlite>::new().table_name(),
"_sqlx_migrator_migrations"
);
let m = Migrator::<Sqlite>::new()
.set_table_prefix("myprefix")
.unwrap();
assert_eq!(m.table_name(), "_myprefix_sqlx_migrator_migrations");
let m = Migrator::<Sqlite>::new().set_schema("myschema").unwrap();
assert_eq!(m.table_name(), "myschema._sqlx_migrator_migrations");
let m = Migrator::<Sqlite>::new()
.set_table_prefix("pfx")
.unwrap()
.set_schema("sch")
.unwrap();
assert_eq!(m.table_name(), "sch._pfx_sqlx_migrator_migrations");
}
#[derive(Default)]
struct CustomMigrator {
internal_migrator: Migrator<Sqlite>,
migrations: Vec<Box<dyn Migration<Sqlite>>>,
applied_migrations: Vec<AppliedMigrationSqlRow>,
}
impl CustomMigrator {
fn add_applied_migrations(
&mut self,
migrations: Vec<Box<dyn Migration<Sqlite>>>,
) -> Result<(), Error> {
for migration in migrations {
self.add_applied_migration(migration)?;
}
Ok(())
}
fn add_applied_migration(
&mut self,
migration: Box<dyn Migration<Sqlite>>,
) -> Result<(), Error> {
let current_length = self.migrations.len();
self.applied_migrations.push(AppliedMigrationSqlRow::new(
i32::try_from(current_length).unwrap(),
migration.app(),
migration.name(),
));
self.internal_migrator.add_migration(migration)
}
}
impl Info<Sqlite> for CustomMigrator {
fn migrations(&self) -> &[Box<dyn Migration<Sqlite>>] {
&self.migrations
}
fn migrations_mut(&mut self) -> &mut Vec<Box<dyn Migration<Sqlite>>> {
&mut self.migrations
}
}
#[async_trait::async_trait]
impl DatabaseOperation<Sqlite> for CustomMigrator {
async fn ensure_migration_table_exists(
&self,
_connection: &mut <Sqlite as Database>::Connection,
) -> Result<(), Error> {
Ok(())
}
async fn drop_migration_table_if_exists(
&self,
_connection: &mut <Sqlite as Database>::Connection,
) -> Result<(), Error> {
Ok(())
}
async fn add_migration_to_db_table(
&self,
_connection: &mut <Sqlite as Database>::Connection,
_migration: &dyn Migration<Sqlite>,
) -> Result<(), Error> {
Ok(())
}
async fn delete_migration_from_db_table(
&self,
_connection: &mut <Sqlite as Database>::Connection,
_migration: &dyn Migration<Sqlite>,
) -> Result<(), Error> {
Ok(())
}
async fn fetch_applied_migration_from_db(
&self,
_connection: &mut <Sqlite as Database>::Connection,
) -> Result<Vec<AppliedMigrationSqlRow>, Error> {
Ok(self.applied_migrations.clone())
}
async fn lock(&self, _connection: &mut <Sqlite as Database>::Connection) -> Result<(), Error> {
Ok(())
}
async fn unlock(
&self,
_connection: &mut <Sqlite as Database>::Connection,
) -> Result<(), Error> {
Ok(())
}
}
impl Migrate<Sqlite> for CustomMigrator {}
impl Synchronize<Sqlite> for CustomMigrator {}
macro_rules! migration {
($op:ty, $name:literal, $parents:expr, $replaces:expr, $run_before:expr) => {
impl crate::migration::Migration<sqlx::Sqlite> for $op {
fn app(&self) -> &str {
"test"
}
fn name(&self) -> &str {
$name
}
fn parents(&self) -> Vec<Box<dyn crate::migration::Migration<sqlx::Sqlite>>> {
$parents
}
fn operations(&self) -> Vec<Box<dyn crate::operation::Operation<sqlx::Sqlite>>> {
vec_box![("", "")]
}
fn replaces(&self) -> Vec<Box<dyn crate::migration::Migration<sqlx::Sqlite>>> {
$replaces
}
fn run_before(&self) -> Vec<Box<dyn crate::migration::Migration<sqlx::Sqlite>>> {
$run_before
}
}
};
}
async fn make_conn() -> sqlx::pool::PoolConnection<Sqlite> {
SqlitePool::connect("sqlite::memory:")
.await
.unwrap()
.acquire()
.await
.unwrap()
}
async fn apply_plan<'a>(
migrator: &'a CustomMigrator,
conn: &mut <Sqlite as Database>::Connection,
plan: Plan,
) -> Result<Vec<&'a Box<dyn Migration<Sqlite>>>, Error> {
migrator.generate_migration_plan(conn, Some(&plan)).await
}
async fn generate_apply_all_plan(
migrator: &mut CustomMigrator,
migration_list: Vec<Box<dyn Migration<Sqlite>>>,
) -> Result<Vec<&Box<dyn Migration<Sqlite>>>, Error> {
migrator.add_migrations(migration_list)?;
let mut conn = make_conn().await;
apply_plan(migrator, &mut conn, Plan::apply_all()).await
}
#[expect(
clippy::borrowed_box,
reason = "plan slices hold &Box<dyn Migration> from generate_migration_plan"
)]
fn plan_names<'a>(plan: &'a [&'a Box<dyn Migration<Sqlite>>]) -> Vec<&'a str> {
plan.iter().map(|m| m.name()).collect()
}
#[test]
fn applied_migration_sql_row_accessors() {
let row = AppliedMigrationSqlRow::new(42, "myapp", "first_migration");
assert_eq!(row.id(), 42);
assert_eq!(row.app(), "myapp");
assert_eq!(row.name(), "first_migration");
assert_eq!(row.applied_time(), "");
}
#[test]
fn migration_equality() {
let m1: Box<dyn Migration<Sqlite>> = Box::new(("app", "name"));
let m2: Box<dyn Migration<Sqlite>> = Box::new(("app", "name"));
assert_eq!(m1.app(), m2.app(), "apps must match for equal migrations");
assert_eq!(
m1.name(),
m2.name(),
"names must match for equal migrations"
);
let m3: Box<dyn Migration<Sqlite>> = Box::new(("app", "other"));
assert_eq!(m1.app(), m3.app(), "app is shared");
assert_ne!(m1.name(), m3.name(), "different names must not be equal");
let m4: Box<dyn Migration<Sqlite>> = Box::new(("other_app", "name"));
assert_ne!(m1.app(), m4.app(), "different apps must not be equal");
assert_eq!(m1.name(), m4.name(), "name is shared");
}
#[test]
fn tuple_migration_is_virtual() {
let m: &dyn Migration<Sqlite> = &("myapp", "mymig");
assert!(m.is_virtual(), "tuple migration must be virtual");
assert!(m.parents().is_empty(), "tuple must have no parents");
assert!(m.operations().is_empty(), "tuple must have no operations");
assert!(m.replaces().is_empty(), "tuple must have no replaces");
assert!(m.run_before().is_empty(), "tuple must have no run_before");
}
#[tokio::test]
async fn operation_default_down_is_irreversible() {
struct NoDownOp;
#[async_trait::async_trait]
impl Operation<Sqlite> for NoDownOp {
async fn up(&self, _connection: &mut sqlx::SqliteConnection) -> Result<(), Error> {
Ok(())
}
}
let op = NoDownOp;
let mut conn = make_conn().await;
let result = op.down(&mut conn).await;
assert!(
matches!(result, Err(Error::IrreversibleOperation)),
"expected IrreversibleOperation, got: {result:?}"
);
}
#[test]
fn operation_is_destructible_default() {
struct DefaultOp;
#[async_trait::async_trait]
impl Operation<Sqlite> for DefaultOp {
async fn up(&self, _connection: &mut sqlx::SqliteConnection) -> Result<(), Error> {
Ok(())
}
}
assert!(
!DefaultOp.is_destructible(),
"default is_destructible must be false"
);
}
#[test]
fn add_migration_duplicate_consistent_is_ok() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
assert!(migrator.add_migration(Box::new(A)).is_ok());
assert!(
migrator.add_migration(Box::new(A)).is_ok(),
"second identical add must succeed"
);
assert_eq!(
migrator.migrations().len(),
1,
"duplicate add must not create a second entry"
);
}
#[test]
fn add_invalid_virtual_migration_is_error() {
struct BadVirtual;
impl Migration<Sqlite> for BadVirtual {
fn app(&self) -> &'static str {
"test"
}
fn name(&self) -> &'static str {
"bad_virtual"
}
fn parents(&self) -> Vec<Box<dyn Migration<Sqlite>>> {
vec_box!(("test", "some_parent"))
}
fn operations(&self) -> Vec<Box<dyn Operation<Sqlite>>> {
vec![]
}
fn is_virtual(&self) -> bool {
true
}
}
let mut migrator = CustomMigrator::default();
let result = migrator.add_migration(Box::new(BadVirtual));
assert!(
matches!(result, Err(Error::InvalidVirtualMigration)),
"expected InvalidVirtualMigration, got: {result:?}"
);
}
#[test]
fn error_display_messages() {
assert_eq!(
Error::IrreversibleOperation.to_string(),
"operation is irreversible"
);
assert_eq!(
Error::InvalidTablePrefix.to_string(),
"table prefix name can only contain [a-z0-9_]"
);
assert_eq!(
Error::InvalidSchema.to_string(),
"schema name can only contain [a-z0-9_] and begin with [a-z_]"
);
assert_eq!(
Error::InconsistentMigration {
app: "myapp".into(),
name: "mymig".into()
}
.to_string(),
"inconsistent migration found for myapp - mymig"
);
assert_eq!(
Error::PlanError {
message: "test error".into()
}
.to_string(),
"plan error: test error"
);
}
#[tokio::test]
async fn linear_dependency_chain_applies_in_order() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(B), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "b", "c"]);
}
#[tokio::test]
async fn apply_all_with_no_migrations_is_error() {
struct _A;
migration!(_A, "a", vec_box!(), vec_box!(), vec_box!());
struct _B;
migration!(_B, "b", vec_box!(_A), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!()).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: no migration are added to migration list".to_string())
);
}
#[tokio::test]
async fn inconsistent_duplicate_migration_name_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(), vec_box!());
struct D;
migration!(D, "b", vec_box!(C), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("inconsistent migration found for test - b".to_string())
);
}
#[tokio::test]
async fn circular_parent_dependency_deadlocks() {
struct A;
migration!(A, "a", vec_box!(B), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: reached deadlock stage during plan generation".to_string())
);
}
#[tokio::test]
async fn replaces_interrelated_test() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(B), vec_box!());
struct B;
migration!(B, "b", vec_box!(), vec_box!(A), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: two migrations replaces each other".to_string())
);
}
#[tokio::test]
async fn run_before_interrelated_test() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!(B));
struct B;
migration!(B, "b", vec_box!(), vec_box!(), vec_box!(A));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: reached deadlock stage during plan generation".to_string())
);
}
#[tokio::test]
async fn self_referencing_parent_deadlocks() {
struct A;
migration!(A, "a", vec_box!(A), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(B), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: reached deadlock stage during plan generation".to_string())
);
}
#[tokio::test]
async fn replaces_depend_on_itself() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(A), vec_box!());
struct B;
migration!(B, "b", vec_box!(), vec_box!(B), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: two migrations replaces each other".to_string())
);
}
#[tokio::test]
async fn run_before_depend_on_itself() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!(A));
struct B;
migration!(B, "b", vec_box!(), vec_box!(), vec_box!(B));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: reached deadlock stage during plan generation".to_string())
);
}
#[tokio::test]
async fn replaced_migrations_skipped_in_plan() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(C), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "d"]);
}
#[tokio::test]
async fn apply_name_for_replacer_includes_transitive_replace_parents() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(C), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B, C, D)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(
&migrator,
&mut conn,
Plan::apply_name("test", &Some("d".to_string())),
)
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "d"]);
}
#[tokio::test]
async fn revert_name_for_parent_includes_transitive_replace_dependents() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(C), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B, C, D)).unwrap();
migrator.add_applied_migrations(vec_box!(A, D)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(
&migrator,
&mut conn,
Plan::revert_name("test", &Some("a".to_string())),
)
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["d", "a"]);
}
#[tokio::test]
async fn run_before_ordering_is_respected() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(), vec_box!(B));
struct D;
migration!(D, "d", vec_box!(), vec_box!(), vec_box!(C));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "d", "c", "b"]);
}
#[tokio::test]
async fn migration_replaced_by_multiple_migrations_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(B), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: migration test:b replaced multiple times".to_string())
);
}
#[tokio::test]
async fn replaced_migration_run_before_transfers_to_replacer() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(), vec_box!(B));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "d", "c"]);
}
#[tokio::test]
async fn chained_replacer_inherits_run_before_constraint() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(C), vec_box!());
struct E;
migration!(E, "e", vec_box!(), vec_box!(), vec_box!(C));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D, E))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "e", "d"]);
}
#[tokio::test]
async fn run_before_on_final_replacer_is_respected() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(C), vec_box!());
struct E;
migration!(E, "e", vec_box!(), vec_box!(), vec_box!(D));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D, E))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "e", "d"]);
}
#[tokio::test]
async fn replacer_absorbing_all_run_before_parties_collapses_plan() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(C, E), vec_box!());
struct E;
migration!(E, "e", vec_box!(), vec_box!(), vec_box!(C));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D, E))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "d"]);
}
#[tokio::test]
async fn replacer_with_run_before_on_replaced_deadlocks() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(C), vec_box!(C));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: reached deadlock stage during plan generation".to_string())
);
}
#[tokio::test]
async fn mutual_replacers_with_run_before_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(C, E), vec_box!());
struct E;
migration!(E, "e", vec_box!(), vec_box!(D), vec_box!(C));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D, E)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: two migrations replaces each other".to_string())
);
}
#[tokio::test]
async fn replaced_run_before_its_replacer_deadlocks() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!(D));
struct D;
migration!(D, "d", vec_box!(), vec_box!(C), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: reached deadlock stage during plan generation".to_string())
);
}
#[tokio::test]
async fn parent_and_run_before_same_migration_deadlocks() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!(A));
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: reached deadlock stage during plan generation".to_string())
);
}
#[tokio::test]
async fn child_applied_without_parent_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_applied_migrations(vec_box!(B)).unwrap();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B)).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some(
"plan error: children migration test:b applied before its parent migration test:a"
.to_string()
)
);
}
#[tokio::test]
async fn transitive_replacer_fully_applied_gives_empty_plan() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
struct D;
migration!(D, "d", vec_box!(), vec_box!(C), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_applied_migrations(vec_box!(A, D)).unwrap();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(D, C, B, A))
.await
.unwrap();
assert!(plan.is_empty(), "all migrations already applied via squash");
}
#[tokio::test]
async fn virtual_references_in_replace_chain_resolves() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(
B,
"b",
vec_box!((A.app(), A.name())),
vec_box!(),
vec_box!()
);
struct C;
migration!(
C,
"c",
vec_box!(),
vec_box!((B.app(), B.name())),
vec_box!()
);
struct D;
migration!(
D,
"d",
vec_box!(),
vec_box!((C.app(), C.name())),
vec_box!()
);
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(B, C, D, A))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "d"]);
}
#[tokio::test]
async fn unreplaced_virtual_migration_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, ("test", "b"))).await;
assert_eq!(
plan.err().map(|e| e.to_string()),
Some("plan error: virtual migrations which is not replaced is present".to_string())
);
}
#[tokio::test]
async fn virtual_migration_with_concrete_replacement_resolves() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, ("test", "b"), B, ("test", "b")))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "b"]);
}
#[tokio::test]
async fn virtual_parent_reference_resolves_dependency_order() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(("test", "a")), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(B, A))
.await
.unwrap();
assert_eq!(plan.len(), 2, "plan must contain both migrations");
let names = plan_names(&plan);
let pos_a = names.iter().position(|&n| n == "a").unwrap();
let pos_b = names.iter().position(|&n| n == "b").unwrap();
assert!(pos_a < pos_b, "A must precede B: {names:?}");
}
#[tokio::test]
async fn apply_plan_diamond_topology_respects_branches() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(
B,
"b",
vec_box!((A.app(), A.name())),
vec_box!(),
vec_box!()
);
struct C;
migration!(
C,
"c",
vec_box!((B.app(), B.name())),
vec_box!(),
vec_box!()
);
struct D;
migration!(
D,
"d",
vec_box!((B.app(), B.name())),
vec_box!(),
vec_box!()
);
struct E;
migration!(
E,
"e",
vec_box!((C.app(), C.name())),
vec_box!(),
vec_box!()
);
struct F;
migration!(
F,
"f",
vec_box!((D.app(), D.name())),
vec_box!(),
vec_box!()
);
struct G;
migration!(
G,
"g",
vec_box!((E.app(), E.name())),
vec_box!(),
vec_box!()
);
let mut migrator = CustomMigrator::default();
migrator
.add_migrations(vec_box!(A, B, C, D, E, F, G))
.unwrap();
let sqlite = SqlitePool::connect("sqlite::memory:").await.unwrap();
let mut conn = sqlite.acquire().await.unwrap();
let full_plan = migrator
.generate_migration_plan(&mut conn, Some(&Plan::apply_all()))
.await
.unwrap();
assert_eq!(
plan_names(&full_plan),
vec!["a", "b", "c", "d", "e", "f", "g"]
);
let plan_till_f = migrator
.generate_migration_plan(
&mut conn,
Some(&Plan::apply_name("test", &Some("f".to_string()))),
)
.await
.unwrap();
assert_eq!(plan_names(&plan_till_f), vec!["a", "b", "d", "f"]);
let plan_till_g = migrator
.generate_migration_plan(
&mut conn,
Some(&Plan::apply_name("test", &Some("g".to_string()))),
)
.await
.unwrap();
assert_eq!(plan_names(&plan_till_g), vec!["a", "b", "c", "e", "g"]);
}
#[tokio::test]
async fn revert_plan_diamond_topology_reverses_correctly() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(
B,
"b",
vec_box!((A.app(), A.name())),
vec_box!(),
vec_box!()
);
struct C;
migration!(
C,
"c",
vec_box!((B.app(), B.name())),
vec_box!(),
vec_box!()
);
struct D;
migration!(
D,
"d",
vec_box!((B.app(), B.name())),
vec_box!(),
vec_box!()
);
struct E;
migration!(
E,
"e",
vec_box!((C.app(), C.name())),
vec_box!(),
vec_box!()
);
struct F;
migration!(
F,
"f",
vec_box!((D.app(), D.name())),
vec_box!(),
vec_box!()
);
struct G;
migration!(
G,
"g",
vec_box!((E.app(), E.name())),
vec_box!(),
vec_box!()
);
let mut migrator = CustomMigrator::default();
migrator
.add_migrations(vec_box!(A, B, C, D, E, F, G))
.unwrap();
migrator
.add_applied_migrations(vec_box!(A, B, C, D, E, F, G))
.unwrap();
let sqlite = SqlitePool::connect("sqlite::memory:").await.unwrap();
let mut conn = sqlite.acquire().await.unwrap();
let revert_plan = migrator
.generate_migration_plan(&mut conn, Some(&Plan::revert_all()))
.await
.unwrap();
assert_eq!(
plan_names(&revert_plan),
vec!["g", "f", "e", "d", "c", "b", "a"]
);
let plan_till_f = migrator
.generate_migration_plan(
&mut conn,
Some(&Plan::revert_name("test", &Some("f".to_string()))),
)
.await
.unwrap();
assert_eq!(plan_names(&plan_till_f), vec!["f"]);
let plan_till_b = migrator
.generate_migration_plan(
&mut conn,
Some(&Plan::revert_name("test", &Some("b".to_string()))),
)
.await
.unwrap();
assert_eq!(plan_names(&plan_till_b), vec!["g", "f", "e", "d", "c", "b"]);
}
#[tokio::test]
async fn revert_all_no_applied_gives_empty_plan() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(&migrator, &mut conn, Plan::revert_all())
.await
.unwrap();
assert!(plan.is_empty(), "no applied migrations → empty revert plan");
}
#[tokio::test]
async fn revert_app_name_reverts_all_for_app() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B)).unwrap();
migrator.add_applied_migrations(vec_box!(A, B)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(&migrator, &mut conn, Plan::revert_name("test", &None))
.await
.unwrap();
let names = plan_names(&plan);
assert!(names.contains(&"a"), "A missing: {names:?}");
assert!(names.contains(&"b"), "B missing: {names:?}");
let pos_a = names.iter().position(|&n| n == "a").unwrap();
let pos_b = names.iter().position(|&n| n == "b").unwrap();
assert!(pos_b < pos_a, "B must be reverted before A: {names:?}");
}
#[tokio::test]
async fn apply_count_limits_to_n_pending() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(
B,
"b",
vec_box!((A.app(), A.name())),
vec_box!(),
vec_box!()
);
struct C;
migration!(
C,
"c",
vec_box!((B.app(), B.name())),
vec_box!(),
vec_box!()
);
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B, C)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(&migrator, &mut conn, Plan::apply_count(2))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "b"]);
}
#[tokio::test]
async fn revert_count_reverts_last_n_applied() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(
B,
"b",
vec_box!((A.app(), A.name())),
vec_box!(),
vec_box!()
);
struct C;
migration!(
C,
"c",
vec_box!((B.app(), B.name())),
vec_box!(),
vec_box!()
);
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B, C)).unwrap();
migrator.add_applied_migrations(vec_box!(A, B, C)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(&migrator, &mut conn, Plan::revert_count(1))
.await
.unwrap();
assert_eq!(plan_names(&plan), vec!["c"]);
}
#[tokio::test]
async fn apply_count_zero_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A)).unwrap();
let mut conn = make_conn().await;
let err = apply_plan(&migrator, &mut conn, Plan::apply_count(0))
.await
.err()
.expect("apply_count(0) must return an error");
assert_eq!(err.to_string(), "plan error: count must be greater than 0");
}
#[tokio::test]
async fn revert_count_zero_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A)).unwrap();
migrator.add_applied_migrations(vec_box!(A)).unwrap();
let mut conn = make_conn().await;
let err = apply_plan(&migrator, &mut conn, Plan::revert_count(0))
.await
.err()
.expect("revert_count(0) must return an error");
assert_eq!(err.to_string(), "plan error: count must be greater than 0");
}
#[tokio::test]
async fn apply_count_exceeds_pending_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A)).unwrap();
let mut conn = make_conn().await;
let err = apply_plan(&migrator, &mut conn, Plan::apply_count(5))
.await
.err()
.expect("apply_count exceeding pending must return an error");
assert_eq!(
err.to_string(),
"plan error: passed count value is larger than migration length: 1"
);
}
#[tokio::test]
async fn revert_count_exceeds_applied_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A)).unwrap();
migrator.add_applied_migrations(vec_box!(A)).unwrap();
let mut conn = make_conn().await;
let err = apply_plan(&migrator, &mut conn, Plan::revert_count(5))
.await
.err()
.expect("revert_count exceeding applied must return an error");
assert_eq!(
err.to_string(),
"plan error: passed count value is larger than migration length: 1"
);
}
#[tokio::test]
async fn apply_all_already_applied_gives_empty_plan() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(
B,
"b",
vec_box!((A.app(), A.name())),
vec_box!(),
vec_box!()
);
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B)).unwrap();
migrator.add_applied_migrations(vec_box!(A, B)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(&migrator, &mut conn, Plan::apply_all())
.await
.unwrap();
assert!(plan.is_empty(), "all already applied → plan must be empty");
}
#[tokio::test]
async fn apply_name_run_before_pulls_in_required_parents() {
struct M;
migration!(M, "m", vec_box!(), vec_box!(), vec_box!());
struct N;
migration!(
N,
"n",
vec_box!((M.app(), M.name())),
vec_box!(),
vec_box!((T.app(), T.name()))
);
struct T;
migration!(T, "t", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(M, N, T)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(
&migrator,
&mut conn,
Plan::apply_name("test", &Some("t".to_string())),
)
.await
.unwrap();
let names = plan_names(&plan);
assert!(names.contains(&"m"), "M missing: {names:?}");
assert!(names.contains(&"n"), "N missing: {names:?}");
assert!(names.contains(&"t"), "T missing: {names:?}");
let pos = |n: &str| names.iter().position(|&x| x == n).unwrap();
assert!(pos("m") < pos("n"), "M must precede N: {names:?}");
assert!(pos("n") < pos("t"), "N must precede T: {names:?}");
}
#[tokio::test]
async fn revert_name_includes_all_dependent_children() {
struct M;
migration!(M, "m", vec_box!(), vec_box!(), vec_box!());
struct N;
migration!(
N,
"n",
vec_box!((M.app(), M.name())),
vec_box!(),
vec_box!((T.app(), T.name()))
);
struct T;
migration!(T, "t", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(M, N, T)).unwrap();
migrator.add_applied_migrations(vec_box!(M, N, T)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(
&migrator,
&mut conn,
Plan::revert_name("test", &Some("m".to_string())),
)
.await
.unwrap();
let names = plan_names(&plan);
assert!(names.contains(&"m"), "M missing: {names:?}");
assert!(names.contains(&"n"), "N missing: {names:?}");
let pos = |n: &str| names.iter().position(|&x| x == n).unwrap();
assert!(
pos("n") < pos("m"),
"N must be reverted before M: {names:?}"
);
}
#[tokio::test]
async fn diamond_dependency_resolves() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(
B,
"b",
vec_box!((A.app(), A.name())),
vec_box!(),
vec_box!()
);
struct C;
migration!(
C,
"c",
vec_box!((A.app(), A.name())),
vec_box!(),
vec_box!()
);
struct D;
migration!(
D,
"d",
vec_box!((B.app(), B.name()), (C.app(), C.name())),
vec_box!(),
vec_box!()
);
let mut migrator = CustomMigrator::default();
let plan = generate_apply_all_plan(&mut migrator, vec_box!(A, B, C, D))
.await
.unwrap();
let names = plan_names(&plan);
assert_eq!(
names.len(),
4,
"all four migrations must appear exactly once"
);
let pos = |n: &str| names.iter().position(|&x| x == n).unwrap();
assert!(pos("a") < pos("b"), "A before B: {names:?}");
assert!(pos("a") < pos("c"), "A before C: {names:?}");
assert!(pos("b") < pos("d"), "B before D: {names:?}");
assert!(pos("c") < pos("d"), "C before D: {names:?}");
}
#[tokio::test]
async fn apply_by_app_name_only() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(
B,
"b",
vec_box!((A.app(), A.name())),
vec_box!(),
vec_box!()
);
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B)).unwrap();
let mut conn = make_conn().await;
let plan = apply_plan(&migrator, &mut conn, Plan::apply_name("test", &None))
.await
.unwrap();
let names = plan_names(&plan);
assert!(names.contains(&"a"), "A missing: {names:?}");
assert!(names.contains(&"b"), "B missing: {names:?}");
}
#[tokio::test]
async fn apply_name_nonexistent_app_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A)).unwrap();
let mut conn = make_conn().await;
let err = apply_plan(
&migrator,
&mut conn,
Plan::apply_name("nonexistent_app", &None),
)
.await
.err()
.expect("nonexistent app must return an error");
assert_eq!(
err.to_string(),
"plan error: app nonexistent_app doesn't exists"
);
}
#[tokio::test]
async fn apply_name_nonexistent_migration_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A)).unwrap();
let mut conn = make_conn().await;
let err = apply_plan(
&migrator,
&mut conn,
Plan::apply_name("test", &Some("no_such_migration".to_string())),
)
.await
.err()
.expect("nonexistent migration name must return an error");
assert_eq!(
err.to_string(),
"plan error: migration test:no_such_migration doesn't exists for app"
);
}
#[tokio::test]
async fn revert_name_nonexistent_app_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A)).unwrap();
migrator.add_applied_migrations(vec_box!(A)).unwrap();
let mut conn = make_conn().await;
let err = apply_plan(
&migrator,
&mut conn,
Plan::revert_name("nonexistent_app", &None),
)
.await
.err()
.expect("nonexistent app must return an error");
assert_eq!(
err.to_string(),
"plan error: app nonexistent_app doesn't exists"
);
}
#[tokio::test]
async fn replace_and_replacer_both_applied_is_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
struct C;
migration!(C, "c", vec_box!(), vec_box!(B), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B, C)).unwrap();
migrator.add_applied_migrations(vec_box!(A, B, C)).unwrap();
let mut conn = make_conn().await;
let err = apply_plan(&migrator, &mut conn, Plan::apply_all())
.await
.err()
.expect("simultaneously applied replacer and replaced must return an error");
assert_eq!(
err.to_string(),
"plan error: migration test:c and its replaces are applied together"
);
}
#[tokio::test]
async fn generate_plan_with_rows_matches_apply_all() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
let mut migrator = CustomMigrator::default();
migrator.add_migrations(vec_box!(A, B)).unwrap();
let plan = migrator
.generate_migration_plan_with_rows(Some(&Plan::apply_all()), &[])
.unwrap();
assert_eq!(plan_names(&plan), vec!["a", "b"]);
}
#[tokio::test]
async fn sync_completes_without_error() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
let mut old_migrator = CustomMigrator::default();
old_migrator.add_migration(Box::new(A)).unwrap();
old_migrator.add_applied_migration(Box::new(A)).unwrap();
let mut new_migrator = CustomMigrator::default();
new_migrator.add_migrations(vec_box!(A, B)).unwrap();
let mut conn = make_conn().await;
new_migrator
.sync(&mut conn, &old_migrator)
.await
.expect("sync must complete without error");
}
#[tokio::test]
async fn sync_skips_unknown_migrations() {
struct A;
migration!(A, "a", vec_box!(), vec_box!(), vec_box!());
struct B;
migration!(B, "b", vec_box!(A), vec_box!(), vec_box!());
let mut old_migrator = CustomMigrator::default();
old_migrator.add_migrations(vec_box!(A, B)).unwrap();
old_migrator.add_applied_migrations(vec_box!(A, B)).unwrap();
let mut new_migrator = CustomMigrator::default();
new_migrator.add_migration(Box::new(A)).unwrap();
let mut conn = make_conn().await;
new_migrator
.sync(&mut conn, &old_migrator)
.await
.expect("sync must skip unknown migrations without error");
}