Skip to content

Commit

Permalink
cli: add --target-version CLI flags for migrate run/revert (#2538)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
inahga committed Jul 31, 2023
1 parent febf9ed commit 84f21e9
Show file tree
Hide file tree
Showing 21 changed files with 438 additions and 11 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/sqlx.yml
Expand Up @@ -90,6 +90,28 @@ jobs:
--manifest-path sqlx-core/Cargo.toml
--features json,_rt-${{ matrix.runtime }},_tls-${{ matrix.tls }}
cli-test:
name: CLI Unit Test
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2

- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true

- uses: Swatinem/rust-cache@v1
with:
key: ${{ runner.os }}-test

- uses: actions-rs/cargo@v1
with:
command: test
args: >
--manifest-path sqlx-cli/Cargo.toml
cli:
name: CLI Binaries
runs-on: ${{ matrix.os }}
Expand Down
53 changes: 52 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions sqlx-cli/Cargo.toml
Expand Up @@ -66,3 +66,6 @@ sqlite = ["sqlx/sqlite"]
openssl-vendored = ["openssl/vendored"]

completions = ["dep:clap_complete"]

[dev-dependencies]
assert_cmd = "2.0.11"
2 changes: 1 addition & 1 deletion sqlx-cli/src/database.rs
Expand Up @@ -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 {
Expand Down
24 changes: 22 additions & 2 deletions sqlx-cli/src/lib.rs
Expand Up @@ -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,
Expand Down
73 changes: 67 additions & 6 deletions sqlx-cli/src/migrate.rs
Expand Up @@ -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?;
Expand All @@ -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))
Expand All @@ -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!(
"{} {}/{} {} {}",
Expand Down Expand Up @@ -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?;
Expand All @@ -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))
Expand All @@ -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!(
"{} {}/{} {} {}",
Expand All @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions sqlx-cli/src/opt.rs
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion sqlx-cli/src/prepare.rs
Expand Up @@ -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 {
Expand Down

0 comments on commit 84f21e9

Please sign in to comment.