use chrono::{DateTime, Utc};
use forge_core::schema::TableDef;
use super::diff::{DatabaseTable, SchemaDiff};
pub struct MigrationGenerator {
output_dir: std::path::PathBuf,
}
impl MigrationGenerator {
pub fn new(output_dir: impl Into<std::path::PathBuf>) -> Self {
Self {
output_dir: output_dir.into(),
}
}
pub fn generate(
&self,
rust_tables: &[TableDef],
db_tables: &[DatabaseTable],
) -> Result<Option<Migration>, GeneratorError> {
let diff = SchemaDiff::from_comparison(rust_tables, db_tables);
if diff.is_empty() {
return Ok(None);
}
let now = Utc::now();
let version = now.format("%Y%m%d_%H%M%S").to_string();
let name = self.generate_name(&diff);
let sql = diff.to_sql().join("\n\n");
let migration = Migration {
version: version.clone(),
name: name.clone(),
sql,
created_at: now,
path: self.output_dir.join(format!("{}_{}.sql", version, name)),
};
Ok(Some(migration))
}
fn generate_name(&self, diff: &SchemaDiff) -> String {
if diff.entries.is_empty() {
return "empty".to_string();
}
let Some(first) = diff.entries.first() else {
return "empty".to_string();
};
match first.action {
super::diff::DiffAction::CreateTable => {
format!("create_{}", first.table_name)
}
super::diff::DiffAction::AddColumn => {
format!("add_column_to_{}", first.table_name)
}
super::diff::DiffAction::DropColumn => {
format!("remove_column_from_{}", first.table_name)
}
super::diff::DiffAction::DropTable => {
format!("drop_{}", first.table_name)
}
_ => "schema_update".to_string(),
}
}
pub fn write_migration(&self, migration: &Migration) -> Result<(), GeneratorError> {
std::fs::create_dir_all(&self.output_dir).map_err(|e| GeneratorError::Io(e.to_string()))?;
let content = format!(
"-- Migration: {}\n-- Generated at: {}\n\n{}\n",
migration.name,
migration.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
migration.sql
);
std::fs::write(&migration.path, content).map_err(|e| GeneratorError::Io(e.to_string()))?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Migration {
pub version: String,
pub name: String,
pub sql: String,
pub created_at: DateTime<Utc>,
pub path: std::path::PathBuf,
}
#[derive(Debug, thiserror::Error)]
pub enum GeneratorError {
#[error("IO error: {0}")]
Io(String),
#[error("Parse error: {0}")]
Parse(String),
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::panic)]
mod tests {
use super::*;
use forge_core::schema::RustType;
use forge_core::schema::{FieldDef, TableDef};
#[test]
fn test_generate_migration() {
let generator = MigrationGenerator::new("/tmp/migrations");
let mut table = TableDef::new("users", "User");
table.fields.push(FieldDef::new("id", RustType::Uuid));
let migration = generator.generate(&[table], &[]).unwrap();
assert!(migration.is_some());
let m = migration.unwrap();
assert!(m.name.contains("users"));
assert!(m.sql.contains("Create table users"));
}
}