-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
333 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
use std::{ | ||
collections::{HashMap, HashSet}, | ||
path::{Path, PathBuf}, | ||
}; | ||
|
||
use anyhow::{anyhow, Context, Result}; | ||
use fs_extra::dir::{DirEntryAttr, DirEntryValue, LsResult}; | ||
|
||
use crate::{config, constants::MIGRATIONS_DIR_NAME, models::ScriptMigration, surrealdb}; | ||
|
||
pub async fn main( | ||
url: Option<String>, | ||
ns: Option<String>, | ||
db: Option<String>, | ||
username: Option<String>, | ||
password: Option<String>, | ||
) -> Result<()> { | ||
let client = surrealdb::create_surrealdb_client(url, ns, db, username, password).await?; | ||
|
||
let migrations_applied = | ||
surrealdb::list_script_migration_ordered_by_execution_date(&client).await?; | ||
|
||
let mut config = HashSet::new(); | ||
config.insert(DirEntryAttr::Name); | ||
config.insert(DirEntryAttr::Path); | ||
config.insert(DirEntryAttr::IsFile); | ||
|
||
let folder_path = config::retrieve_folder_path(); | ||
let migrations_dir_path = concat_path(&folder_path, MIGRATIONS_DIR_NAME); | ||
|
||
let migrations_files = fs_extra::dir::ls(migrations_dir_path, &config)?; | ||
|
||
let migrations_not_applied = get_sorted_migrations_files(&migrations_files) | ||
.into_iter() | ||
.filter(|migration_file| { | ||
is_migration_file_already_applied(migration_file, &migrations_applied).unwrap_or(false) | ||
}) | ||
.collect::<Vec<_>>(); | ||
|
||
let last_migration_applied = migrations_applied.last(); | ||
|
||
let migrations_not_applied_before_last_applied = | ||
if let Some(last_migration_applied) = last_migration_applied { | ||
migrations_not_applied | ||
.iter() | ||
.filter(|migration_file| { | ||
is_migration_file_before_last_applied(migration_file, &last_migration_applied) | ||
.unwrap_or(false) | ||
}) | ||
.collect::<Vec<_>>() | ||
} else { | ||
Vec::new() | ||
}; | ||
|
||
if migrations_not_applied_before_last_applied.len() > 0 { | ||
let migration_names = migrations_not_applied_before_last_applied | ||
.iter() | ||
.map(|migration_file| get_migration_file_name(migration_file).unwrap_or("".to_string())) | ||
.collect::<Vec<_>>(); | ||
|
||
Err(anyhow!( | ||
"The following migrations have not been applied: {}", | ||
migration_names.join(", ") | ||
)) | ||
} else { | ||
Ok(()) | ||
} | ||
} | ||
|
||
fn concat_path(folder_path: &Option<String>, dir_name: &str) -> PathBuf { | ||
match folder_path.to_owned() { | ||
Some(folder_path) => Path::new(&folder_path).join(dir_name), | ||
None => Path::new(dir_name).to_path_buf(), | ||
} | ||
} | ||
|
||
fn get_sorted_migrations_files( | ||
migrations_files: &LsResult, | ||
) -> Vec<&HashMap<DirEntryAttr, DirEntryValue>> { | ||
let mut sorted_migrations_files = migrations_files.items.iter().collect::<Vec<_>>(); | ||
sorted_migrations_files.sort_by(|a, b| { | ||
let a = a.get(&DirEntryAttr::Name); | ||
let b = b.get(&DirEntryAttr::Name); | ||
|
||
let a = match a { | ||
Some(DirEntryValue::String(a)) => Some(a), | ||
_ => None, | ||
}; | ||
|
||
let b = match b { | ||
Some(DirEntryValue::String(b)) => Some(b), | ||
_ => None, | ||
}; | ||
|
||
a.cmp(&b) | ||
}); | ||
|
||
sorted_migrations_files | ||
} | ||
|
||
fn is_migration_file_already_applied( | ||
migration_file: &&std::collections::HashMap<DirEntryAttr, DirEntryValue>, | ||
migrations_applied: &Vec<ScriptMigration>, | ||
) -> Result<bool> { | ||
let is_file = migration_file | ||
.get(&DirEntryAttr::IsFile) | ||
.context("Cannot detect if the migration file is a file or a folder")?; | ||
let is_file = match is_file { | ||
DirEntryValue::Boolean(is_file) => Some(is_file), | ||
_ => None, | ||
}; | ||
let is_file = is_file.context("Cannot detect if the migration file is a file or a folder")?; | ||
|
||
if !is_file { | ||
return Ok(false); | ||
} | ||
|
||
let name = migration_file | ||
.get(&DirEntryAttr::Name) | ||
.context("Cannot get name of the migration file")?; | ||
let name = match name { | ||
DirEntryValue::String(name) => Some(name), | ||
_ => None, | ||
}; | ||
let name = name.context("Cannot get name of the migration file")?; | ||
|
||
let has_already_been_applied = migrations_applied | ||
.iter() | ||
.any(|migration_applied| &migration_applied.script_name == name); | ||
|
||
if has_already_been_applied { | ||
return Ok(false); | ||
} | ||
|
||
return Ok(true); | ||
} | ||
|
||
fn is_migration_file_before_last_applied( | ||
migration_file: &&HashMap<DirEntryAttr, DirEntryValue>, | ||
last_migration_applied: &ScriptMigration, | ||
) -> Result<bool> { | ||
let is_file = migration_file | ||
.get(&DirEntryAttr::IsFile) | ||
.context("Cannot detect if the migration file is a file or a folder")?; | ||
let is_file = match is_file { | ||
DirEntryValue::Boolean(is_file) => Some(is_file), | ||
_ => None, | ||
}; | ||
let is_file = is_file.context("Cannot detect if the migration file is a file or a folder")?; | ||
|
||
if !is_file { | ||
return Ok(false); | ||
} | ||
|
||
let name = migration_file | ||
.get(&DirEntryAttr::Name) | ||
.context("Cannot get name of the migration file")?; | ||
let name = match name { | ||
DirEntryValue::String(name) => Some(name), | ||
_ => None, | ||
}; | ||
let name = name.context("Cannot get name of the migration file")?; | ||
|
||
Ok(name < &last_migration_applied.script_name) | ||
} | ||
|
||
fn get_migration_file_name( | ||
migration_file: &&HashMap<DirEntryAttr, DirEntryValue>, | ||
) -> Result<String> { | ||
let name = migration_file | ||
.get(&DirEntryAttr::Name) | ||
.context("Cannot get name of the migration file")?; | ||
let name = match name { | ||
DirEntryValue::String(name) => Some(name), | ||
_ => None, | ||
}; | ||
let name = name.context("Cannot get name of the migration file")?; | ||
|
||
Ok(name.to_string()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
mod list; | ||
mod up; | ||
mod up_to; | ||
mod validate_version_order; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
use anyhow::{ensure, Result}; | ||
use regex::Regex; | ||
use serial_test::serial; | ||
use surrealdb_migrations::{SurrealdbConfiguration, SurrealdbMigrations}; | ||
|
||
use crate::helpers::common::*; | ||
|
||
#[tokio::test] | ||
#[serial] | ||
async fn ok_if_no_migration_file() -> Result<()> { | ||
run_with_surreal_instance_async(|| { | ||
Box::pin(async { | ||
clear_files_dir()?; | ||
scaffold_empty_template()?; | ||
|
||
let configuration = SurrealdbConfiguration::default(); | ||
let runner = SurrealdbMigrations::new(configuration); | ||
|
||
runner.validate_version_order().await?; | ||
|
||
Ok(()) | ||
}) | ||
}) | ||
.await | ||
} | ||
|
||
#[tokio::test] | ||
#[serial] | ||
async fn ok_if_migrations_applied_but_no_new_migration() -> Result<()> { | ||
run_with_surreal_instance_async(|| { | ||
Box::pin(async { | ||
clear_files_dir()?; | ||
scaffold_blog_template()?; | ||
|
||
let configuration = SurrealdbConfiguration::default(); | ||
let runner = SurrealdbMigrations::new(configuration); | ||
|
||
runner.up().await?; | ||
|
||
runner.validate_version_order().await?; | ||
|
||
Ok(()) | ||
}) | ||
}) | ||
.await | ||
} | ||
|
||
#[tokio::test] | ||
#[serial] | ||
async fn ok_if_migrations_applied_with_new_migration_after_last_applied() -> Result<()> { | ||
run_with_surreal_instance_async(|| { | ||
Box::pin(async { | ||
clear_files_dir()?; | ||
scaffold_blog_template()?; | ||
|
||
let configuration = SurrealdbConfiguration::default(); | ||
let runner = SurrealdbMigrations::new(configuration); | ||
|
||
let first_migration_name = get_first_migration_name()?; | ||
runner.up_to(&first_migration_name).await?; | ||
|
||
runner.validate_version_order().await?; | ||
|
||
Ok(()) | ||
}) | ||
}) | ||
.await | ||
} | ||
|
||
#[tokio::test] | ||
#[serial] | ||
async fn fails_if_migrations_applied_with_new_migration_before_last_applied() -> Result<()> { | ||
run_with_surreal_instance_async(|| { | ||
Box::pin(async { | ||
clear_files_dir()?; | ||
scaffold_blog_template()?; | ||
|
||
let configuration = SurrealdbConfiguration::default(); | ||
let runner = SurrealdbMigrations::new(configuration); | ||
|
||
let first_migration_file = get_first_migration_file()?; | ||
std::fs::remove_file(first_migration_file)?; | ||
|
||
runner.up().await?; | ||
|
||
clear_files_dir()?; | ||
scaffold_blog_template()?; | ||
|
||
let result = runner.validate_version_order().await; | ||
|
||
ensure!(result.is_err()); | ||
|
||
let error_regex = Regex::new( | ||
r"The following migrations have not been applied: \d+_\d+_AddAdminUser", | ||
)?; | ||
|
||
let error_str = result.unwrap_err().to_string(); | ||
let error_str = error_str.as_str(); | ||
|
||
ensure!(error_regex.is_match(error_str)); | ||
|
||
Ok(()) | ||
}) | ||
}) | ||
.await | ||
} |