cli: add --target-version CLI flags for migrate run/revert (#2538)

* cli: add --target-version CLI flags for migrate run/revert

* cli: fix broken test

* cli: test harness for `sqlx migrate` along with --target-version tests

* cli: Fail if version supplied to run/revert is too old/new

After some discussion with my coworkers, we thought about the behavior a bit more:

The behavior is now that for a run, if the provided version is too old, the CLI
will return with failure rather than being a no-op. This gives feedback to the
operator instead of being quiet.

It is still valid to up/downgrade to the latest version, this will still be a no-op
to allow for idempotency.
This commit is contained in:
Ameer Ghani
2023-07-31 15:49:53 -04:00
committed by GitHub
parent febf9ed775
commit 84f21e99ef
21 changed files with 438 additions and 11 deletions

View File

@@ -50,7 +50,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).await
migrate::run(migration_source, connect_opts, false, false, None).await
}
fn ask_to_continue(connect_opts: &ConnectOpts) -> bool {

View File

@@ -35,13 +35,33 @@ pub async fn run(opt: Opt) -> Result<()> {
dry_run,
ignore_missing,
connect_opts,
} => migrate::run(&source, &connect_opts, dry_run, *ignore_missing).await?,
target_version,
} => {
migrate::run(
&source,
&connect_opts,
dry_run,
*ignore_missing,
target_version,
)
.await?
}
MigrateCommand::Revert {
source,
dry_run,
ignore_missing,
connect_opts,
} => migrate::revert(&source, &connect_opts, dry_run, *ignore_missing).await?,
target_version,
} => {
migrate::revert(
&source,
&connect_opts,
dry_run,
*ignore_missing,
target_version,
)
.await?
}
MigrateCommand::Info {
source,
connect_opts,

View File

@@ -267,8 +267,15 @@ pub async fn run(
connect_opts: &ConnectOpts,
dry_run: bool,
ignore_missing: bool,
target_version: Option<i64>,
) -> anyhow::Result<()> {
let migrator = Migrator::new(Path::new(migration_source)).await?;
if let Some(target_version) = target_version {
if !migrator.iter().any(|m| target_version == m.version) {
bail!(MigrateError::VersionNotPresent(target_version));
}
}
let mut conn = crate::connect(connect_opts).await?;
conn.ensure_migrations_table().await?;
@@ -281,6 +288,17 @@ pub async fn run(
let applied_migrations = conn.list_applied_migrations().await?;
validate_applied_migrations(&applied_migrations, &migrator, ignore_missing)?;
let latest_version = applied_migrations
.iter()
.max_by(|x, y| x.version.cmp(&y.version))
.and_then(|migration| Some(migration.version))
.unwrap_or(0);
if let Some(target_version) = target_version {
if target_version < latest_version {
bail!(MigrateError::VersionTooOld(target_version, latest_version));
}
}
let applied_migrations: HashMap<_, _> = applied_migrations
.into_iter()
.map(|m| (m.version, m))
@@ -299,12 +317,23 @@ pub async fn run(
}
}
None => {
let elapsed = if dry_run {
let skip = match target_version {
Some(target_version) if migration.version > target_version => true,
_ => false,
};
let elapsed = if dry_run || skip {
Duration::new(0, 0)
} else {
conn.apply(migration).await?
};
let text = if dry_run { "Can apply" } else { "Applied" };
let text = if skip {
"Skipped"
} else if dry_run {
"Can apply"
} else {
"Applied"
};
println!(
"{} {}/{} {} {}",
@@ -333,8 +362,15 @@ pub async fn revert(
connect_opts: &ConnectOpts,
dry_run: bool,
ignore_missing: bool,
target_version: Option<i64>,
) -> anyhow::Result<()> {
let migrator = Migrator::new(Path::new(migration_source)).await?;
if let Some(target_version) = target_version {
if target_version != 0 && !migrator.iter().any(|m| target_version == m.version) {
bail!(MigrateError::VersionNotPresent(target_version));
}
}
let mut conn = crate::connect(&connect_opts).await?;
conn.ensure_migrations_table().await?;
@@ -347,6 +383,17 @@ pub async fn revert(
let applied_migrations = conn.list_applied_migrations().await?;
validate_applied_migrations(&applied_migrations, &migrator, ignore_missing)?;
let latest_version = applied_migrations
.iter()
.max_by(|x, y| x.version.cmp(&y.version))
.and_then(|migration| Some(migration.version))
.unwrap_or(0);
if let Some(target_version) = target_version {
if target_version > latest_version {
bail!(MigrateError::VersionTooNew(target_version, latest_version));
}
}
let applied_migrations: HashMap<_, _> = applied_migrations
.into_iter()
.map(|m| (m.version, m))
@@ -361,12 +408,22 @@ pub async fn revert(
}
if applied_migrations.contains_key(&migration.version) {
let elapsed = if dry_run {
let skip = match target_version {
Some(target_version) if migration.version <= target_version => true,
_ => false,
};
let elapsed = if dry_run || skip {
Duration::new(0, 0)
} else {
conn.revert(migration).await?
};
let text = if dry_run { "Can apply" } else { "Applied" };
let text = if skip {
"Skipped"
} else if dry_run {
"Can apply"
} else {
"Applied"
};
println!(
"{} {}/{} {} {}",
@@ -378,8 +435,12 @@ pub async fn revert(
);
is_applied = true;
// Only a single migration will be reverted at a time, so we break
break;
// Only a single migration will be reverted at a time if no target
// version is supplied, so we break.
if let None = target_version {
break;
}
}
}
if !is_applied {

View File

@@ -159,6 +159,11 @@ pub enum MigrateCommand {
#[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<i64>,
},
/// Revert the latest migration with a down file.
@@ -175,6 +180,12 @@ pub enum MigrateCommand {
#[clap(flatten)]
connect_opts: ConnectOpts,
/// Revert migrations down to the specified version. If unspecified, revert
/// only the last migration. Set to 0 to revert all migrations. If already
/// at the target version, then no-op.
#[clap(long)]
target_version: Option<i64>,
},
/// List all available migrations.

View File

@@ -361,7 +361,7 @@ mod tests {
let sample_metadata = std::fs::read_to_string(sample_metadata_path)?;
let metadata: Metadata = sample_metadata.parse()?;
let action = minimal_project_recompile_action(&metadata)?;
let action = minimal_project_recompile_action(&metadata);
assert_eq!(
action,
ProjectRecompileAction {