diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs index eaba46eed9..44d58eecca 100644 --- a/sqlx-cli/src/database.rs +++ b/sqlx-cli/src/database.rs @@ -62,7 +62,16 @@ pub async fn setup( connect_opts: &ConnectOpts, ) -> anyhow::Result<()> { create(connect_opts).await?; - migrate::run(config, migration_source, connect_opts, false, false, None).await + migrate::run( + config, + 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 7a2e41b16f..9abd689e96 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -27,7 +27,7 @@ use futures_util::TryFutureExt; use sqlx::AnyConnection; use tokio::{select, signal}; -use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand}; +use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand, OverrideCommand}; pub mod database; pub mod metadata; @@ -99,6 +99,7 @@ async fn do_run(opt: Opt) -> anyhow::Result<()> { dry_run, *ignore_missing, target_version, + false, ) .await? } @@ -124,6 +125,30 @@ async fn do_run(opt: Opt) -> anyhow::Result<()> { ) .await? } + MigrateCommand::Override { command } => match command { + OverrideCommand::Skip { + source, + config, + mut connect_opts, + dry_run, + ignore_missing, + target_version, + } => { + let config = config.load_config().await?; + connect_opts.populate_db_url(&config)?; + + migrate::run( + &config, + &source, + &connect_opts, + dry_run, + *ignore_missing, + target_version, + true, + ) + .await? + } + }, MigrateCommand::Info { source, config, diff --git a/sqlx-cli/src/migrate.rs b/sqlx-cli/src/migrate.rs index 926e264032..b26a9f46a3 100644 --- a/sqlx-cli/src/migrate.rs +++ b/sqlx-cli/src/migrate.rs @@ -220,6 +220,7 @@ pub async fn run( dry_run: bool, ignore_missing: bool, target_version: Option, + skip: bool, ) -> anyhow::Result<()> { let migrator = migration_source.resolve(config).await?; @@ -277,18 +278,23 @@ 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 if skip { + conn.skip(config.migrate.table_name(), migration).await?; Duration::new(0, 0) } else { conn.apply(config.migrate.table_name(), migration).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 cb09bc2ff5..48ec0207ba 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -247,6 +247,12 @@ pub enum MigrateCommand { target_version: Option, }, + /// Override migration state, potentially dangerous operations. + Override { + #[clap(subcommand)] + command: OverrideCommand, + }, + /// Revert the latest migration with a down file. Revert { #[clap(flatten)] @@ -300,6 +306,33 @@ pub enum MigrateCommand { }, } +#[derive(clap::Subcommand, Debug)] +pub enum OverrideCommand { + /// Skip all pending migrations without running them. + Skip { + #[clap(flatten)] + source: MigrationSourceOpt, + + #[clap(flatten)] + config: ConfigOpt, + + #[clap(flatten)] + connect_opts: ConnectOpts, + + /// List all the migrations to be skipped without marking them as applied. + #[clap(long)] + dry_run: bool, + + #[clap(flatten)] + ignore_missing: IgnoreMissing, + + /// 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, + }, +} + #[derive(Args, Debug)] pub struct AddMigrationOpts { pub description: String, diff --git a/sqlx-cli/tests/common/mod.rs b/sqlx-cli/tests/common/mod.rs index 66e7924859..e3bd9f6e83 100644 --- a/sqlx-cli/tests/common/mod.rs +++ b/sqlx-cli/tests/common/mod.rs @@ -13,6 +13,22 @@ pub struct TestDatabase { pub config_path: Option, } +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 => "override skip", + } + } +} + impl TestDatabase { pub fn new(name: &str, migrations: &str) -> Self { // Note: only set when _building_ @@ -58,19 +74,17 @@ 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, + migrate_command: MigrateCommand, + version: Option, + dry_run: bool, + ) -> Assert { let mut command = Command::cargo_bin("sqlx").unwrap(); command - .args([ - "migrate", - match revert { - true => "revert", - false => "run", - }, - "--database-url", - &self.connection_string(), - "--source", - ]) + .arg("migrate") + .args(migrate_command.as_ref().split_whitespace()) + .args(["--database-url", &self.connection_string(), "--source"]) .arg(&self.migrations_path); if let Some(config_path) = &self.config_path { diff --git a/sqlx-cli/tests/migrate.rs b/sqlx-cli/tests/migrate.rs index f33ee5eb0e..002aa4da96 100644 --- a/sqlx-cli/tests/migrate.rs +++ b/sqlx-cli/tests/migrate.rs @@ -1,5 +1,6 @@ mod common; +use common::MigrateCommand; use common::TestDatabase; #[tokio::test] @@ -14,7 +15,7 @@ async fn run_reversible_migrations() { // Without --target-version specified.k { let db = TestDatabase::new("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. @@ -22,17 +23,17 @@ async fn run_reversible_migrations() { let db = TestDatabase::new("run_reversible_latest_explicit", "migrations_reversible"); // 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); } @@ -41,26 +42,26 @@ async fn run_reversible_migrations() { let db = TestDatabase::new("run_reversible_incremental", "migrations_reversible"); // 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, @@ -68,7 +69,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]); } @@ -87,65 +88,150 @@ async fn revert_migrations() { // Without --target-version { let db = TestDatabase::new("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("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::::new()); } } +#[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]); + } +} + #[tokio::test] async fn ignored_chars() { let mut db = TestDatabase::new("ignored-chars", "ignored-chars/LF"); db.config_path = Some("tests/ignored-chars/sqlx.toml".into()); - db.run_migration(false, None, false).success(); + db.run_migration(MigrateCommand::Run, None, false).success(); db.set_migrations("ignored-chars/CRLF"); @@ -155,13 +241,19 @@ async fn ignored_chars() { db.migrate_info().success().stdout(expected_info); // Running migration should be a no-op - db.run_migration(false, None, false).success().stdout(""); + db.run_migration(MigrateCommand::Run, None, false) + .success() + .stdout(""); db.set_migrations("ignored-chars/BOM"); db.migrate_info().success().stdout(expected_info); - db.run_migration(false, None, false).success().stdout(""); + db.run_migration(MigrateCommand::Run, None, false) + .success() + .stdout(""); db.set_migrations("ignored-chars/oops-all-tabs"); db.migrate_info().success().stdout(expected_info); - db.run_migration(false, None, false).success().stdout(""); + db.run_migration(MigrateCommand::Run, None, false) + .success() + .stdout(""); } diff --git a/sqlx-core/src/any/migrate.rs b/sqlx-core/src/any/migrate.rs index 7a894c68bc..deca501cb7 100644 --- a/sqlx-core/src/any/migrate.rs +++ b/sqlx-core/src/any/migrate.rs @@ -99,4 +99,12 @@ impl Migrate for AnyConnection { ) -> BoxFuture<'e, Result> { Box::pin(async { self.get_migrate()?.revert(table_name, migration).await }) } + + fn skip<'e>( + &'e mut self, + table_name: &'e str, + migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async { self.get_migrate()?.skip(table_name, migration).await }) + } } diff --git a/sqlx-core/src/migrate/error.rs b/sqlx-core/src/migrate/error.rs index a04243963a..3c08f57301 100644 --- a/sqlx-core/src/migrate/error.rs +++ b/sqlx-core/src/migrate/error.rs @@ -42,4 +42,7 @@ pub enum MigrateError { #[error("database driver does not support creation of schemas at migrate time: {0}")] CreateSchemasNotSupported(String), + + #[error("database driver does not support skipping migrations")] + SkipNotSupported(), } diff --git a/sqlx-core/src/migrate/migrate.rs b/sqlx-core/src/migrate/migrate.rs index a3c365b807..de4bbc5f9d 100644 --- a/sqlx-core/src/migrate/migrate.rs +++ b/sqlx-core/src/migrate/migrate.rs @@ -78,4 +78,13 @@ pub trait Migrate { table_name: &'e str, migration: &'e Migration, ) -> BoxFuture<'e, Result>; + + // insert new row to [_migrations] table without running SQL from migration + fn skip<'e>( + &'e mut self, + _table_name: &'e str, + _migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async move { Err(MigrateError::SkipNotSupported()) }) + } } diff --git a/sqlx-core/src/migrate/migrator.rs b/sqlx-core/src/migrate/migrator.rs index 53295c92d0..12bba88a4b 100644 --- a/sqlx-core/src/migrate/migrator.rs +++ b/sqlx-core/src/migrate/migrator.rs @@ -180,7 +180,7 @@ impl Migrator { ::Target: Migrate, { let mut conn = migrator.acquire().await?; - self.run_direct(None, &mut *conn).await + self.run_direct(None, &mut *conn, false).await } pub async fn run_to<'a, A>(&self, target: i64, migrator: A) -> Result<(), MigrateError> @@ -189,12 +189,48 @@ impl Migrator { ::Target: Migrate, { let mut conn = migrator.acquire().await?; - self.run_direct(Some(target), &mut *conn).await + self.run_direct(Some(target), &mut *conn, false).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(target, &mut *conn, true).await } // Getting around the annoying "implementation of `Acquire` is not general enough" error #[doc(hidden)] - pub async fn run_direct(&self, target: Option, conn: &mut C) -> Result<(), MigrateError> + pub async fn run_direct( + &self, + target: Option, + conn: &mut C, + skip: bool, + ) -> Result<(), MigrateError> where C: Migrate, { @@ -241,7 +277,11 @@ impl Migrator { } } None => { - conn.apply(&self.table_name, migration).await?; + if skip { + conn.skip(&self.table_name, migration).await?; + } else { + conn.apply(&self.table_name, migration).await?; + } } } } diff --git a/sqlx-core/src/testing/mod.rs b/sqlx-core/src/testing/mod.rs index 17022b4652..379fac690e 100644 --- a/sqlx-core/src/testing/mod.rs +++ b/sqlx-core/src/testing/mod.rs @@ -257,7 +257,7 @@ async fn setup_test_db( if let Some(migrator) = args.migrator { migrator - .run_direct(None, &mut conn) + .run_direct(None, &mut conn, false) .await .expect("failed to apply migrations"); } diff --git a/sqlx-mysql/src/migrate.rs b/sqlx-mysql/src/migrate.rs index 7424dfe27f..54cf1cf660 100644 --- a/sqlx-mysql/src/migrate.rs +++ b/sqlx-mysql/src/migrate.rs @@ -308,6 +308,29 @@ CREATE TABLE IF NOT EXISTS {table_name} ( Ok(elapsed) }) } + + fn skip<'e>( + &'e mut self, + table_name: &'e str, + migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async move { + // language=MySQL + let _ = query(AssertSqlSafe(format!( + r#" + INSERT INTO {table_name} ( version, description, success, checksum, execution_time ) + VALUES ( ?, ?, TRUE, ?, -1 ) + "# + ))) + .bind(migration.version) + .bind(&*migration.description) + .bind(&*migration.checksum) + .execute(self) + .await?; + + Ok(()) + }) + } } async fn current_database(conn: &mut MySqlConnection) -> Result { diff --git a/sqlx-postgres/src/migrate.rs b/sqlx-postgres/src/migrate.rs index f4e55a36e6..4afa2046c9 100644 --- a/sqlx-postgres/src/migrate.rs +++ b/sqlx-postgres/src/migrate.rs @@ -288,6 +288,28 @@ CREATE TABLE IF NOT EXISTS {table_name} ( Ok(elapsed) }) } + + fn skip<'e>( + &'e mut self, + table_name: &'e str, + migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async move { + // language=SQL + let _ = query(AssertSqlSafe(format!( + r#" + INSERT INTO {table_name} ( version, description, success, checksum, execution_time ) + VALUES ( $1, $2, TRUE, $3, -1 ) + "# + ))) + .bind(migration.version) + .bind(&*migration.description) + .bind(&*migration.checksum) + .execute(self) + .await?; + Ok(()) + }) + } } async fn execute_migration( diff --git a/sqlx-sqlite/src/migrate.rs b/sqlx-sqlite/src/migrate.rs index dd9611b873..5864023891 100644 --- a/sqlx-sqlite/src/migrate.rs +++ b/sqlx-sqlite/src/migrate.rs @@ -221,6 +221,29 @@ CREATE TABLE IF NOT EXISTS {table_name} ( Ok(elapsed) }) } + + fn skip<'e>( + &'e mut self, + table_name: &'e str, + migration: &'e Migration, + ) -> BoxFuture<'e, Result<(), MigrateError>> { + Box::pin(async move { + // language=SQLite + let _ = query(AssertSqlSafe(format!( + r#" + INSERT INTO {table_name} ( version, description, success, checksum, execution_time ) + VALUES ( ?1, ?2, TRUE, ?3, -1 ) + "# + ))) + .bind(migration.version) + .bind(&*migration.description) + .bind(&*migration.checksum) + .execute(self) + .await?; + + Ok(()) + }) + } } async fn execute_migration( diff --git a/tests/mysql/migrate.rs b/tests/mysql/migrate.rs index 97caa38005..4a0759c81c 100644 --- a/tests/mysql/migrate.rs +++ b/tests/mysql/migrate.rs @@ -66,6 +66,51 @@ 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 after the first migration manually + let sql = include_str!("migrations_reversible/20220721124650_add_table.up.sql"); + let statements: Vec<&str> = sql.split(';').filter(|s| !s.trim().is_empty()).collect(); + 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..c08e24c731 100644 --- a/tests/postgres/migrate.rs +++ b/tests/postgres/migrate.rs @@ -66,6 +66,51 @@ 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 after the first migration manually + let sql = include_str!("migrations_reversible/20220721124650_add_table.up.sql"); + let statements: Vec<&str> = sql.split(';').filter(|s| !s.trim().is_empty()).collect(); + 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 ed5835225c..9b5dc38bfa 100644 --- a/tests/sqlite/migrate.rs +++ b/tests/sqlite/migrate.rs @@ -66,6 +66,51 @@ 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 after the first migration manually + let sql = include_str!("migrations_reversible/20220721124650_add_table.up.sql"); + let statements: Vec<&str> = sql.split(';').filter(|s| !s.trim().is_empty()).collect(); + 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?;