From eb00efaa9aa6c1773046c6ac4d663a08cd3e4f7c Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Thu, 7 Jan 2016 11:49:38 -0700 Subject: [PATCH] Implement most of the Diesel CLI for 0.4.0 This adds all the plumbing, and 3 of the 4 commands that I want to ship with the CLI for 0.4.0. This adds the ability to run, revert, and redo migrations. There's still a lot of things I'd like to improve about the CLI. We need to give better error output in cases where something unexpected happens, and I'd like to proxy unknown subcommands to `diesel-subcommand` (this last one appears to be [a limitation of clap](https://github.com/kbknapp/clap-rs/issues/372). I've opted to make `diesel_cli` be a separate crate, as cargo doesn't allow you to declare dependencies as only for executables. I don't want to add `clap` or `chrono` (which I'll be adding as a dependency in the follow up to this commit) to become hard dependencies of diesel itself. Another unrelated enhancement I'd like to eventually make is adding `dotenv` support. I think this should be optional, but if it doesn't become a hard dependency of diesel itself, maybe it doesn't matter? --- bin/diesel | 2 + bin/test | 1 + diesel/src/migrations/mod.rs | 66 +++++++++++++++----------- diesel_cli/Cargo.toml | 11 +++++ diesel_cli/src/main.rs | 90 ++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 26 deletions(-) create mode 100755 bin/diesel create mode 100644 diesel_cli/Cargo.toml create mode 100644 diesel_cli/src/main.rs diff --git a/bin/diesel b/bin/diesel new file mode 100755 index 000000000000..90c6bd5171f3 --- /dev/null +++ b/bin/diesel @@ -0,0 +1,2 @@ +#!/bin/sh +cd diesel_cli && cargo run -- $@ diff --git a/bin/test b/bin/test index 4c82c147d808..b13c5920a16d 100755 --- a/bin/test +++ b/bin/test @@ -1,3 +1,4 @@ #!/bin/sh (cd diesel && cargo test --features unstable) && + (cd diesel_cli && cargo test) && (cd diesel_tests && cargo test --features unstable --no-default-features) diff --git a/diesel/src/migrations/mod.rs b/diesel/src/migrations/mod.rs index cde27938f7c8..d7e6fb8f32e8 100644 --- a/diesel/src/migrations/mod.rs +++ b/diesel/src/migrations/mod.rs @@ -97,33 +97,41 @@ pub fn run_pending_migrations(conn: &Connection) -> Result<(), RunMigrationsErro run_migrations(conn, pending_migrations) } -/// Reverts the last migration that was run. This function will return an `Err` if no migrations -/// have ever been run. +/// Reverts the last migration that was run. Returns the version that was reverted. Returns an +/// `Err` if no migrations have ever been run. /// /// See the [module level documentation](index.html) for information on how migrations should be /// structured, and where Diesel will look for them by default. -pub fn revert_latest_migration(conn: &Connection) -> Result<(), RunMigrationsError> { +pub fn revert_latest_migration(conn: &Connection) -> Result { try!(create_schema_migrations_table_if_needed(conn)); let latest_migration_version = try!(latest_run_migration_version(conn)); - revert_migration_with_version(conn, latest_migration_version) + revert_migration_with_version(conn, &latest_migration_version) + .map(|_| latest_migration_version) } -/// Reverts the migration with the given version. This function will return an `Err` if a migration -/// with that version cannot be found, an error occurs reading it, or if an error occurs when -/// reverting it. -/// -/// See the [module level documentation](index.html) for information on how migrations should be -/// structured, and where Diesel will look for them by default. -pub fn revert_migration_with_version(conn: &Connection, ver: String) -> Result<(), RunMigrationsError> { - try!(create_schema_migrations_table_if_needed(conn)); +#[doc(hidden)] +pub fn revert_migration_with_version(conn: &Connection, ver: &str) -> Result<(), RunMigrationsError> { + migration_with_version(ver) + .map_err(|e| e.into()) + .and_then(|m| revert_migration(conn, m)) +} + +#[doc(hidden)] +pub fn run_migration_with_version(conn: &Connection, ver: &str) -> Result<(), RunMigrationsError> { + migration_with_version(ver) + .map_err(|e| e.into()) + .and_then(|m| run_migration(conn, m)) +} + +fn migration_with_version(ver: &str) -> Result, MigrationError> { let migrations_dir = try!(find_migrations_directory()); let all_migrations = try!(migrations_in_directory(&migrations_dir)); - let migration_to_revert = all_migrations.into_iter().find(|m| { + let migration = all_migrations.into_iter().find(|m| { m.version() == ver }); - match migration_to_revert { - Some(m) => revert_migration(&conn, m), - None => Err(UnknownMigrationVersion(ver).into()), + match migration { + Some(m) => Ok(m), + None => Err(UnknownMigrationVersion(ver.into())), } } @@ -167,21 +175,27 @@ fn run_migrations(conn: &Connection, migrations: T) -> Result<(), RunMigrationsError> where T: Iterator> { - use ::query_builder::insert; - for migration in migrations { - try!(conn.transaction(|| { - println!("Running migration {}", migration.version()); - try!(migration.run(conn)); - try!(insert(&NewMigration(migration.version())) - .into(__diesel_schema_migrations) - .execute(&conn)); - Ok(()) - })); + try!(run_migration(conn, migration)); } Ok(()) } +fn run_migration(conn: &Connection, migration: Box) + -> Result<(), RunMigrationsError> +{ + use ::query_builder::insert; + + conn.transaction(|| { + println!("Running migration {}", migration.version()); + try!(migration.run(conn)); + try!(insert(&NewMigration(migration.version())) + .into(__diesel_schema_migrations) + .execute(&conn)); + Ok(()) + }).map_err(|e| e.into()) +} + fn revert_migration(conn: &Connection, migration: Box) -> Result<(), RunMigrationsError> { diff --git a/diesel_cli/Cargo.toml b/diesel_cli/Cargo.toml new file mode 100644 index 000000000000..defd20f7726e --- /dev/null +++ b/diesel_cli/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "diesel_cli" +version = "0.4.0" +authors = ["Sean Griffin "] + +[[bin]] +name = "diesel" + +[dependencies] +diesel = "^0.3.0" +clap = "^1.5.5" diff --git a/diesel_cli/src/main.rs b/diesel_cli/src/main.rs new file mode 100644 index 000000000000..d2221674bd87 --- /dev/null +++ b/diesel_cli/src/main.rs @@ -0,0 +1,90 @@ +#[macro_use] +extern crate clap; +extern crate diesel; + +use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; +use diesel::migrations; +use std::env; + +fn main() { + let database_arg = || Arg::with_name("DATABASE_URL") + .long("database-url") + .help("Specifies the database URL to connect to. Falls back to \ + the DATABASE_URL environment variable if unspecified.") + .takes_value(true); + + let migration_subcommand = SubCommand::with_name("migration") + .setting(AppSettings::VersionlessSubcommands) + .subcommand( + SubCommand::with_name("run") + .about("Runs all pending migrations") + .arg(database_arg()) + ).subcommand( + SubCommand::with_name("revert") + .about("Reverts the latest run migration") + .arg(database_arg()) + ).subcommand( + SubCommand::with_name("redo") + .about("Reverts and re-runs the latest migration. Useful \ + for testing that a migration can in fact be reverted.") + .arg(database_arg()) + ).subcommand( + SubCommand::with_name("generate") + .about("Generate a new migration with the given name, and \ + the current timestamp as the version") + .arg(Arg::with_name("MIGRATION_NAME") + .help("The name of the migration to create") + .required(true) + ) + ).setting(AppSettings::SubcommandRequiredElseHelp); + + let matches = App::new("diesel") + .version(env!("CARGO_PKG_VERSION")) + .setting(AppSettings::VersionlessSubcommands) + .subcommand(migration_subcommand) + .setting(AppSettings::SubcommandRequiredElseHelp) + .get_matches(); + + match matches.subcommand() { + ("migration", Some(matches)) => run_migration_command(matches), + _ => unreachable!(), + } +} + +// FIXME: We can improve the error handling instead of `unwrap` here. +fn run_migration_command(matches: &ArgMatches) { + match matches.subcommand() { + ("run", Some(args)) => { + migrations::run_pending_migrations(&connection(&database_url(args))) + .unwrap(); + } + ("revert", Some(args)) => { + migrations::revert_latest_migration(&connection(&database_url(args))) + .unwrap(); + } + ("redo", Some(args)) => { + let connection = connection(&database_url(args)); + connection.transaction(|| { + let reverted_version = try!(migrations::revert_latest_migration(&connection)); + migrations::run_migration_with_version(&connection, &reverted_version) + }).unwrap(); + } + ("generate", Some(args)) => { + panic!("Migration generator is not implemented this pass") + } + _ => unreachable!("The cli parser should prevent reaching here"), + } +} + +fn database_url(matches: &ArgMatches) -> String { + matches.value_of("DATABASE_URL") + .map(|s| s.into()) + .or(env::var("DATABASE_URL").ok()) + .expect("The --database-url argument must be passed, \ + or the DATABASE_URL environment variable must be set.") +} + +fn connection(database_url: &str) -> diesel::Connection { + diesel::Connection::establish(database_url) + .expect(&format!("Error connecting to {}", database_url)) +}