Dmitriy Pleshevskiy
f98dd4f0c8
I added a single transaction option for apply, upgrade, and downgrade commands, which wraps all migrations into a single transaction. This gives you the ability to safely roll up migrations and, if some unforeseen situation occurs, roll them back. Unfortunately if there is an error in syntax, mysql will not rollback the migration and commits automatically :( I will research this issue. Closes #2
672 lines
17 KiB
Rust
672 lines
17 KiB
Rust
pub use assert_cmd::prelude::*;
|
|
pub use cfg_if::cfg_if;
|
|
pub use predicates::str::contains;
|
|
pub use std::process::Command;
|
|
|
|
pub type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
|
|
|
|
pub const ROOT_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/");
|
|
|
|
pub fn path_to_file<D: std::fmt::Display>(file_name: D) -> String {
|
|
format!("{}{}", ROOT_PATH, file_name)
|
|
}
|
|
|
|
pub fn database_manifest_path<D: std::fmt::Display>(database_name: D) -> String {
|
|
path_to_file(format!("Migra_{}.toml", database_name))
|
|
}
|
|
|
|
pub const DATABASE_URL_DEFAULT_ENV_NAME: &str = "DATABASE_URL";
|
|
pub const POSTGRES_URL: &str = "postgres://postgres:postgres@localhost:6000/migra_tests";
|
|
pub const MYSQL_URL: &str = "mysql://mysql:mysql@localhost:6001/migra_tests";
|
|
|
|
pub struct Env {
|
|
key: &'static str,
|
|
}
|
|
|
|
impl Env {
|
|
pub fn new(key: &'static str, value: &'static str) -> Self {
|
|
std::env::set_var(key, value);
|
|
Env { key }
|
|
}
|
|
}
|
|
|
|
impl Drop for Env {
|
|
fn drop(&mut self) {
|
|
std::env::remove_var(self.key);
|
|
}
|
|
}
|
|
|
|
mod init {
|
|
use super::*;
|
|
use std::fs;
|
|
|
|
#[test]
|
|
fn init_manifest_with_default_config() -> TestResult {
|
|
let manifest_path = "Migra.toml";
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("init")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains(format!("Created {}", &manifest_path)));
|
|
|
|
let content = fs::read_to_string(&manifest_path)?;
|
|
|
|
assert_eq!(
|
|
content,
|
|
r#"root = "database"
|
|
|
|
[database]
|
|
connection = "$DATABASE_URL"
|
|
"#
|
|
);
|
|
|
|
fs::remove_file(&manifest_path)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn init_manifest_in_custom_path() -> TestResult {
|
|
let manifest_path = path_to_file("Migra.toml");
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("init")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains(format!("Created {}", manifest_path.as_str())));
|
|
|
|
let content = fs::read_to_string(&manifest_path)?;
|
|
|
|
assert_eq!(
|
|
content,
|
|
r#"root = "database"
|
|
|
|
[database]
|
|
connection = "$DATABASE_URL"
|
|
"#
|
|
);
|
|
|
|
fs::remove_file(&manifest_path)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
mod list {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn empty_migration_list() -> TestResult {
|
|
Command::cargo_bin("migra")?
|
|
.arg("ls")
|
|
.assert()
|
|
.success()
|
|
.stderr(contains(
|
|
r#"WARNING: Missed "DATABASE_URL" environment variable
|
|
WARNING: No connection to database"#,
|
|
))
|
|
.stdout(contains(
|
|
r#"
|
|
Pending migrations:
|
|
—"#,
|
|
));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn empty_migration_list_with_db() -> TestResult {
|
|
fn inner(connection_string: &'static str) -> TestResult {
|
|
let env = Env::new(DATABASE_URL_DEFAULT_ENV_NAME, connection_string);
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("ls")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains(
|
|
r#"Applied migrations:
|
|
—
|
|
|
|
Pending migrations:
|
|
—"#,
|
|
));
|
|
|
|
drop(env);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "postgres")]
|
|
inner(POSTGRES_URL)?;
|
|
|
|
#[cfg(feature = "mysql")]
|
|
inner(MYSQL_URL)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "postgres")]
|
|
fn empty_migration_list_with_url_in_manifest() -> TestResult {
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(path_to_file("Migra_url_empty.toml"))
|
|
.arg("ls")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains(
|
|
r#"Applied migrations:
|
|
—
|
|
|
|
Pending migrations:
|
|
—"#,
|
|
));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(feature = "postgres")]
|
|
fn empty_migration_list_with_env_in_manifest() -> TestResult {
|
|
let env = Env::new("DB_URL", POSTGRES_URL);
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(path_to_file("Migra_env_empty.toml"))
|
|
.arg("ls")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains(
|
|
r#"Applied migrations:
|
|
—
|
|
|
|
Pending migrations:
|
|
—"#,
|
|
));
|
|
|
|
drop(env);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn empty_applied_migrations() -> TestResult {
|
|
fn inner(database_name: &'static str) -> TestResult {
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(database_manifest_path(database_name))
|
|
.arg("ls")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains(
|
|
r#"Applied migrations:
|
|
—
|
|
|
|
Pending migrations:
|
|
210218232851_create_articles
|
|
210218233414_create_persons
|
|
"#,
|
|
));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "postgres")]
|
|
inner("postgres")?;
|
|
|
|
#[cfg(feature = "mysql")]
|
|
inner("mysql")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn applied_all_migrations() -> TestResult {
|
|
fn inner(database_name: &'static str) -> TestResult {
|
|
let manifest_path = database_manifest_path(database_name);
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("up")
|
|
.assert()
|
|
.success();
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("ls")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains(
|
|
r#"Applied migrations:
|
|
210218232851_create_articles
|
|
210218233414_create_persons
|
|
|
|
Pending migrations:
|
|
—
|
|
"#,
|
|
));
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("down")
|
|
.arg("--all")
|
|
.assert()
|
|
.success();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "postgres")]
|
|
inner("postgres")?;
|
|
|
|
#[cfg(feature = "mysql")]
|
|
inner("mysql")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn applied_one_migrations() -> TestResult {
|
|
fn inner(database_name: &'static str) -> TestResult {
|
|
let manifest_path = database_manifest_path(database_name);
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("up")
|
|
.arg("-n")
|
|
.arg("1")
|
|
.assert()
|
|
.success();
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("ls")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains(
|
|
r#"Applied migrations:
|
|
210218232851_create_articles
|
|
|
|
Pending migrations:
|
|
210218233414_create_persons
|
|
"#,
|
|
));
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("down")
|
|
.assert()
|
|
.success();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "postgres")]
|
|
inner("postgres")?;
|
|
|
|
#[cfg(feature = "mysql")]
|
|
inner("mysql")?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
mod make {
|
|
use super::*;
|
|
use std::fs;
|
|
|
|
#[test]
|
|
fn make_migration_directory() -> TestResult {
|
|
fn inner(database_name: &'static str) -> TestResult {
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(database_manifest_path(database_name))
|
|
.arg("make")
|
|
.arg("test")
|
|
.assert()
|
|
.success()
|
|
.stdout(contains("Structure for migration has been created in"));
|
|
|
|
let entries = fs::read_dir(path_to_file(format!("{}/migrations", database_name)))?
|
|
.map(|entry| entry.map(|e| e.path()))
|
|
.collect::<Result<Vec<_>, std::io::Error>>()?;
|
|
|
|
let dir_paths = entries
|
|
.iter()
|
|
.filter_map(|path| {
|
|
path.to_str().and_then(|path| {
|
|
if path.ends_with("_test") {
|
|
Some(path)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
for dir_path in dir_paths.iter() {
|
|
let upgrade_content = fs::read_to_string(format!("{}/up.sql", dir_path))?;
|
|
let downgrade_content = fs::read_to_string(format!("{}/down.sql", dir_path))?;
|
|
|
|
assert_eq!(upgrade_content, "-- Your SQL goes here\n\n");
|
|
|
|
assert_eq!(
|
|
downgrade_content,
|
|
"-- This file should undo anything in `up.sql`\n\n"
|
|
);
|
|
|
|
fs::remove_dir_all(dir_path)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "postgres")]
|
|
inner("postgres")?;
|
|
|
|
#[cfg(feature = "mysql")]
|
|
inner("mysql")?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
mod upgrade {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn applied_all_migrations() -> TestResult {
|
|
fn inner<ValidateFn>(database_name: &'static str, validate: ValidateFn) -> TestResult
|
|
where
|
|
ValidateFn: Fn() -> TestResult,
|
|
{
|
|
let manifest_path = database_manifest_path(database_name);
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("up")
|
|
.assert()
|
|
.success();
|
|
|
|
validate()?;
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("down")
|
|
.arg("--all")
|
|
.assert()
|
|
.success();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "postgres")]
|
|
inner("postgres", || {
|
|
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
|
|
let res = conn.query("SELECT p.id, a.id FROM persons AS p, articles AS a", &[])?;
|
|
|
|
assert_eq!(
|
|
res.into_iter()
|
|
.map(|row| (row.get(0), row.get(1)))
|
|
.collect::<Vec<(i32, i32)>>(),
|
|
Vec::new()
|
|
);
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
#[cfg(feature = "mysql")]
|
|
inner("mysql", || {
|
|
use mysql::prelude::*;
|
|
|
|
let pool = mysql::Pool::new(MYSQL_URL)?;
|
|
let mut conn = pool.get_conn()?;
|
|
|
|
let res = conn.query_drop("SELECT p.id, a.id FROM persons AS p, articles AS a")?;
|
|
|
|
assert_eq!(res, ());
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn partial_applied_invalid_migrations() -> TestResult {
|
|
fn inner<ValidateFn>(database_name: &'static str, validate: ValidateFn) -> TestResult
|
|
where
|
|
ValidateFn: Fn() -> TestResult,
|
|
{
|
|
let manifest_path = database_manifest_path(database_name);
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("up")
|
|
.assert()
|
|
.failure();
|
|
|
|
validate()?;
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("down")
|
|
.assert()
|
|
.success();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "postgres")]
|
|
inner("postgres_invalid", || {
|
|
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
|
|
let articles_res = conn.query("SELECT a.id FROM articles AS a", &[]);
|
|
let persons_res = conn.query("SELECT p.id FROM persons AS p", &[]);
|
|
|
|
assert!(articles_res.is_ok());
|
|
assert!(persons_res.is_err());
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
#[cfg(feature = "mysql")]
|
|
inner("mysql_invalid", || {
|
|
use mysql::prelude::*;
|
|
|
|
let pool = mysql::Pool::new(MYSQL_URL)?;
|
|
let mut conn = pool.get_conn()?;
|
|
|
|
let articles_res = conn.query_drop("SELECT a.id FROM articles AS a");
|
|
let persons_res = conn.query_drop("SELECT p.id FROM persons AS p");
|
|
|
|
assert!(articles_res.is_ok());
|
|
assert!(persons_res.is_err());
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn cannot_applied_invalid_migrations_in_single_transaction() -> TestResult {
|
|
fn inner<ValidateFn>(database_name: &'static str, validate: ValidateFn) -> TestResult
|
|
where
|
|
ValidateFn: Fn() -> TestResult,
|
|
{
|
|
let manifest_path = database_manifest_path(database_name);
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("up")
|
|
.arg("--single-transaction")
|
|
.assert()
|
|
.failure();
|
|
|
|
validate()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "postgres")]
|
|
inner("postgres_invalid", || {
|
|
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
|
|
let articles_res = conn.query("SELECT a.id FROM articles AS a", &[]);
|
|
let persons_res = conn.query("SELECT p.id FROM persons AS p", &[]);
|
|
|
|
assert!(articles_res.is_err());
|
|
assert!(persons_res.is_err());
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
// TODO: Need to investigate how fix single transaction for Mysql
|
|
// #[cfg(feature = "mysql")]
|
|
// inner("mysql_invalid", || {
|
|
// use mysql::prelude::*;
|
|
|
|
// let pool = mysql::Pool::new(MYSQL_URL)?;
|
|
// let mut conn = pool.get_conn()?;
|
|
|
|
// let articles_res = conn.query_drop("SELECT a.id FROM articles AS a");
|
|
// let persons_res = conn.query_drop("SELECT p.id FROM persons AS p");
|
|
|
|
// assert!(articles_res.is_err());
|
|
// assert!(persons_res.is_err());
|
|
|
|
// Ok(())
|
|
// })?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
mod apply {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn apply_files() -> TestResult {
|
|
fn inner<ValidateFn>(
|
|
database_name: &'static str,
|
|
file_paths: Vec<&'static str>,
|
|
validate: ValidateFn,
|
|
) -> TestResult
|
|
where
|
|
ValidateFn: Fn() -> TestResult,
|
|
{
|
|
let manifest_path = database_manifest_path(database_name);
|
|
|
|
Command::cargo_bin("migra")?
|
|
.arg("-c")
|
|
.arg(&manifest_path)
|
|
.arg("apply")
|
|
.args(file_paths)
|
|
.assert()
|
|
.success();
|
|
|
|
validate()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
cfg_if! {
|
|
if #[cfg(feature = "postgres")] {
|
|
inner(
|
|
"postgres",
|
|
vec![
|
|
"migrations/210218232851_create_articles/up",
|
|
"migrations/210218233414_create_persons/up",
|
|
],
|
|
|| {
|
|
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
|
|
let res = conn.query("SELECT p.id, a.id FROM persons AS p, articles AS a", &[])?;
|
|
|
|
assert_eq!(
|
|
res.into_iter()
|
|
.map(|row| (row.get(0), row.get(1)))
|
|
.collect::<Vec<(i32, i32)>>(),
|
|
Vec::new()
|
|
);
|
|
|
|
Ok(())
|
|
},
|
|
)?;
|
|
|
|
inner(
|
|
"postgres",
|
|
vec![
|
|
"migrations/210218233414_create_persons/down",
|
|
"migrations/210218232851_create_articles/down",
|
|
],
|
|
|| {
|
|
let mut conn = postgres::Client::connect(POSTGRES_URL, postgres::NoTls)?;
|
|
let res = conn.query("SELECT p.id, a.id FROM persons AS p, articles AS a", &[]);
|
|
|
|
assert!(res.is_err());
|
|
|
|
Ok(())
|
|
},
|
|
)?;
|
|
}
|
|
}
|
|
|
|
cfg_if! {
|
|
if #[cfg(feature = "mysql")] {
|
|
inner(
|
|
"mysql",
|
|
vec![
|
|
"migrations/210218232851_create_articles/up",
|
|
"migrations/210218233414_create_persons/up",
|
|
],
|
|
|| {
|
|
use mysql::prelude::*;
|
|
|
|
let pool = mysql::Pool::new(MYSQL_URL)?;
|
|
let mut conn = pool.get_conn()?;
|
|
|
|
let res = conn.query_drop("SELECT p.id, a.id FROM persons AS p, articles AS a")?;
|
|
|
|
assert_eq!(res, ());
|
|
|
|
Ok(())
|
|
},
|
|
)?;
|
|
|
|
inner(
|
|
"mysql",
|
|
vec![
|
|
"migrations/210218233414_create_persons/down",
|
|
"migrations/210218232851_create_articles/down",
|
|
],
|
|
|| {
|
|
use mysql::prelude::*;
|
|
|
|
let pool = mysql::Pool::new(MYSQL_URL)?;
|
|
let mut conn = pool.get_conn()?;
|
|
|
|
let res = conn.query_drop("SELECT p.id, a.id FROM persons AS p, articles AS a");
|
|
|
|
assert!(res.is_err());
|
|
|
|
Ok(())
|
|
}
|
|
)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|