Skip to content

Commit

Permalink
Implement most of the Diesel CLI for 0.4.0
Browse files Browse the repository at this point in the history
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](clap-rs/clap#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?
  • Loading branch information
sgrif committed Jan 7, 2016
1 parent 57f6ffd commit 0781243
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 26 deletions.
2 changes: 2 additions & 0 deletions bin/diesel
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
cd diesel_cli && cargo run -- $@
1 change: 1 addition & 0 deletions bin/test
Original file line number Diff line number Diff line change
@@ -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)
66 changes: 40 additions & 26 deletions diesel/src/migrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, RunMigrationsError> {
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<Box<Migration>, 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())),
}
}

Expand Down Expand Up @@ -167,21 +175,27 @@ fn run_migrations<T>(conn: &Connection, migrations: T)
-> Result<(), RunMigrationsError> where
T: Iterator<Item=Box<Migration>>
{
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<Migration>)
-> 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<Migration>)
-> Result<(), RunMigrationsError>
{
Expand Down
11 changes: 11 additions & 0 deletions diesel_cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "diesel_cli"
version = "0.4.0"
authors = ["Sean Griffin <[email protected]>"]

[[bin]]
name = "diesel"

[dependencies]
diesel = "^0.3.0"
clap = "^1.5.5"
90 changes: 90 additions & 0 deletions diesel_cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -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))
}

0 comments on commit 0781243

Please sign in to comment.