toolu_orm_core/diff/
engine.rs1use 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}