use chrono::Local;
use console::style;
use std::fs;
use std::path::Path;
pub fn run(name: String) {
let file_name = to_snake_case(&name);
if !is_valid_identifier(&file_name) {
eprintln!(
"{} '{}' is not a valid migration name",
style("Error:").red().bold(),
name
);
std::process::exit(1);
}
let table_name = extract_table_name(&file_name);
let table_enum_name = to_pascal_case(&table_name);
let migrations_dir = Path::new("src/migrations");
if !migrations_dir.exists() {
if let Err(e) = fs::create_dir_all(migrations_dir) {
eprintln!(
"{} Failed to create migrations directory: {}",
style("Error:").red().bold(),
e
);
std::process::exit(1);
}
println!("{} Created src/migrations directory", style("✓").green());
}
let timestamp = Local::now().format("%Y%m%d_%H%M%S").to_string();
let migration_file_name = format!("m{timestamp}_{file_name}");
let migration_file = migrations_dir.join(format!("{migration_file_name}.rs"));
let mod_file = migrations_dir.join("mod.rs");
if migration_file.exists() {
eprintln!(
"{} Migration '{}' already exists at {}",
style("Info:").yellow().bold(),
migration_file_name,
migration_file.display()
);
std::process::exit(0);
}
let migration_content = migration_template(&table_name, &table_enum_name);
if let Err(e) = fs::write(&migration_file, &migration_content) {
eprintln!(
"{} Failed to write migration file: {}",
style("Error:").red().bold(),
e
);
std::process::exit(1);
}
println!(
"{} Created {}",
style("✓").green(),
migration_file.display()
);
if mod_file.exists() {
if let Err(e) = update_mod_file(&mod_file, &migration_file_name) {
eprintln!(
"{} Failed to update mod.rs: {}",
style("Error:").red().bold(),
e
);
std::process::exit(1);
}
println!("{} Updated src/migrations/mod.rs", style("✓").green());
} else {
let mod_content = migrator_mod_template(&migration_file_name);
if let Err(e) = fs::write(&mod_file, mod_content) {
eprintln!(
"{} Failed to create mod.rs: {}",
style("Error:").red().bold(),
e
);
std::process::exit(1);
}
println!("{} Created src/migrations/mod.rs", style("✓").green());
}
println!();
println!(
"Migration {} created successfully!",
style(&migration_file_name).cyan().bold()
);
println!();
println!("Next steps:");
println!(
" {} Edit the migration file to define your schema",
style("1.").dim()
);
println!(
" {} Run {} to apply the migration",
style("2.").dim(),
style("ferro migrate").cyan()
);
println!();
}
fn is_valid_identifier(name: &str) -> bool {
if name.is_empty() {
return false;
}
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_')
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
} else if c == '-' || c == ' ' {
result.push('_');
} else {
result.push(c);
}
}
result
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' || c == '-' || c == ' ' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_uppercase().next().unwrap());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
fn extract_table_name(name: &str) -> String {
if name.starts_with("create_") && name.ends_with("_table") {
let without_prefix = name.strip_prefix("create_").unwrap();
let without_suffix = without_prefix.strip_suffix("_table").unwrap();
return without_suffix.to_string();
}
if name.contains("_to_") {
if let Some(pos) = name.rfind("_to_") {
return name[pos + 4..].to_string();
}
}
if name.starts_with("drop_") && name.ends_with("_table") {
let without_prefix = name.strip_prefix("drop_").unwrap();
let without_suffix = without_prefix.strip_suffix("_table").unwrap();
return without_suffix.to_string();
}
name.to_string()
}
fn migration_template(table_name: &str, table_enum_name: &str) -> String {
format!(
r#"use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {{
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
manager
.create_table(
Table::create()
.table({table_enum_name}::Table)
.if_not_exists()
.col(
ColumnDef::new({table_enum_name}::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new({table_enum_name}::CreatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.col(
ColumnDef::new({table_enum_name}::UpdatedAt)
.timestamp_with_time_zone()
.not_null()
.default(Expr::current_timestamp()),
)
.to_owned(),
)
.await
}}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
manager
.drop_table(Table::drop().table({table_enum_name}::Table).to_owned())
.await
}}
}}
/// Table and column identifiers for {table_name}
#[derive(DeriveIden)]
enum {table_enum_name} {{
Table,
Id,
CreatedAt,
UpdatedAt,
}}
"#
)
}
fn migrator_mod_template(migration_name: &str) -> String {
format!(
r#"pub use sea_orm_migration::prelude::*;
mod {migration_name};
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {{
fn migrations() -> Vec<Box<dyn MigrationTrait>> {{
vec![
Box::new({migration_name}::Migration),
]
}}
}}
"#
)
}
fn update_mod_file(mod_file: &Path, migration_name: &str) -> Result<(), String> {
let content =
fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
let mod_decl = format!("mod {migration_name};");
if content.contains(&mod_decl) {
return Ok(());
}
let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
let mut last_mod_idx = None;
for (i, line) in lines.iter().enumerate() {
if line.trim().starts_with("mod ") && !line.contains("mod tests") {
last_mod_idx = Some(i);
}
}
let insert_idx = match last_mod_idx {
Some(idx) => idx + 1,
None => {
let mut insert_idx = 0;
for (i, line) in lines.iter().enumerate() {
if line.contains("sea_orm_migration") || line.is_empty() {
insert_idx = i + 1;
} else if line.starts_with("mod ") || line.starts_with("pub struct") {
break;
}
}
insert_idx
}
};
lines.insert(insert_idx, mod_decl);
let box_new_line = format!(" Box::new({migration_name}::Migration),");
let mut insert_vec_idx = None;
for (i, line) in lines.iter().enumerate() {
if line.contains("vec![]") {
lines[i] = line.replace("vec![]", &format!("vec![\n{box_new_line}\n ]"));
let new_content = lines.join("\n");
fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
return Ok(());
}
if line.contains("vec![") && !line.contains("vec![]") {
for (j, inner_line) in lines.iter().enumerate().skip(i + 1) {
if inner_line.trim() == "]" || inner_line.trim().starts_with("]") {
insert_vec_idx = Some(j);
break;
}
}
break;
}
}
if let Some(idx) = insert_vec_idx {
lines.insert(idx, box_new_line);
}
let new_content = lines.join("\n");
fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
Ok(())
}