forge_runtime/migrations/
generator.rs1use chrono::{DateTime, Utc};
2use forge_core::schema::TableDef;
3
4use super::diff::{DatabaseTable, SchemaDiff};
5
6pub struct MigrationGenerator {
8 output_dir: std::path::PathBuf,
10}
11
12impl MigrationGenerator {
13 pub fn new(output_dir: impl Into<std::path::PathBuf>) -> Self {
15 Self {
16 output_dir: output_dir.into(),
17 }
18 }
19
20 pub fn generate(
22 &self,
23 rust_tables: &[TableDef],
24 db_tables: &[DatabaseTable],
25 ) -> Result<Option<Migration>, GeneratorError> {
26 let diff = SchemaDiff::from_comparison(rust_tables, db_tables);
27
28 if diff.is_empty() {
29 return Ok(None);
30 }
31
32 let now = Utc::now();
33 let version = now.format("%Y%m%d_%H%M%S").to_string();
34 let name = self.generate_name(&diff);
35
36 let sql = diff.to_sql().join("\n\n");
37
38 let migration = Migration {
39 version: version.clone(),
40 name: name.clone(),
41 sql,
42 created_at: now,
43 path: self.output_dir.join(format!("{}_{}.sql", version, name)),
44 };
45
46 Ok(Some(migration))
47 }
48
49 fn generate_name(&self, diff: &SchemaDiff) -> String {
51 if diff.entries.is_empty() {
52 return "empty".to_string();
53 }
54
55 let first = &diff.entries[0];
57 match first.action {
58 super::diff::DiffAction::CreateTable => {
59 format!("create_{}", first.table_name)
60 }
61 super::diff::DiffAction::AddColumn => {
62 format!("add_column_to_{}", first.table_name)
63 }
64 super::diff::DiffAction::DropColumn => {
65 format!("remove_column_from_{}", first.table_name)
66 }
67 super::diff::DiffAction::DropTable => {
68 format!("drop_{}", first.table_name)
69 }
70 _ => "schema_update".to_string(),
71 }
72 }
73
74 pub fn write_migration(&self, migration: &Migration) -> Result<(), GeneratorError> {
76 std::fs::create_dir_all(&self.output_dir).map_err(|e| GeneratorError::Io(e.to_string()))?;
77
78 let content = format!(
79 "-- Migration: {}\n-- Generated at: {}\n\n{}\n",
80 migration.name,
81 migration.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
82 migration.sql
83 );
84
85 std::fs::write(&migration.path, content).map_err(|e| GeneratorError::Io(e.to_string()))?;
86
87 Ok(())
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct Migration {
94 pub version: String,
96 pub name: String,
98 pub sql: String,
100 pub created_at: DateTime<Utc>,
102 pub path: std::path::PathBuf,
104}
105
106#[derive(Debug, thiserror::Error)]
108pub enum GeneratorError {
109 #[error("IO error: {0}")]
110 Io(String),
111 #[error("Parse error: {0}")]
112 Parse(String),
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use forge_core::schema::FieldAttribute;
119 use forge_core::schema::RustType;
120 use forge_core::schema::{FieldDef, TableDef};
121
122 #[test]
123 fn test_generate_migration() {
124 let generator = MigrationGenerator::new("/tmp/migrations");
125
126 let mut table = TableDef::new("users", "User");
127 let mut id_field = FieldDef::new("id", RustType::Uuid);
128 id_field.attributes.push(FieldAttribute::Id);
129 table.fields.push(id_field);
130
131 let migration = generator.generate(&[table], &[]).unwrap();
132
133 assert!(migration.is_some());
134 let m = migration.unwrap();
135 assert!(m.name.contains("users"));
136 assert!(m.sql.contains("CREATE TABLE"));
137 }
138}