Skip to main content

gaman_core/diff/
mod.rs

1use std::collections::{BTreeSet, HashMap, HashSet};
2
3use thiserror::Error;
4
5use crate::dialects::Dialect;
6use crate::opaque::{
7    opaque_option_sources_equal, opaque_sources_equal, table_option_sources_equal,
8};
9use crate::operations::Operation;
10use crate::states::{
11    Column, Constraint, EnumDef, ExtensionDef, FunctionDef, Schema, Table, TriggerDef, ViewDef,
12};
13
14#[derive(Debug, Error)]
15pub enum DiffError {
16    #[error("dependency cycle detected among operations — this is a bug in the diff engine")]
17    DependencyCycle,
18    #[error(
19        "primary key changes on existing table '{0}' are not generated automatically; use an explicit SQL statement migration"
20    )]
21    PrimaryKeyMutation(String),
22}
23
24// Walk the schema maps directly. The BTreeMap keys are the canonical identity
25// for each entity, so a plain keyed comparison is enough here.
26
27pub fn generate_diff(current: &Schema, previous: &Schema) -> Vec<Operation> {
28    let mut ops: Vec<Operation> = Vec::new();
29
30    diff_map_with_eq(
31        &current.extensions,
32        &previous.extensions,
33        &mut ops,
34        extension_equal,
35        diff_extension,
36    );
37    diff_map(&current.enums, &previous.enums, &mut ops, diff_enum);
38    diff_map_with_eq(
39        &current.functions,
40        &previous.functions,
41        &mut ops,
42        function_equal,
43        diff_function,
44    );
45    diff_map_with_eq(
46        &current.views,
47        &previous.views,
48        &mut ops,
49        view_equal,
50        diff_view,
51    );
52    diff_map(
53        &current.tables,
54        &previous.tables,
55        &mut ops,
56        |curr, prev, ops| diff_table(curr, prev, ops, None),
57    );
58
59    ops
60}
61
62fn diff_map<T: PartialEq>(
63    current: &std::collections::BTreeMap<String, T>,
64    previous: &std::collections::BTreeMap<String, T>,
65    ops: &mut Vec<Operation>,
66    handler: fn(Option<&T>, Option<&T>, &mut Vec<Operation>),
67) {
68    for (key, curr_val) in current {
69        match previous.get(key) {
70            None => handler(Some(curr_val), None, ops),
71            Some(prev_val) if prev_val != curr_val => handler(Some(curr_val), Some(prev_val), ops),
72            _ => {}
73        }
74    }
75    for (key, prev_val) in previous {
76        if !current.contains_key(key) {
77            handler(None, Some(prev_val), ops);
78        }
79    }
80}
81
82fn diff_map_with_eq<T>(
83    current: &std::collections::BTreeMap<String, T>,
84    previous: &std::collections::BTreeMap<String, T>,
85    ops: &mut Vec<Operation>,
86    equal: fn(&T, &T) -> bool,
87    handler: fn(Option<&T>, Option<&T>, &mut Vec<Operation>),
88) {
89    for (key, curr_val) in current {
90        match previous.get(key) {
91            None => handler(Some(curr_val), None, ops),
92            Some(prev_val) if !equal(curr_val, prev_val) => {
93                handler(Some(curr_val), Some(prev_val), ops)
94            }
95            _ => {}
96        }
97    }
98    for (key, prev_val) in previous {
99        if !current.contains_key(key) {
100            handler(None, Some(prev_val), ops);
101        }
102    }
103}
104
105fn diff_extension(
106    curr: Option<&ExtensionDef>,
107    prev: Option<&ExtensionDef>,
108    ops: &mut Vec<Operation>,
109) {
110    match (curr, prev) {
111        (Some(c), None) => ops.push(Operation::CreateExtension {
112            extension: c.clone(),
113        }),
114        (None, Some(p)) => ops.push(Operation::DropExtension {
115            extension: p.clone(),
116        }),
117        (Some(c), Some(p)) => {
118            ops.push(Operation::DropExtension {
119                extension: p.clone(),
120            });
121            ops.push(Operation::CreateExtension {
122                extension: c.clone(),
123            });
124        }
125        _ => {}
126    }
127}
128
129fn diff_enum(curr: Option<&EnumDef>, prev: Option<&EnumDef>, ops: &mut Vec<Operation>) {
130    match (curr, prev) {
131        (Some(c), None) => ops.push(Operation::CreateEnum {
132            enum_def: c.clone(),
133        }),
134        (None, Some(p)) => ops.push(Operation::DropEnum {
135            enum_def: p.clone(),
136        }),
137        (Some(c), Some(p)) => {
138            let old_set: HashSet<&str> = p.values.iter().map(|v| v.as_str()).collect();
139            let new_set: HashSet<&str> = c.values.iter().map(|v| v.as_str()).collect();
140            if old_set.is_subset(&new_set) && values_are_subsequence(&p.values, &c.values) {
141                ops.push(Operation::AlterEnum {
142                    old: p.clone(),
143                    new: c.clone(),
144                });
145            } else {
146                ops.push(Operation::DropEnum {
147                    enum_def: p.clone(),
148                });
149                ops.push(Operation::CreateEnum {
150                    enum_def: c.clone(),
151                });
152            }
153        }
154        _ => {}
155    }
156}
157
158fn values_are_subsequence(old: &[String], new: &[String]) -> bool {
159    let mut new_iter = new.iter();
160    old.iter()
161        .all(|old_value| new_iter.by_ref().any(|new_value| new_value == old_value))
162}
163
164fn diff_function(curr: Option<&FunctionDef>, prev: Option<&FunctionDef>, ops: &mut Vec<Operation>) {
165    match (curr, prev) {
166        (Some(c), None) => ops.push(Operation::CreateFunction {
167            function: c.clone(),
168        }),
169        (None, Some(p)) => ops.push(Operation::DropFunction {
170            function: p.clone(),
171        }),
172        (Some(c), Some(p)) => {
173            ops.push(Operation::AlterFunction {
174                old: p.clone(),
175                new: c.clone(),
176            });
177        }
178        _ => {}
179    }
180}
181
182fn diff_view(curr: Option<&ViewDef>, prev: Option<&ViewDef>, ops: &mut Vec<Operation>) {
183    match (curr, prev) {
184        (Some(c), None) => ops.push(Operation::CreateView { view: c.clone() }),
185        (None, Some(p)) => ops.push(Operation::DropView { view: p.clone() }),
186        (Some(c), Some(p)) => {
187            ops.push(Operation::ReplaceView {
188                old: p.clone(),
189                new: c.clone(),
190            });
191        }
192        _ => {}
193    }
194}
195
196fn diff_table(
197    curr: Option<&Table>,
198    prev: Option<&Table>,
199    ops: &mut Vec<Operation>,
200    dialect: Option<&Dialect>,
201) {
202    match (curr, prev) {
203        (Some(c), None) => ops.push(Operation::CreateTable { table: c.clone() }),
204        (None, Some(p)) => ops.push(Operation::DropTable { table: p.clone() }),
205        (Some(c), Some(p)) => diff_table_children(p, c, ops, dialect),
206        _ => {}
207    }
208}
209
210fn diff_table_children(
211    prev: &Table,
212    curr: &Table,
213    ops: &mut Vec<Operation>,
214    dialect: Option<&Dialect>,
215) {
216    let table_name = curr.qualified_name();
217    if !table_option_sources_equal(
218        &prev.options.header_raw,
219        &prev.options.tail_raw,
220        &curr.options.header_raw,
221        &curr.options.tail_raw,
222    ) {
223        ops.push(Operation::AcknowledgeTableOptions {
224            table_name: table_name.clone(),
225            old: prev.options.clone(),
226            new: curr.options.clone(),
227        });
228    }
229
230    for change in diff_by_name(
231        &prev.columns,
232        &curr.columns,
233        |c| c.name.as_str(),
234        |left, right| column_equal(left, right, dialect),
235    ) {
236        match change {
237            SubChange::Removed(col) => ops.push(Operation::DropColumn {
238                table_name: table_name.clone(),
239                column: col.clone(),
240                cascade: false,
241            }),
242            SubChange::Added(col) => ops.push(Operation::AddColumn {
243                table_name: table_name.clone(),
244                column: col.clone(),
245            }),
246            SubChange::Modified(old, new) => ops.push(Operation::AlterColumn {
247                table_name: table_name.clone(),
248                old: old.clone(),
249                new: new.clone(),
250                cast_expr: None,
251            }),
252        }
253    }
254
255    for change in diff_by_name(
256        &prev.foreign_keys,
257        &curr.foreign_keys,
258        |fk| fk.name.as_str(),
259        PartialEq::eq,
260    ) {
261        match change {
262            SubChange::Removed(fk) => ops.push(Operation::DropForeignKey {
263                table_name: table_name.clone(),
264                foreign_key: fk.clone(),
265                cascade: false,
266            }),
267            SubChange::Added(fk) => ops.push(Operation::AddForeignKey {
268                table_name: table_name.clone(),
269                foreign_key: fk.clone(),
270            }),
271            SubChange::Modified(old, new) => {
272                ops.push(Operation::DropForeignKey {
273                    table_name: table_name.clone(),
274                    foreign_key: old.clone(),
275                    cascade: false,
276                });
277                ops.push(Operation::AddForeignKey {
278                    table_name: table_name.clone(),
279                    foreign_key: new.clone(),
280                });
281            }
282        }
283    }
284
285    for change in diff_by_name(
286        &prev.indexes,
287        &curr.indexes,
288        |i| i.name.as_str(),
289        index_equal,
290    ) {
291        match change {
292            SubChange::Removed(idx) => ops.push(Operation::DropIndex {
293                table_name: table_name.clone(),
294                index: idx.clone(),
295                concurrent: false,
296            }),
297            SubChange::Added(idx) => ops.push(Operation::AddIndex {
298                table_name: table_name.clone(),
299                index: idx.clone(),
300                concurrent: false,
301            }),
302            SubChange::Modified(old, new) => {
303                ops.push(Operation::DropIndex {
304                    table_name: table_name.clone(),
305                    index: old.clone(),
306                    concurrent: false,
307                });
308                ops.push(Operation::AddIndex {
309                    table_name: table_name.clone(),
310                    index: new.clone(),
311                    concurrent: false,
312                });
313            }
314        }
315    }
316
317    for change in diff_by_name(
318        &prev.constraints,
319        &curr.constraints,
320        |c| c.name(),
321        constraint_equal,
322    ) {
323        match change {
324            SubChange::Removed(con) => ops.push(Operation::DropConstraint {
325                table_name: table_name.clone(),
326                constraint: con.clone(),
327            }),
328            SubChange::Added(con) => ops.push(Operation::AddConstraint {
329                table_name: table_name.clone(),
330                constraint: con.clone(),
331            }),
332            SubChange::Modified(old, new) => {
333                ops.push(Operation::DropConstraint {
334                    table_name: table_name.clone(),
335                    constraint: old.clone(),
336                });
337                ops.push(Operation::AddConstraint {
338                    table_name: table_name.clone(),
339                    constraint: new.clone(),
340                });
341            }
342        }
343    }
344
345    for change in diff_by_name(
346        &prev.triggers,
347        &curr.triggers,
348        |t| t.name.as_deref().unwrap_or(""),
349        trigger_equal,
350    ) {
351        match change {
352            SubChange::Removed(trg) => ops.push(Operation::DropTrigger {
353                table_name: table_name.clone(),
354                trigger: trg.clone(),
355            }),
356            SubChange::Added(trg) => ops.push(Operation::CreateTrigger {
357                table_name: table_name.clone(),
358                trigger: trg.clone(),
359            }),
360            SubChange::Modified(old, new) => ops.push(Operation::AlterTrigger {
361                table_name: table_name.clone(),
362                old: old.clone(),
363                new: new.clone(),
364            }),
365        }
366    }
367}
368
369fn column_equal(left: &Column, right: &Column, dialect: Option<&Dialect>) -> bool {
370    let type_equal = dialect.map_or_else(
371        || left.col_type == right.col_type,
372        |dialect| {
373            dialect.type_comparison_key(&left.col_type)
374                == dialect.type_comparison_key(&right.col_type)
375        },
376    );
377    type_equal
378        && left.name == right.name
379        && left.nullable == right.nullable
380        && dialect.map_or_else(
381            || left.default == right.default,
382            |dialect| match (&left.default, &right.default) {
383                (Some(left), Some(right)) => dialect.default_expressions_equal(left, right),
384                (None, None) => true,
385                _ => false,
386            },
387        )
388        && left.primary_key == right.primary_key
389        && left.references == right.references
390        && left.check == right.check
391        && left.generated == right.generated
392}
393
394fn function_equal(left: &FunctionDef, right: &FunctionDef) -> bool {
395    if left.is_opaque() || right.is_opaque() {
396        return left.qualified_name() == right.qualified_name()
397            && opaque_option_sources_equal(&left.opaque.raw, &right.opaque.raw);
398    }
399    left.name == right.name
400        && left.schema == right.schema
401        && left.arguments == right.arguments
402        && left.returns == right.returns
403        && left.language == right.language
404        && left.volatility == right.volatility
405        && left.security_definer == right.security_definer
406        && opaque_sources_equal(&left.body, &right.body)
407}
408
409fn view_equal(left: &ViewDef, right: &ViewDef) -> bool {
410    if left.is_opaque() || right.is_opaque() {
411        return left.qualified_name() == right.qualified_name()
412            && opaque_option_sources_equal(&left.opaque.raw, &right.opaque.raw);
413    }
414    left.name == right.name
415        && left.schema == right.schema
416        && opaque_sources_equal(&left.definition, &right.definition)
417}
418
419fn trigger_equal(left: &TriggerDef, right: &TriggerDef) -> bool {
420    if left.is_opaque() || right.is_opaque() {
421        return left.name == right.name
422            && opaque_option_sources_equal(&left.opaque.raw, &right.opaque.raw);
423    }
424    left.name == right.name
425        && left.timing == right.timing
426        && left.events == right.events
427        && left.scope == right.scope
428        && left.function_name == right.function_name
429        && opaque_option_sources_equal(&left.when, &right.when)
430        && opaque_option_sources_equal(&left.query, &right.query)
431        && left.language == right.language
432}
433
434fn constraint_equal(left: &Constraint, right: &Constraint) -> bool {
435    if left.is_opaque() || right.is_opaque() {
436        let left_raw = left.raw_sql().map(ToOwned::to_owned);
437        let right_raw = right.raw_sql().map(ToOwned::to_owned);
438        return left.name() == right.name() && opaque_option_sources_equal(&left_raw, &right_raw);
439    }
440    left == right
441}
442
443fn extension_equal(left: &ExtensionDef, right: &ExtensionDef) -> bool {
444    if left.is_opaque() || right.is_opaque() {
445        return left.qualified_name() == right.qualified_name()
446            && opaque_option_sources_equal(&left.opaque.raw, &right.opaque.raw);
447    }
448    left == right
449}
450
451fn index_equal(left: &crate::states::Index, right: &crate::states::Index) -> bool {
452    if left.is_opaque() || right.is_opaque() {
453        return left.name == right.name
454            && opaque_option_sources_equal(&left.opaque.raw, &right.opaque.raw);
455    }
456    left == right
457}
458
459enum SubChange<'a, T> {
460    Added(&'a T),
461    Removed(&'a T),
462    Modified(&'a T, &'a T),
463}
464
465fn diff_by_name<'a, T>(
466    prev: &'a [T],
467    curr: &'a [T],
468    name_fn: impl Fn(&T) -> &str,
469    equal: impl Fn(&T, &T) -> bool,
470) -> Vec<SubChange<'a, T>> {
471    let prev_map: HashMap<&str, &T> = prev.iter().map(|v| (name_fn(v), v)).collect();
472    let curr_map: HashMap<&str, &T> = curr.iter().map(|v| (name_fn(v), v)).collect();
473    let mut changes = Vec::new();
474
475    for (&name, &prev_val) in &prev_map {
476        if !curr_map.contains_key(name) {
477            changes.push(SubChange::Removed(prev_val));
478        }
479    }
480    for (&name, &curr_val) in &curr_map {
481        match prev_map.get(name) {
482            None => changes.push(SubChange::Added(curr_val)),
483            Some(&prev_val) if !equal(prev_val, curr_val) => {
484                changes.push(SubChange::Modified(prev_val, curr_val))
485            }
486            _ => {}
487        }
488    }
489    changes
490}
491
492// When a function is dropped but a surviving table still carries a trigger
493// referencing it, that trigger is not detected by the detection pass (the
494// trigger node is identical in both states). We inject the DropTrigger here.
495fn inject_orphan_triggers(ops: Vec<Operation>, previous: &Schema) -> Vec<Operation> {
496    let dropped_fns: HashSet<String> = ops
497        .iter()
498        .filter_map(|op| match op {
499            Operation::DropFunction { function } => Some(function.qualified_name()),
500            _ => None,
501        })
502        .collect();
503
504    if dropped_fns.is_empty() {
505        return ops;
506    }
507
508    let dropped_tables: HashSet<String> = ops
509        .iter()
510        .filter_map(|op| match op {
511            Operation::DropTable { table } => Some(table.qualified_name()),
512            _ => None,
513        })
514        .collect();
515
516    let existing_drop_keys: HashSet<String> = ops
517        .iter()
518        .filter_map(|op| match op {
519            Operation::DropTrigger {
520                table_name,
521                trigger,
522            } => trigger
523                .name
524                .as_deref()
525                .map(|n| format!("{}:{}", table_name, n)),
526            _ => None,
527        })
528        .collect();
529
530    let mut injected: Vec<Operation> = Vec::new();
531    for (table_name, table) in &previous.tables {
532        if dropped_tables.contains(table_name) {
533            continue;
534        }
535        for trg in &table.triggers {
536            let references_dropped = trg
537                .function_name
538                .as_deref()
539                .is_some_and(|f| dropped_fns.contains(f));
540            if !references_dropped {
541                continue;
542            }
543            let key = trg
544                .name
545                .as_deref()
546                .map(|n| format!("{}:{}", table_name, n))
547                .unwrap_or_default();
548            if !existing_drop_keys.contains(&key) {
549                injected.push(Operation::DropTrigger {
550                    table_name: table_name.clone(),
551                    trigger: trg.clone(),
552                });
553            }
554        }
555    }
556
557    let mut result = ops;
558    result.extend(injected);
559    result
560}
561
562// Incompatible enum changes (drop+recreate) require surviving columns that use
563// the enum to be temporarily cast to text and back. Without this, DropEnum fails
564// because the type is still in use.
565// Limitation: enum references inside SQL (defaults, check constraints, function
566// bodies, view definitions) cannot be tracked without parsing SQL.
567fn inject_enum_column_casts(ops: Vec<Operation>, previous: &Schema) -> Vec<Operation> {
568    let dropped_enums: HashSet<&str> = ops
569        .iter()
570        .filter_map(|op| match op {
571            Operation::DropEnum { enum_def } => Some(enum_def.name.as_str()),
572            _ => None,
573        })
574        .collect();
575    let created_enums: HashSet<&str> = ops
576        .iter()
577        .filter_map(|op| match op {
578            Operation::CreateEnum { enum_def } => Some(enum_def.name.as_str()),
579            _ => None,
580        })
581        .collect();
582
583    let recreated: HashSet<&str> = dropped_enums
584        .intersection(&created_enums)
585        .copied()
586        .collect();
587    if recreated.is_empty() {
588        return ops;
589    }
590
591    let dropped_tables: HashSet<String> = ops
592        .iter()
593        .filter_map(|op| match op {
594            Operation::DropTable { table } => Some(table.qualified_name()),
595            _ => None,
596        })
597        .collect();
598
599    let touched_columns: HashSet<(&str, &str)> = ops
600        .iter()
601        .filter_map(|op| match op {
602            Operation::DropColumn {
603                table_name, column, ..
604            } => Some((table_name.as_str(), column.name.as_str())),
605            Operation::AlterColumn {
606                table_name, old, ..
607            } => Some((table_name.as_str(), old.name.as_str())),
608            _ => None,
609        })
610        .collect();
611
612    let mut casts: Vec<Operation> = Vec::new();
613
614    for (table_name, table) in &previous.tables {
615        if dropped_tables.contains(table_name) {
616            continue;
617        }
618        for col in &table.columns {
619            if !recreated.contains(col.col_type.as_str()) {
620                continue;
621            }
622            if touched_columns.contains(&(table_name.as_str(), col.name.as_str())) {
623                continue;
624            }
625            let text_col = Column {
626                col_type: "text".to_string(),
627                ..col.clone()
628            };
629            casts.push(Operation::AlterColumn {
630                table_name: table_name.clone(),
631                old: col.clone(),
632                new: text_col.clone(),
633                cast_expr: Some(format!("{}::text", col.name)),
634            });
635            casts.push(Operation::AlterColumn {
636                table_name: table_name.clone(),
637                old: text_col,
638                new: col.clone(),
639                cast_expr: Some(format!("{}::{}", col.name, col.col_type)),
640            });
641        }
642    }
643
644    if casts.is_empty() {
645        return ops;
646    }
647
648    let mut result = Vec::with_capacity(ops.len() + casts.len());
649    result.extend(ops);
650    result.extend(casts);
651    result
652}
653
654// Splits compound CreateTable/DropTable into atomic operations so
655// the topological sort can order sub-entities independently.
656// CreateTable → CreateTable(columns only) + AddFK* + AddIndex* + AddConstraint* + CreateTrigger*
657// DropTable → DropTrigger* + DropConstraint* + DropIndex* + DropFK* + DropTable(columns only)
658fn decompose(ops: Vec<Operation>) -> Vec<Operation> {
659    let mut result = Vec::with_capacity(ops.len() * 2);
660    for op in ops {
661        match op {
662            Operation::CreateTable { mut table } => {
663                let fks = std::mem::take(&mut table.foreign_keys);
664                let indexes = std::mem::take(&mut table.indexes);
665                let constraints = std::mem::take(&mut table.constraints);
666                let triggers = std::mem::take(&mut table.triggers);
667                let tname = table.name.clone();
668                result.push(Operation::CreateTable { table });
669                for fk in fks {
670                    result.push(Operation::AddForeignKey {
671                        table_name: tname.clone(),
672                        foreign_key: fk,
673                    });
674                }
675                for idx in indexes {
676                    result.push(Operation::AddIndex {
677                        table_name: tname.clone(),
678                        index: idx,
679                        concurrent: false,
680                    });
681                }
682                for con in constraints {
683                    result.push(Operation::AddConstraint {
684                        table_name: tname.clone(),
685                        constraint: con,
686                    });
687                }
688                for trg in triggers {
689                    result.push(Operation::CreateTrigger {
690                        table_name: tname.clone(),
691                        trigger: trg,
692                    });
693                }
694            }
695            Operation::DropTable { mut table } => {
696                let fks = std::mem::take(&mut table.foreign_keys);
697                let indexes = std::mem::take(&mut table.indexes);
698                let constraints = std::mem::take(&mut table.constraints);
699                let triggers = std::mem::take(&mut table.triggers);
700                let tname = table.name.clone();
701                for trg in triggers {
702                    result.push(Operation::DropTrigger {
703                        table_name: tname.clone(),
704                        trigger: trg,
705                    });
706                }
707                for con in constraints {
708                    result.push(Operation::DropConstraint {
709                        table_name: tname.clone(),
710                        constraint: con,
711                    });
712                }
713                for idx in indexes {
714                    result.push(Operation::DropIndex {
715                        table_name: tname.clone(),
716                        index: idx,
717                        concurrent: false,
718                    });
719                }
720                for fk in fks {
721                    result.push(Operation::DropForeignKey {
722                        table_name: tname.clone(),
723                        foreign_key: fk,
724                        cascade: false,
725                    });
726                }
727                result.push(Operation::DropTable { table });
728            }
729            other => result.push(other),
730        }
731    }
732    result
733}
734
735// After sorting, folds decomposed sub-entity ops back into their parent
736// CreateTable when the dialect supports inline definitions (e.g. Postgres
737// can inline FKs, indexes, constraints). This produces cleaner migrations.
738// FKs are only inlined when the reference is self-referential or points to a
739// table that already existed before this migration — cross-table FKs within
740// the same migration stay as standalone ops (already sorted after all creates).
741fn fk_can_be_inlined(
742    to_table: &str,
743    owner_table: &str,
744    tables_being_created: &HashSet<String>,
745) -> bool {
746    to_table == owner_table || !tables_being_created.contains(to_table)
747}
748
749fn merge_operations(ops: Vec<Operation>, dialect: &Dialect) -> Vec<Operation> {
750    let tables_being_created: HashSet<String> = ops
751        .iter()
752        .filter_map(|op| match op {
753            Operation::CreateTable { table } => Some(table.qualified_name()),
754            _ => None,
755        })
756        .collect();
757
758    let mut result: Vec<Operation> = Vec::with_capacity(ops.len());
759    let mut created_tables: HashMap<String, usize> = HashMap::new();
760    for op in ops {
761        let merged = match &op {
762            Operation::AddForeignKey {
763                table_name,
764                foreign_key,
765            } if created_tables.contains_key(table_name)
766                && dialect.should_merge(table_name, &op)
767                && fk_can_be_inlined(&foreign_key.to_table, table_name, &tables_being_created) =>
768            {
769                let idx = created_tables[table_name];
770                if let Operation::CreateTable { ref mut table } = result[idx] {
771                    table.foreign_keys.push(foreign_key.clone());
772                }
773                true
774            }
775            Operation::AddIndex {
776                table_name, index, ..
777            } if created_tables.contains_key(table_name)
778                && dialect.should_merge(table_name, &op) =>
779            {
780                let idx = created_tables[table_name];
781                if let Operation::CreateTable { ref mut table } = result[idx] {
782                    table.indexes.push(index.clone());
783                }
784                true
785            }
786            Operation::AddConstraint {
787                table_name,
788                constraint,
789            } if created_tables.contains_key(table_name)
790                && dialect.should_merge(table_name, &op) =>
791            {
792                let idx = created_tables[table_name];
793                if let Operation::CreateTable { ref mut table } = result[idx] {
794                    table.constraints.push(constraint.clone());
795                }
796                true
797            }
798            Operation::CreateTrigger {
799                table_name,
800                trigger,
801            } if created_tables.contains_key(table_name)
802                && dialect.should_merge(table_name, &op) =>
803            {
804                let idx = created_tables[table_name];
805                if let Operation::CreateTable { ref mut table } = result[idx] {
806                    table.triggers.push(trigger.clone());
807                }
808                true
809            }
810            _ => false,
811        };
812        if !merged {
813            if let Operation::CreateTable { ref table } = op {
814                created_tables.insert(table.qualified_name(), result.len());
815            }
816            result.push(op);
817        }
818    }
819    result
820}
821
822// Topologically sort operations with Kahn's algorithm. When multiple nodes are
823// otherwise equal, break ties deterministically so output stays reproducible.
824
825pub fn sort_operations(ops: Vec<Operation>) -> Result<Vec<Operation>, DiffError> {
826    let n = ops.len();
827    if n == 0 {
828        return Ok(ops);
829    }
830
831    let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
832    let mut in_deg: Vec<usize> = vec![0; n];
833
834    build_dependency_edges(&ops, &mut adj, &mut in_deg);
835
836    // Kahn's BFS with deterministic tie-breaking via BTreeSet.
837    let mut queue: BTreeSet<(u8, u8, String, usize)> = BTreeSet::new();
838    for i in 0..n {
839        if in_deg[i] == 0 {
840            let (tp, sp) = tiebreak_priority(&ops[i]);
841            queue.insert((tp, sp, ops[i].entity_name().to_string(), i));
842        }
843    }
844
845    let mut sorted_indices: Vec<usize> = Vec::with_capacity(n);
846    while let Some(entry) = queue.iter().next().cloned() {
847        queue.remove(&entry);
848        let idx = entry.3;
849        sorted_indices.push(idx);
850        for &dep in &adj[idx] {
851            in_deg[dep] -= 1;
852            if in_deg[dep] == 0 {
853                let (tp, sp) = tiebreak_priority(&ops[dep]);
854                queue.insert((tp, sp, ops[dep].entity_name().to_string(), dep));
855            }
856        }
857    }
858
859    if sorted_indices.len() != n {
860        return Err(DiffError::DependencyCycle);
861    }
862
863    let mut slots: Vec<Option<Operation>> = ops.into_iter().map(Some).collect();
864    let result = sorted_indices
865        .into_iter()
866        .map(|i| slots[i].take().expect("index used twice"))
867        .collect();
868    Ok(result)
869}
870
871fn tiebreak_priority(op: &Operation) -> (u8, u8) {
872    match op {
873        Operation::CreateExtension { .. } => (0, 0),
874        Operation::CreateEnum { .. }
875        | Operation::RenameEnumValue { .. }
876        | Operation::AlterEnum { .. } => (1, 0),
877        Operation::DropView { .. } => (2, 0),
878        Operation::ReplaceView { .. } => (11, 0),
879        Operation::DropTable { .. } => (4, 0),
880        Operation::CreateFunction { .. } | Operation::AlterFunction { .. } => (5, 0),
881        Operation::CreateTable { .. } => (6, 0),
882        Operation::DropTrigger { .. } => (8, 0),
883        Operation::DropConstraint { .. } => (8, 1),
884        Operation::DropIndex { .. } => (8, 2),
885        Operation::DropForeignKey { .. } => (8, 3),
886        Operation::DropColumn { .. } => (8, 4),
887        Operation::AlterColumn { .. } => (8, 5),
888        Operation::AddColumn { .. } => (8, 6),
889        Operation::AddForeignKey { .. } => (8, 7),
890        Operation::AddIndex { .. } => (8, 8),
891        Operation::AddConstraint { .. } => (8, 9),
892        Operation::CreateTrigger { .. } | Operation::AlterTrigger { .. } => (8, 10),
893        Operation::DropFunction { .. } => (10, 0),
894        Operation::CreateView { .. } => (11, 0),
895        Operation::DropEnum { .. } => (12, 0),
896        Operation::DropExtension { .. } => (13, 0),
897        Operation::Statement { .. }
898        | Operation::AcknowledgeTableOptions { .. }
899        | Operation::RenameTable { .. }
900        | Operation::RenameColumn { .. } => (8, 5),
901    }
902}
903
904use crate::states::types::{Dep, EntityKind};
905
906fn build_dependency_edges(ops: &[Operation], adj: &mut [Vec<usize>], in_deg: &mut [usize]) {
907    let mut seen: HashSet<(usize, usize)> = HashSet::new();
908
909    let entity_names: Vec<std::borrow::Cow<str>> = ops.iter().map(|op| op.entity_name()).collect();
910    let mut create_idx: HashMap<(EntityKind, &str), Vec<usize>> = HashMap::new();
911    let mut drop_idx: HashMap<(EntityKind, &str), Vec<usize>> = HashMap::new();
912
913    for (i, op) in ops.iter().enumerate() {
914        if let Some(kind) = op.entity_kind() {
915            let name: &str = &entity_names[i];
916            if op.is_drop() {
917                drop_idx.entry((kind, name)).or_default().push(i);
918            } else {
919                create_idx.entry((kind, name)).or_default().push(i);
920            }
921        }
922    }
923
924    let mut add_edge = |from: usize, to: usize| {
925        if from != to && seen.insert((from, to)) {
926            adj[from].push(to);
927            in_deg[to] += 1;
928        }
929    };
930
931    let resolve_create = |dep: &Dep| -> Vec<usize> {
932        match &dep.name {
933            Some(name) => create_idx
934                .get(&(dep.kind, name.as_str()))
935                .cloned()
936                .unwrap_or_default(),
937            None => create_idx
938                .iter()
939                .filter(|((k, _), _)| *k == dep.kind)
940                .flat_map(|(_, v)| v.iter().copied())
941                .collect(),
942        }
943    };
944
945    let resolve_drop = |dep: &Dep| -> Vec<usize> {
946        match &dep.name {
947            Some(name) => drop_idx
948                .get(&(dep.kind, name.as_str()))
949                .cloned()
950                .unwrap_or_default(),
951            None => drop_idx
952                .iter()
953                .filter(|((k, _), _)| *k == dep.kind)
954                .flat_map(|(_, v)| v.iter().copied())
955                .collect(),
956        }
957    };
958
959    for (i, op) in ops.iter().enumerate() {
960        if !op.is_drop() {
961            for dep in op.forward_deps() {
962                for j in resolve_create(&dep) {
963                    add_edge(j, i);
964                }
965            }
966        }
967        if !op.is_create() {
968            for dep in op.backward_deps() {
969                for k in resolve_drop(&dep) {
970                    add_edge(i, k);
971                }
972            }
973        }
974    }
975
976    let mut by_table: HashMap<&str, Vec<usize>> = HashMap::new();
977    for (i, op) in ops.iter().enumerate() {
978        if let Some(tn) = op.table_name() {
979            let (tp, _) = tiebreak_priority(op);
980            if tp == 8 {
981                by_table.entry(tn).or_default().push(i);
982            }
983        }
984    }
985    for indices in by_table.values() {
986        for &a in indices {
987            for &b in indices {
988                if a == b {
989                    continue;
990                }
991                let (_, spa) = tiebreak_priority(&ops[a]);
992                let (_, spb) = tiebreak_priority(&ops[b]);
993                if spa < spb {
994                    add_edge(a, b);
995                }
996            }
997        }
998    }
999}
1000
1001// Public entry point for diff generation.
1002
1003pub struct DiffEngine;
1004
1005impl DiffEngine {
1006    pub fn new() -> Self {
1007        Self
1008    }
1009
1010    pub fn diff(
1011        &self,
1012        current: &Schema,
1013        previous: &Schema,
1014        dialect: &Dialect,
1015    ) -> Result<Vec<Operation>, DiffError> {
1016        let mut current = current.clone();
1017        let mut previous = previous.clone();
1018        current.canonicalize(dialect);
1019        previous.canonicalize(dialect);
1020        reject_primary_key_mutations(&current, &previous)?;
1021
1022        let raw_ops = generate_diff_for_dialect(&current, &previous, dialect);
1023        let ops = inject_orphan_triggers(raw_ops, &previous);
1024        let ops = inject_enum_column_casts(ops, &previous);
1025        let ops = decompose(ops);
1026        let ops = sort_operations(ops)?;
1027        Ok(merge_operations(ops, dialect))
1028    }
1029}
1030
1031fn generate_diff_for_dialect(
1032    current: &Schema,
1033    previous: &Schema,
1034    dialect: &Dialect,
1035) -> Vec<Operation> {
1036    let mut current_without_tables = current.clone();
1037    current_without_tables.tables.clear();
1038    let mut previous_without_tables = previous.clone();
1039    previous_without_tables.tables.clear();
1040    let mut ops = generate_diff(&current_without_tables, &previous_without_tables);
1041
1042    for (key, current_table) in &current.tables {
1043        match previous.tables.get(key) {
1044            None => diff_table(Some(current_table), None, &mut ops, Some(dialect)),
1045            Some(previous_table) => {
1046                diff_table(
1047                    Some(current_table),
1048                    Some(previous_table),
1049                    &mut ops,
1050                    Some(dialect),
1051                );
1052            }
1053        }
1054    }
1055    for (key, previous_table) in &previous.tables {
1056        if !current.tables.contains_key(key) {
1057            diff_table(None, Some(previous_table), &mut ops, Some(dialect));
1058        }
1059    }
1060    ops
1061}
1062
1063fn reject_primary_key_mutations(current: &Schema, previous: &Schema) -> Result<(), DiffError> {
1064    for (name, current_table) in &current.tables {
1065        let Some(previous_table) = previous.tables.get(name) else {
1066            continue;
1067        };
1068        if current_table.primary_key != previous_table.primary_key {
1069            return Err(DiffError::PrimaryKeyMutation(name.clone()));
1070        }
1071    }
1072    Ok(())
1073}
1074
1075impl Default for DiffEngine {
1076    fn default() -> Self {
1077        Self::new()
1078    }
1079}
1080
1081#[cfg(test)]
1082mod test_diff;