1use crate::SchemaGraph;
4use std::collections::BTreeSet;
5
6#[derive(Debug, Default, PartialEq, Eq)]
12pub struct SchemaDiff {
13 pub added_tables: Vec<String>,
14 pub removed_tables: Vec<String>,
15 pub changed_tables: Vec<TableChange>,
16 pub added_fields: Vec<(String, String)>,
17 pub removed_fields: Vec<(String, String)>,
18 pub changed_fields: Vec<FieldTypeChange>,
19 pub added_indexes: Vec<(String, String)>,
20 pub removed_indexes: Vec<(String, String)>,
21 pub added_events: Vec<(String, String)>,
22 pub removed_events: Vec<(String, String)>,
23 pub added_functions: Vec<String>,
24 pub removed_functions: Vec<String>,
25}
26
27#[derive(Debug, PartialEq, Eq)]
29pub struct TableChange {
30 pub name: String,
31 pub before_full: bool,
32 pub after_full: bool,
33}
34
35#[derive(Debug, PartialEq, Eq)]
37pub struct FieldTypeChange {
38 pub table: String,
39 pub field: String,
40 pub before_type: String,
41 pub after_type: String,
42}
43
44impl SchemaDiff {
45 pub fn is_empty(&self) -> bool {
46 self.added_tables.is_empty()
47 && self.removed_tables.is_empty()
48 && self.changed_tables.is_empty()
49 && self.added_fields.is_empty()
50 && self.removed_fields.is_empty()
51 && self.changed_fields.is_empty()
52 && self.added_indexes.is_empty()
53 && self.removed_indexes.is_empty()
54 && self.added_events.is_empty()
55 && self.removed_events.is_empty()
56 && self.added_functions.is_empty()
57 && self.removed_functions.is_empty()
58 }
59}
60
61impl std::fmt::Display for SchemaDiff {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 if self.is_empty() {
64 return write!(f, "No schema changes.");
65 }
66
67 for t in &self.added_tables {
68 writeln!(f, "+ TABLE {t}")?;
69 }
70 for t in &self.removed_tables {
71 writeln!(f, "- TABLE {t}")?;
72 }
73 for c in &self.changed_tables {
74 let before = if c.before_full {
75 "SCHEMAFULL"
76 } else {
77 "SCHEMALESS"
78 };
79 let after = if c.after_full {
80 "SCHEMAFULL"
81 } else {
82 "SCHEMALESS"
83 };
84 writeln!(f, "~ TABLE {}: {before} -> {after}", c.name)?;
85 }
86 for (table, field) in &self.added_fields {
87 writeln!(f, "+ FIELD {field} ON {table}")?;
88 }
89 for (table, field) in &self.removed_fields {
90 writeln!(f, "- FIELD {field} ON {table}")?;
91 }
92 for change in &self.changed_fields {
93 writeln!(
94 f,
95 "~ FIELD {} ON {}: {} -> {}",
96 change.field, change.table, change.before_type, change.after_type
97 )?;
98 }
99 for (table, idx) in &self.added_indexes {
100 writeln!(f, "+ INDEX {idx} ON {table}")?;
101 }
102 for (table, idx) in &self.removed_indexes {
103 writeln!(f, "- INDEX {idx} ON {table}")?;
104 }
105 for (table, ev) in &self.added_events {
106 writeln!(f, "+ EVENT {ev} ON {table}")?;
107 }
108 for (table, ev) in &self.removed_events {
109 writeln!(f, "- EVENT {ev} ON {table}")?;
110 }
111 for func in &self.added_functions {
112 writeln!(f, "+ FUNCTION fn::{func}")?;
113 }
114 for func in &self.removed_functions {
115 writeln!(f, "- FUNCTION fn::{func}")?;
116 }
117 Ok(())
118 }
119}
120
121pub fn compare_schemas(before: &SchemaGraph, after: &SchemaGraph) -> SchemaDiff {
145 let mut diff = SchemaDiff::default();
146
147 let before_tables: BTreeSet<&str> = before.table_names().collect();
148 let after_tables: BTreeSet<&str> = after.table_names().collect();
149
150 for &name in &after_tables {
151 if !before_tables.contains(name) {
152 diff.added_tables.push(name.to_string());
153 let Some(table) = after.table(name) else {
155 continue;
156 };
157 for field in &table.fields {
158 diff.added_fields
159 .push((name.to_string(), field.name.clone()));
160 }
161 for idx in &table.indexes {
162 diff.added_indexes
163 .push((name.to_string(), idx.name.clone()));
164 }
165 for ev in &table.events {
166 diff.added_events.push((name.to_string(), ev.name.clone()));
167 }
168 }
169 }
170 for &name in &before_tables {
171 if !after_tables.contains(name) {
172 diff.removed_tables.push(name.to_string());
173 let Some(table) = before.table(name) else {
175 continue;
176 };
177 for field in &table.fields {
178 diff.removed_fields
179 .push((name.to_string(), field.name.clone()));
180 }
181 for idx in &table.indexes {
182 diff.removed_indexes
183 .push((name.to_string(), idx.name.clone()));
184 }
185 for ev in &table.events {
186 diff.removed_events
187 .push((name.to_string(), ev.name.clone()));
188 }
189 }
190 }
191
192 let common_tables: BTreeSet<&str> =
193 before_tables.intersection(&after_tables).copied().collect();
194 for &name in &common_tables {
195 let Some(before_table) = before.table(name) else {
197 continue;
198 };
199 let Some(after_table) = after.table(name) else {
200 continue;
201 };
202
203 if before_table.full != after_table.full {
204 diff.changed_tables.push(TableChange {
205 name: name.to_string(),
206 before_full: before_table.full,
207 after_full: after_table.full,
208 });
209 }
210
211 let before_fields: BTreeSet<&str> = before_table
212 .fields
213 .iter()
214 .map(|f| f.name.as_str())
215 .collect();
216 let after_fields: BTreeSet<&str> =
217 after_table.fields.iter().map(|f| f.name.as_str()).collect();
218 for &field in &after_fields {
219 if !before_fields.contains(field) {
220 diff.added_fields
221 .push((name.to_string(), field.to_string()));
222 }
223 }
224 for &field in &before_fields {
225 if !after_fields.contains(field) {
226 diff.removed_fields
227 .push((name.to_string(), field.to_string()));
228 }
229 }
230
231 for before_field in &before_table.fields {
233 if let Some(after_field) = after_table
234 .fields
235 .iter()
236 .find(|f| f.name == before_field.name)
237 && before_field.kind != after_field.kind
238 {
239 diff.changed_fields.push(FieldTypeChange {
240 table: name.to_string(),
241 field: before_field.name.clone(),
242 before_type: before_field.kind.clone().unwrap_or_default(),
243 after_type: after_field.kind.clone().unwrap_or_default(),
244 });
245 }
246 }
247
248 let before_indexes: BTreeSet<&str> = before_table
249 .indexes
250 .iter()
251 .map(|i| i.name.as_str())
252 .collect();
253 let after_indexes: BTreeSet<&str> = after_table
254 .indexes
255 .iter()
256 .map(|i| i.name.as_str())
257 .collect();
258 for &idx in &after_indexes {
259 if !before_indexes.contains(idx) {
260 diff.added_indexes.push((name.to_string(), idx.to_string()));
261 }
262 }
263 for &idx in &before_indexes {
264 if !after_indexes.contains(idx) {
265 diff.removed_indexes
266 .push((name.to_string(), idx.to_string()));
267 }
268 }
269
270 let before_events: BTreeSet<&str> = before_table
271 .events
272 .iter()
273 .map(|e| e.name.as_str())
274 .collect();
275 let after_events: BTreeSet<&str> =
276 after_table.events.iter().map(|e| e.name.as_str()).collect();
277 for &ev in &after_events {
278 if !before_events.contains(ev) {
279 diff.added_events.push((name.to_string(), ev.to_string()));
280 }
281 }
282 for &ev in &before_events {
283 if !after_events.contains(ev) {
284 diff.removed_events.push((name.to_string(), ev.to_string()));
285 }
286 }
287 }
288
289 let before_fns: BTreeSet<&str> = before.function_names().collect();
290 let after_fns: BTreeSet<&str> = after.function_names().collect();
291 for &name in &after_fns {
292 if !before_fns.contains(name) {
293 diff.added_functions.push(name.to_string());
294 }
295 }
296 for &name in &before_fns {
297 if !after_fns.contains(name) {
298 diff.removed_functions.push(name.to_string());
299 }
300 }
301
302 diff
303}