Skip to content

Commit

Permalink
Add a way to redo all or a specific number of migrations
Browse files Browse the repository at this point in the history
This commit also modifies the revert behavior slightly. It will
no longer produce the rolling back message for the
00000000000000_diesel_initial_setup migration file. In doing this,
we are following the same behavior as redo.
  • Loading branch information
theredfish committed Oct 11, 2020
1 parent 917ad75 commit 28d0d41
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 46 deletions.
21 changes: 20 additions & 1 deletion diesel_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,31 @@ pub fn build_cli() -> App<'static, 'static> {
.arg(
Arg::with_name("REDO_ALL")
.long("all")
.short("a")
.help("Reverts and re-runs all migrations.")
.long_help(
"When this option is specified all migrations \
will be reverted and re-runs. Useful for testing \
that your migrations can be reverted and applied.",
),
)
.takes_value(false)
.conflicts_with("REDO_NUMBER"),
)
.arg(
Arg::with_name("REDO_NUMBER")
.long("number")
.short("n")
.help("Redo the last `n` migration files")
.long_help(
"When this option is specified the last `n` migration files \
will be reverted and re-runs. By default redo the last migration.",
)
// TODO : when upgrading to clap 3.0 add default_value("1").
// Then update code in main.rs for the revert subcommand.
// See https://github.com/clap-rs/clap/issues/1605
.takes_value(true)
.validator(is_positive_int)
.conflicts_with("REDO_ALL"),
),
)
.subcommand(
Expand Down
56 changes: 41 additions & 15 deletions diesel_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ fn run_migration_command(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {

regenerate_schema_if_file_specified(matches)?;
}
("redo", Some(_)) => {
("redo", Some(args)) => {
let database_url = database::database_url(matches);
let dir = migrations_dir(matches).unwrap_or_else(handle_error);
call_with_conn!(database_url, redo_latest_migration(&dir));

call_with_conn!(database_url, redo_migrations(&dir, args));
regenerate_schema_if_file_specified(matches)?;
}
("list", Some(_)) => {
Expand Down Expand Up @@ -351,27 +352,52 @@ fn search_for_directory_containing_file(path: &Path, file: &str) -> DatabaseResu
}
}

/// Reverts the most recent migration, and then runs it again, all in a
/// If g
/// Reverts the most recent migrations, and then runs it again, all in a
/// transaction. If either part fails, the transaction is not committed.
fn redo_latest_migration<Conn>(conn: &Conn, migrations_dir: &Path)
fn redo_migrations<Conn>(conn: &Conn, migrations_dir: &Path, args: &ArgMatches)
where
Conn: MigrationConnection + Any,
{
let migration_inner = || {
let reverted_version =
migrations::revert_latest_migration_in_directory(conn, migrations_dir)?;
migrations::run_migration_with_version(
conn,
migrations_dir,
&reverted_version,
&mut stdout(),
)
let migrations_inner = || -> Result<(), diesel::migration::RunMigrationsError> {
let reverted_versions = if args.is_present("REDO_ALL") {
migrations::revert_all_migrations_in_directory(conn, migrations_dir)?
} else {
// TODO : remove this logic when upgrading to clap 3.0.
// We handle the default_value here instead of doing it
// in the cli. This is because arguments with default
// values conflict even if not used.
// See https://github.com/clap-rs/clap/issues/1605
let number = match args.value_of("REDO_NUMBER") {
None => "1",
Some(number) => number,
};

migrations::revert_latest_migrations_in_directory(
conn,
migrations_dir,
// We can unwrap since the argument is validated from the cli module.
number.parse::<u64>().unwrap(),
)?
};

for reverted_version in reverted_versions {
migrations::run_migration_with_version(
conn,
migrations_dir,
&reverted_version,
&mut stdout(),
)?;
}

Ok(())
};

if should_redo_migration_in_transaction(conn) {
conn.transaction(migration_inner)
conn.transaction(migrations_inner)
.unwrap_or_else(handle_error);
} else {
migration_inner().unwrap_or_else(handle_error);
migrations_inner().unwrap_or_else(handle_error);
}
}

Expand Down
227 changes: 226 additions & 1 deletion diesel_cli/tests/migration_redo.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::support::project;
use crate::support::{database, project};

#[test]
fn migration_redo_runs_the_last_migration_down_and_up() {
Expand Down Expand Up @@ -27,6 +27,64 @@ Running migration 12345_create_users_table
);
}

#[test]
fn migration_redo_runs_the_last_two_migrations_down_and_up() {
let p = project("migration_redo_runs_the_last_two_migrations_down_and_up")
.folder("migrations")
.build();
let db = database(&p.database_url());

p.create_migration(
"2017-08-31-210424_create_customers",
"CREATE TABLE customers ( id INTEGER PRIMARY KEY )",
"DROP TABLE customers",
);

p.create_migration(
"2017-09-03-210424_create_contracts",
"CREATE TABLE contracts ( id INTEGER PRIMARY KEY )",
"DROP TABLE contracts",
);

p.create_migration(
"2017-09-12-210424_create_bills",
"CREATE TABLE bills ( id INTEGER PRIMARY KEY )",
"DROP TABLE bills",
);

// Make sure the project is setup
p.command("setup").run();

assert!(db.table_exists("customers"));
assert!(db.table_exists("contracts"));
assert!(db.table_exists("bills"));

// Redo the last two migration files. The `contracts` and `bills` tables should be re-runs.
// The `customers` table shouldn't be redo.
let result = p
.command("migration")
.arg("redo")
.arg("-n")
.arg("2")
.run();

assert!(result.is_success(), "Result was unsuccessful {:?}", result);

assert!(
result.stdout()
== "Rolling back migration 2017-09-12-210424_create_bills\n\
Rolling back migration 2017-09-03-210424_create_contracts\n\
Running migration 2017-09-12-210424_create_bills\n\
Running migration 2017-09-03-210424_create_contracts\n",
"Unexpected stdout : {}",
result.stdout()
);

assert!(db.table_exists("customers"));
assert!(db.table_exists("contracts"));
assert!(db.table_exists("bills"));
}

#[test]
fn migration_redo_respects_migration_dir_var() {
let p = project("migration_redo_var").folder("foo").build();
Expand Down Expand Up @@ -174,3 +232,170 @@ Running migration 12345_create_users_table
result.stdout()
);
}

#[test]
fn migration_redo_all_runs_all_migrations_down_and_up() {
let p = project("migration_redo_all_runs_all_migrations_down_and_up")
.folder("migrations")
.build();
let db = database(&p.database_url());

p.create_migration(
"2017-08-31-210424_create_customers",
"CREATE TABLE customers ( id INTEGER PRIMARY KEY )",
"DROP TABLE customers",
);

p.create_migration(
"2017-09-03-210424_create_contracts",
"CREATE TABLE contracts ( id INTEGER PRIMARY KEY )",
"DROP TABLE contracts",
);

p.create_migration(
"2017-09-12-210424_create_bills",
"CREATE TABLE bills ( id INTEGER PRIMARY KEY )",
"DROP TABLE bills",
);

// Make sure the project is setup
p.command("setup").run();

assert!(db.table_exists("customers"));
assert!(db.table_exists("contracts"));
assert!(db.table_exists("bills"));

let result = p.command("migration").arg("redo").arg("--all").run();

assert!(result.is_success(), "Result was unsuccessful {:?}", result);

assert!(
result.stdout()
== "Rolling back migration 2017-09-12-210424_create_bills\n\
Rolling back migration 2017-09-03-210424_create_contracts\n\
Rolling back migration 2017-08-31-210424_create_customers\n\
Running migration 2017-09-12-210424_create_bills\n\
Running migration 2017-09-03-210424_create_contracts\n\
Running migration 2017-08-31-210424_create_customers\n",
"Unexpected stdout : {}",
result.stdout()
);

assert!(db.table_exists("customers"));
assert!(db.table_exists("contracts"));
assert!(db.table_exists("bills"));
}

#[test]
fn migration_redo_with_more_than_max_should_redo_all() {
let p = project("migration_redo_with_more_than_max_should_redo_all")
.folder("migrations")
.build();
let db = database(&p.database_url());

p.create_migration(
"2017-08-31-210424_create_customers",
"CREATE TABLE customers ( id INTEGER PRIMARY KEY )",
"DROP TABLE customers",
);

p.create_migration(
"2017-09-03-210424_create_contracts",
"CREATE TABLE contracts ( id INTEGER PRIMARY KEY )",
"DROP TABLE contracts",
);

p.create_migration(
"2017-09-12-210424_create_bills",
"CREATE TABLE bills ( id INTEGER PRIMARY KEY )",
"DROP TABLE bills",
);

// Make sure the project is setup
p.command("setup").run();

assert!(db.table_exists("customers"));
assert!(db.table_exists("contracts"));
assert!(db.table_exists("bills"));

let result = p
.command("migration")
.arg("redo")
.arg("-n")
.arg("1000")
.run();

assert!(result.is_success(), "Result was unsuccessful {:?}", result);

assert!(
result.stdout()
== "Rolling back migration 2017-09-12-210424_create_bills\n\
Rolling back migration 2017-09-03-210424_create_contracts\n\
Rolling back migration 2017-08-31-210424_create_customers\n\
Running migration 2017-09-12-210424_create_bills\n\
Running migration 2017-09-03-210424_create_contracts\n\
Running migration 2017-08-31-210424_create_customers\n",
"Unexpected stdout : {}",
result.stdout()
);

assert!(db.table_exists("customers"));
assert!(db.table_exists("contracts"));
assert!(db.table_exists("bills"));
}

#[test]
fn migration_redo_n_with_a_string_should_throw_an_error() {
let p = project("migration_redo_n_with_a_string_should_throw_an_error")
.folder("migrations")
.build();

// Make sure the project is setup
p.command("setup").run();

// Should not revert any migration.
let result = p
.command("migration")
.arg("redo")
.arg("-n")
.arg("infinite")
.run();

assert!(!result.is_success(), "Result was unsuccessful {:?}", result);

assert!(
result.stderr() == "error: Invalid value for '--number <REDO_NUMBER>': infinite isn't a positive integer.\n",
"Unexpected stderr : {}",
result.stderr()
);
}

#[test]
fn migration_redo_with_zero_should_not_revert_any_migration() {
let p = project("migration_redo_with_zero_should_not_revert_any_migration")
.folder("migrations")
.build();
let db = database(&p.database_url());

p.create_migration(
"2017-08-31-210424_create_customers",
"CREATE TABLE customers ( id INTEGER PRIMARY KEY )",
"DROP TABLE customers",
);

// Make sure the project is setup
p.command("setup").run();

assert!(db.table_exists("customers"));

// Should not revert any migration.
let result = p
.command("migration")
.arg("redo")
.arg("-n")
.arg("0")
.run();

assert!(!result.is_success(), "Result was unsuccessful {:?}", result);
assert!(result.stdout() == "");
}
Loading

0 comments on commit 28d0d41

Please sign in to comment.