forge_runtime/migrations/
diff.rs1use forge_core::schema::{FieldDef, TableDef};
2
3#[derive(Debug, Clone)]
5pub struct SchemaDiff {
6 pub entries: Vec<DiffEntry>,
8}
9
10impl SchemaDiff {
11 pub fn new() -> Self {
13 Self {
14 entries: Vec::new(),
15 }
16 }
17
18 pub fn from_comparison(rust_tables: &[TableDef], db_tables: &[DatabaseTable]) -> Self {
20 let mut entries = Vec::new();
21
22 for rust_table in rust_tables {
24 let db_table = db_tables.iter().find(|t| t.name == rust_table.name);
25
26 match db_table {
27 None => {
28 entries.push(DiffEntry {
30 action: DiffAction::CreateTable,
31 table_name: rust_table.name.clone(),
32 details: format!("Create table {}", rust_table.name),
33 sql: rust_table.to_create_table_sql(),
34 });
35 }
36 Some(db) => {
37 for rust_field in &rust_table.fields {
39 let db_column =
40 db.columns.iter().find(|c| c.name == rust_field.column_name);
41
42 match db_column {
43 None => {
44 entries.push(DiffEntry {
46 action: DiffAction::AddColumn,
47 table_name: rust_table.name.clone(),
48 details: format!("Add column {}", rust_field.column_name),
49 sql: Self::add_column_sql(&rust_table.name, rust_field),
50 });
51 }
52 Some(db_col) => {
53 let rust_type = rust_field.sql_type.to_sql();
55 if db_col.data_type != rust_type {
56 entries.push(DiffEntry {
57 action: DiffAction::AlterColumn,
58 table_name: rust_table.name.clone(),
59 details: format!(
60 "Change column {} type from {} to {}",
61 rust_field.column_name, db_col.data_type, rust_type
62 ),
63 sql: format!(
64 "ALTER TABLE {} ALTER COLUMN {} TYPE {};",
65 rust_table.name, rust_field.column_name, rust_type
66 ),
67 });
68 }
69 }
70 }
71 }
72
73 for db_col in &db.columns {
75 let exists_in_rust = rust_table
76 .fields
77 .iter()
78 .any(|f| f.column_name == db_col.name);
79
80 if !exists_in_rust {
81 entries.push(DiffEntry {
82 action: DiffAction::DropColumn,
83 table_name: rust_table.name.clone(),
84 details: format!("Drop column {}", db_col.name),
85 sql: format!(
86 "ALTER TABLE {} DROP COLUMN {};",
87 rust_table.name, db_col.name
88 ),
89 });
90 }
91 }
92 }
93 }
94 }
95
96 for db_table in db_tables {
98 let exists_in_rust = rust_tables.iter().any(|t| t.name == db_table.name);
99
100 if !exists_in_rust && !db_table.name.starts_with("forge_") {
101 entries.push(DiffEntry {
102 action: DiffAction::DropTable,
103 table_name: db_table.name.clone(),
104 details: format!("Drop table {}", db_table.name),
105 sql: format!("DROP TABLE {};", db_table.name),
106 });
107 }
108 }
109
110 Self { entries }
111 }
112
113 fn add_column_sql(table_name: &str, field: &FieldDef) -> String {
114 let mut sql = format!(
115 "ALTER TABLE {} ADD COLUMN {} {}",
116 table_name,
117 field.column_name,
118 field.sql_type.to_sql()
119 );
120
121 if !field.nullable {
122 if let Some(ref default) = field.default {
123 sql.push_str(&format!(" NOT NULL DEFAULT {}", default));
124 } else {
125 let default_val =
127 match field.sql_type {
128 forge_core::schema::SqlType::Varchar(_)
129 | forge_core::schema::SqlType::Text => "''",
130 forge_core::schema::SqlType::Integer
131 | forge_core::schema::SqlType::BigInt => "0",
132 forge_core::schema::SqlType::Boolean => "false",
133 forge_core::schema::SqlType::Timestamptz => "NOW()",
134 _ => "NULL",
135 };
136 sql.push_str(&format!(" NOT NULL DEFAULT {}", default_val));
137 }
138 }
139
140 sql.push(';');
141 sql
142 }
143
144 pub fn is_empty(&self) -> bool {
146 self.entries.is_empty()
147 }
148
149 pub fn to_sql(&self) -> Vec<String> {
151 self.entries.iter().map(|e| e.sql.clone()).collect()
152 }
153}
154
155impl Default for SchemaDiff {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161#[derive(Debug, Clone)]
163pub struct DiffEntry {
164 pub action: DiffAction,
166 pub table_name: String,
168 pub details: String,
170 pub sql: String,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum DiffAction {
177 CreateTable,
178 DropTable,
179 AddColumn,
180 DropColumn,
181 AlterColumn,
182 AddIndex,
183 DropIndex,
184 CreateEnum,
185 AlterEnum,
186}
187
188#[derive(Debug, Clone)]
190pub struct DatabaseTable {
191 pub name: String,
192 pub columns: Vec<DatabaseColumn>,
193}
194
195#[derive(Debug, Clone)]
197pub struct DatabaseColumn {
198 pub name: String,
199 pub data_type: String,
200 pub nullable: bool,
201 pub default: Option<String>,
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use forge_core::schema::FieldAttribute;
208 use forge_core::schema::RustType;
209 use forge_core::schema::{FieldDef, TableDef};
210
211 #[test]
212 fn test_empty_diff() {
213 let diff = SchemaDiff::new();
214 assert!(diff.is_empty());
215 }
216
217 #[test]
218 fn test_create_table_diff() {
219 let mut table = TableDef::new("users", "User");
220 let mut id_field = FieldDef::new("id", RustType::Uuid);
221 id_field.attributes.push(FieldAttribute::Id);
222 table.fields.push(id_field);
223
224 let diff = SchemaDiff::from_comparison(&[table], &[]);
225
226 assert_eq!(diff.entries.len(), 1);
227 assert_eq!(diff.entries[0].action, DiffAction::CreateTable);
228 }
229
230 #[test]
231 fn test_add_column_diff() {
232 let mut rust_table = TableDef::new("users", "User");
233 let id_field = FieldDef::new("id", RustType::Uuid);
234 let email_field = FieldDef::new("email", RustType::String);
235 rust_table.fields.push(id_field);
236 rust_table.fields.push(email_field);
237
238 let db_table = DatabaseTable {
239 name: "users".to_string(),
240 columns: vec![DatabaseColumn {
241 name: "id".to_string(),
242 data_type: "UUID".to_string(),
243 nullable: false,
244 default: None,
245 }],
246 };
247
248 let diff = SchemaDiff::from_comparison(&[rust_table], &[db_table]);
249
250 assert_eq!(diff.entries.len(), 1);
251 assert_eq!(diff.entries[0].action, DiffAction::AddColumn);
252 assert!(diff.entries[0].details.contains("email"));
253 }
254}