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