use std::path::{Path, PathBuf};
use clap::Args;
use sz_orm_core::migration::{FileMigrationResolver, Migration, MigrationResolver};
use sz_orm_core::DbType;
use crate::error::CliError;
#[derive(Args, Debug)]
pub struct MigrateArgs {
#[arg(long)]
pub rollback: bool,
#[arg(short = 'p', long, default_value = "migrations")]
pub path: String,
#[arg(long, default_value = "postgres")]
pub db_type: String,
#[arg(long)]
pub show_sql: bool,
}
pub fn execute_migrate(args: &MigrateArgs) -> Result<(), CliError> {
let path = PathBuf::from(&args.path);
if !path.exists() {
return Err(CliError::Migration(format!(
"Migration directory not found: {}",
path.display()
)));
}
let db_type = DbType::from_str(&args.db_type)
.ok_or_else(|| CliError::Migration(format!("Unknown database type: {}", args.db_type)))?;
let migrations = resolve_migrations(&path, db_type)?;
if migrations.is_empty() {
println!("No migrations found in: {}", path.display());
return Ok(());
}
if args.rollback {
println!("Rolling back last batch in: {}", path.display());
if let Some(last) = migrations.last() {
println!(" Would rollback: {} ({})", last.version, last.name);
if args.show_sql {
print_sql_block("SQL DOWN", &last.sql_down);
}
}
println!("Note: Actual rollback requires database connection (offline mode).");
} else {
println!("Running migrations in: {}", path.display());
for m in &migrations {
println!(" Would apply: {} ({})", m.version, m.name);
if args.show_sql {
print_sql_block("SQL UP", &m.sql_up);
}
}
println!(
"Total: {} migration(s). Note: Actual execution requires database connection (offline mode).",
migrations.len()
);
}
Ok(())
}
pub fn execute_status(path: &str) -> Result<(), CliError> {
execute_status_with(path, "postgres", false)
}
pub fn execute_status_with(path: &str, db_type_str: &str, show_sql: bool) -> Result<(), CliError> {
let path_buf = PathBuf::from(path);
if !path_buf.exists() {
return Err(CliError::Migration(format!(
"Migration directory not found: {}",
path_buf.display()
)));
}
let db_type = DbType::from_str(db_type_str)
.ok_or_else(|| CliError::Migration(format!("Unknown database type: {}", db_type_str)))?;
let migrations = resolve_migrations(&path_buf, db_type)?;
if migrations.is_empty() {
println!("No migrations found in: {}", path_buf.display());
return Ok(());
}
println!(
"{:<15} {:<30} {:<20}",
"Version", "Migration Name", "Status"
);
println!("{}", "-".repeat(65));
for m in &migrations {
println!("{:<15} {:<30} {:<20}", m.version, m.name, "Pending*");
if show_sql {
print_sql_block("SQL UP", &m.sql_up);
print_sql_block("SQL DOWN", &m.sql_down);
}
}
println!();
println!("* Status cannot be determined without database connection (offline mode).");
Ok(())
}
fn resolve_migrations(path: &Path, db_type: DbType) -> Result<Vec<Migration>, CliError> {
let resolver = FileMigrationResolver::new(path.to_path_buf());
resolver
.resolve(db_type)
.map_err(|e| CliError::Migration(format!("Failed to resolve migrations: {}", e)))
}
fn print_sql_block(title: &str, sql: &str) {
if sql.is_empty() {
return;
}
println!(" --- {} ---", title);
for line in sql.lines() {
println!(" {}", line);
}
println!(" {}", "-".repeat(title.len() + 8));
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
fn create_test_migration(dir: &Path, version: &str, name: &str) {
let up_name = format!("{}_{}_up.sql", version, name);
let down_name = format!("{}_{}_down.sql", version, name);
let up_path = dir.join(up_name);
let down_path = dir.join(down_name);
let mut up_file = fs::File::create(&up_path).unwrap();
writeln!(up_file, "-- {} up", name).unwrap();
let mut down_file = fs::File::create(&down_path).unwrap();
writeln!(down_file, "-- {} down", name).unwrap();
}
#[test]
fn test_resolve_migrations_empty() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_resolve_migrations_with_files() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "create_users");
create_test_migration(&path, "002", "add_index");
let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].version, "001");
assert_eq!(result[0].name, "create_users");
assert_eq!(result[1].version, "002");
assert_eq!(result[1].name, "add_index");
}
#[test]
fn test_resolve_migrations_returns_sql_content() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let up_path = path.join("001_init_up.sql");
let down_path = path.join("001_init_down.sql");
fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
fs::write(&down_path, "DROP TABLE users;").unwrap();
let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].sql_up.contains("CREATE TABLE users"));
assert!(result[0].sql_down.contains("DROP TABLE users"));
}
#[test]
fn test_resolve_migrations_supports_multiple_db_types() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "init");
let mysql_result = resolve_migrations(&path, DbType::MySQL).unwrap();
let pg_result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
assert_eq!(mysql_result.len(), 1);
assert_eq!(pg_result.len(), 1);
}
#[test]
fn test_execute_status_nonexistent_dir() {
let result = execute_status("/nonexistent/path/migrations");
assert!(matches!(result, Err(CliError::Migration(_))));
}
#[test]
fn test_execute_status_empty_dir() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_str().unwrap();
let result = execute_status(path);
assert!(result.is_ok());
}
#[test]
fn test_execute_status_with_migrations() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "create_users");
let path_str = temp.path().to_str().unwrap();
let result = execute_status(path_str);
assert!(result.is_ok());
}
#[test]
fn test_execute_status_with_invalid_db_type() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_str().unwrap();
let result = execute_status_with(path, "invalid_db_type", false);
assert!(matches!(result, Err(CliError::Migration(_))));
}
#[test]
fn test_execute_status_with_show_sql() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let up_path = path.join("001_init_up.sql");
let down_path = path.join("001_init_down.sql");
fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
fs::write(&down_path, "DROP TABLE users;").unwrap();
let path_str = temp.path().to_str().unwrap();
let result = execute_status_with(path_str, "postgres", true);
assert!(result.is_ok());
}
#[test]
fn test_execute_migrate_nonexistent_dir() {
let args = MigrateArgs {
rollback: false,
path: "/nonexistent/migrations".to_string(),
db_type: "postgres".to_string(),
show_sql: false,
};
let result = execute_migrate(&args);
assert!(matches!(result, Err(CliError::Migration(_))));
}
#[test]
fn test_execute_migrate_empty_dir() {
let temp = tempfile::tempdir().unwrap();
let args = MigrateArgs {
rollback: false,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
};
let result = execute_migrate(&args);
assert!(result.is_ok());
}
#[test]
fn test_execute_migrate_with_files() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "create_users");
let args = MigrateArgs {
rollback: false,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
};
let result = execute_migrate(&args);
assert!(result.is_ok());
}
#[test]
fn test_execute_migrate_with_show_sql() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let up_path = path.join("001_init_up.sql");
let down_path = path.join("001_init_down.sql");
fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
fs::write(&down_path, "DROP TABLE users;").unwrap();
let args = MigrateArgs {
rollback: false,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: true,
};
let result = execute_migrate(&args);
assert!(result.is_ok());
}
#[test]
fn test_execute_migrate_with_invalid_db_type() {
let temp = tempfile::tempdir().unwrap();
let args = MigrateArgs {
rollback: false,
path: temp.path().to_str().unwrap().to_string(),
db_type: "invalid_db_type".to_string(),
show_sql: false,
};
let result = execute_migrate(&args);
assert!(matches!(result, Err(CliError::Migration(_))));
}
#[test]
fn test_execute_migrate_rollback() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "create_users");
create_test_migration(&path, "002", "add_index");
let args = MigrateArgs {
rollback: true,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
};
let result = execute_migrate(&args);
assert!(result.is_ok());
}
#[test]
fn test_execute_migrate_rollback_with_show_sql() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let up_path = path.join("001_init_up.sql");
let down_path = path.join("001_init_down.sql");
fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
fs::write(&down_path, "DROP TABLE users;").unwrap();
let args = MigrateArgs {
rollback: true,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: true,
};
let result = execute_migrate(&args);
assert!(result.is_ok());
}
#[test]
fn test_print_sql_block_empty_sql() {
print_sql_block("SQL UP", "");
}
#[test]
fn test_print_sql_block_with_content() {
print_sql_block("SQL UP", "CREATE TABLE users (id INT);");
}
}