diff --git a/Cargo.lock b/Cargo.lock index 533b8fd954..b4ed9021d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,6 +287,18 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" +[[package]] +name = "cargo-sqlx" +version = "0.1.0" +dependencies = [ + "chrono", + "dotenv", + "futures 0.3.4", + "sqlx 0.2.6", + "structopt", + "tokio 0.2.13", +] + [[package]] name = "cc" version = "1.0.50" diff --git a/Cargo.toml b/Cargo.toml index 7eef470c24..faf62aaa40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "sqlx-core", "sqlx-macros", "sqlx-test", + "cargo-sqlx", "examples/mysql/todos", "examples/postgres/listen", "examples/postgres/realworld", diff --git a/cargo-sqlx/.gitignore b/cargo-sqlx/.gitignore new file mode 100644 index 0000000000..4b0df7dd35 --- /dev/null +++ b/cargo-sqlx/.gitignore @@ -0,0 +1,4 @@ +/target +/migrations +Cargo.lock +.env \ No newline at end of file diff --git a/cargo-sqlx/Cargo.toml b/cargo-sqlx/Cargo.toml new file mode 100644 index 0000000000..224908d541 --- /dev/null +++ b/cargo-sqlx/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "cargo-sqlx" +version = "0.1.0" +description = "Simple postgres migrator without support for down migration" +authors = ["Jesper Axelsson "] +edition = "2018" +readme = "README.md" +homepage = "https://github.com/launchbadge/sqlx" +repository = "https://github.com/launchbadge/sqlx" +keywords = ["database", "postgres", "database-management", "migration"] +categories = ["database", "command-line-utilities"] + +[[bin]] +name = "sqlx" +path = "src/main.rs" + +[dependencies] +dotenv = "0.15" + +tokio = { version = "0.2", features = ["macros"] } +# sqlx = { path = "..", default-features = false, features = [ "runtime-tokio", "macros", "postgres" ] } +sqlx = { version = "0.2", default-features = false, features = [ "runtime-tokio", "macros", "postgres" ] } +futures="0.3" + +structopt = "0.3" +chrono = "0.4" diff --git a/cargo-sqlx/README.md b/cargo-sqlx/README.md new file mode 100644 index 0000000000..8c9553273a --- /dev/null +++ b/cargo-sqlx/README.md @@ -0,0 +1,14 @@ +# cargo-sqlx + +Sqlx migrator runs all `*.sql` files under `migrations` folder and remembers which ones has been run. + +Database url is supplied through either env variable or `.env` file containing `DATABASE_URL="postgres://postgres:postgres@localhost/realworld"`. + +##### Commands +- `add ` - add new migration to your migrations folder named `_.sql` +- `run` - Runs all migrations in your migrations folder + + +##### Limitations +- No down migrations! If you need down migrations, there are other more feature complete migrators to use. +- Only support postgres. Could be convinced to add other databases if there is need and easy to use database connection libs. diff --git a/cargo-sqlx/src/main.rs b/cargo-sqlx/src/main.rs new file mode 100644 index 0000000000..89513ad337 --- /dev/null +++ b/cargo-sqlx/src/main.rs @@ -0,0 +1,206 @@ +use std::env; +use std::fs; +use std::fs::File; +use std::io::prelude::*; + +use dotenv::dotenv; + +use sqlx::PgConnection; +use sqlx::PgPool; + +use structopt::StructOpt; + +const MIGRATION_FOLDER: &'static str = "migrations"; + +/// Sqlx commandline tool +#[derive(StructOpt, Debug)] +#[structopt(name = "Sqlx")] +enum Opt { + // #[structopt(subcommand)] + Migrate(MigrationCommand), +} + +/// Simple postgres migrator +#[derive(StructOpt, Debug)] +#[structopt(name = "Sqlx migrator")] +enum MigrationCommand { + /// Initalizes new migration directory with db create script + // Init { + // // #[structopt(long)] + // database_name: String, + // }, + + /// Add new migration with name _.sql + Add { + // #[structopt(long)] + name: String, + }, + + /// Run all migrations + Run, +} + +#[tokio::main] +async fn main() { + let opt = Opt::from_args(); + + match opt { + Opt::Migrate(command) => match command { + // Opt::Init { database_name } => init_migrations(&database_name), + MigrationCommand::Add { name } => add_migration_file(&name), + MigrationCommand::Run => run_migrations().await, + }, + } + + println!("All done!"); +} + +// fn init_migrations(db_name: &str) { +// println!("Initing the migrations so hard! db: {:#?}", db_name); +// } + +fn add_migration_file(name: &str) { + use chrono::prelude::*; + use std::path::Path; + use std::path::PathBuf; + + if !Path::new(MIGRATION_FOLDER).exists() { + fs::create_dir(MIGRATION_FOLDER).expect("Failed to create 'migrations' dir") + } + + let dt = Utc::now(); + let mut file_name = dt.format("%Y-%m-%d_%H-%M-%S").to_string(); + file_name.push_str("_"); + file_name.push_str(name); + file_name.push_str(".sql"); + + let mut path = PathBuf::new(); + path.push(MIGRATION_FOLDER); + path.push(&file_name); + + if path.exists() { + eprintln!("Migration already exists!"); + return; + } + + let mut file = File::create(path).expect("Failed to create file"); + file.write_all(b"-- Add migration script here") + .expect("Could not write to file"); + + println!("Created migration: '{}'", file_name); +} + +pub struct Migration { + pub name: String, + pub sql: String, +} + +fn load_migrations() -> Vec { + let entries = fs::read_dir(&MIGRATION_FOLDER).expect("Could not find 'migrations' dir"); + + let mut migrations = Vec::new(); + + for e in entries { + if let Ok(e) = e { + if let Ok(meta) = e.metadata() { + if !meta.is_file() { + continue; + } + + if let Some(ext) = e.path().extension() { + if ext != "sql" { + println!("Wrong ext: {:?}", ext); + continue; + } + } else { + continue; + } + + let mut file = + File::open(e.path()).expect(&format!("Failed to open: '{:?}'", e.file_name())); + let mut contents = String::new(); + file.read_to_string(&mut contents) + .expect(&format!("Failed to read: '{:?}'", e.file_name())); + + migrations.push(Migration { + name: e.file_name().to_str().unwrap().to_string(), + sql: contents, + }); + } + } + } + + migrations.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap()); + + migrations +} + +async fn run_migrations() { + dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("Failed to find 'DATABASE_URL'"); + + let mut pool = PgPool::new(&db_url) + .await + .expect("Failed to connect to pool"); + + create_migration_table(&mut pool).await; + + let migrations = load_migrations(); + + for mig in migrations.iter() { + let mut tx = pool.begin().await.unwrap(); + + if check_if_applied(&mut tx, &mig.name).await { + println!("Already applied migration: '{}'", mig.name); + continue; + } + println!("Applying migration: '{}'", mig.name); + + sqlx::query(&mig.sql) + .execute(&mut tx) + .await + .expect(&format!("Failed to run migration {:?}", &mig.name)); + + save_applied_migration(&mut tx, &mig.name).await; + + tx.commit().await.unwrap(); + } +} + +async fn create_migration_table(mut pool: &PgPool) { + sqlx::query( + r#" +CREATE TABLE IF NOT EXISTS __migrations ( + migration VARCHAR (255) PRIMARY KEY, + created TIMESTAMP NOT NULL DEFAULT current_timestamp +); + "#, + ) + .execute(&mut pool) + .await + .expect("Failed to create migration table"); +} + +async fn check_if_applied(pool: &mut PgConnection, migration: &str) -> bool { + use sqlx::row::Row; + + let row = sqlx::query( + "select exists(select migration from __migrations where migration = $1) as exists", + ) + .bind(migration.to_string()) + .fetch_one(pool) + .await + .expect("Failed to check migration table"); + + let exists: bool = row.get("exists"); + + exists +} + +async fn save_applied_migration(pool: &mut PgConnection, migration: &str) { + sqlx::query("insert into __migrations (migration) values ($1)") + .bind(migration.to_string()) + .execute(pool) + .await + .expect("Failed to insert migration "); +}