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