Skip to main content

toolu_orm_core/diff/
engine.rs

1//! Schema diff algorithm comparing two snapshots to produce migration operations.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::column::ColumnDef;
6use crate::index::IndexDef;
7use crate::rename::{NoRenames, RenameResolver};
8use crate::schema::SchemaRegistry;
9use crate::snapshot::Snapshot;
10use crate::table::TableDef;
11
12use super::column::compute_column_changes;
13use super::enums::diff_enums;
14use super::fk::{diff_check_constraints_inner, diff_foreign_keys_inner};
15use super::operation::Operation;
16
17pub fn diff(old_snapshot: &Snapshot, new_schema: &SchemaRegistry) -> Vec<Operation> {
18  diff_with_resolver(old_snapshot, new_schema, &NoRenames)
19}
20
21pub fn diff_with_resolver(
22  old_snapshot: &Snapshot,
23  new_schema: &SchemaRegistry,
24  resolver: &impl RenameResolver,
25) -> Vec<Operation> {
26  let new_snap = Snapshot::from_registry(new_schema);
27  let mut ops = diff_enums(old_snapshot, &new_snap);
28  ops.extend(diff_tables(old_snapshot, &new_snap, new_schema, resolver));
29  ops
30}
31
32fn diff_tables(
33  old_snapshot: &Snapshot,
34  new_snap: &Snapshot,
35  new_schema: &SchemaRegistry,
36  resolver: &impl RenameResolver,
37) -> Vec<Operation> {
38  let mut ops = Vec::new();
39  let old_names: BTreeSet<String> = old_snapshot.tables.keys().cloned().collect();
40  let new_names: BTreeSet<String> = new_snap.tables.keys().cloned().collect();
41
42  let mut added: Vec<String> = new_names.difference(&old_names).cloned().collect();
43  let mut removed: Vec<String> = old_names.difference(&new_names).cloned().collect();
44
45  let renames = resolver.resolve_tables(&added, &removed);
46  for (old_n, new_n) in &renames {
47    added.retain(|x| x != new_n);
48    removed.retain(|x| x != old_n);
49    ops.push(Operation::RenameTable {
50      old: old_n.clone(),
51      new: new_n.clone(),
52    });
53  }
54
55  for name in &removed {
56    ops.push(Operation::DropTable { name: name.clone() });
57  }
58
59  let mut new_to_old: BTreeMap<String, String> = BTreeMap::new();
60  for t in new_schema.tables() {
61    new_to_old.insert(t.name.clone(), t.name.clone());
62  }
63  for (o, n) in &renames {
64    new_to_old.insert(n.clone(), o.clone());
65  }
66
67  let pure_added: BTreeSet<String> = added.iter().cloned().collect();
68
69  for table in new_schema.tables() {
70    if pure_added.contains(&table.name) {
71      ops.push(Operation::CreateTable {
72        table: table.clone(),
73      });
74      for idx in &table.indexes {
75        ops.push(Operation::CreateIndex {
76          table: table.name.clone(),
77          index: idx.clone(),
78        });
79      }
80      continue;
81    }
82
83    let old_name = new_to_old
84      .get(&table.name)
85      .cloned()
86      .unwrap_or_else(|| table.name.clone());
87    let Some(old_st) = old_snapshot.tables.get(&old_name) else {
88      continue;
89    };
90    let Some(new_st) = new_snap.tables.get(&table.name) else {
91      continue;
92    };
93
94    diff_columns_for_table(
95      &mut ops,
96      &table.name,
97      &old_st.columns,
98      &new_st.columns,
99      table,
100      resolver,
101    );
102    diff_indexes_inner(&mut ops, &table.name, &old_st.indexes, &new_st.indexes);
103    diff_foreign_keys_inner(
104      &mut ops,
105      &table.name,
106      &old_st.foreign_keys,
107      &new_st.foreign_keys,
108    );
109    diff_check_constraints_inner(
110      &mut ops,
111      &table.name,
112      &old_st.check_constraints,
113      &new_st.check_constraints,
114    );
115  }
116
117  ops
118}
119
120fn diff_columns_for_table(
121  ops: &mut Vec<Operation>,
122  table_name: &str,
123  old_cols: &BTreeMap<String, ColumnDef>,
124  new_cols: &BTreeMap<String, ColumnDef>,
125  new_table_def: &TableDef,
126  resolver: &impl RenameResolver,
127) {
128  let added: Vec<String> = new_cols
129    .keys()
130    .filter(|k| !old_cols.contains_key(*k))
131    .cloned()
132    .collect();
133  let removed: Vec<String> = old_cols
134    .keys()
135    .filter(|k| !new_cols.contains_key(*k))
136    .cloned()
137    .collect();
138
139  let renames = resolver.resolve_columns(table_name, &added, &removed);
140  let renamed_old: Vec<&str> = renames.iter().map(|(o, _)| o.as_str()).collect();
141  let renamed_new: Vec<&str> = renames.iter().map(|(_, n)| n.as_str()).collect();
142
143  for (old_name, new_name) in &renames {
144    ops.push(Operation::RenameColumn {
145      table: table_name.to_owned(),
146      old: old_name.clone(),
147      new: new_name.clone(),
148    });
149  }
150
151  for name in &removed {
152    if !renamed_old.contains(&name.as_str()) {
153      ops.push(Operation::DropColumn {
154        table: table_name.to_owned(),
155        column: name.clone(),
156      });
157    }
158  }
159
160  for name in &added {
161    if !renamed_new.contains(&name.as_str()) {
162      if let Some(col) = new_cols.get(name) {
163        ops.push(Operation::AddColumn {
164          table: table_name.to_owned(),
165          column: col.clone(),
166        });
167      }
168    }
169  }
170
171  for name in new_cols.keys() {
172    let Some(old_col) = old_cols.get(name) else {
173      continue;
174    };
175    let new_col = &new_cols[name];
176    let changes = compute_column_changes(name, old_col, new_col);
177    if !changes.is_empty() {
178      ops.push(Operation::AlterColumn {
179        table: table_name.to_owned(),
180        changes,
181        table_def: new_table_def.clone(),
182      });
183    }
184  }
185}
186
187fn diff_indexes_inner(
188  ops: &mut Vec<Operation>,
189  table_name: &str,
190  old_indexes: &BTreeMap<String, IndexDef>,
191  new_indexes: &BTreeMap<String, IndexDef>,
192) {
193  for (name, old_idx) in old_indexes {
194    match new_indexes.get(name) {
195      None => {
196        ops.push(Operation::DropIndex { name: name.clone() });
197      },
198      Some(new_idx) if new_idx != old_idx => {
199        ops.push(Operation::DropIndex { name: name.clone() });
200        ops.push(Operation::CreateIndex {
201          table: table_name.to_owned(),
202          index: new_idx.clone(),
203        });
204      },
205      _ => {},
206    }
207  }
208  for (name, new_idx) in new_indexes {
209    if !old_indexes.contains_key(name) {
210      ops.push(Operation::CreateIndex {
211        table: table_name.to_owned(),
212        index: new_idx.clone(),
213      });
214    }
215  }
216}