use camino::{Utf8Path, Utf8PathBuf};
use std::error::Error;
use walkdir::{DirEntry, WalkDir};
mod diesel;
mod sqlx;
pub use diesel::DieselAdapter;
pub use sqlx::SqlxAdapter;
pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Debug, Clone, serde::Serialize)]
pub struct MigrationContext {
pub run_in_transaction: bool,
pub no_transaction_hint: &'static str,
}
impl Default for MigrationContext {
fn default() -> Self {
Self {
run_in_transaction: true,
no_transaction_hint: "",
}
}
}
#[derive(Debug, Clone)]
pub struct MigrationFile {
pub path: Utf8PathBuf,
pub timestamp: String,
}
impl MigrationFile {
pub fn new(path: Utf8PathBuf, timestamp: String) -> Self {
Self { path, timestamp }
}
}
pub trait MigrationAdapter: Send + Sync {
fn collect_migration_files(
&self,
dir: &Utf8Path,
start_after: Option<&str>,
check_down: bool,
) -> Result<Vec<MigrationFile>>;
fn parse_timestamp(&self, name: &str) -> Option<String>;
fn validate_timestamp(&self, timestamp: &str) -> Result<()>;
fn extract_migration_metadata(&self, file_path: &Utf8Path) -> MigrationContext;
}
pub(crate) fn should_check_migration(start_after: Option<&str>, migration_timestamp: &str) -> bool {
let Some(start_after) = start_after else {
return true; };
let start_normalized = start_after.replace(['_', '-'], "");
let migration_normalized = migration_timestamp.replace(['_', '-'], "");
match (
migration_normalized.parse::<i64>(),
start_normalized.parse::<i64>(),
) {
(Ok(mig), Ok(start)) => mig > start,
_ => migration_normalized > start_normalized,
}
}
pub(crate) fn is_single_migration_dir(dir: &Utf8Path) -> bool {
dir.join("up.sql").exists()
}
pub(crate) fn collect_and_sort_entries(dir: &Utf8Path) -> Vec<DirEntry> {
let mut entries = Vec::new();
for result in WalkDir::new(dir).max_depth(1).min_depth(1) {
match result {
Ok(entry) => entries.push(entry),
Err(e) => {
eprintln!("Warning: Failed to read entry in {dir}: {e}");
}
}
}
entries.sort_by(|a, b| a.path().cmp(b.path()));
entries
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_is_single_migration_dir_with_up_sql() {
let temp_dir = TempDir::new().unwrap();
let dir = Utf8Path::from_path(temp_dir.path()).unwrap();
fs::write(dir.join("up.sql"), "CREATE TABLE t();").unwrap();
assert!(is_single_migration_dir(dir));
}
#[test]
fn test_is_single_migration_dir_without_up_sql() {
let temp_dir = TempDir::new().unwrap();
let dir = Utf8Path::from_path(temp_dir.path()).unwrap();
assert!(!is_single_migration_dir(dir));
}
#[test]
fn test_should_check_migration_non_timestamp_directory_name() {
assert!(should_check_migration(
Some("20240101000000"),
"create_users"
));
}
}