1use std::collections::BTreeMap;
29
30use crate::schema::{FieldDef, FieldKind, ForeignKey, Schema, TableDef};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
34#[serde(rename_all = "lowercase")]
35#[non_exhaustive]
39pub enum Step {
40 Expand,
42 Contract,
45 Rewrite,
47}
48
49macro_rules! steps {
58 ($($variant:ident),+ $(,)?) => {
59 impl Step {
60 pub const ALL: &'static [Step] = &[$(Step::$variant),+];
62 }
63
64 #[expect(dead_code, reason = "its only job is to be exhaustive")]
67 fn every_step_is_in_all(step: Step) {
68 match step {
69 $(Step::$variant => {}),+
70 }
71 }
72 };
73}
74
75steps!(Expand, Contract, Rewrite);
76
77impl Step {
78 #[must_use]
80 pub fn as_str(self) -> &'static str {
81 match self {
82 Self::Expand => "expand",
83 Self::Contract => "contract",
84 Self::Rewrite => "rewrite",
85 }
86 }
87}
88
89impl std::fmt::Display for Step {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.write_str(self.as_str())
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
97pub struct Change {
98 pub table: String,
100 pub field: Option<String>,
102 pub step: Step,
103 pub detail: String,
106}
107
108impl Change {
109 fn table(table: &str, step: Step, detail: impl Into<String>) -> Self {
110 Self {
111 table: table.to_owned(),
112 field: None,
113 step,
114 detail: detail.into(),
115 }
116 }
117
118 fn field(table: &str, field: &str, step: Step, detail: impl Into<String>) -> Self {
119 Self {
120 table: table.to_owned(),
121 field: Some(field.to_owned()),
122 step,
123 detail: detail.into(),
124 }
125 }
126
127 #[must_use]
129 pub fn line(&self) -> String {
130 match &self.field {
131 Some(field) => format!("{}.{field}: {}", self.table, self.detail),
132 None => format!("{}: {}", self.table, self.detail),
133 }
134 }
135}
136
137#[must_use]
143pub fn diff(previous: &Schema, current: &Schema) -> Vec<Change> {
144 let before: BTreeMap<&str, &TableDef> = previous
145 .tables
146 .iter()
147 .map(|table| (table.name.as_str(), table))
148 .collect();
149 let after: BTreeMap<&str, &TableDef> = current
150 .tables
151 .iter()
152 .map(|table| (table.name.as_str(), table))
153 .collect();
154
155 let mut changes = Vec::new();
156 for (name, table) in &after {
157 match before.get(name) {
158 None => changes.push(Change::table(
159 name,
160 Step::Expand,
161 format!("new table with {} fields", table.fields.len()),
162 )),
163 Some(old) => table_changes(old, table, &mut changes),
164 }
165 }
166 for name in before.keys() {
167 if !after.contains_key(name) {
168 changes.push(Change::table(
169 name,
170 Step::Contract,
171 "table is gone; every row in it goes with it",
172 ));
173 }
174 }
175 changes.sort_by(|a, b| (&a.table, &a.field).cmp(&(&b.table, &b.field)));
176 changes
177}
178
179fn table_changes(old: &TableDef, new: &TableDef, out: &mut Vec<Change>) {
180 if old.primary_key != new.primary_key {
181 out.push(Change::table(
182 &new.name,
183 Step::Rewrite,
184 format!(
185 "primary key changes from ({}) to ({}); no engine alters one in place",
186 old.primary_key.join(", "),
187 new.primary_key.join(", ")
188 ),
189 ));
190 }
191 foreign_key_changes(old, new, out);
192
193 let before: BTreeMap<&str, &FieldDef> = old
194 .fields
195 .iter()
196 .map(|field| (field.name.as_str(), field))
197 .collect();
198 for field in &new.fields {
199 match before.get(field.name.as_str()) {
200 None => out.push(added_field(&new.name, field)),
201 Some(old_field) => field_changes(&new.name, old_field, field, out),
202 }
203 }
204 for field in &old.fields {
205 if !new.fields.iter().any(|kept| kept.name == field.name) {
206 out.push(Change::field(
207 &new.name,
208 &field.name,
209 Step::Contract,
210 "column is gone; the values in it go with it",
211 ));
212 }
213 }
214}
215
216fn added_field(table: &str, field: &FieldDef) -> Change {
220 if field.required && field.default.is_none() {
221 return Change::field(
222 table,
223 &field.name,
224 Step::Rewrite,
225 "new required column with no default; every existing row would need a value",
226 );
227 }
228 if field.unique {
229 return Change::field(
230 table,
231 &field.name,
232 Step::Rewrite,
233 "new unique column; existing rows would all take the same value and collide",
234 );
235 }
236 Change::field(table, &field.name, Step::Expand, "new optional column")
237}
238
239fn field_changes(table: &str, old: &FieldDef, new: &FieldDef, out: &mut Vec<Change>) {
240 if std::mem::discriminant(&old.kind) == std::mem::discriminant(&new.kind) {
241 kind_changes(table, &old.kind, &new.kind, &new.name, out);
242 } else {
243 out.push(Change::field(
244 table,
245 &new.name,
246 Step::Rewrite,
247 format!(
248 "type changes from {} to {}; the values already stored are the old type",
249 old.kind.as_str(),
250 new.kind.as_str()
251 ),
252 ));
253 }
254
255 match (old.required, new.required) {
256 (false, true) => out.push(Change::field(
257 table,
258 &new.name,
259 Step::Rewrite,
260 "column becomes required; rows that left it empty have nothing to put there",
261 )),
262 (true, false) => out.push(Change::field(
263 table,
264 &new.name,
265 Step::Expand,
266 "column becomes optional",
267 )),
268 _ => {}
269 }
270
271 match (old.unique, new.unique) {
272 (false, true) => out.push(Change::field(
273 table,
274 &new.name,
275 Step::Rewrite,
276 "column becomes unique; the rows already there may hold duplicates",
277 )),
278 (true, false) => out.push(Change::field(
279 table,
280 &new.name,
281 Step::Expand,
282 "column stops being unique",
283 )),
284 _ => {}
285 }
286
287 if old.indexed != new.indexed {
288 let detail = if new.indexed {
291 "indexed"
292 } else {
293 "no longer indexed"
294 };
295 out.push(Change::field(table, &new.name, Step::Expand, detail));
296 }
297
298 if old.default != new.default {
299 out.push(Change::field(
303 table,
304 &new.name,
305 Step::Expand,
306 "default changes; rows already written keep the value they have",
307 ));
308 }
309}
310
311fn kind_changes(table: &str, old: &FieldKind, new: &FieldKind, field: &str, out: &mut Vec<Change>) {
314 match (old, new) {
315 (
316 FieldKind::Text {
317 min_len: old_min,
318 max_len: old_max,
319 format: old_format,
320 },
321 FieldKind::Text {
322 min_len: new_min,
323 max_len: new_max,
324 format: new_format,
325 },
326 ) => {
327 bounds(
328 table, field, *old_min, *new_min, *old_max, *new_max, "length ", out,
329 );
330 if old_format != new_format {
331 let (step, detail) = if new_format.is_some() {
334 (
335 Step::Rewrite,
336 "a format is required now; stored values were not checked against it",
337 )
338 } else {
339 (Step::Expand, "the format requirement is gone")
340 };
341 out.push(Change::field(table, field, step, detail));
342 }
343 }
344 (
345 FieldKind::Integer {
346 min: old_min,
347 max: old_max,
348 },
349 FieldKind::Integer {
350 min: new_min,
351 max: new_max,
352 },
353 ) => bounds(
354 table, field, *old_min, *new_min, *old_max, *new_max, "", out,
355 ),
356 (
357 FieldKind::Real {
358 min: old_min,
359 max: old_max,
360 },
361 FieldKind::Real {
362 min: new_min,
363 max: new_max,
364 },
365 ) => bounds(
366 table, field, *old_min, *new_min, *old_max, *new_max, "", out,
367 ),
368 (FieldKind::Enum { values: old_values }, FieldKind::Enum { values: new_values }) => {
369 enum_changes(table, field, old_values, new_values, out);
370 }
371 _ => {}
373 }
374}
375
376#[allow(clippy::too_many_arguments)]
380fn bounds<T: PartialOrd + Copy>(
381 table: &str,
382 field: &str,
383 old_min: Option<T>,
384 new_min: Option<T>,
385 old_max: Option<T>,
386 new_max: Option<T>,
387 what: &str,
388 out: &mut Vec<Change>,
389) {
390 if narrows(old_min, new_min, Bound::Lower) || narrows(old_max, new_max, Bound::Upper) {
391 out.push(Change::field(
392 table,
393 field,
394 Step::Rewrite,
395 format!("{what}bounds narrow; stored values may already be outside them"),
396 ));
397 } else if !same(old_min, new_min) || !same(old_max, new_max) {
398 out.push(Change::field(
399 table,
400 field,
401 Step::Expand,
402 format!("{what}bounds widen"),
403 ));
404 }
405}
406
407fn same<T: PartialOrd>(a: Option<T>, b: Option<T>) -> bool {
410 match (a, b) {
411 (None, None) => true,
412 (Some(a), Some(b)) => a == b,
413 _ => false,
414 }
415}
416
417fn enum_changes(
418 table: &str,
419 field: &str,
420 old_values: &[String],
421 new_values: &[String],
422 out: &mut Vec<Change>,
423) {
424 let removed: Vec<&String> = old_values
425 .iter()
426 .filter(|value| !new_values.contains(value))
427 .collect();
428 if !removed.is_empty() {
429 out.push(Change::field(
430 table,
431 field,
432 Step::Rewrite,
433 format!(
434 "enum drops {}; rows already holding one would fail the new CHECK",
435 removed
436 .iter()
437 .map(|value| format!("`{value}`"))
438 .collect::<Vec<_>>()
439 .join(", ")
440 ),
441 ));
442 }
443 let added = new_values
444 .iter()
445 .filter(|value| !old_values.contains(value))
446 .count();
447 if added > 0 {
448 out.push(Change::field(
449 table,
450 field,
451 Step::Expand,
452 format!("enum gains {added} value(s)"),
453 ));
454 }
455}
456
457#[derive(Clone, Copy)]
458enum Bound {
459 Lower,
460 Upper,
461}
462
463fn narrows<T: PartialOrd>(old: Option<T>, new: Option<T>, which: Bound) -> bool {
466 match (old, new) {
467 (_, None) => false,
468 (None, Some(_)) => true,
469 (Some(old), Some(new)) => match which {
470 Bound::Lower => new > old,
471 Bound::Upper => new < old,
472 },
473 }
474}
475
476fn foreign_key_changes(old: &TableDef, new: &TableDef, out: &mut Vec<Change>) {
477 let same = |a: &ForeignKey, b: &ForeignKey| a.field == b.field && a.references == b.references;
478 for key in &new.foreign_keys {
479 if !old.foreign_keys.iter().any(|had| same(had, key)) {
480 out.push(Change::field(
481 &new.name,
482 &key.field,
483 Step::Rewrite,
484 format!(
485 "new reference to `{}`; rows already there may point at nothing",
486 key.references
487 ),
488 ));
489 }
490 }
491 for key in &old.foreign_keys {
492 if !new.foreign_keys.iter().any(|kept| same(kept, key)) {
493 out.push(Change::field(
494 &new.name,
495 &key.field,
496 Step::Expand,
497 format!("reference to `{}` is gone", key.references),
498 ));
499 }
500 }
501}