1use super::schema::{MigrationHint, Schema};
7use crate::ast::{Action, Constraint, Expr, IndexDef, Qail};
8
9pub fn diff_schemas(old: &Schema, new: &Schema) -> Vec<Qail> {
14 let mut cmds = Vec::new();
15
16 for hint in &new.migrations {
18 match hint {
19 MigrationHint::Rename { from, to } => {
20 if let (Some((from_table, from_col)), Some((to_table, to_col))) =
21 (parse_table_col(from), parse_table_col(to))
22 && from_table == to_table
23 {
24 cmds.push(Qail {
26 action: Action::Mod,
27 table: from_table.to_string(),
28 columns: vec![Expr::Named(format!("{} -> {}", from_col, to_col))],
29 ..Default::default()
30 });
31 }
32 }
33 MigrationHint::Transform { expression, target } => {
34 if let Some((table, _col)) = parse_table_col(target) {
35 cmds.push(Qail {
36 action: Action::Set,
37 table: table.to_string(),
38 columns: vec![Expr::Named(format!("/* TRANSFORM: {} */", expression))],
39 ..Default::default()
40 });
41 }
42 }
43 MigrationHint::Drop {
44 target,
45 confirmed: true,
46 } => {
47 if target.contains('.') {
48 if let Some((table, col)) = parse_table_col(target) {
50 cmds.push(Qail {
51 action: Action::AlterDrop,
52 table: table.to_string(),
53 columns: vec![Expr::Named(col.to_string())],
54 ..Default::default()
55 });
56 }
57 } else {
58 cmds.push(Qail {
60 action: Action::Drop,
61 table: target.clone(),
62 ..Default::default()
63 });
64 }
65 }
66 _ => {}
67 }
68 }
69
70 let mut new_table_names: Vec<&String> = new
72 .tables
73 .keys()
74 .filter(|name| !old.tables.contains_key(*name))
75 .collect();
76
77 new_table_names.sort_by_key(|name| {
80 new.tables
81 .get(*name)
82 .map(|t| t.columns.iter().filter(|c| c.foreign_key.is_some()).count())
83 .unwrap_or(0)
84 });
85
86 for name in new_table_names {
88 let table = &new.tables[name];
89 let columns: Vec<Expr> = table
90 .columns
91 .iter()
92 .map(|col| {
93 let mut constraints = Vec::new();
94 if col.primary_key {
95 constraints.push(Constraint::PrimaryKey);
96 }
97 if col.nullable {
98 constraints.push(Constraint::Nullable);
99 }
100 if col.unique {
101 constraints.push(Constraint::Unique);
102 }
103 if let Some(def) = &col.default {
104 constraints.push(Constraint::Default(def.clone()));
105 }
106 if let Some(ref fk) = col.foreign_key {
107 constraints.push(Constraint::References(format!(
108 "{}({})",
109 fk.table, fk.column
110 )));
111 }
112
113 Expr::Def {
114 name: col.name.clone(),
115 data_type: col.data_type.to_pg_type(),
116 constraints,
117 }
118 })
119 .collect();
120
121 cmds.push(Qail {
122 action: Action::Make,
123 table: name.clone(),
124 columns,
125 ..Default::default()
126 });
127 }
128
129 for name in old.tables.keys() {
131 if !new.tables.contains_key(name) {
132 let already_dropped = new.migrations.iter().any(
133 |h| matches!(h, MigrationHint::Drop { target, confirmed: true } if target == name),
134 );
135 if !already_dropped {
136 cmds.push(Qail {
137 action: Action::Drop,
138 table: name.clone(),
139 ..Default::default()
140 });
141 }
142 }
143 }
144
145 for (name, new_table) in &new.tables {
147 if let Some(old_table) = old.tables.get(name) {
148 let old_cols: std::collections::HashSet<_> =
149 old_table.columns.iter().map(|c| &c.name).collect();
150 let new_cols: std::collections::HashSet<_> =
151 new_table.columns.iter().map(|c| &c.name).collect();
152
153 for col in &new_table.columns {
155 if !old_cols.contains(&col.name) {
156 let is_rename_target = new.migrations.iter().any(|h| {
157 matches!(h, MigrationHint::Rename { to, .. } if to.ends_with(&format!(".{}", col.name)))
158 });
159
160 if !is_rename_target {
161 let mut constraints = Vec::new();
162 if col.nullable {
163 constraints.push(Constraint::Nullable);
164 }
165 if col.unique {
166 constraints.push(Constraint::Unique);
167 }
168 if let Some(def) = &col.default {
169 constraints.push(Constraint::Default(def.clone()));
170 }
171
172 cmds.push(Qail {
173 action: Action::Alter,
174 table: name.clone(),
175 columns: vec![Expr::Def {
176 name: col.name.clone(),
177 data_type: col.data_type.to_pg_type(),
178 constraints,
179 }],
180 ..Default::default()
181 });
182 }
183 }
184 }
185
186 for col in &old_table.columns {
188 if !new_cols.contains(&col.name) {
189 let is_rename_source = new.migrations.iter().any(|h| {
190 matches!(h, MigrationHint::Rename { from, .. } if from.ends_with(&format!(".{}", col.name)))
191 });
192
193 if !is_rename_source {
194 cmds.push(Qail {
195 action: Action::AlterDrop,
196 table: name.clone(),
197 columns: vec![Expr::Named(col.name.clone())],
198 ..Default::default()
199 });
200 }
201 }
202 }
203
204 for new_col in &new_table.columns {
206 if let Some(old_col) = old_table.columns.iter().find(|c| c.name == new_col.name) {
207 let old_type = old_col.data_type.to_pg_type();
208 let new_type = new_col.data_type.to_pg_type();
209
210 if old_type != new_type {
211 cmds.push(Qail {
213 action: Action::AlterType,
214 table: name.clone(),
215 columns: vec![Expr::Def {
216 name: new_col.name.clone(),
217 data_type: new_type,
218 constraints: vec![],
219 }],
220 ..Default::default()
221 });
222 }
223 }
224 }
225 }
226 }
227
228 for new_idx in &new.indexes {
230 let exists = old.indexes.iter().any(|i| i.name == new_idx.name);
231 if !exists {
232 cmds.push(Qail {
233 action: Action::Index,
234 table: String::new(),
235 index_def: Some(IndexDef {
236 name: new_idx.name.clone(),
237 table: new_idx.table.clone(),
238 columns: new_idx.columns.clone(),
239 unique: new_idx.unique,
240 }),
241 ..Default::default()
242 });
243 }
244 }
245
246 for old_idx in &old.indexes {
248 let exists = new.indexes.iter().any(|i| i.name == old_idx.name);
249 if !exists {
250 cmds.push(Qail {
251 action: Action::DropIndex,
252 table: old_idx.name.clone(),
253 ..Default::default()
254 });
255 }
256 }
257
258 cmds
259}
260
261fn parse_table_col(s: &str) -> Option<(&str, &str)> {
263 let parts: Vec<&str> = s.splitn(2, '.').collect();
264 if parts.len() == 2 {
265 Some((parts[0], parts[1]))
266 } else {
267 None
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::super::schema::{Column, Table};
274 use super::*;
275
276 #[test]
277 fn test_diff_new_table() {
278 use super::super::types::ColumnType;
279 let old = Schema::default();
280 let mut new = Schema::default();
281 new.add_table(
282 Table::new("users")
283 .column(Column::new("id", ColumnType::Serial).primary_key())
284 .column(Column::new("name", ColumnType::Text).not_null()),
285 );
286
287 let cmds = diff_schemas(&old, &new);
288 assert_eq!(cmds.len(), 1);
289 assert!(matches!(cmds[0].action, Action::Make));
290 }
291
292 #[test]
293 fn test_diff_rename_with_hint() {
294 use super::super::types::ColumnType;
295 let mut old = Schema::default();
296 old.add_table(Table::new("users").column(Column::new("username", ColumnType::Text)));
297
298 let mut new = Schema::default();
299 new.add_table(Table::new("users").column(Column::new("name", ColumnType::Text)));
300 new.add_hint(MigrationHint::Rename {
301 from: "users.username".into(),
302 to: "users.name".into(),
303 });
304
305 let cmds = diff_schemas(&old, &new);
306 assert!(cmds.iter().any(|c| matches!(c.action, Action::Mod)));
308 assert!(!cmds.iter().any(|c| matches!(c.action, Action::AlterDrop)));
309 }
310
311 #[test]
313 fn test_fk_ordering_parent_before_child() {
314 use super::super::types::ColumnType;
315
316 let old = Schema::default();
317
318 let mut new = Schema::default();
319 new.add_table(
321 Table::new("child")
322 .column(Column::new("id", ColumnType::Serial).primary_key())
323 .column(Column::new("parent_id", ColumnType::Int).references("parent", "id")),
324 );
325 new.add_table(
327 Table::new("parent")
328 .column(Column::new("id", ColumnType::Serial).primary_key())
329 .column(Column::new("name", ColumnType::Text)),
330 );
331
332 let cmds = diff_schemas(&old, &new);
333
334 let make_cmds: Vec<_> = cmds.iter().filter(|c| matches!(c.action, Action::Make)).collect();
336 assert_eq!(make_cmds.len(), 2);
337
338 let parent_idx = make_cmds.iter().position(|c| c.table == "parent").unwrap();
340 let child_idx = make_cmds.iter().position(|c| c.table == "child").unwrap();
341 assert!(parent_idx < child_idx, "parent table should be created before child with FK");
342 }
343
344 #[test]
346 fn test_fk_ordering_multiple_dependencies() {
347 use super::super::types::ColumnType;
348
349 let old = Schema::default();
350
351 let mut new = Schema::default();
352 new.add_table(
354 Table::new("order_items")
355 .column(Column::new("id", ColumnType::Serial).primary_key())
356 .column(Column::new("order_id", ColumnType::Int).references("orders", "id"))
357 .column(Column::new("product_id", ColumnType::Int).references("products", "id")),
358 );
359 new.add_table(
361 Table::new("orders")
362 .column(Column::new("id", ColumnType::Serial).primary_key())
363 .column(Column::new("user_id", ColumnType::Int).references("users", "id")),
364 );
365 new.add_table(
367 Table::new("users")
368 .column(Column::new("id", ColumnType::Serial).primary_key()),
369 );
370 new.add_table(
371 Table::new("products")
372 .column(Column::new("id", ColumnType::Serial).primary_key()),
373 );
374
375 let cmds = diff_schemas(&old, &new);
376
377 let make_cmds: Vec<_> = cmds.iter().filter(|c| matches!(c.action, Action::Make)).collect();
378 assert_eq!(make_cmds.len(), 4);
379
380 let users_idx = make_cmds.iter().position(|c| c.table == "users").unwrap();
382 let products_idx = make_cmds.iter().position(|c| c.table == "products").unwrap();
383 let orders_idx = make_cmds.iter().position(|c| c.table == "orders").unwrap();
384 let items_idx = make_cmds.iter().position(|c| c.table == "order_items").unwrap();
385
386 assert!(users_idx < orders_idx, "users (0 FK) before orders (1 FK)");
388 assert!(products_idx < items_idx, "products (0 FK) before order_items (2 FK)");
389
390 assert!(orders_idx < items_idx, "orders (1 FK) before order_items (2 FK)");
392 }
393}
394