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