Merge branch 'feature/cargo-sqlx-migrate' of git://github.com/JesperAxelsson/sqlx into JesperAxelsson-feature/cargo-sqlx-migrate

This commit is contained in:
Ryan Leckey 2020-04-07 14:32:10 -07:00
commit 70387214a9
6 changed files with 263 additions and 0 deletions

12
Cargo.lock generated
View file

@ -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"

View file

@ -4,6 +4,7 @@ members = [
"sqlx-core",
"sqlx-macros",
"sqlx-test",
"cargo-sqlx",
"examples/mysql/todos",
"examples/postgres/listen",
"examples/postgres/realworld",

4
cargo-sqlx/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
/migrations
Cargo.lock
.env

26
cargo-sqlx/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "cargo-sqlx"
version = "0.1.0"
description = "Simple postgres migrator without support for down migration"
authors = ["Jesper Axelsson <jesperaxe@gmail.com>"]
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"

14
cargo-sqlx/README.md Normal file
View file

@ -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 <name>` - add new migration to your migrations folder named `<timestamp>_<name>.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.

206
cargo-sqlx/src/main.rs Normal file
View file

@ -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 <timestamp>_<migration_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<Migration> {
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 ");
}