From acee7353429d041c79572f4136454df619c75178 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Mon, 21 Jul 2025 10:13:08 +0200 Subject: [PATCH 1/5] #277: Adding '+refinery NO TRANSACTION' directive --- README.md | 1 + .../migrations/V4__insert_entries_to_cars.sql | 4 + refinery/src/lib.rs | 4 +- refinery/tests/mysql.rs | 16 ++- refinery/tests/mysql_async.rs | 16 ++- refinery/tests/postgres.rs | 54 ++++++-- refinery/tests/rusqlite.rs | 55 ++++++-- refinery/tests/tiberius.rs | 16 ++- refinery/tests/tokio_postgres.rs | 53 ++++++-- refinery_cli/src/migrate.rs | 5 +- refinery_core/src/drivers/config.rs | 32 ++++- refinery_core/src/drivers/mysql.rs | 44 ++++++- refinery_core/src/drivers/mysql_async.rs | 26 +++- refinery_core/src/drivers/postgres.rs | 30 ++++- refinery_core/src/drivers/rusqlite.rs | 24 +++- refinery_core/src/drivers/tiberius.rs | 26 +++- refinery_core/src/drivers/tokio_postgres.rs | 26 +++- refinery_core/src/lib.rs | 4 +- refinery_core/src/runner.rs | 32 ++++- refinery_core/src/traits/async.rs | 37 ++++-- refinery_core/src/traits/mod.rs | 7 + refinery_core/src/traits/sync.rs | 122 ++++++++++++------ refinery_core/src/util.rs | 41 +++++- refinery_macros/src/lib.rs | 27 ++-- 24 files changed, 561 insertions(+), 141 deletions(-) create mode 100644 examples/migrations/V4__insert_entries_to_cars.sql diff --git a/README.md b/README.md index 105cf2ac..1c4ccb0a 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ This would stop developer 1's migration from ever running if you were using cont refinery works by creating a table that keeps all the applied migrations' versions and their metadata. When you [run](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.run) the migrations `Runner`, refinery compares the applied migrations with the ones to be applied, checking for [divergent](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.set_abort_divergent) and [missing](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.set_abort_missing) and executing unapplied migrations.\ By default, refinery runs each migration in a single transaction. Alternatively, you can also configure refinery to wrap the entire execution of all migrations in a single transaction by setting [set_grouped](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.set_grouped) to true. +Directive `-- +refinery NO TRANSACTION` can be used to escape running a migration within a transaction. [!IMPORTANT]: `set_grouped` takes precedence over no transaction directive. The rust crate intentionally ignores new migration files until your sourcecode is rebuild. This prevents accidental migrations and altering the database schema without any code changes. We can also bake the migrations into the binary, so no additional files are needed when deployed. ### Rollback diff --git a/examples/migrations/V4__insert_entries_to_cars.sql b/examples/migrations/V4__insert_entries_to_cars.sql new file mode 100644 index 00000000..2e4ea2e8 --- /dev/null +++ b/examples/migrations/V4__insert_entries_to_cars.sql @@ -0,0 +1,4 @@ +-- +refinery NO TRANSACTION +BEGIN; +INSERT INTO cars(id, name, brand) VALUES (2, "muscle", "toyota"); +COMMIT; diff --git a/refinery/src/lib.rs b/refinery/src/lib.rs index 93cfaac5..24fb69a1 100644 --- a/refinery/src/lib.rs +++ b/refinery/src/lib.rs @@ -32,7 +32,9 @@ for more examples refer to the [examples](https://github.com/rust-db/refinery/tr */ pub use refinery_core::config; -pub use refinery_core::{error, load_sql_migrations, Error, Migration, Report, Runner, Target}; +pub use refinery_core::{ + error, load_sql_migrations, Error, Migration, MigrationFlags, Report, Runner, Target, +}; #[doc(hidden)] pub use refinery_core::{AsyncMigrate, Migrate}; pub use refinery_macros::embed_migrations; diff --git a/refinery/tests/mysql.rs b/refinery/tests/mysql.rs index a9c487bd..ccedea11 100644 --- a/refinery/tests/mysql.rs +++ b/refinery/tests/mysql.rs @@ -35,30 +35,38 @@ mod mysql { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + &migrations::V1__initial::migration(), + Default::default(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), + Default::default(), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), + Default::default(), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", &migrations::V4__add_year_to_motos_table::migration(), + Default::default(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); @@ -473,6 +481,7 @@ mod mysql { let migration = Migration::unapplied( "V4__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); let err = conn @@ -509,6 +518,7 @@ mod mysql { let migration = Migration::unapplied( "V2__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); let err = conn @@ -552,12 +562,14 @@ mod mysql { "city varchar(255)", ");" ), + Default::default(), ) .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_table", include_str!("./migrations_missing/V2__add_cars_table.sql"), + Default::default(), ) .unwrap(); let err = conn diff --git a/refinery/tests/mysql_async.rs b/refinery/tests/mysql_async.rs index a3892ae4..4bdb1b4d 100644 --- a/refinery/tests/mysql_async.rs +++ b/refinery/tests/mysql_async.rs @@ -19,30 +19,38 @@ mod mysql_async { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + &migrations::V1__initial::migration(), + Default::default(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), + Default::default(), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), + Default::default(), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", &migrations::V4__add_year_to_motos_table::migration(), + Default::default(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); @@ -483,6 +491,7 @@ mod mysql_async { let migration = Migration::unapplied( "V4__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); @@ -528,6 +537,7 @@ mod mysql_async { let migration = Migration::unapplied( "V2__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); @@ -575,12 +585,14 @@ mod mysql_async { "city varchar(255)", ");" ), + Default::default(), ) .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_table", include_str!("./migrations_missing/V2__add_cars_table.sql"), + Default::default(), ) .unwrap(); let err = pool diff --git a/refinery/tests/postgres.rs b/refinery/tests/postgres.rs index ec10a2c8..b684c9b7 100644 --- a/refinery/tests/postgres.rs +++ b/refinery/tests/postgres.rs @@ -5,7 +5,8 @@ mod postgres { use assert_cmd::prelude::*; use predicates::str::contains; use refinery::{ - config::Config, embed_migrations, error::Kind, Migrate, Migration, Runner, Target, + config::Config, embed_migrations, error::Kind, Migrate, Migration, MigrationFlags, Runner, + Target, }; use refinery_core::postgres::{Client, NoTls}; use std::process::Command; @@ -36,34 +37,53 @@ mod postgres { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + &migrations::V1__initial::migration(), + Default::default(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), + Default::default(), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), + Default::default(), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", &migrations::V4__add_year_to_motos_table::migration(), + Default::default(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), + ) + .unwrap(); + + let migration6 = Migration::unapplied( + "V6__index_motos_table_concurrently", + "CREATE INDEX CONCURRENTLY motos_name ON motos(name);", + MigrationFlags { + run_in_transaction: false, + }, ) .unwrap(); - vec![migration1, migration2, migration3, migration4, migration5] + vec![ + migration1, migration2, migration3, migration4, migration5, migration6, + ] } fn prep_database() { @@ -342,7 +362,7 @@ mod postgres { let migrations = get_migrations(); - let mchecksum = migrations[4].checksum(); + let mchecksum = migrations[5].checksum(); client .migrate( &migrations, @@ -359,7 +379,7 @@ mod postgres { .unwrap() .unwrap(); - assert_eq!(5, current.version()); + assert_eq!(6, current.version()); assert_eq!(mchecksum, current.checksum()); }); } @@ -445,6 +465,7 @@ mod postgres { let migration = Migration::unapplied( "V4__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); let err = client @@ -478,6 +499,7 @@ mod postgres { let migration = Migration::unapplied( "V2__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); let err = client @@ -518,12 +540,14 @@ mod postgres { "city varchar(255)", ");" ), + Default::default(), ) .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_table", include_str!("./migrations_missing/V2__add_cars_table.sql"), + Default::default(), ) .unwrap(); let err = client @@ -560,25 +584,28 @@ mod postgres { runner.run(&mut config).unwrap(); let applied_migrations = runner.get_applied_migrations(&mut config).unwrap(); - assert_eq!(5, applied_migrations.len()); + assert_eq!(6, applied_migrations.len()); assert_eq!(migrations[0].version(), applied_migrations[0].version()); assert_eq!(migrations[1].version(), applied_migrations[1].version()); assert_eq!(migrations[2].version(), applied_migrations[2].version()); assert_eq!(migrations[3].version(), applied_migrations[3].version()); assert_eq!(migrations[4].version(), applied_migrations[4].version()); + assert_eq!(migrations[5].version(), applied_migrations[5].version()); assert_eq!(migrations[0].name(), migrations[0].name()); assert_eq!(migrations[1].name(), applied_migrations[1].name()); assert_eq!(migrations[2].name(), applied_migrations[2].name()); assert_eq!(migrations[3].name(), applied_migrations[3].name()); assert_eq!(migrations[4].name(), applied_migrations[4].name()); + assert_eq!(migrations[5].name(), applied_migrations[5].name()); assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); assert_eq!(migrations[4].checksum(), applied_migrations[4].checksum()); + assert_eq!(migrations[5].checksum(), applied_migrations[5].checksum()); }) } @@ -596,25 +623,28 @@ mod postgres { let report = runner.run(&mut config).unwrap(); let applied_migrations = report.applied_migrations(); - assert_eq!(5, applied_migrations.len()); + assert_eq!(6, applied_migrations.len()); assert_eq!(migrations[0].version(), applied_migrations[0].version()); assert_eq!(migrations[1].version(), applied_migrations[1].version()); assert_eq!(migrations[2].version(), applied_migrations[2].version()); assert_eq!(migrations[3].version(), applied_migrations[3].version()); assert_eq!(migrations[4].version(), applied_migrations[4].version()); + assert_eq!(migrations[5].version(), applied_migrations[5].version()); assert_eq!(migrations[0].name(), migrations[0].name()); assert_eq!(migrations[1].name(), applied_migrations[1].name()); assert_eq!(migrations[2].name(), applied_migrations[2].name()); assert_eq!(migrations[3].name(), applied_migrations[3].name()); assert_eq!(migrations[4].name(), applied_migrations[4].name()); + assert_eq!(migrations[5].name(), applied_migrations[5].name()); assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); assert_eq!(migrations[4].checksum(), applied_migrations[4].checksum()); + assert_eq!(migrations[5].checksum(), applied_migrations[5].checksum()); }) } @@ -635,11 +665,11 @@ mod postgres { .get_last_applied_migration(&mut config) .unwrap() .unwrap(); - assert_eq!(5, applied_migration.version()); + assert_eq!(6, applied_migration.version()); - assert_eq!(migrations[4].version(), applied_migration.version()); - assert_eq!(migrations[4].name(), applied_migration.name()); - assert_eq!(migrations[4].checksum(), applied_migration.checksum()); + assert_eq!(migrations[5].version(), applied_migration.version()); + assert_eq!(migrations[5].name(), applied_migration.name()); + assert_eq!(migrations[5].checksum(), applied_migration.checksum()); }) } diff --git a/refinery/tests/rusqlite.rs b/refinery/tests/rusqlite.rs index 258af424..bdc8eb02 100644 --- a/refinery/tests/rusqlite.rs +++ b/refinery/tests/rusqlite.rs @@ -8,7 +8,7 @@ mod rusqlite { config::{Config, ConfigDbType}, embed_migrations, error::Kind, - Migrate, Migration, Runner, Target, + Migrate, Migration, MigrationFlags, Runner, Target, }; use refinery_core::rusqlite::Error; use refinery_core::rusqlite::{Connection, OptionalExtension}; @@ -50,34 +50,55 @@ mod rusqlite { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + &migrations::V1__initial::migration(), + Default::default(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), + Default::default(), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), + Default::default(), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", &migrations::V4__add_year_to_motos_table::migration(), + Default::default(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), + ) + .unwrap(); + + let migration6 = Migration::unapplied( + "V6__add_entries_to_cars", + r#"BEGIN; + INSERT INTO cars(id, name, brand) VALUES (2, "muscle", "toyota"); + COMMIT;"#, + MigrationFlags { + run_in_transaction: false, + }, ) .unwrap(); - vec![migration1, migration2, migration3, migration4, migration5] + vec![ + migration1, migration2, migration3, migration4, migration5, migration6, + ] } #[test] @@ -441,7 +462,7 @@ mod rusqlite { let migrations = get_migrations(); - let mchecksum = migrations[4].checksum(); + let mchecksum = migrations[5].checksum(); conn.migrate( &migrations, true, @@ -457,7 +478,7 @@ mod rusqlite { .unwrap() .unwrap(); - assert_eq!(5, current.version()); + assert_eq!(6, current.version()); assert_eq!(mchecksum, current.checksum()); } @@ -573,6 +594,7 @@ mod rusqlite { let migration = Migration::unapplied( "V4__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); let err = conn @@ -604,6 +626,7 @@ mod rusqlite { let migration = Migration::unapplied( "V2__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); let err = conn @@ -642,12 +665,14 @@ mod rusqlite { "city varchar(255)", ");" ), + Default::default(), ) .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_table", include_str!("./migrations_missing/V2__add_cars_table.sql"), + Default::default(), ) .unwrap(); let err = conn @@ -683,25 +708,28 @@ mod rusqlite { runner.run(&mut config).unwrap(); let applied_migrations = runner.get_applied_migrations(&mut config).unwrap(); - assert_eq!(5, applied_migrations.len()); + assert_eq!(6, applied_migrations.len()); assert_eq!(migrations[0].version(), applied_migrations[0].version()); assert_eq!(migrations[1].version(), applied_migrations[1].version()); assert_eq!(migrations[2].version(), applied_migrations[2].version()); assert_eq!(migrations[3].version(), applied_migrations[3].version()); assert_eq!(migrations[4].version(), applied_migrations[4].version()); + assert_eq!(migrations[5].version(), applied_migrations[5].version()); assert_eq!(migrations[0].name(), migrations[0].name()); assert_eq!(migrations[1].name(), applied_migrations[1].name()); assert_eq!(migrations[2].name(), applied_migrations[2].name()); assert_eq!(migrations[3].name(), applied_migrations[3].name()); assert_eq!(migrations[4].name(), applied_migrations[4].name()); + assert_eq!(migrations[5].name(), applied_migrations[5].name()); assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); assert_eq!(migrations[4].checksum(), applied_migrations[4].checksum()); + assert_eq!(migrations[5].checksum(), applied_migrations[5].checksum()); } #[test] @@ -718,25 +746,28 @@ mod rusqlite { let report = runner.run(&mut config).unwrap(); let applied_migrations = report.applied_migrations(); - assert_eq!(5, applied_migrations.len()); + assert_eq!(6, applied_migrations.len()); assert_eq!(migrations[0].version(), applied_migrations[0].version()); assert_eq!(migrations[1].version(), applied_migrations[1].version()); assert_eq!(migrations[2].version(), applied_migrations[2].version()); assert_eq!(migrations[3].version(), applied_migrations[3].version()); assert_eq!(migrations[4].version(), applied_migrations[4].version()); + assert_eq!(migrations[5].version(), applied_migrations[5].version()); assert_eq!(migrations[0].name(), migrations[0].name()); assert_eq!(migrations[1].name(), applied_migrations[1].name()); assert_eq!(migrations[2].name(), applied_migrations[2].name()); assert_eq!(migrations[3].name(), applied_migrations[3].name()); assert_eq!(migrations[4].name(), applied_migrations[4].name()); + assert_eq!(migrations[5].name(), applied_migrations[5].name()); assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); assert_eq!(migrations[4].checksum(), applied_migrations[4].checksum()); + assert_eq!(migrations[5].checksum(), applied_migrations[5].checksum()); } #[test] @@ -756,11 +787,11 @@ mod rusqlite { .get_last_applied_migration(&mut config) .unwrap() .unwrap(); - assert_eq!(5, applied_migration.version()); + assert_eq!(6, applied_migration.version()); - assert_eq!(migrations[4].version(), applied_migration.version()); - assert_eq!(migrations[4].name(), applied_migration.name()); - assert_eq!(migrations[4].checksum(), applied_migration.checksum()); + assert_eq!(migrations[5].version(), applied_migration.version()); + assert_eq!(migrations[5].name(), applied_migration.name()); + assert_eq!(migrations[5].checksum(), applied_migration.checksum()); } #[test] diff --git a/refinery/tests/tiberius.rs b/refinery/tests/tiberius.rs index f6346063..a03a5234 100644 --- a/refinery/tests/tiberius.rs +++ b/refinery/tests/tiberius.rs @@ -22,30 +22,38 @@ mod tiberius { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + &migrations::V1__initial::migration(), + Default::default(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), + Default::default(), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), + Default::default(), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", &migrations::V4__add_year_to_motos_table::migration(), + Default::default(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); @@ -128,6 +136,7 @@ mod tiberius { let migration = Migration::unapplied( "V4__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); let err = client @@ -179,6 +188,7 @@ mod tiberius { let migration = Migration::unapplied( "V2__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); let err = client @@ -237,12 +247,14 @@ mod tiberius { "city varchar(255)", ");" ), + Default::default(), ) .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_table", include_str!("./migrations_missing/V2__add_cars_table.sql"), + Default::default(), ) .unwrap(); let err = client diff --git a/refinery/tests/tokio_postgres.rs b/refinery/tests/tokio_postgres.rs index 2f7f7fc6..2b30d54d 100644 --- a/refinery/tests/tokio_postgres.rs +++ b/refinery/tests/tokio_postgres.rs @@ -7,7 +7,7 @@ mod tokio_postgres { config::{Config, ConfigDbType}, embed_migrations, error::Kind, - AsyncMigrate, Migration, Runner, Target, + AsyncMigrate, Migration, MigrationFlags, Runner, Target, }; use refinery_core::tokio_postgres; use refinery_core::tokio_postgres::NoTls; @@ -19,34 +19,53 @@ mod tokio_postgres { fn get_migrations() -> Vec { embed_migrations!("./tests/migrations"); - let migration1 = - Migration::unapplied("V1__initial.rs", &migrations::V1__initial::migration()).unwrap(); + let migration1 = Migration::unapplied( + "V1__initial.rs", + &migrations::V1__initial::migration(), + Default::default(), + ) + .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_and_motos_table.sql", include_str!("./migrations/V1-2/V2__add_cars_and_motos_table.sql"), + Default::default(), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", include_str!("./migrations/V3/V3__add_brand_to_cars_table.sql"), + Default::default(), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_to_motos_table.rs", &migrations::V4__add_year_to_motos_table::migration(), + Default::default(), ) .unwrap(); let migration5 = Migration::unapplied( "V5__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), + ) + .unwrap(); + + let migration6 = Migration::unapplied( + "V6__index_motos_table_concurrently", + "CREATE INDEX CONCURRENTLY motos_name ON motos(name);", + MigrationFlags { + run_in_transaction: false, + }, ) .unwrap(); - vec![migration1, migration2, migration3, migration4, migration5] + vec![ + migration1, migration2, migration3, migration4, migration5, migration6, + ] } mod embedded { @@ -485,7 +504,7 @@ mod tokio_postgres { .unwrap(); let migrations = get_migrations(); - let mchecksum = migrations[4].checksum(); + let mchecksum = migrations[5].checksum(); client .migrate( @@ -504,7 +523,7 @@ mod tokio_postgres { .await .unwrap() .unwrap(); - assert_eq!(5, current.version()); + assert_eq!(6, current.version()); assert_eq!(mchecksum, current.checksum()); }) .await; @@ -621,6 +640,7 @@ mod tokio_postgres { let migration = Migration::unapplied( "V4__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); let err = client @@ -666,6 +686,7 @@ mod tokio_postgres { let migration = Migration::unapplied( "V2__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); @@ -719,12 +740,14 @@ mod tokio_postgres { "city varchar(255)", ");" ), + Default::default(), ) .unwrap(); let migration2 = Migration::unapplied( "V2__add_cars_table", include_str!("./migrations_missing/V2__add_cars_table.sql"), + Default::default(), ) .unwrap(); let err = client @@ -771,25 +794,28 @@ mod tokio_postgres { .get_applied_migrations_async(&mut config) .await .unwrap(); - assert_eq!(5, applied_migrations.len()); + assert_eq!(6, applied_migrations.len()); assert_eq!(migrations[0].version(), applied_migrations[0].version()); assert_eq!(migrations[1].version(), applied_migrations[1].version()); assert_eq!(migrations[2].version(), applied_migrations[2].version()); assert_eq!(migrations[3].version(), applied_migrations[3].version()); assert_eq!(migrations[4].version(), applied_migrations[4].version()); + assert_eq!(migrations[5].version(), applied_migrations[5].version()); assert_eq!(migrations[0].name(), migrations[0].name()); assert_eq!(migrations[1].name(), applied_migrations[1].name()); assert_eq!(migrations[2].name(), applied_migrations[2].name()); assert_eq!(migrations[3].name(), applied_migrations[3].name()); assert_eq!(migrations[4].name(), applied_migrations[4].name()); + assert_eq!(migrations[5].name(), applied_migrations[5].name()); assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); assert_eq!(migrations[4].checksum(), applied_migrations[4].checksum()); + assert_eq!(migrations[5].checksum(), applied_migrations[5].checksum()); }) .await; } @@ -812,25 +838,28 @@ mod tokio_postgres { let report = runner.run_async(&mut config).await.unwrap(); let applied_migrations = report.applied_migrations(); - assert_eq!(5, applied_migrations.len()); + assert_eq!(6, applied_migrations.len()); assert_eq!(migrations[0].version(), applied_migrations[0].version()); assert_eq!(migrations[1].version(), applied_migrations[1].version()); assert_eq!(migrations[2].version(), applied_migrations[2].version()); assert_eq!(migrations[3].version(), applied_migrations[3].version()); assert_eq!(migrations[4].version(), applied_migrations[4].version()); + assert_eq!(migrations[5].version(), applied_migrations[5].version()); assert_eq!(migrations[0].name(), migrations[0].name()); assert_eq!(migrations[1].name(), applied_migrations[1].name()); assert_eq!(migrations[2].name(), applied_migrations[2].name()); assert_eq!(migrations[3].name(), applied_migrations[3].name()); assert_eq!(migrations[4].name(), applied_migrations[4].name()); + assert_eq!(migrations[5].name(), applied_migrations[5].name()); assert_eq!(migrations[0].checksum(), applied_migrations[0].checksum()); assert_eq!(migrations[1].checksum(), applied_migrations[1].checksum()); assert_eq!(migrations[2].checksum(), applied_migrations[2].checksum()); assert_eq!(migrations[3].checksum(), applied_migrations[3].checksum()); assert_eq!(migrations[4].checksum(), applied_migrations[4].checksum()); + assert_eq!(migrations[5].checksum(), applied_migrations[5].checksum()); }) .await; } @@ -857,11 +886,11 @@ mod tokio_postgres { .await .unwrap() .unwrap(); - assert_eq!(5, applied_migration.version()); + assert_eq!(6, applied_migration.version()); - assert_eq!(migrations[4].version(), applied_migration.version()); - assert_eq!(migrations[4].name(), applied_migration.name()); - assert_eq!(migrations[4].checksum(), applied_migration.checksum()); + assert_eq!(migrations[5].version(), applied_migration.version()); + assert_eq!(migrations[5].name(), applied_migration.name()); + assert_eq!(migrations[5].checksum(), applied_migration.checksum()); }) .await; } diff --git a/refinery_cli/src/migrate.rs b/refinery_cli/src/migrate.rs index b61fa876..967d34f3 100644 --- a/refinery_cli/src/migrate.rs +++ b/refinery_cli/src/migrate.rs @@ -3,7 +3,7 @@ use std::path::Path; use anyhow::Context; use refinery_core::{ config::{Config, ConfigDbType}, - find_migration_files, Migration, MigrationType, Runner, Target, + find_migration_files, parse_flags, Migration, MigrationType, Runner, Target, }; use crate::cli::MigrateArgs; @@ -47,7 +47,8 @@ fn run_migrations( .and_then(|file| file.to_os_string().into_string().ok()) .unwrap(); - let migration = Migration::unapplied(&filename, &sql) + let flags = parse_flags(&sql, MigrationType::Sql); + let migration = Migration::unapplied(&filename, &sql, flags) .with_context(|| format!("could not read migration file name {}", path.display()))?; migrations.push(migration); } diff --git a/refinery_core/src/drivers/config.rs b/refinery_core/src/drivers/config.rs index 639ff108..60c8d283 100644 --- a/refinery_core/src/drivers/config.rs +++ b/refinery_core/src/drivers/config.rs @@ -7,20 +7,29 @@ use crate::config::build_db_url; use crate::config::{Config, ConfigDbType}; use crate::error::WrapMigrationError; -use crate::traits::r#async::{AsyncQuery, AsyncTransaction}; -use crate::traits::sync::{Query, Transaction}; +use crate::traits::r#async::{AsyncExecutor, AsyncQuery}; +use crate::traits::sync::{Executor, Query}; use crate::traits::{GET_APPLIED_MIGRATIONS_QUERY, GET_LAST_APPLIED_MIGRATION_QUERY}; -use crate::{Error, Migration, Report, Target}; +use crate::{Error, Migration, MigrationFlags, Report, Target}; use async_trait::async_trait; use std::convert::Infallible; // we impl all the dependent traits as noop's and then override the methods that call them on Migrate and AsyncMigrate -impl Transaction for Config { +impl Executor for Config { type Error = Infallible; fn execute(&mut self, _queries: &[&str]) -> Result { Ok(0) } + + fn execute_single( + &mut self, + _query: &str, + _update_query: &str, + _flags: &MigrationFlags, + ) -> Result { + Ok(0) + } } impl Query> for Config { @@ -30,12 +39,21 @@ impl Query> for Config { } #[async_trait] -impl AsyncTransaction for Config { +impl AsyncExecutor for Config { type Error = Infallible; async fn execute(&mut self, _queries: &[&str]) -> Result { Ok(0) } + + async fn execute_single( + &mut self, + _query: &str, + _update_query: &str, + _flags: &MigrationFlags, + ) -> Result { + Ok(0) + } } #[async_trait] @@ -43,7 +61,7 @@ impl AsyncQuery> for Config { async fn query( &mut self, _query: &str, - ) -> Result, ::Error> { + ) -> Result, ::Error> { Ok(Vec::new()) } } @@ -161,7 +179,7 @@ macro_rules! with_connection_async { } } -// rewrite all the default methods as we overrode Transaction and Query +// rewrite all the default methods as we overrode Executor and Query #[cfg(any(feature = "mysql", feature = "postgres", feature = "rusqlite"))] impl crate::Migrate for Config { fn get_last_applied_migration( diff --git a/refinery_core/src/drivers/mysql.rs b/refinery_core/src/drivers/mysql.rs index 33148d94..42cf27a9 100644 --- a/refinery_core/src/drivers/mysql.rs +++ b/refinery_core/src/drivers/mysql.rs @@ -1,5 +1,5 @@ -use crate::traits::sync::{Migrate, Query, Transaction}; -use crate::Migration; +use crate::traits::sync::{Executor, Migrate, Query}; +use crate::{Migration, MigrationFlags}; use mysql::{ error::Error as MError, prelude::Queryable, Conn, IsolationLevel, PooledConn, Transaction as MTransaction, TxOpts, @@ -40,7 +40,7 @@ fn query_applied_migrations( Ok(applied) } -impl Transaction for Conn { +impl Executor for Conn { type Error = MError; fn execute(&mut self, queries: &[&str]) -> Result { @@ -53,9 +53,27 @@ impl Transaction for Conn { transaction.commit()?; Ok(count as usize) } + + fn execute_single( + &mut self, + query: &str, + update_query: &str, + flags: &MigrationFlags, + ) -> Result { + if flags.run_in_transaction { + Executor::execute(self, &[query, update_query]) + } else { + self.query_iter(query)?; + if let Err(e) = self.query_iter(update_query) { + log::error!("applied migration but schema history table update failed"); + return Err(e); + } + Ok(2) + } + } } -impl Transaction for PooledConn { +impl Executor for PooledConn { type Error = MError; fn execute(&mut self, queries: &[&str]) -> Result { @@ -69,6 +87,24 @@ impl Transaction for PooledConn { transaction.commit()?; Ok(count as usize) } + + fn execute_single( + &mut self, + query: &str, + update_query: &str, + flags: &MigrationFlags, + ) -> Result { + if flags.run_in_transaction { + Executor::execute(self, &[query, update_query]) + } else { + self.query_iter(query)?; + if let Err(e) = self.query_iter(update_query) { + log::error!("applied migration but schema history table update failed"); + return Err(e); + } + Ok(2) + } + } } impl Query> for Conn { diff --git a/refinery_core/src/drivers/mysql_async.rs b/refinery_core/src/drivers/mysql_async.rs index 101ab0ab..5b5a235b 100644 --- a/refinery_core/src/drivers/mysql_async.rs +++ b/refinery_core/src/drivers/mysql_async.rs @@ -1,5 +1,5 @@ -use crate::traits::r#async::{AsyncMigrate, AsyncQuery, AsyncTransaction}; -use crate::Migration; +use crate::traits::r#async::{AsyncExecutor, AsyncMigrate, AsyncQuery}; +use crate::{Migration, MigrationFlags}; use async_trait::async_trait; use mysql_async::{ prelude::Queryable, Error as MError, IsolationLevel, Pool, Transaction as MTransaction, TxOpts, @@ -36,7 +36,7 @@ async fn query_applied_migrations<'a>( } #[async_trait] -impl AsyncTransaction for Pool { +impl AsyncExecutor for Pool { type Error = MError; async fn execute(&mut self, queries: &[&str]) -> Result { @@ -53,6 +53,24 @@ impl AsyncTransaction for Pool { transaction.commit().await?; Ok(count as usize) } + + async fn execute_single( + &mut self, + query: &str, + update_query: &str, + flags: &MigrationFlags, + ) -> Result { + if flags.run_in_transaction { + AsyncExecutor::execute(self, &[query, update_query]).await + } else { + self.query(query).await?; + if let Err(e) = self.query(update_query).await { + log::error!("applied migration but schema history table update failed"); + return Err(e); + } + Ok(2) + } + } } #[async_trait] @@ -60,7 +78,7 @@ impl AsyncQuery> for Pool { async fn query( &mut self, query: &str, - ) -> Result, ::Error> { + ) -> Result, ::Error> { let mut conn = self.get_conn().await?; let mut options = TxOpts::new(); options.with_isolation_level(Some(IsolationLevel::ReadCommitted)); diff --git a/refinery_core/src/drivers/postgres.rs b/refinery_core/src/drivers/postgres.rs index 3d177509..a2c38de1 100644 --- a/refinery_core/src/drivers/postgres.rs +++ b/refinery_core/src/drivers/postgres.rs @@ -1,5 +1,5 @@ -use crate::traits::sync::{Migrate, Query, Transaction}; -use crate::Migration; +use crate::traits::sync::{Executor, Migrate, Query}; +use crate::{Migration, MigrationFlags}; use postgres::{Client as PgClient, Error as PgError, Transaction as PgTransaction}; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; @@ -30,19 +30,37 @@ fn query_applied_migrations( Ok(applied) } -impl Transaction for PgClient { +impl Executor for PgClient { type Error = PgError; fn execute(&mut self, queries: &[&str]) -> Result { - let mut transaction = PgClient::transaction(self)?; + let mut tx = self.transaction()?; let mut count = 0; for query in queries.iter() { - PgTransaction::batch_execute(&mut transaction, query)?; + tx.batch_execute(&query)?; count += 1; } - transaction.commit()?; + tx.commit()?; Ok(count as usize) } + + fn execute_single( + &mut self, + query: &str, + update_query: &str, + flags: &MigrationFlags, + ) -> Result { + if flags.run_in_transaction { + Executor::execute(self, &[query, update_query]) + } else { + self.simple_query(query)?; + if let Err(e) = self.simple_query(update_query) { + log::error!("applied migration but schema history table update failed"); + return Err(e); + } + Ok(2) + } + } } impl Query> for PgClient { diff --git a/refinery_core/src/drivers/rusqlite.rs b/refinery_core/src/drivers/rusqlite.rs index 9547ba48..f3f38995 100644 --- a/refinery_core/src/drivers/rusqlite.rs +++ b/refinery_core/src/drivers/rusqlite.rs @@ -1,5 +1,5 @@ -use crate::traits::sync::{Migrate, Query, Transaction}; -use crate::Migration; +use crate::traits::sync::{Executor, Migrate, Query}; +use crate::{Migration, MigrationFlags}; use rusqlite::{Connection as RqlConnection, Error as RqlError}; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; @@ -30,7 +30,7 @@ fn query_applied_migrations( Ok(applied) } -impl Transaction for RqlConnection { +impl Executor for RqlConnection { type Error = RqlError; fn execute(&mut self, queries: &[&str]) -> Result { let transaction = self.transaction()?; @@ -42,6 +42,24 @@ impl Transaction for RqlConnection { transaction.commit()?; Ok(count) } + + fn execute_single( + &mut self, + query: &str, + update_query: &str, + flags: &MigrationFlags, + ) -> Result { + if flags.run_in_transaction { + Executor::execute(self, &[query, update_query]) + } else { + self.execute_batch(query)?; + if let Err(e) = self.execute_batch(update_query) { + log::error!("applied migration but schema history table update failed"); + return Err(e); + } + Ok(2) + } + } } impl Query> for RqlConnection { diff --git a/refinery_core/src/drivers/tiberius.rs b/refinery_core/src/drivers/tiberius.rs index 117c6344..103bc08e 100644 --- a/refinery_core/src/drivers/tiberius.rs +++ b/refinery_core/src/drivers/tiberius.rs @@ -1,5 +1,5 @@ -use crate::traits::r#async::{AsyncMigrate, AsyncQuery, AsyncTransaction}; -use crate::Migration; +use crate::traits::r#async::{AsyncExecutor, AsyncMigrate, AsyncQuery}; +use crate::{Migration, MigrationFlags}; use async_trait::async_trait; use futures::{ @@ -40,7 +40,7 @@ async fn query_applied_migrations( } #[async_trait] -impl AsyncTransaction for Client +impl AsyncExecutor for Client where S: AsyncRead + AsyncWrite + Unpin + Send, { @@ -63,6 +63,24 @@ where self.simple_query("COMMIT TRAN T1").await?; Ok(count as usize) } + + async fn execute_single( + &mut self, + query: &str, + update_query: &str, + flags: &MigrationFlags, + ) -> Result { + if flags.run_in_transaction { + AsyncExecutor::execute(self, &[query, update_query]).await + } else { + self.simple_query(query).await?; + if let Err(e) = self.simple_query(update_query).await { + log::error!("applied migration but schema history table update failed"); + return Err(e); + } + Ok(2) + } + } } #[async_trait] @@ -73,7 +91,7 @@ where async fn query( &mut self, query: &str, - ) -> Result, ::Error> { + ) -> Result, ::Error> { let applied = query_applied_migrations(self, query).await?; Ok(applied) } diff --git a/refinery_core/src/drivers/tokio_postgres.rs b/refinery_core/src/drivers/tokio_postgres.rs index ec8bb9c8..8cccc272 100644 --- a/refinery_core/src/drivers/tokio_postgres.rs +++ b/refinery_core/src/drivers/tokio_postgres.rs @@ -1,5 +1,5 @@ -use crate::traits::r#async::{AsyncMigrate, AsyncQuery, AsyncTransaction}; -use crate::Migration; +use crate::traits::r#async::{AsyncExecutor, AsyncMigrate, AsyncQuery}; +use crate::{Migration, MigrationFlags}; use async_trait::async_trait; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; @@ -32,7 +32,7 @@ async fn query_applied_migrations( } #[async_trait] -impl AsyncTransaction for Client { +impl AsyncExecutor for Client { type Error = PgError; async fn execute(&mut self, queries: &[&str]) -> Result { @@ -45,6 +45,24 @@ impl AsyncTransaction for Client { transaction.commit().await?; Ok(count as usize) } + + async fn execute_single( + &mut self, + query: &str, + update_query: &str, + flags: &MigrationFlags, + ) -> Result { + if flags.run_in_transaction { + AsyncExecutor::execute(self, &[query, update_query]).await + } else { + self.simple_query(query).await?; + if let Err(e) = self.simple_query(update_query).await { + log::error!("applied migration but schema history table update failed"); + return Err(e); + } + Ok(2) + } + } } #[async_trait] @@ -52,7 +70,7 @@ impl AsyncQuery> for Client { async fn query( &mut self, query: &str, - ) -> Result, ::Error> { + ) -> Result, ::Error> { let transaction = self.transaction().await?; let applied = query_applied_migrations(&transaction, query).await?; transaction.commit().await?; diff --git a/refinery_core/src/lib.rs b/refinery_core/src/lib.rs index b4d85b77..f1db53c0 100644 --- a/refinery_core/src/lib.rs +++ b/refinery_core/src/lib.rs @@ -6,11 +6,11 @@ pub mod traits; mod util; pub use crate::error::Error; -pub use crate::runner::{Migration, Report, Runner, Target}; +pub use crate::runner::{Migration, MigrationFlags, Report, Runner, Target}; pub use crate::traits::r#async::AsyncMigrate; pub use crate::traits::sync::Migrate; pub use crate::util::{ - find_migration_files, load_sql_migrations, parse_migration_name, MigrationType, + find_migration_files, load_sql_migrations, parse_flags, parse_migration_name, MigrationType, }; #[cfg(feature = "rusqlite")] diff --git a/refinery_core/src/runner.rs b/refinery_core/src/runner.rs index af8e2ab5..6f9b3c26 100644 --- a/refinery_core/src/runner.rs +++ b/refinery_core/src/runner.rs @@ -8,7 +8,7 @@ use std::fmt; use std::hash::{Hash, Hasher}; use crate::traits::{sync::migrate as sync_migrate, DEFAULT_MIGRATION_TABLE_NAME}; -use crate::util::parse_migration_name; +use crate::util::{parse_flags, parse_migration_name}; use crate::{AsyncMigrate, Error, Migrate}; use std::fmt::Formatter; @@ -70,12 +70,32 @@ pub struct Migration { prefix: Type, sql: Option, applied_on: Option, + flags: MigrationFlags, +} + +#[derive(Clone, Debug)] +pub struct MigrationFlags { + // Migrations by default run in transaction except explicitly specified by the + // `-- +refinery NO TRANSACTION` directive + pub run_in_transaction: bool, +} + +impl Default for MigrationFlags { + fn default() -> Self { + Self { + run_in_transaction: true, + } + } } impl Migration { /// Create an unapplied migration, name and version are parsed from the input_name, /// which must be named in the format (U|V){1}__{2}.rs where {1} represents the migration version and {2} the name. - pub fn unapplied(input_name: &str, sql: &str) -> Result { + pub fn unapplied( + input_name: &str, + sql: &str, + flags: MigrationFlags, + ) -> Result { let (prefix, version, name) = parse_migration_name(input_name)?; // Previously, `std::collections::hash_map::DefaultHasher` was used @@ -100,6 +120,7 @@ impl Migration { sql: Some(sql.into()), applied_on: None, checksum, + flags, }) } @@ -119,6 +140,9 @@ impl Migration { prefix: Type::Versioned, sql: None, applied_on: Some(applied_on), + flags: MigrationFlags { + run_in_transaction: true, + }, } } @@ -133,6 +157,10 @@ impl Migration { self.sql.as_deref() } + pub fn flags(&self) -> &MigrationFlags { + &self.flags + } + /// Get the Migration version pub fn version(&self) -> u32 { self.version as u32 diff --git a/refinery_core/src/traits/async.rs b/refinery_core/src/traits/async.rs index fc9e4f75..aeb4b47a 100644 --- a/refinery_core/src/traits/async.rs +++ b/refinery_core/src/traits/async.rs @@ -3,25 +3,35 @@ use crate::traits::{ insert_migration_query, verify_migrations, ASSERT_MIGRATIONS_TABLE_QUERY, GET_APPLIED_MIGRATIONS_QUERY, GET_LAST_APPLIED_MIGRATION_QUERY, }; -use crate::{Error, Migration, Report, Target}; +use crate::{Error, Migration, MigrationFlags, Report, Target}; use async_trait::async_trait; use std::string::ToString; #[async_trait] -pub trait AsyncTransaction { +pub trait AsyncExecutor { type Error: std::error::Error + Send + Sync + 'static; + // Run multiple queries implicitly in a transaction async fn execute(&mut self, query: &[&str]) -> Result; + + // Run single query along with a query to update the migration table. + // Offers more granular control via MigrationFlags + async fn execute_single( + &mut self, + query: &str, + update_query: &str, + flags: &MigrationFlags, + ) -> Result; } #[async_trait] -pub trait AsyncQuery: AsyncTransaction { +pub trait AsyncQuery: AsyncExecutor { async fn query(&mut self, query: &str) -> Result; } -async fn migrate( - transaction: &mut T, +async fn migrate_individual( + executor: &mut T, migrations: Vec, target: Target, migration_table_name: &str, @@ -42,11 +52,12 @@ async fn migrate( log::info!("applying migration: {}", migration); migration.set_applied(); let update_query = insert_migration_query(&migration, migration_table_name); - transaction - .execute(&[ - migration.sql().as_ref().expect("sql must be Some!"), + executor + .execute_single( + &migration.sql().as_ref().expect("sql must be Some!"), &update_query, - ]) + migration.flags(), + ) .await .migration_err( &format!("error applying migration {}", migration), @@ -57,8 +68,8 @@ async fn migrate( Ok(Report::new(applied_migrations)) } -async fn migrate_grouped( - transaction: &mut T, +async fn migrate_grouped( + executor: &mut T, migrations: Vec, target: Target, migration_table_name: &str, @@ -107,7 +118,7 @@ async fn migrate_grouped( let refs: Vec<&str> = grouped_migrations.iter().map(AsRef::as_ref).collect(); - transaction + executor .execute(refs.as_ref()) .await .migration_err("error applying migrations", None)?; @@ -189,7 +200,7 @@ where if grouped || matches!(target, Target::Fake | Target::FakeVersion(_)) { migrate_grouped(self, migrations, target, migration_table_name).await } else { - migrate(self, migrations, target, migration_table_name).await + migrate_individual(self, migrations, target, migration_table_name).await } } } diff --git a/refinery_core/src/traits/mod.rs b/refinery_core/src/traits/mod.rs index d3eef6d3..23701c42 100644 --- a/refinery_core/src/traits/mod.rs +++ b/refinery_core/src/traits/mod.rs @@ -127,6 +127,7 @@ mod tests { let migration1 = Migration::unapplied( "V1__initial.sql", "CREATE TABLE persons (id int, name varchar(255), city varchar(255));", + Default::default(), ) .unwrap(); @@ -135,18 +136,21 @@ mod tests { include_str!( "../../../refinery/tests/migrations/V1-2/V2__add_cars_and_motos_table.sql" ), + Default::default(), ) .unwrap(); let migration3 = Migration::unapplied( "V3__add_brand_to_cars_table", include_str!("../../../refinery/tests/migrations/V3/V3__add_brand_to_cars_table.sql"), + Default::default(), ) .unwrap(); let migration4 = Migration::unapplied( "V4__add_year_field_to_cars", "ALTER TABLE cars ADD year INTEGER;", + Default::default(), ) .unwrap(); @@ -185,6 +189,7 @@ mod tests { include_str!( "../../../refinery/tests/migrations/V3/V3__add_brand_to_cars_table.sql" ), + Default::default(), ) .unwrap(), ]; @@ -211,6 +216,7 @@ mod tests { include_str!( "../../../refinery/tests/migrations/V3/V3__add_brand_to_cars_table.sql" ), + Default::default(), ) .unwrap(), ]; @@ -283,6 +289,7 @@ mod tests { include_str!( "../../../refinery/tests/migrations_unversioned/U0__merge_out_of_order.sql" ), + Default::default(), ) .unwrap(), ); diff --git a/refinery_core/src/traits/sync.rs b/refinery_core/src/traits/sync.rs index 23cc7a90..4416a95f 100644 --- a/refinery_core/src/traits/sync.rs +++ b/refinery_core/src/traits/sync.rs @@ -3,88 +3,136 @@ use crate::traits::{ insert_migration_query, verify_migrations, ASSERT_MIGRATIONS_TABLE_QUERY, GET_APPLIED_MIGRATIONS_QUERY, GET_LAST_APPLIED_MIGRATION_QUERY, }; -use crate::{Error, Migration, Report, Target}; +use crate::{Error, Migration, MigrationFlags, Report, Target}; -pub trait Transaction { +pub trait Executor { type Error: std::error::Error + Send + Sync + 'static; + // Run multiple queries implicitly in a transaction fn execute(&mut self, queries: &[&str]) -> Result; + + // Run single query along with a query to update the migration table. + // Offers more granular control via MigrationFlags + fn execute_single( + &mut self, + query: &str, + update_query: &str, + flags: &MigrationFlags, + ) -> Result; } -pub trait Query: Transaction { +pub trait Query: Executor { fn query(&mut self, query: &str) -> Result; } -pub fn migrate( - transaction: &mut T, +fn migrate_grouped( + executor: &mut T, migrations: Vec, target: Target, migration_table_name: &str, - batched: bool, ) -> Result { - let mut migration_batch = Vec::new(); + let mut grouped_migrations = Vec::new(); let mut applied_migrations = Vec::new(); for mut migration in migrations.into_iter() { if let Target::Version(input_target) | Target::FakeVersion(input_target) = target { if input_target < migration.version() { - log::info!( - "stopping at migration: {}, due to user option", - input_target - ); break; } } - log::info!("applying migration: {}", migration); migration.set_applied(); - let insert_migration = insert_migration_query(&migration, migration_table_name); - let migration_sql = migration.sql().expect("sql must be Some!").to_string(); + let query = insert_migration_query(&migration, migration_table_name); + + let sql = migration.sql().expect("sql must be Some!").to_owned(); // If Target is Fake, we only update schema migrations table if !matches!(target, Target::Fake | Target::FakeVersion(_)) { applied_migrations.push(migration); - migration_batch.push(migration_sql); + grouped_migrations.push(sql); } - migration_batch.push(insert_migration); + grouped_migrations.push(query); } - match (target, batched) { - (Target::Fake | Target::FakeVersion(_), _) => { + match target { + Target::Fake | Target::FakeVersion(_) => { log::info!("not going to apply any migration as fake flag is enabled"); } - (Target::Latest | Target::Version(_), true) => { + Target::Latest | Target::Version(_) => { log::info!( "going to apply batch migrations in single transaction: {:#?}", applied_migrations.iter().map(ToString::to_string) ); } - (Target::Latest | Target::Version(_), false) => { - log::info!( - "preparing to apply {} migrations: {:#?}", - applied_migrations.len(), - applied_migrations.iter().map(ToString::to_string) - ); - } }; - let refs: Vec<&str> = migration_batch.iter().map(AsRef::as_ref).collect(); + if let Target::Version(input_target) = target { + log::info!( + "stopping at migration: {}, due to user option", + input_target + ); + } - if batched { - transaction - .execute(refs.as_ref()) - .migration_err("error applying migrations", None)?; - } else { - for (i, update) in refs.iter().enumerate() { - transaction - .execute(&[update]) - .migration_err("error applying update", Some(&applied_migrations[0..i / 2]))?; + let refs: Vec<_> = grouped_migrations.iter().map(AsRef::as_ref).collect(); + + executor + .execute(refs.as_ref()) + .migration_err("error applying migrations", None)?; + + Ok(Report::new(applied_migrations)) +} + +fn migrate_individual( + executor: &mut T, + migrations: Vec, + target: Target, + migration_table_name: &str, +) -> Result { + let mut applied_migrations = vec![]; + + for mut migration in migrations.into_iter() { + if let Target::Version(input_target) = target { + if input_target < migration.version() { + log::info!( + "stopping at migration: {}, due to user option", + input_target + ); + break; + } } - } + log::info!("applying migration: {}", migration); + migration.set_applied(); + let update_query = insert_migration_query(&migration, migration_table_name); + executor + .execute_single( + &migration.sql().expect("migration has no content"), + &update_query, + migration.flags(), + ) + .migration_err( + &format!("error applying migration {}", migration), + Some(&applied_migrations), + )?; + applied_migrations.push(migration); + } Ok(Report::new(applied_migrations)) } +pub fn migrate( + executor: &mut T, + migrations: Vec, + target: Target, + migration_table_name: &str, + batched: bool, +) -> Result { + if batched { + migrate_grouped(executor, migrations, target, migration_table_name) + } else { + migrate_individual(executor, migrations, target, migration_table_name) + } +} + pub trait Migrate: Query> where Self: Sized, diff --git a/refinery_core/src/util.rs b/refinery_core/src/util.rs index 4b416f45..b799881f 100644 --- a/refinery_core/src/util.rs +++ b/refinery_core/src/util.rs @@ -1,5 +1,5 @@ use crate::error::{Error, Kind}; -use crate::runner::Type; +use crate::runner::{MigrationFlags, Type}; use crate::Migration; use regex::Regex; use std::ffi::OsStr; @@ -27,6 +27,22 @@ fn file_re_all() -> &'static Regex { RE.get_or_init(|| Regex::new([STEM_RE, r"\.(rs|sql)$"].concat().as_str()).unwrap()) } +/// Matches the annotation `-- +refinery NO TRANSACTION` at the start of a +/// commented line of a .sql file, implying that the query should ran outside +/// of a transaction. +fn query_no_transaction_re_sql() -> &'static Regex { + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| Regex::new(r"^[-]{2,}[\s]?(\+refinery NO TRANSACTION)$").unwrap()) +} + +/// Matches the annotation `// +refinery NO TRANSACTION` at the start of a +/// commented line of a .rs|.sql file, implying that the query should ran outside +/// of a transaction. +fn query_no_transaction_re_all() -> &'static Regex { + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| Regex::new(r"^[-|\/]{2,}[\s]?(\+refinery NO TRANSACTION)$").unwrap()) +} + /// enum containing the migration types used to search for migrations /// either just .sql files or both .sql and .rs pub enum MigrationType { @@ -41,6 +57,13 @@ impl MigrationType { MigrationType::Sql => file_re_sql(), } } + + fn no_transaction_re(&self) -> &'static Regex { + match self { + MigrationType::All => query_no_transaction_re_all(), + MigrationType::Sql => query_no_transaction_re_sql(), + } + } } /// Parse a migration filename stem into a prefix, version, and name. @@ -63,6 +86,19 @@ pub fn parse_migration_name(name: &str) -> Result<(Type, i32, String), Error> { Ok((prefix, version, name)) } +pub fn parse_flags(file_content: &str, migration_type: MigrationType) -> MigrationFlags { + let mut default_flags = MigrationFlags::default(); + // TODO: Keep behind a flag as it could be slow + let no_tx_re = migration_type.no_transaction_re(); + for line in file_content.lines() { + if no_tx_re.is_match(line) { + default_flags.run_in_transaction = false; + break; + } + } + default_flags +} + /// find migrations on file system recursively across directories given a location and [MigrationType] pub fn find_migration_files( location: impl AsRef, @@ -123,7 +159,8 @@ pub fn load_sql_migrations(location: impl AsRef) -> Result, .and_then(|file| file.to_os_string().into_string().ok()) .unwrap(); - let migration = Migration::unapplied(&filename, &sql)?; + let flags = parse_flags(&sql, MigrationType::Sql); + let migration = Migration::unapplied(&filename, &sql, flags)?; migrations.push(migration); } diff --git a/refinery_macros/src/lib.rs b/refinery_macros/src/lib.rs index a185058b..9a2b38d2 100644 --- a/refinery_macros/src/lib.rs +++ b/refinery_macros/src/lib.rs @@ -6,6 +6,7 @@ use proc_macro::TokenStream; use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; use quote::quote; use quote::ToTokens; +use refinery_core::parse_flags; use refinery_core::{find_migration_files, MigrationType}; use std::path::PathBuf; use std::{env, fs}; @@ -19,12 +20,15 @@ pub(crate) fn crate_root() -> PathBuf { fn migration_fn_quoted(_migrations: Vec) -> TokenStream2 { let result = quote! { - use refinery::{Migration, Runner}; + use refinery::{Migration, MigrationFlags, Runner}; pub fn runner() -> Runner { - let quoted_migrations: Vec<(&str, String)> = vec![#(#_migrations),*]; + let quoted_migrations: Vec<(&str, String, bool)> = vec![#(#_migrations),*]; let mut migrations: Vec = Vec::new(); for module in quoted_migrations.into_iter() { - migrations.push(Migration::unapplied(module.0, &module.1).unwrap()); + let flags = MigrationFlags { + run_in_transaction: module.2, + }; + migrations.push(Migration::unapplied(module.0, &module.1, flags).unwrap()); } Runner::new(&migrations) } @@ -104,9 +108,13 @@ pub fn embed_migrations(input: TokenStream) -> TokenStream { let path = migration.display().to_string(); let extension = migration.extension().unwrap(); migration_filenames.push(filename.clone()); + let content = fs::read_to_string(&path).unwrap(); + let flags = parse_flags(&content, MigrationType::All); + let run_in_transaction = flags.run_in_transaction; if extension == "sql" { - _migrations.push(quote! {(#filename, include_str!(#path).to_string())}); + _migrations + .push(quote! {(#filename, include_str!(#path).to_string(), #run_in_transaction)}); } else if extension == "rs" { let rs_content = fs::read_to_string(&path) .unwrap() @@ -118,7 +126,7 @@ pub fn embed_migrations(input: TokenStream) -> TokenStream { // also include the file as str so we trigger recompilation if it changes const _RECOMPILE_IF_CHANGED: &str = include_str!(#path); }}; - _migrations.push(quote! {(#filename, #ident::migration())}); + _migrations.push(quote! {(#filename, #ident::migration(), #run_in_transaction)}); migrations_mods.push(mig_mod); } } @@ -164,12 +172,15 @@ mod tests { fn test_quote_fn() { let migs = vec![quote!("V1__first", "valid_sql_file")]; let expected = concat! { - "use refinery :: { Migration , Runner } ; ", + "use refinery :: { Migration , MigrationFlags , Runner } ; ", "pub fn runner () -> Runner { ", - "let quoted_migrations : Vec < (& str , String) > = vec ! [\"V1__first\" , \"valid_sql_file\"] ; ", + "let quoted_migrations : Vec < (& str , String , bool) > = vec ! [\"V1__first\" , \"valid_sql_file\"] ; ", "let mut migrations : Vec < Migration > = Vec :: new () ; ", "for module in quoted_migrations . into_iter () { ", - "migrations . push (Migration :: unapplied (module . 0 , & module . 1) . unwrap ()) ; ", + "let flags = MigrationFlags {", + " run_in_transaction : module . 2 , ", + "} ; ", + "migrations . push (Migration :: unapplied (module . 0 , & module . 1 , flags) . unwrap ()) ; ", "} ", "Runner :: new (& migrations) }" }; From d107bede7dcc0afcf54840d1004836cf1c922028 Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Mon, 21 Jul 2025 10:24:21 +0200 Subject: [PATCH 2/5] Fix regex to allow multiple whitespaces --- examples/migrations/V4__insert_entries_to_cars.sql | 2 +- refinery_core/src/util.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/migrations/V4__insert_entries_to_cars.sql b/examples/migrations/V4__insert_entries_to_cars.sql index 2e4ea2e8..51ae57b3 100644 --- a/examples/migrations/V4__insert_entries_to_cars.sql +++ b/examples/migrations/V4__insert_entries_to_cars.sql @@ -1,4 +1,4 @@ --- +refinery NO TRANSACTION +-- +refinery NO TRANSACTION BEGIN; INSERT INTO cars(id, name, brand) VALUES (2, "muscle", "toyota"); COMMIT; diff --git a/refinery_core/src/util.rs b/refinery_core/src/util.rs index b799881f..7b2f1bf1 100644 --- a/refinery_core/src/util.rs +++ b/refinery_core/src/util.rs @@ -32,7 +32,9 @@ fn file_re_all() -> &'static Regex { /// of a transaction. fn query_no_transaction_re_sql() -> &'static Regex { static RE: OnceLock = OnceLock::new(); - RE.get_or_init(|| Regex::new(r"^[-]{2,}[\s]?(\+refinery NO TRANSACTION)$").unwrap()) + RE.get_or_init(|| { + Regex::new(r"^[-]{2,}[\s]?(\+refinery[\s]+NO[\s]+TRANSACTION[\s]?)$").unwrap() + }) } /// Matches the annotation `// +refinery NO TRANSACTION` at the start of a @@ -40,7 +42,9 @@ fn query_no_transaction_re_sql() -> &'static Regex { /// of a transaction. fn query_no_transaction_re_all() -> &'static Regex { static RE: OnceLock = OnceLock::new(); - RE.get_or_init(|| Regex::new(r"^[-|\/]{2,}[\s]?(\+refinery NO TRANSACTION)$").unwrap()) + RE.get_or_init(|| { + Regex::new(r"^[-|\/]{2,}[\s]?(\+refinery[\s]+NO[\s]+TRANSACTION[\s]?)$").unwrap() + }) } /// enum containing the migration types used to search for migrations From 3e96c0cc8b6b8a71c06fd0a8abfed3b13c1dd3dd Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Mon, 21 Jul 2025 15:58:38 +0200 Subject: [PATCH 3/5] Incompatible set_grouped with no transaction --- README.md | 2 +- refinery/tests/postgres.rs | 31 +++++++++++++++++++++++++++++++ refinery_core/src/error.rs | 3 +++ refinery_core/src/traits/async.rs | 8 +++++++- refinery_core/src/traits/sync.rs | 9 ++++++++- 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1c4ccb0a..6ff9d51f 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ This would stop developer 1's migration from ever running if you were using cont refinery works by creating a table that keeps all the applied migrations' versions and their metadata. When you [run](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.run) the migrations `Runner`, refinery compares the applied migrations with the ones to be applied, checking for [divergent](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.set_abort_divergent) and [missing](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.set_abort_missing) and executing unapplied migrations.\ By default, refinery runs each migration in a single transaction. Alternatively, you can also configure refinery to wrap the entire execution of all migrations in a single transaction by setting [set_grouped](https://docs.rs/refinery/latest/refinery/struct.Runner.html#method.set_grouped) to true. -Directive `-- +refinery NO TRANSACTION` can be used to escape running a migration within a transaction. [!IMPORTANT]: `set_grouped` takes precedence over no transaction directive. +Directive `-- +refinery NO TRANSACTION` can be used to escape running a migration within a transaction. [!IMPORTANT]: `set_grouped` is incompatible with the no transaction directive. The rust crate intentionally ignores new migration files until your sourcecode is rebuild. This prevents accidental migrations and altering the database schema without any code changes. We can also bake the migrations into the binary, so no additional files are needed when deployed. ### Rollback diff --git a/refinery/tests/postgres.rs b/refinery/tests/postgres.rs index b684c9b7..c42d44cf 100644 --- a/refinery/tests/postgres.rs +++ b/refinery/tests/postgres.rs @@ -384,6 +384,37 @@ mod postgres { }); } + #[test] + fn no_transaction_fails_in_set_grouped() { + run_test(|| { + let mut client = Client::connect(&db_uri(), NoTls).unwrap(); + + embedded::migrations::runner().run(&mut client).unwrap(); + + let migrations = get_migrations(); + + let mchecksum = migrations[5].checksum(); + let err = client + .migrate( + &migrations, + true, + true, + true, + Target::Latest, + DEFAULT_TABLE_NAME, + ) + .unwrap_err(); + + match err.kind() { + Kind::NoTransactionGroupedMigration(last) => { + assert_eq!(6, last.version()); + assert_eq!(mchecksum, last.checksum()); + } + _ => panic!("failed test"), + } + }); + } + #[test] fn migrates_to_target_migration() { run_test(|| { diff --git a/refinery_core/src/error.rs b/refinery_core/src/error.rs index 2b1b4661..c4935243 100644 --- a/refinery_core/src/error.rs +++ b/refinery_core/src/error.rs @@ -57,6 +57,9 @@ pub enum Kind { /// An Error from an divergent version, the applied version is different to the filesystem one #[error("applied migration {0} is different than filesystem one {1}")] DivergentVersion(Migration, Migration), + /// An Error from running in grouped mode with a migration that opts out of transactions + #[error("migration {0} opts out of transactions, cannot run with `set-grouped`")] + NoTransactionGroupedMigration(Migration), /// An Error from an divergent version, the applied version is missing on the filesystem #[error("migration {0} is missing from the filesystem")] MissingVersion(Migration), diff --git a/refinery_core/src/traits/async.rs b/refinery_core/src/traits/async.rs index f5b2a77d..1006f03e 100644 --- a/refinery_core/src/traits/async.rs +++ b/refinery_core/src/traits/async.rs @@ -1,4 +1,4 @@ -use crate::error::WrapMigrationError; +use crate::error::{Kind, WrapMigrationError}; use crate::traits::{ insert_migration_query, verify_migrations, ASSERT_MIGRATIONS_TABLE_QUERY, GET_APPLIED_MIGRATIONS_QUERY, GET_LAST_APPLIED_MIGRATION_QUERY, @@ -83,6 +83,12 @@ async fn migrate_grouped( break; } } + if !migration.flags().run_in_transaction { + return Err(Error::new( + Kind::NoTransactionGroupedMigration(migration), + None, + )); + } migration.set_applied(); let query = insert_migration_query(&migration, migration_table_name); diff --git a/refinery_core/src/traits/sync.rs b/refinery_core/src/traits/sync.rs index 3e4f1f49..1007b4b2 100644 --- a/refinery_core/src/traits/sync.rs +++ b/refinery_core/src/traits/sync.rs @@ -1,4 +1,4 @@ -use crate::error::WrapMigrationError; +use crate::error::{Kind, WrapMigrationError}; use crate::traits::{ insert_migration_query, verify_migrations, ASSERT_MIGRATIONS_TABLE_QUERY, GET_APPLIED_MIGRATIONS_QUERY, GET_LAST_APPLIED_MIGRATION_QUERY, @@ -41,6 +41,13 @@ fn migrate_grouped( } } + if !migration.flags().run_in_transaction { + return Err(Error::new( + Kind::NoTransactionGroupedMigration(migration), + None, + )); + } + migration.set_applied(); let query = insert_migration_query(&migration, migration_table_name); From 3d6ad3bf15c9c23a3ba64c147bedb317f0b95f6b Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Fri, 8 Aug 2025 23:36:53 +0200 Subject: [PATCH 4/5] Fix AsyncExecutor for mysql_async driver --- refinery_core/src/drivers/mysql_async.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refinery_core/src/drivers/mysql_async.rs b/refinery_core/src/drivers/mysql_async.rs index 8cee61de..8dcbcc34 100644 --- a/refinery_core/src/drivers/mysql_async.rs +++ b/refinery_core/src/drivers/mysql_async.rs @@ -65,7 +65,7 @@ impl AsyncExecutor for Pool { flags: &MigrationFlags, ) -> Result { if flags.run_in_transaction { - AsyncExecutor::execute(self, &[query, update_query]).await + AsyncExecutor::execute(self, [query, update_query].into_iter()).await } else { self.query(query).await?; if let Err(e) = self.query(update_query).await { From 07d72b6378d26d6676a9efef78b324e056cb9b1f Mon Sep 17 00:00:00 2001 From: Diretnan Domnan Date: Sat, 9 Aug 2025 13:17:10 +0200 Subject: [PATCH 5/5] Fix failing postgres tests --- refinery/tests/postgres.rs | 8 ++++---- refinery/tests/tokio_postgres.rs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/refinery/tests/postgres.rs b/refinery/tests/postgres.rs index b33f1191..53fe3420 100644 --- a/refinery/tests/postgres.rs +++ b/refinery/tests/postgres.rs @@ -852,16 +852,16 @@ mod postgres { let report = runner.run(&mut config).unwrap(); let applied_migrations = report.applied_migrations(); - assert_eq!(5, applied_migrations.len()); + assert_eq!(6, applied_migrations.len()); let last_migration = runner .get_last_applied_migration(&mut config) .unwrap() .unwrap(); - assert_eq!(5, last_migration.version()); - assert_eq!(migrations[4].name(), last_migration.name()); - assert_eq!(migrations[4].checksum(), last_migration.checksum()); + assert_eq!(6, last_migration.version()); + assert_eq!(migrations[5].name(), last_migration.name()); + assert_eq!(migrations[5].checksum(), last_migration.checksum()); assert!(config.use_tls()); }); diff --git a/refinery/tests/tokio_postgres.rs b/refinery/tests/tokio_postgres.rs index a634f991..c7a86c5f 100644 --- a/refinery/tests/tokio_postgres.rs +++ b/refinery/tests/tokio_postgres.rs @@ -998,7 +998,7 @@ mod tokio_postgres { let report = runner.run_async(&mut config).await.unwrap(); let applied_migrations = report.applied_migrations(); - assert_eq!(5, applied_migrations.len()); + assert_eq!(6, applied_migrations.len()); let last_migration = runner .get_last_applied_migration_async(&mut config) @@ -1006,9 +1006,9 @@ mod tokio_postgres { .unwrap() .unwrap(); - assert_eq!(5, last_migration.version()); - assert_eq!(migrations[4].name(), last_migration.name()); - assert_eq!(migrations[4].checksum(), last_migration.checksum()); + assert_eq!(6, last_migration.version()); + assert_eq!(migrations[5].name(), last_migration.name()); + assert_eq!(migrations[5].checksum(), last_migration.checksum()); assert!(config.use_tls()); })