1use super::ddl::{
10 CheckConstraint, Column, ForeignKey, Index, PrimaryKey, SqliteEntity, Table, UniqueConstraint,
11 View,
12};
13use crate::collection::EntityCollection;
14use crate::traits::EntityKind;
15use std::borrow::Cow;
16use std::collections::{HashMap, HashSet};
17
18impl EntityCollection<Table> {
24 #[must_use]
26 pub fn one(&self, name: &str) -> Option<&Table> {
27 self.entities.iter().find(|t| t.name == name)
28 }
29
30 pub fn delete(&mut self, name: &str) -> Option<Table> {
32 if let Some(pos) = self.entities.iter().position(|t| t.name == name) {
33 Some(self.entities.remove(pos))
34 } else {
35 None
36 }
37 }
38}
39
40impl EntityCollection<Column> {
42 #[must_use]
44 pub fn one(&self, table: &str, name: &str) -> Option<&Column> {
45 self.entities
46 .iter()
47 .find(|c| c.table == table && c.name == name)
48 }
49
50 #[must_use]
52 pub fn for_table(&self, table: &str) -> Vec<&Column> {
53 self.entities.iter().filter(|c| c.table == table).collect()
54 }
55
56 pub fn delete(&mut self, table: &str, name: &str) -> Option<Column> {
58 if let Some(pos) = self
59 .entities
60 .iter()
61 .position(|c| c.table == table && c.name == name)
62 {
63 Some(self.entities.remove(pos))
64 } else {
65 None
66 }
67 }
68}
69
70impl EntityCollection<Index> {
72 #[must_use]
74 pub fn one(&self, name: &str) -> Option<&Index> {
75 self.entities.iter().find(|i| i.name == name)
76 }
77
78 #[must_use]
80 pub fn for_table(&self, table: &str) -> Vec<&Index> {
81 self.entities.iter().filter(|i| i.table == table).collect()
82 }
83}
84
85impl EntityCollection<ForeignKey> {
87 #[must_use]
89 pub fn one(&self, name: &str) -> Option<&ForeignKey> {
90 self.entities.iter().find(|f| f.name == name)
91 }
92
93 #[must_use]
95 pub fn for_table(&self, table: &str) -> Vec<&ForeignKey> {
96 self.entities.iter().filter(|f| f.table == table).collect()
97 }
98}
99
100impl EntityCollection<PrimaryKey> {
102 #[must_use]
104 pub fn for_table(&self, table: &str) -> Option<&PrimaryKey> {
105 self.entities.iter().find(|p| p.table == table)
106 }
107}
108
109impl EntityCollection<UniqueConstraint> {
111 #[must_use]
113 pub fn one(&self, name: &str) -> Option<&UniqueConstraint> {
114 self.entities.iter().find(|u| u.name == name)
115 }
116
117 #[must_use]
119 pub fn for_table(&self, table: &str) -> Vec<&UniqueConstraint> {
120 self.entities.iter().filter(|u| u.table == table).collect()
121 }
122}
123
124impl EntityCollection<CheckConstraint> {
126 #[must_use]
128 pub fn one(&self, name: &str) -> Option<&CheckConstraint> {
129 self.entities.iter().find(|c| c.name == name)
130 }
131
132 #[must_use]
134 pub fn for_table(&self, table: &str) -> Vec<&CheckConstraint> {
135 self.entities.iter().filter(|c| c.table == table).collect()
136 }
137}
138
139impl EntityCollection<View> {
141 #[must_use]
143 pub fn one(&self, name: &str) -> Option<&View> {
144 self.entities.iter().find(|v| v.name == name)
145 }
146}
147
148#[derive(Debug, Clone, Default)]
157pub struct SQLiteDDL {
158 pub tables: EntityCollection<Table>,
159 pub columns: EntityCollection<Column>,
160 pub indexes: EntityCollection<Index>,
161 pub fks: EntityCollection<ForeignKey>,
162 pub pks: EntityCollection<PrimaryKey>,
163 pub uniques: EntityCollection<UniqueConstraint>,
164 pub checks: EntityCollection<CheckConstraint>,
165 pub views: EntityCollection<View>,
166}
167
168impl SQLiteDDL {
169 #[must_use]
171 pub fn new() -> Self {
172 Self::default()
173 }
174
175 #[must_use]
177 pub fn from_entities(entities: Vec<SqliteEntity>) -> Self {
178 let mut ddl = Self::new();
179 for entity in entities {
180 ddl.push_entity(entity);
181 }
182 ddl
183 }
184
185 pub fn push_entity(&mut self, entity: SqliteEntity) {
187 match entity {
188 SqliteEntity::Table(t) => self.tables.push(t),
189 SqliteEntity::Column(c) => self.columns.push(c),
190 SqliteEntity::Index(i) => self.indexes.push(i),
191 SqliteEntity::ForeignKey(f) => self.fks.push(f),
192 SqliteEntity::PrimaryKey(p) => self.pks.push(p),
193 SqliteEntity::UniqueConstraint(u) => self.uniques.push(u),
194 SqliteEntity::CheckConstraint(c) => self.checks.push(c),
195 SqliteEntity::View(v) => self.views.push(v),
196 };
197 }
198
199 #[must_use]
201 pub fn to_entities(&self) -> Vec<SqliteEntity> {
202 let mut entities = Vec::new();
203
204 for t in self.tables.list() {
206 entities.push(SqliteEntity::Table(t.clone()));
207 }
208 for c in self.columns.list() {
210 entities.push(SqliteEntity::Column(c.clone()));
211 }
212 for i in self.indexes.list() {
214 entities.push(SqliteEntity::Index(i.clone()));
215 }
216 for f in self.fks.list() {
217 entities.push(SqliteEntity::ForeignKey(f.clone()));
218 }
219 for p in self.pks.list() {
220 entities.push(SqliteEntity::PrimaryKey(p.clone()));
221 }
222 for u in self.uniques.list() {
223 entities.push(SqliteEntity::UniqueConstraint(u.clone()));
224 }
225 for c in self.checks.list() {
226 entities.push(SqliteEntity::CheckConstraint(c.clone()));
227 }
228 for v in self.views.list() {
229 entities.push(SqliteEntity::View(v.clone()));
230 }
231
232 entities
233 }
234
235 #[must_use]
237 pub const fn is_empty(&self) -> bool {
238 self.tables.is_empty()
239 && self.columns.is_empty()
240 && self.indexes.is_empty()
241 && self.fks.is_empty()
242 && self.pks.is_empty()
243 && self.uniques.is_empty()
244 && self.checks.is_empty()
245 && self.views.is_empty()
246 }
247
248 #[must_use]
250 pub fn table_entities<'a>(&'a self, table_name: &str) -> TableEntities<'a> {
251 TableEntities {
252 columns: self.columns.for_table(table_name),
253 indexes: self.indexes.for_table(table_name),
254 fks: self.fks.for_table(table_name),
255 pk: self.pks.for_table(table_name),
256 uniques: self.uniques.for_table(table_name),
257 checks: self.checks.for_table(table_name),
258 }
259 }
260}
261
262pub struct TableEntities<'a> {
264 pub columns: Vec<&'a Column>,
265 pub indexes: Vec<&'a Index>,
266 pub fks: Vec<&'a ForeignKey>,
267 pub pk: Option<&'a PrimaryKey>,
268 pub uniques: Vec<&'a UniqueConstraint>,
269 pub checks: Vec<&'a CheckConstraint>,
270}
271
272pub use crate::traits::DiffType;
278
279#[derive(Debug, Clone)]
281pub struct EntityDiff {
282 pub diff_type: DiffType,
283 pub kind: EntityKind,
284 pub table: Option<String>,
285 pub name: String,
286 pub changes: HashMap<String, (String, String)>,
288 pub left: Option<SqliteEntity>,
290 pub right: Option<SqliteEntity>,
292}
293
294#[must_use]
296pub fn diff_ddl(left: &SQLiteDDL, right: &SQLiteDDL) -> Vec<EntityDiff> {
297 let mut diffs = Vec::new();
298
299 diff_entity_type(
301 left.tables.list(),
302 right.tables.list(),
303 |t| t.name.to_string(),
304 |t| SqliteEntity::Table(t.clone()),
305 None,
306 EntityKind::Table,
307 &mut diffs,
308 );
309
310 let integer_pks: HashSet<(String, String)> = inline_integer_pk_columns(left)
315 .into_iter()
316 .chain(inline_integer_pk_columns(right))
317 .collect();
318 diff_entity_type_with(
319 left.columns.list(),
320 right.columns.list(),
321 |c| format!("{}:{}", c.table, c.name),
322 |c| SqliteEntity::Column(c.clone()),
323 Some(&|c: &Column| c.table.to_string()),
324 EntityKind::Column,
325 |l, r| columns_equivalent(l, r, &integer_pks),
326 &mut diffs,
327 );
328
329 diff_entity_type(
331 left.indexes.list(),
332 right.indexes.list(),
333 |i| i.name.to_string(),
334 |i| SqliteEntity::Index(i.clone()),
335 Some(&|i: &Index| i.table.to_string()),
336 EntityKind::Index,
337 &mut diffs,
338 );
339
340 diff_entity_type_with(
345 left.fks.list(),
346 right.fks.list(),
347 fk_structural_key,
348 |f| SqliteEntity::ForeignKey(f.clone()),
349 Some(&|f: &ForeignKey| f.table.to_string()),
350 EntityKind::ForeignKey,
351 foreign_keys_equivalent,
352 &mut diffs,
353 );
354
355 diff_entity_type_with(
358 left.pks.list(),
359 right.pks.list(),
360 |p| p.table.to_string(),
361 |p| SqliteEntity::PrimaryKey(p.clone()),
362 Some(&|p: &PrimaryKey| p.table.to_string()),
363 EntityKind::PrimaryKey,
364 primary_keys_equivalent,
365 &mut diffs,
366 );
367
368 diff_entity_type(
370 left.uniques.list(),
371 right.uniques.list(),
372 |u| u.name.to_string(),
373 |u| SqliteEntity::UniqueConstraint(u.clone()),
374 Some(&|u: &UniqueConstraint| u.table.to_string()),
375 EntityKind::UniqueConstraint,
376 &mut diffs,
377 );
378
379 diff_entity_type(
381 left.checks.list(),
382 right.checks.list(),
383 |c| c.name.to_string(),
384 |c| SqliteEntity::CheckConstraint(c.clone()),
385 Some(&|c: &CheckConstraint| c.table.to_string()),
386 EntityKind::CheckConstraint,
387 &mut diffs,
388 );
389
390 diff_entity_type(
392 left.views.list(),
393 right.views.list(),
394 |v| v.name.to_string(),
395 |v| SqliteEntity::View(v.clone()),
396 None,
397 EntityKind::View,
398 &mut diffs,
399 );
400
401 diffs
402}
403
404fn diff_entity_type<T: Clone + PartialEq>(
406 left: &[T],
407 right: &[T],
408 key_fn: impl Fn(&T) -> String,
409 to_entity: impl Fn(&T) -> SqliteEntity,
410 table_fn: Option<&dyn Fn(&T) -> String>,
411 kind: EntityKind,
412 diffs: &mut Vec<EntityDiff>,
413) {
414 diff_entity_type_with(
415 left,
416 right,
417 key_fn,
418 to_entity,
419 table_fn,
420 kind,
421 PartialEq::eq,
422 diffs,
423 );
424}
425
426#[allow(clippy::too_many_arguments)]
427fn diff_entity_type_with<T: Clone>(
428 left: &[T],
429 right: &[T],
430 key_fn: impl Fn(&T) -> String,
431 to_entity: impl Fn(&T) -> SqliteEntity,
432 table_fn: Option<&dyn Fn(&T) -> String>,
433 kind: EntityKind,
434 equivalent: impl Fn(&T, &T) -> bool,
435 diffs: &mut Vec<EntityDiff>,
436) {
437 let left_map: HashMap<String, &T> = left.iter().map(|e| (key_fn(e), e)).collect();
438 let right_map: HashMap<String, &T> = right.iter().map(|e| (key_fn(e), e)).collect();
439
440 for left_entity in left {
442 let key = key_fn(left_entity);
443 if !right_map.contains_key(&key) {
444 diffs.push(EntityDiff {
445 diff_type: DiffType::Drop,
446 kind,
447 table: table_fn.map(|f| f(left_entity)),
448 name: key,
449 changes: HashMap::new(),
450 left: Some(to_entity(left_entity)),
451 right: None,
452 });
453 }
454 }
455
456 for right_entity in right {
458 let key = key_fn(right_entity);
459 if !left_map.contains_key(&key) {
460 diffs.push(EntityDiff {
461 diff_type: DiffType::Create,
462 kind,
463 table: table_fn.map(|f| f(right_entity)),
464 name: key,
465 changes: HashMap::new(),
466 left: None,
467 right: Some(to_entity(right_entity)),
468 });
469 }
470 }
471
472 for left_entity in left {
474 let key = key_fn(left_entity);
475 if let Some(right_entity) = right_map.get(&key)
476 && !equivalent(left_entity, right_entity)
477 {
478 diffs.push(EntityDiff {
479 diff_type: DiffType::Alter,
480 kind,
481 table: table_fn.map(|f| f(right_entity)),
482 name: key,
483 changes: HashMap::new(), left: Some(to_entity(left_entity)),
485 right: Some(to_entity(right_entity)),
486 });
487 }
488 }
489}
490
491fn strip_outer_parens(expr: &str) -> &str {
503 let expr = expr.trim();
504 let bytes = expr.as_bytes();
505 if bytes.len() < 2 || bytes[0] != b'(' || bytes[bytes.len() - 1] != b')' {
506 return expr;
507 }
508 let mut depth = 0i32;
509 for (i, ch) in expr.char_indices() {
510 match ch {
511 '(' => depth += 1,
512 ')' => {
513 depth -= 1;
514 if depth == 0 {
515 if i == expr.len() - 1 {
516 return expr[1..expr.len() - 1].trim();
517 }
518 return expr;
519 }
520 }
521 _ => {}
522 }
523 }
524 expr
525}
526
527fn normalize_default_literal(default: &str) -> String {
531 let s = strip_outer_parens(default);
532 let bytes = s.as_bytes();
533 let unquoted = if bytes.len() >= 2 && bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'' {
534 s[1..s.len() - 1].replace("''", "'")
535 } else if bytes.len() >= 2 && bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"' {
536 s[1..s.len() - 1].replace("\"\"", "\"")
537 } else {
538 s.to_string()
539 };
540
541 if let Ok(int) = unquoted.parse::<i128>() {
542 return int.to_string();
543 }
544 if let Ok(float) = unquoted.parse::<f64>()
545 && float.is_finite()
546 {
547 return float.to_string();
548 }
549 unquoted
550}
551
552fn inline_integer_pk_columns(ddl: &SQLiteDDL) -> HashSet<(String, String)> {
555 let mut out = HashSet::new();
556
557 let mut candidates: Vec<(String, String)> = ddl
558 .pks
559 .list()
560 .iter()
561 .filter(|pk| pk.columns.len() == 1)
562 .map(|pk| (pk.table.to_string(), pk.columns[0].to_string()))
563 .collect();
564
565 let mut flag_pks: HashMap<String, Vec<String>> = HashMap::new();
567 for c in ddl.columns.list() {
568 if c.primary_key == Some(true) {
569 flag_pks
570 .entry(c.table.to_string())
571 .or_default()
572 .push(c.name.to_string());
573 }
574 }
575 for (table, cols) in flag_pks {
576 if let [col] = cols.as_slice() {
577 candidates.push((table, col.clone()));
578 }
579 }
580
581 for (table, col) in candidates {
582 let is_integer = ddl
583 .columns
584 .one(&table, &col)
585 .is_some_and(|c| c.sql_type.to_ascii_lowercase().starts_with("int"));
586 if is_integer {
587 out.insert((table, col));
588 }
589 }
590 out
591}
592
593fn columns_equivalent(
594 left: &Column,
595 right: &Column,
596 integer_pks: &HashSet<(String, String)>,
597) -> bool {
598 let normalize = |column: &Column| -> Column {
599 let mut c = column.clone();
600 c.sql_type = Cow::Owned(c.sql_type.to_ascii_lowercase());
601 c.ordinal_position = None;
603 if c.primary_key == Some(false) {
605 c.primary_key = None;
606 }
607 if c.unique == Some(false) {
608 c.unique = None;
609 }
610 if c.autoincrement == Some(false) {
611 c.autoincrement = None;
612 }
613 if integer_pks.contains(&(c.table.to_string(), c.name.to_string())) {
617 c.not_null = true;
618 }
619 if let Some(default) = c.default.as_ref() {
621 c.default = Some(Cow::Owned(normalize_default_literal(default)));
622 }
623 if let Some(generated) = c.generated.as_mut() {
626 generated.expression =
627 Cow::Owned(strip_outer_parens(&generated.expression).to_string());
628 }
629 c
630 };
631
632 normalize(left) == normalize(right)
633}
634
635fn normalize_fk_action(action: &Option<Cow<'static, str>>) -> Option<Cow<'static, str>> {
636 match action.as_deref() {
637 None => None,
638 Some(action) if action.eq_ignore_ascii_case("NO ACTION") => None,
639 Some(action) => Some(Cow::Owned(action.to_ascii_uppercase())),
640 }
641}
642
643fn fk_structural_key(fk: &ForeignKey) -> String {
647 let cols: Vec<&str> = fk.columns.iter().map(AsRef::as_ref).collect();
648 let cols_to: Vec<&str> = fk.columns_to.iter().map(AsRef::as_ref).collect();
649 format!(
650 "{}({})->{}({})",
651 fk.table,
652 cols.join(","),
653 fk.table_to,
654 cols_to.join(",")
655 )
656}
657
658fn foreign_keys_equivalent(left: &ForeignKey, right: &ForeignKey) -> bool {
659 let mut left = left.clone();
660 let mut right = right.clone();
661 left.on_delete = normalize_fk_action(&left.on_delete);
662 left.on_update = normalize_fk_action(&left.on_update);
663 right.on_delete = normalize_fk_action(&right.on_delete);
664 right.on_update = normalize_fk_action(&right.on_update);
665 left.name = Cow::Borrowed("");
667 right.name = Cow::Borrowed("");
668 left.name_explicit = false;
669 right.name_explicit = false;
670 left == right
671}
672
673fn primary_keys_equivalent(left: &PrimaryKey, right: &PrimaryKey) -> bool {
675 if left.table != right.table {
676 return false;
677 }
678 let mut left_cols: Vec<&str> = left.columns.iter().map(AsRef::as_ref).collect();
679 let mut right_cols: Vec<&str> = right.columns.iter().map(AsRef::as_ref).collect();
680 left_cols.sort_unstable();
681 right_cols.sort_unstable();
682 left_cols == right_cols
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688
689 #[test]
690 fn test_ddl_collection_push() {
691 let mut ddl = SQLiteDDL::new();
692
693 ddl.tables.push(Table::new("users"));
694 ddl.columns.push(Column::new("users", "id", "integer"));
695 ddl.columns.push(Column::new("users", "name", "text"));
696
697 assert_eq!(ddl.tables.len(), 1);
698 assert_eq!(ddl.columns.len(), 2);
699 assert_eq!(ddl.columns.for_table("users").len(), 2);
700 }
701
702 #[test]
703 fn test_ddl_to_entities() {
704 let mut ddl = SQLiteDDL::new();
705 ddl.tables.push(Table::new("users"));
706 ddl.columns
707 .push(Column::new("users", "id", "integer").not_null());
708
709 let entities = ddl.to_entities();
710 assert_eq!(entities.len(), 2);
711 }
712
713 #[test]
714 fn test_diff_create() {
715 let left = SQLiteDDL::new();
716 let mut right = SQLiteDDL::new();
717 right.tables.push(Table::new("users"));
718
719 let diffs = diff_ddl(&left, &right);
720 assert_eq!(diffs.len(), 1);
721 assert_eq!(diffs[0].diff_type, DiffType::Create);
722 assert_eq!(diffs[0].kind, EntityKind::Table);
723 }
724
725 #[test]
726 fn test_diff_drop() {
727 let mut left = SQLiteDDL::new();
728 left.tables.push(Table::new("users"));
729 let right = SQLiteDDL::new();
730
731 let diffs = diff_ddl(&left, &right);
732 assert_eq!(diffs.len(), 1);
733 assert_eq!(diffs[0].diff_type, DiffType::Drop);
734 }
735
736 #[test]
737 fn ordinal_position_is_ignored_in_column_equivalence() {
738 let mut introspected = SQLiteDDL::new();
739 introspected.tables.push(Table::new("t"));
740 let mut col = Column::new("t", "name", "text").not_null();
741 col.ordinal_position = Some(3);
742 introspected.columns.push(col);
743
744 let mut snapshot = SQLiteDDL::new();
745 snapshot.tables.push(Table::new("t"));
746 snapshot
747 .columns
748 .push(Column::new("t", "name", "TEXT").not_null());
749
750 let diffs = diff_ddl(&introspected, &snapshot);
751 assert!(diffs.is_empty(), "unexpected diffs: {diffs:#?}");
752 }
753
754 #[test]
755 fn integer_pk_not_null_mismatch_is_reconciled() {
756 use crate::sqlite::ddl::PrimaryKey;
757
758 let mut introspected = SQLiteDDL::new();
760 introspected.tables.push(Table::new("t"));
761 introspected.columns.push(Column::new("t", "id", "integer"));
762 introspected.pks.push(PrimaryKey::from_strings(
763 "t".to_string(),
764 "t_pk".to_string(),
765 vec!["id".to_string()],
766 ));
767
768 let mut snapshot = SQLiteDDL::new();
770 snapshot.tables.push(Table::new("t"));
771 snapshot
772 .columns
773 .push(Column::new("t", "id", "INTEGER").not_null());
774 snapshot.pks.push(PrimaryKey::from_strings(
775 "t".to_string(),
776 "t_pk".to_string(),
777 vec!["id".to_string()],
778 ));
779
780 let diffs = diff_ddl(&introspected, &snapshot);
781 assert!(diffs.is_empty(), "unexpected diffs: {diffs:#?}");
782 }
783
784 #[test]
785 fn default_literal_quoting_is_normalized() {
786 for (left_default, right_default) in [
787 ("'hello'", "hello"),
788 ("\"hello\"", "'hello'"),
789 ("'it''s'", "it's"),
790 ("0.0", "0"),
791 ("(42)", "42"),
792 ] {
793 let mut left = SQLiteDDL::new();
794 left.tables.push(Table::new("t"));
795 left.columns
796 .push(Column::new("t", "c", "text").default_value(left_default.to_string()));
797
798 let mut right = SQLiteDDL::new();
799 right.tables.push(Table::new("t"));
800 right
801 .columns
802 .push(Column::new("t", "c", "text").default_value(right_default.to_string()));
803
804 let diffs = diff_ddl(&left, &right);
805 assert!(
806 diffs.is_empty(),
807 "{left_default:?} vs {right_default:?} should be equivalent: {diffs:#?}"
808 );
809 }
810
811 let mut left = SQLiteDDL::new();
813 left.tables.push(Table::new("t"));
814 left.columns
815 .push(Column::new("t", "c", "text").default_value("'a'"));
816 let mut right = SQLiteDDL::new();
817 right.tables.push(Table::new("t"));
818 right
819 .columns
820 .push(Column::new("t", "c", "text").default_value("'b'"));
821 assert_eq!(diff_ddl(&left, &right).len(), 1);
822 }
823
824 #[test]
825 fn generated_expression_parens_are_normalized() {
826 use crate::sqlite::ddl::{Generated, GeneratedType};
827
828 let make = |expr: &str| {
829 let mut ddl = SQLiteDDL::new();
830 ddl.tables.push(Table::new("t"));
831 let mut col = Column::new("t", "g", "text");
832 col.generated = Some(Generated {
833 expression: expr.to_string().into(),
834 gen_type: GeneratedType::Virtual,
835 });
836 ddl.columns.push(col);
837 ddl
838 };
839
840 let diffs = diff_ddl(&make("(length(name))"), &make("length(name)"));
842 assert!(diffs.is_empty(), "unexpected diffs: {diffs:#?}");
843
844 assert_eq!(
846 diff_ddl(&make("(length(name))"), &make("length(other)")).len(),
847 1
848 );
849 }
850
851 #[test]
852 fn primary_keys_compare_by_column_set_not_name() {
853 use crate::sqlite::ddl::PrimaryKey;
854
855 let make = |name: &str, cols: Vec<&str>| {
856 let mut ddl = SQLiteDDL::new();
857 ddl.tables.push(Table::new("t"));
858 ddl.columns
859 .push(Column::new("t", "a", "integer").not_null());
860 ddl.columns
861 .push(Column::new("t", "b", "integer").not_null());
862 ddl.pks.push(PrimaryKey::from_strings(
863 "t".to_string(),
864 name.to_string(),
865 cols.into_iter().map(str::to_string).collect(),
866 ));
867 ddl
868 };
869
870 let diffs = diff_ddl(
872 &make("t_pk", vec!["a", "b"]),
873 &make("custom", vec!["b", "a"]),
874 );
875 assert!(diffs.is_empty(), "unexpected diffs: {diffs:#?}");
876
877 assert_eq!(
879 diff_ddl(&make("t_pk", vec!["a", "b"]), &make("t_pk", vec!["a"])).len(),
880 1
881 );
882 }
883
884 #[test]
885 fn foreign_keys_compare_structurally_not_by_name() {
886 let make = |name: &str| {
887 let mut ddl = SQLiteDDL::new();
888 ddl.tables.push(Table::new("child"));
889 ddl.tables.push(Table::new("parent"));
890 ddl.columns
891 .push(Column::new("child", "parent_id", "integer").not_null());
892 ddl.fks.push(ForeignKey::from_strings(
893 "child".to_string(),
894 name.to_string(),
895 vec!["parent_id".to_string()],
896 "parent".to_string(),
897 vec!["id".to_string()],
898 ));
899 ddl
900 };
901
902 let diffs = diff_ddl(&make("fk_child_parent_id_parent_id_fk"), &make("my_fk"));
906 assert!(diffs.is_empty(), "unexpected diffs: {diffs:#?}");
907
908 let mut with_cascade = make("a");
910 with_cascade.fks.list_mut()[0].on_delete = Some("CASCADE".into());
911 let diffs = diff_ddl(&make("b"), &with_cascade);
912 assert_eq!(diffs.len(), 1);
913 assert_eq!(diffs[0].diff_type, DiffType::Alter);
914 }
915
916 #[test]
917 fn introspected_types_and_no_action_fks_match_macro_snapshots() {
918 let mut introspected = SQLiteDDL::new();
919 introspected.tables.push(Table::new("child"));
920 introspected.tables.push(Table::new("parent"));
921 introspected
922 .columns
923 .push(Column::new("child", "id", "integer").not_null());
924 introspected
925 .columns
926 .push(Column::new("child", "parent_id", "integer").not_null());
927 introspected.fks.push(
928 ForeignKey::from_strings(
929 "child".to_string(),
930 "child_parent_id_fk".to_string(),
931 vec!["parent_id".to_string()],
932 "parent".to_string(),
933 vec!["id".to_string()],
934 )
935 .on_delete("NO ACTION")
936 .on_update("no action"),
937 );
938
939 let mut macro_snapshot = SQLiteDDL::new();
940 macro_snapshot.tables.push(Table::new("child"));
941 macro_snapshot.tables.push(Table::new("parent"));
942 macro_snapshot
943 .columns
944 .push(Column::new("child", "id", "INTEGER").not_null());
945 macro_snapshot
946 .columns
947 .push(Column::new("child", "parent_id", "INTEGER").not_null());
948 macro_snapshot.fks.push(ForeignKey::from_strings(
949 "child".to_string(),
950 "child_parent_id_fk".to_string(),
951 vec!["parent_id".to_string()],
952 "parent".to_string(),
953 vec!["id".to_string()],
954 ));
955
956 let diffs = diff_ddl(&introspected, ¯o_snapshot);
957 assert!(diffs.is_empty(), "unexpected diffs: {diffs:#?}");
958 }
959}