Skip to content

Commit

Permalink
Implement diesel setup for diesel CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
mcasper committed Jan 24, 2016
1 parent db8deab commit d7077f7
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 8 deletions.
3 changes: 2 additions & 1 deletion diesel/src/migrations/mod.rs
Expand Up @@ -135,7 +135,8 @@ fn migration_with_version(ver: &str) -> Result<Box<Migration>, MigrationError> {
}
}

fn create_schema_migrations_table_if_needed<Conn: Connection>(conn: &Conn) -> QueryResult<usize> {
#[doc(hidden)]
pub fn create_schema_migrations_table_if_needed<Conn: Connection>(conn: &Conn) -> QueryResult<usize> {
conn.silence_notices(|| {
conn.execute("CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (
version VARCHAR PRIMARY KEY NOT NULL,
Expand Down
4 changes: 4 additions & 0 deletions diesel_cli/Cargo.toml
Expand Up @@ -16,3 +16,7 @@ name = "diesel"
diesel = "^0.4.0"
clap = "^1.5.5"
chrono = "^0.2.17"

[dev-dependencies]
dotenv = "^0.6.0"
tempdir = "^0.3.4"
31 changes: 26 additions & 5 deletions diesel_cli/README.md
Expand Up @@ -9,7 +9,7 @@ Getting Started

```shell
cargo install diesel_cli
mkdir migrations
diesel setup --database-url='postgres://localhost/my_db'
diesel migration generate create_users_table
```

Expand Down Expand Up @@ -42,8 +42,29 @@ Alternatively, you can call
Diesel will automatically keep track of which migrations have already been run,
ensuring that they're never run twice.

[pending-migrations]: http://sgrif.github.io/diesel/diesel/migrations/fn.run_pending_migrations.html
### Commands
#### `setup`
Searches for a `migrations/` directory, and if it can't find one, creates one
in the same directory as the first `Cargo.toml` it finds. It then tries to
connect to the provided DATABASE_URL, and will create the given database if it
cannot connect to it. Finally it will create diesel's internal table for
tracking which migrations have been run, and run any existing migrations if the
internal table did not previously exist.

#### `migration generate`
Takes the name of your migration as an argument, and will create a migration
directory with `migrations/` in the format of
`migrations/{current_timestamp}_{migration_name}`. It will also generate
`up.sql` and `down.sql` files, for running your migration up and down
respectively.

#### `migration run`
Runs all pending migrations, as determined by diesel's internal schema table.

If you ever need to revert or make changes to your migrations, the commands
`diesel migration revert` and `diesel migration redo`. Type `diesel migration
--help` for more information.
#### `migration revert`
Runs the `down.sql` for the most recent migration.

#### `migration redo`
Runs the `down.sql` and then the `up.sql` for the most recent migration.

[pending-migrations]: http://sgrif.github.io/diesel/diesel/migrations/fn.run_pending_migrations.html
148 changes: 146 additions & 2 deletions diesel_cli/src/main.rs
Expand Up @@ -3,11 +3,16 @@ extern crate chrono;
extern crate clap;
extern crate diesel;

mod setup_error;

use chrono::*;
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use diesel::{migrations, Connection};
use diesel::connection::PgConnection;
use self::setup_error::SetupError;
use std::{env, fs};
use std::error::Error;
use std::path::{PathBuf, Path};

fn main() {
let database_arg = || Arg::with_name("DATABASE_URL")
Expand Down Expand Up @@ -41,15 +46,22 @@ fn main() {
)
).setting(AppSettings::SubcommandRequiredElseHelp);

let setup_subcommand = SubCommand::with_name("setup")
.about("Creates the migrations directory, creates the database \
specified in your DATABASE_URL, and runs existing migrations.")
.arg(database_arg());

let matches = App::new("diesel")
.version(env!("CARGO_PKG_VERSION"))
.setting(AppSettings::VersionlessSubcommands)
.subcommand(migration_subcommand)
.subcommand(setup_subcommand)
.setting(AppSettings::SubcommandRequiredElseHelp)
.get_matches();

match matches.subcommand() {
("migration", Some(matches)) => run_migration_command(matches),
("setup", Some(matches)) => run_setup_command(matches),
_ => unreachable!(),
}
}
Expand Down Expand Up @@ -91,6 +103,74 @@ fn run_migration_command(matches: &ArgMatches) {
}
}

fn run_setup_command(matches: &ArgMatches) {
migrations::find_migrations_directory()
.unwrap_or_else(|_|
create_migrations_directory()
.map_err(handle_error).unwrap()
);

if PgConnection::establish(&database_url(matches)).is_err() {
create_database(database_url(matches)).unwrap_or_else(handle_error);
}

let connection = connection(&database_url(matches));

if !schema_table_exists(&connection).map_err(handle_error).unwrap() {
migrations::create_schema_migrations_table_if_needed(&connection)
.map_err(handle_error).unwrap();
migrations::run_pending_migrations(&connection)
.map_err(handle_error).unwrap();
}
}

/// Creates the database specified in the connection url. It returns a
/// `SetupError::ConnectionError` if it can't connect to the postgres
/// connection url, and returns a `SetupError::QueryError` if it is unable
/// to create the database.
fn create_database(database_url: String) -> Result<(), SetupError> {
let mut split: Vec<&str> = database_url.split("/").collect();
let database = split.pop().unwrap();
let postgres_url = split.join("/");
let connection = try!(PgConnection::establish(&postgres_url));
try!(connection.execute(&format!("CREATE DATABASE {};", database)));
Ok(())
}

/// Looks for a migrations directory in the current path and all parent paths,
/// and creates one in the same directory as the Cargo.toml if it can't find
/// one. It also sticks a .gitkeep in the directory so git will pick it up.
/// Returns a `SetupError::CargoTomlNotFound` if no Cargo.toml is found.
fn create_migrations_directory() -> Result<PathBuf, SetupError> {
let project_root = try!(find_project_root());
try!(fs::create_dir(project_root.join("migrations")));
try!(fs::File::create(project_root.join("migrations/.gitkeep")));
Ok(project_root)
}

fn find_project_root() -> Result<PathBuf, SetupError> {
search_for_cargo_toml_directory(&try!(env::current_dir()))
}

fn search_for_cargo_toml_directory(path: &Path) -> Result<PathBuf, SetupError> {
let toml_path = path.join("Cargo.toml");
if toml_path.is_file() {
Ok(path.to_owned())
} else {
path.parent().map(search_for_cargo_toml_directory)
.unwrap_or(Err(SetupError::CargoTomlNotFound))
}
}

/// Returns true if the '__diesel_schema_migrations' table exists in the
/// database we connect to, returns false if it does not.
pub fn schema_table_exists(connection: &PgConnection) -> Result<bool, SetupError> {
let result = try!(connection.execute("SELECT 1
FROM information_schema.tables
WHERE table_name = '__diesel_schema_migrations';"));
Ok(result != 0)
}

fn handle_error<E: Error>(error: E) {
println!("{}", error);
std::process::exit(1);
Expand All @@ -104,7 +184,71 @@ fn database_url(matches: &ArgMatches) -> String {
or the DATABASE_URL environment variable must be set.")
}

fn connection(database_url: &str) -> diesel::connection::PgConnection {
diesel::connection::PgConnection::establish(database_url)
fn connection(database_url: &str) -> PgConnection {
PgConnection::establish(database_url)
.expect(&format!("Error connecting to {}", database_url))
}

#[cfg(test)]
mod tests {
extern crate diesel;
extern crate dotenv;
extern crate tempdir;

use self::tempdir::TempDir;
use self::diesel::Connection;

use setup_error::SetupError;

use super::{schema_table_exists, search_for_cargo_toml_directory};

use std::{env, fs};

fn connection() -> diesel::connection::PgConnection {
dotenv::dotenv().ok();
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set in order to run diesel_cli tests");
diesel::connection::PgConnection::establish(&database_url).unwrap()
}

#[test]
fn toml_directory_find_cargo_toml() {
let dir = TempDir::new("diesel").unwrap();
let temp_path = dir.path().canonicalize().unwrap();
let toml_path = temp_path.join("Cargo.toml");

fs::File::create(&toml_path).unwrap();

assert_eq!(Ok(temp_path.clone()), search_for_cargo_toml_directory(&temp_path));
}

#[test]
fn cargo_toml_not_found_if_no_cargo_toml() {
let dir = TempDir::new("diesel").unwrap();
let temp_path = dir.path().canonicalize().unwrap();

assert_eq!(Err(SetupError::CargoTomlNotFound),
search_for_cargo_toml_directory(&temp_path));
}

#[test]
fn schema_table_exists_finds_table() {
let connection = connection();
connection.silence_notices(|| {
connection.execute("DROP TABLE IF EXISTS __diesel_schema_migrations").unwrap();
connection.execute("CREATE TABLE __diesel_schema_migrations ()").unwrap();
});

assert!(schema_table_exists(&connection).unwrap());
}

#[test]
fn schema_table_exists_doesnt_find_table() {
let connection = connection();
connection.silence_notices(|| {
connection.execute("DROP TABLE IF EXISTS __diesel_schema_migrations").unwrap();
});

assert!(!schema_table_exists(&connection).unwrap());
}
}
63 changes: 63 additions & 0 deletions diesel_cli/src/setup_error.rs
@@ -0,0 +1,63 @@
use diesel::result;

use std::convert::From;
use std::{fmt, io};
use std::error::Error;

use self::SetupError::*;

#[derive(Debug)]
pub enum SetupError {
#[allow(dead_code)]
CargoTomlNotFound,
IoError(io::Error),
QueryError(result::Error),
ConnectionError(result::ConnectionError),
}

impl From<io::Error> for SetupError {
fn from(e: io::Error) -> Self {
IoError(e)
}
}

impl From<result::Error> for SetupError {
fn from(e: result::Error) -> Self {
QueryError(e)
}
}

impl From<result::ConnectionError> for SetupError {
fn from(e: result::ConnectionError) -> Self {
ConnectionError(e)
}
}

impl Error for SetupError {
fn description(&self) -> &str {
match *self {
CargoTomlNotFound => "Unable to find Cargo.toml in this directory or any parent directories.",
IoError(ref error) => error.cause().map(|e| e.description()).unwrap_or(error.description()),
QueryError(ref error) => error.cause().map(|e| e.description()).unwrap_or(error.description()),
ConnectionError(ref error) => error.cause().map(|e| e.description()).unwrap_or(error.description()),
}
}
}

impl fmt::Display for SetupError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
self.description().fmt(f)
}
}

impl PartialEq for SetupError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
&CargoTomlNotFound,
&CargoTomlNotFound,
) => true,
_ => false
}
}
}

0 comments on commit d7077f7

Please sign in to comment.