mirror of
https://github.com/launchbadge/sqlx
synced 2024-11-10 14:34:19 +00:00
Merge branch 'feature/cargo-sqlx-migrate' of git://github.com/JesperAxelsson/sqlx into JesperAxelsson-feature/cargo-sqlx-migrate
This commit is contained in:
commit
70387214a9
6 changed files with 263 additions and 0 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
4
cargo-sqlx/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/target
|
||||
/migrations
|
||||
Cargo.lock
|
||||
.env
|
26
cargo-sqlx/Cargo.toml
Normal file
26
cargo-sqlx/Cargo.toml
Normal 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
14
cargo-sqlx/README.md
Normal 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
206
cargo-sqlx/src/main.rs
Normal 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 ");
|
||||
}
|
Loading…
Reference in a new issue