diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs index 7a9bc6bf2f..65f394b5f2 100644 --- a/sqlx-cli/src/database.rs +++ b/sqlx-cli/src/database.rs @@ -57,7 +57,7 @@ pub async fn reset( pub async fn setup(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow::Result<()> { create(connect_opts).await?; - migrate::run(migration_source, connect_opts, false, false, None).await + migrate::run(migration_source, connect_opts, false, false, None, false).await } async fn ask_to_continue_drop(db_url: String) -> bool { diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index cb31205b4f..b94d885bc9 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -73,6 +73,24 @@ async fn do_run(opt: Opt) -> Result<()> { dry_run, *ignore_missing, target_version, + false, + ) + .await? + } + MigrateCommand::Skip { + source, + dry_run, + ignore_missing, + connect_opts, + target_version, + } => { + migrate::run( + &source, + &connect_opts, + dry_run, + *ignore_missing, + target_version, + true, ) .await? } diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index e00f6de651..6a938fe4fb 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -277,6 +277,7 @@ pub async fn run( dry_run: bool, ignore_missing: bool, target_version: Option, + skip: bool, ) -> anyhow::Result<()> { let migrator = Migrator::new(Path::new(migration_source)).await?; if let Some(target_version) = target_version { @@ -326,18 +327,20 @@ pub async fn run( } } None => { - let skip = + let exceeds_target = target_version.is_some_and(|target_version| migration.version > target_version); - let elapsed = if dry_run || skip { + let elapsed = if dry_run || exceeds_target { Duration::new(0, 0) } else { - conn.apply(migration).await? + conn.apply(migration, skip).await? }; - let text = if skip { + let text = if exceeds_target { "Skipped" } else if dry_run { "Can apply" + } else if skip { + "Skipped on request" } else { "Applied" }; diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index a0e19e47f4..b1f587a7c2 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -183,6 +183,27 @@ pub enum MigrateCommand { target_version: Option, }, + /// Skip all pending migrations. + Skip { + #[clap(flatten)] + source: Source, + + /// List all the migrations to be skipped without marking them as applied. + #[clap(long)] + dry_run: bool, + + #[clap(flatten)] + ignore_missing: IgnoreMissing, + + #[clap(flatten)] + connect_opts: ConnectOpts, + + /// Apply migrations up to the specified version. If unspecified, apply all + /// pending migrations. If already at the target version, then no-op. + #[clap(long)] + target_version: Option, + }, + /// Revert the latest migration with a down file. Revert { #[clap(flatten)] diff --git a/sqlx-cli/tests/common/mod.rs b/sqlx-cli/tests/common/mod.rs index 43c0dbc1e1..d9647ee998 100644 --- a/sqlx-cli/tests/common/mod.rs +++ b/sqlx-cli/tests/common/mod.rs @@ -12,6 +12,22 @@ pub struct TestDatabase { migrations: String, } +pub enum MigrateCommand { + Run, + Revert, + Skip, +} + +impl AsRef for MigrateCommand { + fn as_ref(&self) -> &str { + match self { + MigrateCommand::Run => "run", + MigrateCommand::Revert => "revert", + MigrateCommand::Skip => "skip", + } + } +} + impl TestDatabase { pub fn new(name: &str, migrations: &str) -> Self { let migrations_path = Path::new("tests").join(migrations); @@ -38,7 +54,12 @@ impl TestDatabase { format!("sqlite://{}", self.file_path.display()) } - pub fn run_migration(&self, revert: bool, version: Option, dry_run: bool) -> Assert { + pub fn run_migration( + &self, + command: MigrateCommand, + version: Option, + dry_run: bool, + ) -> Assert { let ver = match version { Some(v) => v.to_string(), None => String::from(""), @@ -50,10 +71,7 @@ impl TestDatabase { vec![ "sqlx", "migrate", - match revert { - true => "revert", - false => "run", - }, + command.as_ref(), "--database-url", &self.connection_string(), "--source", diff --git a/sqlx-cli/tests/migrate.rs b/sqlx-cli/tests/migrate.rs index 0ea9d4620d..5d8e69106f 100644 --- a/sqlx-cli/tests/migrate.rs +++ b/sqlx-cli/tests/migrate.rs @@ -1,6 +1,6 @@ mod common; -use common::TestDatabase; +use common::{MigrateCommand, TestDatabase}; #[tokio::test] async fn run_reversible_migrations() { @@ -11,10 +11,10 @@ async fn run_reversible_migrations() { 20230401000000, 20230501000000, ]; - // Without --target-version specified.k + // Without --target-version specified. { let db = TestDatabase::new("migrate_run_reversible_latest", "migrations_reversible"); - db.run_migration(false, None, false).success(); + db.run_migration(MigrateCommand::Run, None, false).success(); assert_eq!(db.applied_migrations().await, all_migrations); } // With --target-version specified. @@ -25,17 +25,17 @@ async fn run_reversible_migrations() { ); // Move to latest, explicitly specified. - db.run_migration(false, Some(20230501000000), false) + db.run_migration(MigrateCommand::Run, Some(20230501000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations); // Move to latest when we're already at the latest. - db.run_migration(false, Some(20230501000000), false) + db.run_migration(MigrateCommand::Run, Some(20230501000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations); // Upgrade to an old version. - db.run_migration(false, Some(20230301000000), false) + db.run_migration(MigrateCommand::Run, Some(20230301000000), false) .failure(); assert_eq!(db.applied_migrations().await, all_migrations); } @@ -47,26 +47,26 @@ async fn run_reversible_migrations() { ); // First version - db.run_migration(false, Some(20230101000000), false) + db.run_migration(MigrateCommand::Run, Some(20230101000000), false) .success(); assert_eq!(db.applied_migrations().await, vec![20230101000000]); // Dry run upgrade to latest. - db.run_migration(false, None, true).success(); + db.run_migration(MigrateCommand::Run, None, true).success(); assert_eq!(db.applied_migrations().await, vec![20230101000000]); // Dry run upgrade + 2 - db.run_migration(false, Some(20230301000000), true) + db.run_migration(MigrateCommand::Run, Some(20230301000000), true) .success(); assert_eq!(db.applied_migrations().await, vec![20230101000000]); // Upgrade to non-existent version. - db.run_migration(false, Some(20230901000000999), false) + db.run_migration(MigrateCommand::Run, Some(20230901000000999), false) .failure(); assert_eq!(db.applied_migrations().await, vec![20230101000000]); // Upgrade + 1 - db.run_migration(false, Some(20230201000000), false) + db.run_migration(MigrateCommand::Run, Some(20230201000000), false) .success(); assert_eq!( db.applied_migrations().await, @@ -74,7 +74,7 @@ async fn run_reversible_migrations() { ); // Upgrade + 2 - db.run_migration(false, Some(20230401000000), false) + db.run_migration(MigrateCommand::Run, Some(20230401000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations[..4]); } @@ -93,55 +93,139 @@ async fn revert_migrations() { // Without --target-version { let db = TestDatabase::new("migrate_revert_incremental", "migrations_reversible"); - db.run_migration(false, None, false).success(); + db.run_migration(MigrateCommand::Run, None, false).success(); // Dry-run - db.run_migration(true, None, true).success(); + db.run_migration(MigrateCommand::Revert, None, true) + .success(); assert_eq!(db.applied_migrations().await, all_migrations); // Downgrade one - db.run_migration(true, None, false).success(); + db.run_migration(MigrateCommand::Revert, None, false) + .success(); assert_eq!(db.applied_migrations().await, all_migrations[..4]); // Downgrade one - db.run_migration(true, None, false).success(); + db.run_migration(MigrateCommand::Revert, None, false) + .success(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); } // With --target-version { let db = TestDatabase::new("migrate_revert_incremental", "migrations_reversible"); - db.run_migration(false, None, false).success(); + db.run_migration(MigrateCommand::Run, None, false).success(); // Dry-run downgrade to version 3. - db.run_migration(true, Some(20230301000000), true).success(); + db.run_migration(MigrateCommand::Revert, Some(20230301000000), true) + .success(); assert_eq!(db.applied_migrations().await, all_migrations); // Downgrade to version 3. - db.run_migration(true, Some(20230301000000), false) + db.run_migration(MigrateCommand::Revert, Some(20230301000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); // Try downgrading to the same version. - db.run_migration(true, Some(20230301000000), false) + db.run_migration(MigrateCommand::Revert, Some(20230301000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); // Try downgrading to a newer version. - db.run_migration(true, Some(20230401000000), false) + db.run_migration(MigrateCommand::Revert, Some(20230401000000), false) .failure(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); // Try downgrading to a non-existent version. - db.run_migration(true, Some(9999), false).failure(); + db.run_migration(MigrateCommand::Revert, Some(9999), false) + .failure(); assert_eq!(db.applied_migrations().await, all_migrations[..3]); // Ensure we can still upgrade - db.run_migration(false, Some(20230401000000), false) + db.run_migration(MigrateCommand::Run, Some(20230401000000), false) .success(); assert_eq!(db.applied_migrations().await, all_migrations[..4]); // Downgrade to zero. - db.run_migration(true, Some(0), false).success(); + db.run_migration(MigrateCommand::Revert, Some(0), false) + .success(); assert_eq!(db.applied_migrations().await, vec![] as Vec); } } +#[tokio::test] +async fn skip_reversible_migrations() { + let all_migrations: Vec = vec![ + 20230101000000, + 20230201000000, + 20230301000000, + 20230401000000, + 20230501000000, + ]; + // Without --target-version specified. + { + let db = TestDatabase::new("migrate_skip_reversible_latest", "migrations_reversible"); + db.run_migration(MigrateCommand::Skip, None, false) + .success(); + assert_eq!(db.applied_migrations().await, all_migrations); + } + // With --target-version specified. + { + let db = TestDatabase::new( + "migrate_skip_reversible_latest_explicit", + "migrations_reversible", + ); + + // Move to latest, explicitly specified. + db.run_migration(MigrateCommand::Run, Some(20230501000000), false) + .success(); + assert_eq!(db.applied_migrations().await, all_migrations); + + // Skip to latest when we're already at the latest. + db.run_migration(MigrateCommand::Skip, Some(20230501000000), false) + .success(); + assert_eq!(db.applied_migrations().await, all_migrations); + + // Upgrade to an old version. + db.run_migration(MigrateCommand::Skip, Some(20230301000000), false) + .failure(); + assert_eq!(db.applied_migrations().await, all_migrations); + } + // With --target-version, incrementally upgrade. + { + let db = TestDatabase::new( + "migrate_skip_reversible_incremental", + "migrations_reversible", + ); + + // Run first version + db.run_migration(MigrateCommand::Run, Some(20230101000000), false) + .success(); + assert_eq!(db.applied_migrations().await, vec![20230101000000]); + + // Skip and dry run upgrade to latest. + db.run_migration(MigrateCommand::Skip, None, true).success(); + assert_eq!(db.applied_migrations().await, vec![20230101000000]); + + // Skip and dry run upgrade + 2 + db.run_migration(MigrateCommand::Skip, Some(20230301000000), true) + .success(); + assert_eq!(db.applied_migrations().await, vec![20230101000000]); + + // Skip to to non-existent version. + db.run_migration(MigrateCommand::Skip, Some(20230901000000999), false) + .failure(); + assert_eq!(db.applied_migrations().await, vec![20230101000000]); + + // Upgrade + 1 + db.run_migration(MigrateCommand::Run, Some(20230201000000), false) + .success(); + assert_eq!( + db.applied_migrations().await, + vec![20230101000000, 20230201000000] + ); + + // Skip + 2 + db.run_migration(MigrateCommand::Skip, Some(20230401000000), false) + .success(); + assert_eq!(db.applied_migrations().await, all_migrations[..4]); + } +} diff --git a/sqlx-core/src/any/migrate.rs b/sqlx-core/src/any/migrate.rs index cb4f72c340..2ca7c6533b 100644 --- a/sqlx-core/src/any/migrate.rs +++ b/sqlx-core/src/any/migrate.rs @@ -69,8 +69,9 @@ impl Migrate for AnyConnection { fn apply<'e: 'm, 'm>( &'e mut self, migration: &'m Migration, + skip: bool, ) -> BoxFuture<'m, Result> { - Box::pin(async { self.get_migrate()?.apply(migration).await }) + Box::pin(async move { self.get_migrate()?.apply(migration, skip).await }) } fn revert<'e: 'm, 'm>( diff --git a/sqlx-core/src/migrate/migrate.rs b/sqlx-core/src/migrate/migrate.rs index 0e4448a9bd..927e6944db 100644 --- a/sqlx-core/src/migrate/migrate.rs +++ b/sqlx-core/src/migrate/migrate.rs @@ -47,12 +47,13 @@ pub trait Migrate { // migrations have been run. fn unlock(&mut self) -> BoxFuture<'_, Result<(), MigrateError>>; - // run SQL from migration in a DDL transaction + // run SQL from migration in a DDL transaction if skip is True // insert new row to [_migrations] table on completion (success or failure) // returns the time taking to run the migration SQL fn apply<'e: 'm, 'm>( &'e mut self, migration: &'m Migration, + skip: bool, ) -> BoxFuture<'m, Result>; // run a revert SQL from migration in a DDL transaction diff --git a/sqlx-core/src/migrate/migrator.rs b/sqlx-core/src/migrate/migrator.rs index 3209ba6e45..a63b056946 100644 --- a/sqlx-core/src/migrate/migrator.rs +++ b/sqlx-core/src/migrate/migrator.rs @@ -134,12 +134,48 @@ impl Migrator { ::Target: Migrate, { let mut conn = migrator.acquire().await?; - self.run_direct(&mut *conn).await + self.run_direct(&mut *conn, false, None).await + } + + /// Skip any pending migrations until a specific version against the database; + /// Additionally validate previously applied migrations against the current migration + /// source to detect accidental changes in previously-applied migrations. + /// + /// Skipping entails not executing the SQL of the migrations, but marking them as + /// applied in the [_migrations] table. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use sqlx::migrate::MigrateError; + /// # fn main() -> Result<(), MigrateError> { + /// # sqlx::__rt::test_block_on(async move { + /// use sqlx::migrate::Migrator; + /// use sqlx::sqlite::SqlitePoolOptions; + /// + /// let m = Migrator::new(std::path::Path::new("./migrations")).await?; + /// let pool = SqlitePoolOptions::new().connect("sqlite::memory:").await?; + /// m.skip(&pool, Some(17)).await + /// # }) + /// # } + /// ``` + pub async fn skip<'a, A>(&self, migrator: A, target: Option) -> Result<(), MigrateError> + where + A: Acquire<'a>, + ::Target: Migrate, + { + let mut conn = migrator.acquire().await?; + self.run_direct(&mut *conn, true, target).await } // Getting around the annoying "implementation of `Acquire` is not general enough" error #[doc(hidden)] - pub async fn run_direct(&self, conn: &mut C) -> Result<(), MigrateError> + pub async fn run_direct( + &self, + conn: &mut C, + skip: bool, + target: Option, + ) -> Result<(), MigrateError> where C: Migrate, { @@ -166,7 +202,9 @@ impl Migrator { .collect(); for migration in self.iter() { - if migration.migration_type.is_down_migration() { + if migration.migration_type.is_down_migration() + || target.is_some_and(|target| migration.version > target) + { continue; } @@ -177,7 +215,7 @@ impl Migrator { } } None => { - conn.apply(migration).await?; + conn.apply(migration, skip).await?; } } } diff --git a/sqlx-core/src/testing/mod.rs b/sqlx-core/src/testing/mod.rs index 051353383b..7965e96b2b 100644 --- a/sqlx-core/src/testing/mod.rs +++ b/sqlx-core/src/testing/mod.rs @@ -256,7 +256,7 @@ async fn setup_test_db( if let Some(migrator) = args.migrator { migrator - .run_direct(&mut conn) + .run_direct(&mut conn, false, None) .await .expect("failed to apply migrations"); } diff --git a/sqlx-mysql/src/migrate.rs b/sqlx-mysql/src/migrate.rs index 79b55ace3c..74e92393a0 100644 --- a/sqlx-mysql/src/migrate.rs +++ b/sqlx-mysql/src/migrate.rs @@ -170,6 +170,7 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations ( fn apply<'e: 'm, 'm>( &'e mut self, migration: &'m Migration, + skip: bool, ) -> BoxFuture<'m, Result> { Box::pin(async move { // Use a single transaction for the actual migration script and the essential bookeeping so we never @@ -199,10 +200,12 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations ( .execute(&mut *tx) .await?; - let _ = tx - .execute(&*migration.sql) - .await - .map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?; + if !skip { + let _ = tx + .execute(&*migration.sql) + .await + .map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?; + } // language=MySQL let _ = query( diff --git a/sqlx-postgres/src/migrate.rs b/sqlx-postgres/src/migrate.rs index c37e92f4d6..55df17cf10 100644 --- a/sqlx-postgres/src/migrate.rs +++ b/sqlx-postgres/src/migrate.rs @@ -206,13 +206,14 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations ( fn apply<'e: 'm, 'm>( &'e mut self, migration: &'m Migration, + skip: bool, ) -> BoxFuture<'m, Result> { Box::pin(async move { let start = Instant::now(); // execute migration queries if migration.no_tx { - execute_migration(self, migration).await?; + execute_migration(self, migration, skip).await?; } else { // Use a single transaction for the actual migration script and the essential bookeeping so we never // execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966. @@ -220,7 +221,7 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations ( // data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1 // and update it once the actual transaction completed. let mut tx = self.begin().await?; - execute_migration(&mut tx, migration).await?; + execute_migration(&mut tx, migration, skip).await?; tx.commit().await?; } @@ -275,12 +276,14 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations ( async fn execute_migration( conn: &mut PgConnection, migration: &Migration, + skip: bool, ) -> Result<(), MigrateError> { - let _ = conn - .execute(&*migration.sql) - .await - .map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?; - + if !skip { + let _ = conn + .execute(&*migration.sql) + .await + .map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?; + } // language=SQL let _ = query( r#" diff --git a/sqlx-sqlite/src/migrate.rs b/sqlx-sqlite/src/migrate.rs index b9ce22dccd..fb954d62ff 100644 --- a/sqlx-sqlite/src/migrate.rs +++ b/sqlx-sqlite/src/migrate.rs @@ -131,20 +131,23 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations ( fn apply<'e: 'm, 'm>( &'e mut self, migration: &'m Migration, + skip: bool, ) -> BoxFuture<'m, Result> { Box::pin(async move { let mut tx = self.begin().await?; let start = Instant::now(); - // Use a single transaction for the actual migration script and the essential bookeeping so we never - // execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966. - // The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for - // data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1 - // and update it once the actual transaction completed. - let _ = tx - .execute(&*migration.sql) - .await - .map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?; + if !skip { + // Use a single transaction for the actual migration script and the essential bookkeeping so we never + // execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966. + // The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for + // data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1 + // and update it once the actual transaction completed. + let _ = tx + .execute(&*migration.sql) + .await + .map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?; + } // language=SQL let _ = query( diff --git a/tests/mysql/migrate.rs b/tests/mysql/migrate.rs index 97caa38005..dc4ebe0552 100644 --- a/tests/mysql/migrate.rs +++ b/tests/mysql/migrate.rs @@ -3,6 +3,7 @@ use sqlx::mysql::{MySql, MySqlConnection}; use sqlx::pool::PoolConnection; use sqlx::Executor; use sqlx::Row; +use std::fs; use std::path::Path; #[sqlx::test(migrations = false)] @@ -66,6 +67,52 @@ async fn reversible(mut conn: PoolConnection) -> anyhow::Result<()> { Ok(()) } +#[sqlx::test(migrations = false)] +async fn skip(mut conn: PoolConnection) -> anyhow::Result<()> { + clean_up(&mut conn).await?; + let migrator = Migrator::new(Path::new("tests/mysql/migrations_reversible")).await?; + + // get to the state of the first migration manually + let sql = + fs::read_to_string("tests/mysql/migrations_reversible/20220721124650_add_table.up.sql")?; + let statements = sql.split(';').map(|s| s.trim()).filter(|s| !s.is_empty()); + for statement in statements { + conn.execute(statement).await?; + } + + // skip first migration + migrator.skip(&mut conn, Some(20220721124650)).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + // run remaining migration + migrator.run(&mut conn).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 101); + + // roll back one version + migrator.undo(&mut conn, 20220721124650).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + Ok(()) +} + /// Ensure that we have a clean initial state. async fn clean_up(conn: &mut MySqlConnection) -> anyhow::Result<()> { conn.execute("DROP TABLE migrations_simple_test").await.ok(); diff --git a/tests/postgres/migrate.rs b/tests/postgres/migrate.rs index 636dffe860..a6e9180cec 100644 --- a/tests/postgres/migrate.rs +++ b/tests/postgres/migrate.rs @@ -3,6 +3,7 @@ use sqlx::pool::PoolConnection; use sqlx::postgres::{PgConnection, Postgres}; use sqlx::Executor; use sqlx::Row; +use std::fs; use std::path::Path; #[sqlx::test(migrations = false)] @@ -66,6 +67,52 @@ async fn reversible(mut conn: PoolConnection) -> anyhow::Result<()> { Ok(()) } +#[sqlx::test(migrations = false)] +async fn skip(mut conn: PoolConnection) -> anyhow::Result<()> { + clean_up(&mut conn).await?; + let migrator = Migrator::new(Path::new("tests/postgres/migrations_reversible")).await?; + + // get to the state of the first migration manually + let sql = + fs::read_to_string("tests/postgres/migrations_reversible/20220721124650_add_table.up.sql")?; + let statements = sql.split(';'); + for statement in statements { + conn.execute(statement).await?; + } + + // skip first migration + migrator.skip(&mut conn, Some(20220721124650)).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + // run remaining migration + migrator.run(&mut conn).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 101); + + // roll back one version + migrator.undo(&mut conn, 20220721124650).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + Ok(()) +} + #[sqlx::test(migrations = false)] async fn no_tx(mut conn: PoolConnection) -> anyhow::Result<()> { clean_up(&mut conn).await?; diff --git a/tests/sqlite/migrate.rs b/tests/sqlite/migrate.rs index 19e8690f9a..41c4d39788 100644 --- a/tests/sqlite/migrate.rs +++ b/tests/sqlite/migrate.rs @@ -3,6 +3,7 @@ use sqlx::pool::PoolConnection; use sqlx::sqlite::{Sqlite, SqliteConnection}; use sqlx::Executor; use sqlx::Row; +use std::fs; use std::path::Path; #[sqlx::test(migrations = false)] @@ -66,6 +67,52 @@ async fn reversible(mut conn: PoolConnection) -> anyhow::Result<()> { Ok(()) } +#[sqlx::test(migrations = false)] +async fn skip(mut conn: PoolConnection) -> anyhow::Result<()> { + clean_up(&mut conn).await?; + let migrator = Migrator::new(Path::new("tests/sqlite/migrations_reversible")).await?; + + // get to the state of the first migration manually + let sql = + fs::read_to_string("tests/sqlite/migrations_reversible/20220721124650_add_table.up.sql")?; + let statements = sql.split(';'); + for statement in statements { + conn.execute(statement).await?; + } + + // skip first migration + migrator.skip(&mut conn, Some(20220721124650)).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + // run remaining migration + migrator.run(&mut conn).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 101); + + // roll back one version + migrator.undo(&mut conn, 20220721124650).await?; + + // check outcome + let res: i64 = conn + .fetch_one("SELECT some_payload FROM migrations_reversible_test") + .await? + .get(0); + assert_eq!(res, 100); + + Ok(()) +} + /// Ensure that we have a clean initial state. async fn clean_up(conn: &mut SqliteConnection) -> anyhow::Result<()> { conn.execute("DROP TABLE migrations_simple_test").await.ok();