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 Some(first) = diff.entries.first() else {
57 return "empty".to_string();
58 };
59 match first.action {
60 super::diff::DiffAction::CreateTable => {
61 format!("create_{}", first.table_name)
62 }
63 super::diff::DiffAction::AddColumn => {
64 format!("add_column_to_{}", first.table_name)
65 }
66 super::diff::DiffAction::DropColumn => {
67 format!("remove_column_from_{}", first.table_name)
68 }
69 super::diff::DiffAction::DropTable => {
70 format!("drop_{}", first.table_name)
71 }
72 _ => "schema_update".to_string(),
73 }
74 }
75
76 pub fn write_migration(&self, migration: &Migration) -> Result<(), GeneratorError> {
78 std::fs::create_dir_all(&self.output_dir).map_err(|e| GeneratorError::Io(e.to_string()))?;
79
80 let content = format!(
81 "-- Migration: {}\n-- Generated at: {}\n\n{}\n",
82 migration.name,
83 migration.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
84 migration.sql
85 );
86
87 std::fs::write(&migration.path, content).map_err(|e| GeneratorError::Io(e.to_string()))?;
88
89 Ok(())
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct Migration {
96 pub version: String,
98 pub name: String,
100 pub sql: String,
102 pub created_at: DateTime<Utc>,
104 pub path: std::path::PathBuf,
106}
107
108#[derive(Debug, thiserror::Error)]
110pub enum GeneratorError {
111 #[error("IO error: {0}")]
112 Io(String),
113 #[error("Parse error: {0}")]
114 Parse(String),
115}
116
117#[cfg(test)]
118#[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::panic)]
119mod tests {
120 use super::*;
121 use forge_core::schema::RustType;
122 use forge_core::schema::{FieldDef, TableDef};
123
124 #[test]
125 fn test_generate_migration() {
126 let generator = MigrationGenerator::new("/tmp/migrations");
127
128 let mut table = TableDef::new("users", "User");
129 table.fields.push(FieldDef::new("id", RustType::Uuid));
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 users"));
138 }
139}