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