Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement most of the Diesel CLI for 0.4.0 #79

Merged
merged 1 commit into from
Jan 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this branch is at a different level of abstraction than the others. What do you think about extracting this into a function also?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anything I'm actually thinking of moving the other branches down. I don't like having #[doc(hidden)] public functions in the main library, and really the only "public" use case I can see is running pending migrations as part of your build script.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given that this is main.rs, are any of the things in here actually public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's what I mean. I'd rather move more of the logic into here and out of diesel proper.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok. Either way, this is mostly fine, it just caught me as a little odd that the branches weren't balanced. Do with that information what your much better informed than me self wants to do :)

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))
}