1use std::collections::BTreeSet;
8
9use crate::schema::{RustTypeMapping, TableSchema};
10
11use super::column_builder::ColumnDefinition;
12use super::dialect::MigrationDialect;
13use super::operation::{
14 AddColumnOp, AddForeignKeyOp, AlterColumnChange, AlterColumnOp, CreateIndexOp, CreateTableOp,
15 DropColumnOp, DropForeignKeyOp, DropIndexOp, DropTableOp, Operation,
16};
17use super::snapshot::{
18 ColumnSnapshot, ForeignKeySnapshot, IndexSnapshot, SchemaSnapshot, TableSnapshot,
19};
20
21const RENAME_SIMILARITY_THRESHOLD: f64 = 0.4;
24
25fn levenshtein(a: &str, b: &str) -> usize {
31 let a: Vec<char> = a.chars().collect();
32 let b: Vec<char> = b.chars().collect();
33 let m = a.len();
34 let n = b.len();
35 let mut prev = (0..=n).collect::<Vec<_>>();
36 let mut curr = vec![0; n + 1];
37 for i in 1..=m {
38 curr[0] = i;
39 for j in 1..=n {
40 let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
41 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
42 }
43 std::mem::swap(&mut prev, &mut curr);
44 }
45 prev[n]
46}
47
48fn similarity(a: &str, b: &str) -> f64 {
51 let max_len = a.len().max(b.len());
52 if max_len == 0 {
53 return 1.0;
54 }
55 1.0 - (levenshtein(a, b) as f64 / max_len as f64)
56}
57
58#[derive(Debug, Clone, PartialEq)]
65pub enum AmbiguousChange {
66 PossibleRename {
69 table: String,
71 old_column: String,
73 new_column: String,
75 similarity: f64,
77 },
78 PossibleTableRename {
81 old_table: String,
83 new_table: String,
85 similarity: f64,
87 },
88}
89
90#[derive(Debug, Clone, PartialEq)]
94pub enum DiffWarning {
95 PrimaryKeyChange {
98 table: String,
100 column: String,
102 new_value: bool,
104 },
105 AutoincrementChange {
108 table: String,
110 column: String,
112 new_value: bool,
114 },
115 ColumnOrderChanged {
118 table: String,
120 old_order: Vec<String>,
122 new_order: Vec<String>,
124 },
125}
126
127#[derive(Debug, Clone, PartialEq)]
129pub struct SchemaDiff {
130 pub operations: Vec<Operation>,
132 pub ambiguous: Vec<AmbiguousChange>,
134 pub warnings: Vec<DiffWarning>,
137}
138
139impl SchemaDiff {
140 #[must_use]
143 pub fn is_empty(&self) -> bool {
144 self.operations.is_empty() && self.ambiguous.is_empty() && self.warnings.is_empty()
145 }
146
147 #[must_use]
150 pub fn to_sql(&self, dialect: &impl MigrationDialect) -> Vec<String> {
151 self.operations
152 .iter()
153 .map(|op| dialect.generate_sql(op))
154 .collect()
155 }
156
157 #[must_use]
160 pub fn reverse(&self) -> Option<Self> {
161 let mut reversed = Vec::new();
162 for op in self.operations.iter().rev() {
163 reversed.push(op.reverse()?);
164 }
165 Some(Self {
166 operations: reversed,
167 ambiguous: vec![],
168 warnings: vec![],
169 })
170 }
171
172 #[must_use]
174 pub fn is_reversible(&self) -> bool {
175 self.operations.iter().all(Operation::is_reversible)
176 }
177
178 #[must_use]
180 pub fn non_reversible_operations(&self) -> Vec<&Operation> {
181 self.operations
182 .iter()
183 .filter(|op| !op.is_reversible())
184 .collect()
185 }
186}
187
188fn diff_table(table_name: &str, old: &TableSnapshot, new: &TableSnapshot) -> SchemaDiff {
195 let old_names: BTreeSet<&str> = old.columns.iter().map(|c| c.name.as_str()).collect();
196 let new_names: BTreeSet<&str> = new.columns.iter().map(|c| c.name.as_str()).collect();
197
198 let dropped: Vec<&str> = old_names.difference(&new_names).copied().collect();
199 let added: Vec<&str> = new_names.difference(&old_names).copied().collect();
200 let common: BTreeSet<&str> = old_names.intersection(&new_names).copied().collect();
201
202 let mut operations = Vec::new();
203 let mut ambiguous = Vec::new();
204 let mut warnings = Vec::new();
205
206 let mut rename_dropped: BTreeSet<&str> = BTreeSet::new();
208 let mut rename_added: BTreeSet<&str> = BTreeSet::new();
209
210 let mut candidates: Vec<(&str, &str, f64)> = Vec::new();
212 for &d in &dropped {
213 let old_col = old.column(d).unwrap();
214 for &a in &added {
215 let new_col = new.column(a).unwrap();
216 if old_col.data_type == new_col.data_type {
217 let sim = similarity(d, a);
218 if sim >= RENAME_SIMILARITY_THRESHOLD {
219 candidates.push((d, a, sim));
220 }
221 }
222 }
223 }
224 candidates.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());
226 for (d, a, sim) in &candidates {
227 if rename_dropped.contains(d) || rename_added.contains(a) {
228 continue;
229 }
230 ambiguous.push(AmbiguousChange::PossibleRename {
231 table: table_name.to_string(),
232 old_column: d.to_string(),
233 new_column: a.to_string(),
234 similarity: *sim,
235 });
236 rename_dropped.insert(d);
237 rename_added.insert(a);
238 }
239
240 for &name in &added {
242 if rename_added.contains(name) {
243 continue;
244 }
245 let col = new.column(name).unwrap();
246 operations.push(Operation::AddColumn(AddColumnOp {
247 table: table_name.to_string(),
248 column: snapshot_to_column_def(col),
249 }));
250 }
251
252 for &name in &common {
254 let old_col = old.column(name).unwrap();
255 let new_col = new.column(name).unwrap();
256
257 if old_col.data_type != new_col.data_type {
258 operations.push(Operation::AlterColumn(AlterColumnOp {
259 table: table_name.to_string(),
260 column: name.to_string(),
261 change: AlterColumnChange::SetDataType(new_col.data_type.clone()),
262 }));
263 }
264
265 if old_col.nullable != new_col.nullable {
266 operations.push(Operation::AlterColumn(AlterColumnOp {
267 table: table_name.to_string(),
268 column: name.to_string(),
269 change: AlterColumnChange::SetNullable(new_col.nullable),
270 }));
271 }
272
273 if old_col.unique != new_col.unique {
274 operations.push(Operation::AlterColumn(AlterColumnOp {
275 table: table_name.to_string(),
276 column: name.to_string(),
277 change: AlterColumnChange::SetUnique(new_col.unique),
278 }));
279 }
280
281 if old_col.primary_key != new_col.primary_key {
282 warnings.push(DiffWarning::PrimaryKeyChange {
283 table: table_name.to_string(),
284 column: name.to_string(),
285 new_value: new_col.primary_key,
286 });
287 }
288
289 if old_col.autoincrement != new_col.autoincrement {
290 warnings.push(DiffWarning::AutoincrementChange {
291 table: table_name.to_string(),
292 column: name.to_string(),
293 new_value: new_col.autoincrement,
294 });
295 }
296
297 match (&old_col.default, &new_col.default) {
298 (None, Some(new_default)) => {
299 operations.push(Operation::AlterColumn(AlterColumnOp {
300 table: table_name.to_string(),
301 column: name.to_string(),
302 change: AlterColumnChange::SetDefault(new_default.clone()),
303 }));
304 }
305 (Some(_), None) => {
306 operations.push(Operation::AlterColumn(AlterColumnOp {
307 table: table_name.to_string(),
308 column: name.to_string(),
309 change: AlterColumnChange::DropDefault,
310 }));
311 }
312 (Some(old_def), Some(new_def)) if old_def != new_def => {
313 operations.push(Operation::AlterColumn(AlterColumnOp {
314 table: table_name.to_string(),
315 column: name.to_string(),
316 change: AlterColumnChange::SetDefault(new_def.clone()),
317 }));
318 }
319 _ => {}
320 }
321 }
322
323 for &name in &dropped {
325 if rename_dropped.contains(name) {
326 continue;
327 }
328 operations.push(Operation::DropColumn(DropColumnOp {
329 table: table_name.to_string(),
330 column: name.to_string(),
331 }));
332 }
333
334 diff_indexes(table_name, old, new, &mut operations);
336
337 diff_foreign_keys(table_name, old, new, &mut operations);
339
340 detect_column_order_change(table_name, old, new, &common, &mut warnings);
342
343 SchemaDiff {
344 operations,
345 ambiguous,
346 warnings,
347 }
348}
349
350fn indexes_equivalent(a: &IndexSnapshot, b: &IndexSnapshot) -> bool {
358 a.columns == b.columns
359 && a.unique == b.unique
360 && a.index_type == b.index_type
361 && a.condition == b.condition
362}
363
364fn diff_indexes(
366 table_name: &str,
367 old: &TableSnapshot,
368 new: &TableSnapshot,
369 operations: &mut Vec<Operation>,
370) {
371 for old_idx in &old.indexes {
373 let still_exists = new.indexes.iter().any(|n| indexes_equivalent(old_idx, n));
374 if !still_exists {
375 operations.push(Operation::DropIndex(DropIndexOp {
376 name: old_idx.name.clone(),
377 table: Some(table_name.to_string()),
378 if_exists: false,
379 }));
380 }
381 }
382 for new_idx in &new.indexes {
384 let already_exists = old.indexes.iter().any(|o| indexes_equivalent(o, new_idx));
385 if !already_exists {
386 operations.push(Operation::CreateIndex(CreateIndexOp {
387 name: new_idx.name.clone(),
388 table: table_name.to_string(),
389 columns: new_idx.columns.clone(),
390 unique: new_idx.unique,
391 index_type: new_idx.index_type,
392 if_not_exists: false,
393 condition: new_idx.condition.clone(),
394 }));
395 }
396 }
397}
398
399fn fks_equivalent(a: &ForeignKeySnapshot, b: &ForeignKeySnapshot) -> bool {
402 a.columns == b.columns
403 && a.references_table == b.references_table
404 && a.references_columns == b.references_columns
405 && a.on_delete == b.on_delete
406 && a.on_update == b.on_update
407}
408
409fn diff_foreign_keys(
411 table_name: &str,
412 old: &TableSnapshot,
413 new: &TableSnapshot,
414 operations: &mut Vec<Operation>,
415) {
416 for old_fk in &old.foreign_keys {
418 let still_exists = new.foreign_keys.iter().any(|n| fks_equivalent(old_fk, n));
419 if !still_exists {
420 if let Some(ref name) = old_fk.name {
421 operations.push(Operation::DropForeignKey(DropForeignKeyOp {
422 table: table_name.to_string(),
423 name: name.clone(),
424 }));
425 }
426 }
427 }
428 for new_fk in &new.foreign_keys {
430 let already_exists = old.foreign_keys.iter().any(|o| fks_equivalent(o, new_fk));
431 if !already_exists {
432 operations.push(Operation::AddForeignKey(AddForeignKeyOp {
433 table: table_name.to_string(),
434 name: new_fk.name.clone(),
435 columns: new_fk.columns.clone(),
436 references_table: new_fk.references_table.clone(),
437 references_columns: new_fk.references_columns.clone(),
438 on_delete: new_fk.on_delete,
439 on_update: new_fk.on_update,
440 }));
441 }
442 }
443}
444
445fn detect_column_order_change(
452 table_name: &str,
453 old: &TableSnapshot,
454 new: &TableSnapshot,
455 common: &BTreeSet<&str>,
456 warnings: &mut Vec<DiffWarning>,
457) {
458 let old_order: Vec<String> = old
459 .columns
460 .iter()
461 .filter(|c| common.contains(c.name.as_str()))
462 .map(|c| c.name.clone())
463 .collect();
464 let new_order: Vec<String> = new
465 .columns
466 .iter()
467 .filter(|c| common.contains(c.name.as_str()))
468 .map(|c| c.name.clone())
469 .collect();
470
471 if old_order != new_order {
472 warnings.push(DiffWarning::ColumnOrderChanged {
473 table: table_name.to_string(),
474 old_order,
475 new_order,
476 });
477 }
478}
479
480fn snapshot_to_column_def(col: &ColumnSnapshot) -> ColumnDefinition {
487 ColumnDefinition {
488 name: col.name.clone(),
489 data_type: col.data_type.clone(),
490 nullable: col.nullable,
491 default: col.default.clone(),
492 primary_key: col.primary_key,
493 unique: col.unique,
494 autoincrement: col.autoincrement,
495 references: None,
496 check: None,
497 collation: None,
498 }
499}
500
501pub fn auto_diff_schema(current: &SchemaSnapshot, desired: &SchemaSnapshot) -> SchemaDiff {
511 let current_tables: BTreeSet<&str> = current.tables.keys().map(String::as_str).collect();
512 let desired_tables: BTreeSet<&str> = desired.tables.keys().map(String::as_str).collect();
513
514 let dropped_tables: Vec<&str> = current_tables
515 .difference(&desired_tables)
516 .copied()
517 .collect();
518 let added_tables: Vec<&str> = desired_tables
519 .difference(¤t_tables)
520 .copied()
521 .collect();
522 let common_tables: Vec<&str> = current_tables
523 .intersection(&desired_tables)
524 .copied()
525 .collect();
526
527 let mut create_ops = Vec::new();
528 let mut add_ops = Vec::new();
529 let mut alter_ops = Vec::new();
530 let mut drop_col_ops = Vec::new();
531 let mut drop_table_ops = Vec::new();
532 let mut ambiguous = Vec::new();
533 let mut warnings = Vec::new();
534
535 let mut rename_dropped: BTreeSet<&str> = BTreeSet::new();
537 let mut rename_added: BTreeSet<&str> = BTreeSet::new();
538
539 let mut candidates: Vec<(&str, &str, f64)> = Vec::new();
541 for &d in &dropped_tables {
542 let old_table = ¤t.tables[d];
543 for &a in &added_tables {
544 let new_table = &desired.tables[a];
545 if tables_have_same_columns(old_table, new_table) {
546 let sim = similarity(d, a);
547 candidates.push((d, a, sim));
548 }
549 }
550 }
551 candidates.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());
552 for (d, a, sim) in &candidates {
553 if rename_dropped.contains(d) || rename_added.contains(a) {
554 continue;
555 }
556 ambiguous.push(AmbiguousChange::PossibleTableRename {
557 old_table: d.to_string(),
558 new_table: a.to_string(),
559 similarity: *sim,
560 });
561 rename_dropped.insert(d);
562 rename_added.insert(a);
563 }
564
565 for &name in &added_tables {
567 if rename_added.contains(name) {
568 continue;
569 }
570 let table = &desired.tables[name];
571 let columns = table.columns.iter().map(snapshot_to_column_def).collect();
572 create_ops.push(Operation::CreateTable(CreateTableOp {
573 name: name.to_string(),
574 columns,
575 constraints: vec![],
576 if_not_exists: false,
577 }));
578 }
579
580 for &name in &common_tables {
582 let old_table = ¤t.tables[name];
583 let new_table = &desired.tables[name];
584 let table_diff = diff_table(name, old_table, new_table);
585
586 for op in table_diff.operations {
587 match &op {
588 Operation::AddColumn(_) => add_ops.push(op),
589 Operation::AlterColumn(_) => {
590 alter_ops.push(op);
591 }
592 Operation::DropColumn(_) => {
593 drop_col_ops.push(op);
594 }
595 _ => add_ops.push(op),
596 }
597 }
598 ambiguous.extend(table_diff.ambiguous);
599 warnings.extend(table_diff.warnings);
600 }
601
602 for &name in &dropped_tables {
604 if rename_dropped.contains(name) {
605 continue;
606 }
607 drop_table_ops.push(Operation::DropTable(DropTableOp {
608 name: name.to_string(),
609 if_exists: false,
610 cascade: false,
611 }));
612 }
613
614 let mut operations = Vec::new();
616 operations.extend(create_ops);
617 operations.extend(add_ops);
618 operations.extend(alter_ops);
619 operations.extend(drop_col_ops);
620 operations.extend(drop_table_ops);
621
622 SchemaDiff {
623 operations,
624 ambiguous,
625 warnings,
626 }
627}
628
629pub fn auto_diff_table<T: TableSchema>(
632 current: &TableSnapshot,
633 dialect: &impl RustTypeMapping,
634) -> SchemaDiff {
635 let desired = TableSnapshot::from_table_schema::<T>(dialect);
636 diff_table(&desired.name, current, &desired)
637}
638
639fn tables_have_same_columns(a: &TableSnapshot, b: &TableSnapshot) -> bool {
642 if a.columns.len() != b.columns.len() {
643 return false;
644 }
645 a.columns.iter().zip(b.columns.iter()).all(|(ac, bc)| {
646 ac.name == bc.name
647 && ac.data_type == bc.data_type
648 && ac.nullable == bc.nullable
649 && ac.primary_key == bc.primary_key
650 && ac.unique == bc.unique
651 && ac.autoincrement == bc.autoincrement
652 && ac.default == bc.default
653 })
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659 use crate::ast::DataType;
660 use crate::migrations::column_builder::{DefaultValue, ForeignKeyAction};
661 use crate::migrations::operation::IndexType;
662
663 fn col(name: &str, data_type: DataType, nullable: bool) -> ColumnSnapshot {
668 ColumnSnapshot {
669 name: name.to_string(),
670 data_type,
671 nullable,
672 primary_key: false,
673 unique: false,
674 autoincrement: false,
675 default: None,
676 }
677 }
678
679 fn pk_col(name: &str, data_type: DataType) -> ColumnSnapshot {
680 ColumnSnapshot {
681 name: name.to_string(),
682 data_type,
683 nullable: false,
684 primary_key: true,
685 unique: false,
686 autoincrement: true,
687 default: None,
688 }
689 }
690
691 fn table(name: &str, columns: Vec<ColumnSnapshot>) -> TableSnapshot {
692 TableSnapshot {
693 name: name.to_string(),
694 columns,
695 indexes: vec![],
696 foreign_keys: vec![],
697 }
698 }
699
700 fn schema(tables: Vec<TableSnapshot>) -> SchemaSnapshot {
701 let mut s = SchemaSnapshot::new();
702 for t in tables {
703 s.add_table(t);
704 }
705 s
706 }
707
708 #[test]
713 fn levenshtein_basic() {
714 assert_eq!(levenshtein("", ""), 0);
715 assert_eq!(levenshtein("abc", "abc"), 0);
716 assert_eq!(levenshtein("abc", ""), 3);
717 assert_eq!(levenshtein("", "abc"), 3);
718 assert_eq!(levenshtein("kitten", "sitting"), 3);
719 }
720
721 #[test]
722 fn similarity_basic() {
723 assert!((similarity("abc", "abc") - 1.0).abs() < f64::EPSILON);
724 assert!((similarity("", "") - 1.0).abs() < f64::EPSILON);
725 let s = similarity("name", "full_name");
727 assert!(s > 0.4 && s < 0.5, "sim={s}");
728 }
729
730 #[test]
735 fn no_changes_produces_empty_diff() {
736 let t = table(
737 "users",
738 vec![
739 pk_col("id", DataType::Bigint),
740 col("name", DataType::Text, false),
741 ],
742 );
743 let diff = diff_table("users", &t, &t);
744 assert!(diff.is_empty());
745 }
746
747 #[test]
748 fn new_table_detected() {
749 let current = schema(vec![]);
750 let desired = schema(vec![table("users", vec![pk_col("id", DataType::Bigint)])]);
751 let diff = auto_diff_schema(¤t, &desired);
752 assert_eq!(diff.operations.len(), 1);
753 assert!(matches!(
754 &diff.operations[0],
755 Operation::CreateTable(op) if op.name == "users"
756 ));
757 }
758
759 #[test]
760 fn dropped_table_detected() {
761 let current = schema(vec![table("users", vec![pk_col("id", DataType::Bigint)])]);
762 let desired = schema(vec![]);
763 let diff = auto_diff_schema(¤t, &desired);
764 assert_eq!(diff.operations.len(), 1);
765 assert!(matches!(
766 &diff.operations[0],
767 Operation::DropTable(op) if op.name == "users"
768 ));
769 }
770
771 #[test]
772 fn added_column_detected() {
773 let old = table("users", vec![pk_col("id", DataType::Bigint)]);
774 let new = table(
775 "users",
776 vec![
777 pk_col("id", DataType::Bigint),
778 col("email", DataType::Text, true),
779 ],
780 );
781 let diff = diff_table("users", &old, &new);
782 assert_eq!(diff.operations.len(), 1);
783 assert!(matches!(
784 &diff.operations[0],
785 Operation::AddColumn(op)
786 if op.table == "users"
787 && op.column.name == "email"
788 ));
789 }
790
791 #[test]
792 fn dropped_column_detected() {
793 let old = table(
794 "users",
795 vec![
796 pk_col("id", DataType::Bigint),
797 col("email", DataType::Text, true),
798 ],
799 );
800 let new = table("users", vec![pk_col("id", DataType::Bigint)]);
801 let diff = diff_table("users", &old, &new);
802 assert_eq!(diff.operations.len(), 1);
803 assert!(matches!(
804 &diff.operations[0],
805 Operation::DropColumn(op)
806 if op.table == "users" && op.column == "email"
807 ));
808 }
809
810 #[test]
811 fn type_change_detected() {
812 let old = table(
813 "users",
814 vec![
815 pk_col("id", DataType::Bigint),
816 col("score", DataType::Integer, false),
817 ],
818 );
819 let new = table(
820 "users",
821 vec![
822 pk_col("id", DataType::Bigint),
823 col("score", DataType::Bigint, false),
824 ],
825 );
826 let diff = diff_table("users", &old, &new);
827 assert_eq!(diff.operations.len(), 1);
828 assert!(matches!(
829 &diff.operations[0],
830 Operation::AlterColumn(op)
831 if op.column == "score"
832 && op.change
833 == AlterColumnChange::SetDataType(
834 DataType::Bigint
835 )
836 ));
837 }
838
839 #[test]
840 fn nullable_change_detected() {
841 let old = table(
842 "users",
843 vec![
844 pk_col("id", DataType::Bigint),
845 col("email", DataType::Text, false),
846 ],
847 );
848 let new = table(
849 "users",
850 vec![
851 pk_col("id", DataType::Bigint),
852 col("email", DataType::Text, true),
853 ],
854 );
855 let diff = diff_table("users", &old, &new);
856 assert_eq!(diff.operations.len(), 1);
857 assert!(matches!(
858 &diff.operations[0],
859 Operation::AlterColumn(op)
860 if op.column == "email"
861 && op.change
862 == AlterColumnChange::SetNullable(true)
863 ));
864 }
865
866 #[test]
867 fn default_added() {
868 let old = table("t", vec![col("active", DataType::Boolean, false)]);
869 let mut new_col = col("active", DataType::Boolean, false);
870 new_col.default = Some(DefaultValue::Expression("TRUE".into()));
871 let new = table("t", vec![new_col]);
872 let diff = diff_table("t", &old, &new);
873 assert_eq!(diff.operations.len(), 1);
874 assert!(matches!(
875 &diff.operations[0],
876 Operation::AlterColumn(op)
877 if matches!(
878 &op.change,
879 AlterColumnChange::SetDefault(
880 DefaultValue::Expression(s)
881 ) if s == "TRUE"
882 )
883 ));
884 }
885
886 #[test]
887 fn default_changed() {
888 let mut old_col = col("count", DataType::Integer, false);
889 old_col.default = Some(DefaultValue::Integer(0));
890 let old = table("t", vec![old_col]);
891
892 let mut new_col = col("count", DataType::Integer, false);
893 new_col.default = Some(DefaultValue::Integer(1));
894 let new = table("t", vec![new_col]);
895
896 let diff = diff_table("t", &old, &new);
897 assert_eq!(diff.operations.len(), 1);
898 assert!(matches!(
899 &diff.operations[0],
900 Operation::AlterColumn(op)
901 if op.change
902 == AlterColumnChange::SetDefault(
903 DefaultValue::Integer(1)
904 )
905 ));
906 }
907
908 #[test]
909 fn default_removed() {
910 let mut old_col = col("active", DataType::Boolean, false);
911 old_col.default = Some(DefaultValue::Expression("TRUE".into()));
912 let old = table("t", vec![old_col]);
913 let new = table("t", vec![col("active", DataType::Boolean, false)]);
914 let diff = diff_table("t", &old, &new);
915 assert_eq!(diff.operations.len(), 1);
916 assert!(matches!(
917 &diff.operations[0],
918 Operation::AlterColumn(op)
919 if op.change == AlterColumnChange::DropDefault
920 ));
921 }
922
923 #[test]
928 fn ambiguous_rename_detected() {
929 let old = table(
931 "users",
932 vec![
933 pk_col("id", DataType::Bigint),
934 col("name", DataType::Text, false),
935 ],
936 );
937 let new = table(
938 "users",
939 vec![
940 pk_col("id", DataType::Bigint),
941 col("full_name", DataType::Text, false),
942 ],
943 );
944 let diff = diff_table("users", &old, &new);
945
946 assert!(diff.operations.is_empty());
947 assert_eq!(diff.ambiguous.len(), 1);
948 match &diff.ambiguous[0] {
949 AmbiguousChange::PossibleRename {
950 table,
951 old_column,
952 new_column,
953 ..
954 } => {
955 assert_eq!(table, "users");
956 assert_eq!(old_column, "name");
957 assert_eq!(new_column, "full_name");
958 }
959 other => {
960 panic!("Expected PossibleRename, got {other:?}")
961 }
962 }
963 }
964
965 #[test]
966 fn ambiguous_rename_not_triggered_different_types() {
967 let old = table(
968 "users",
969 vec![
970 pk_col("id", DataType::Bigint),
971 col("name", DataType::Text, false),
972 ],
973 );
974 let new = table(
975 "users",
976 vec![
977 pk_col("id", DataType::Bigint),
978 col("full_name", DataType::Integer, false),
979 ],
980 );
981 let diff = diff_table("users", &old, &new);
982 assert!(diff.ambiguous.is_empty());
983 assert_eq!(diff.operations.len(), 2);
984 }
985
986 #[test]
987 fn low_similarity_produces_add_drop_not_rename() {
988 let old = table(
990 "t",
991 vec![
992 pk_col("id", DataType::Bigint),
993 col("body", DataType::Text, false),
994 ],
995 );
996 let new = table(
997 "t",
998 vec![
999 pk_col("id", DataType::Bigint),
1000 col("summary", DataType::Text, false),
1001 ],
1002 );
1003 let diff = diff_table("t", &old, &new);
1004
1005 assert!(diff.ambiguous.is_empty());
1007 assert_eq!(diff.operations.len(), 2);
1008 }
1009
1010 #[test]
1011 fn n_m_rename_detection() {
1012 let old = table(
1016 "t",
1017 vec![
1018 pk_col("id", DataType::Bigint),
1019 col("user_name", DataType::Text, false),
1020 col("addr", DataType::Text, false),
1021 ],
1022 );
1023 let new = table(
1024 "t",
1025 vec![
1026 pk_col("id", DataType::Bigint),
1027 col("username", DataType::Text, false),
1028 col("address", DataType::Text, false),
1029 ],
1030 );
1031 let diff = diff_table("t", &old, &new);
1032
1033 assert!(diff.operations.is_empty());
1034 assert_eq!(diff.ambiguous.len(), 2);
1035 }
1036
1037 #[test]
1038 fn multiple_changes_combined() {
1039 let old = table(
1040 "users",
1041 vec![
1042 pk_col("id", DataType::Bigint),
1043 col("name", DataType::Text, false),
1044 col("old_field", DataType::Integer, false),
1045 ],
1046 );
1047 let new = table(
1048 "users",
1049 vec![
1050 pk_col("id", DataType::Bigint),
1051 col("name", DataType::Varchar(Some(255)), true),
1052 col("new_field", DataType::Boolean, false),
1053 ],
1054 );
1055 let diff = diff_table("users", &old, &new);
1056
1057 assert!(diff.ambiguous.is_empty());
1061 assert_eq!(diff.operations.len(), 4);
1062 }
1063
1064 #[test]
1065 fn operation_ordering_in_schema_diff() {
1066 let current = schema(vec![
1067 table(
1068 "to_drop",
1069 vec![
1070 pk_col("id", DataType::Bigint),
1071 col("legacy", DataType::Text, false),
1072 ],
1073 ),
1074 table(
1075 "to_alter",
1076 vec![
1077 pk_col("id", DataType::Bigint),
1078 col("alpha", DataType::Text, false),
1079 col("beta", DataType::Integer, false),
1080 ],
1081 ),
1082 ]);
1083 let desired = schema(vec![
1084 table("to_create", vec![pk_col("id", DataType::Bigint)]),
1085 table(
1086 "to_alter",
1087 vec![
1088 pk_col("id", DataType::Bigint),
1089 col("xxx", DataType::Boolean, false),
1090 col("yyy", DataType::Real, false),
1091 ],
1092 ),
1093 ]);
1094 let diff = auto_diff_schema(¤t, &desired);
1095
1096 let mut saw_create = false;
1097 let mut saw_add = false;
1098 let mut saw_drop_col = false;
1099 let mut saw_drop_table = false;
1100
1101 for op in &diff.operations {
1102 match op {
1103 Operation::CreateTable(_) => {
1104 assert!(!saw_add && !saw_drop_col && !saw_drop_table);
1105 saw_create = true;
1106 }
1107 Operation::AddColumn(_) => {
1108 assert!(
1109 !saw_drop_col && !saw_drop_table,
1110 "AddColumn before DropColumn/DropTable"
1111 );
1112 saw_add = true;
1113 }
1114 Operation::DropColumn(_) => {
1115 assert!(!saw_drop_table, "DropColumn before DropTable");
1116 saw_drop_col = true;
1117 }
1118 Operation::DropTable(_) => {
1119 saw_drop_table = true;
1120 }
1121 _ => {}
1122 }
1123 }
1124
1125 assert!(saw_create);
1126 assert!(saw_add);
1127 assert!(saw_drop_col);
1128 assert!(saw_drop_table);
1129 }
1130
1131 #[test]
1132 fn possible_table_rename_detected() {
1133 let current = schema(vec![table(
1134 "users",
1135 vec![
1136 pk_col("id", DataType::Bigint),
1137 col("name", DataType::Text, false),
1138 ],
1139 )]);
1140 let desired = schema(vec![table(
1141 "accounts",
1142 vec![
1143 pk_col("id", DataType::Bigint),
1144 col("name", DataType::Text, false),
1145 ],
1146 )]);
1147 let diff = auto_diff_schema(¤t, &desired);
1148
1149 assert!(diff.operations.is_empty());
1150 assert_eq!(diff.ambiguous.len(), 1);
1151 match &diff.ambiguous[0] {
1152 AmbiguousChange::PossibleTableRename {
1153 old_table,
1154 new_table,
1155 ..
1156 } => {
1157 assert_eq!(old_table, "users");
1158 assert_eq!(new_table, "accounts");
1159 }
1160 other => {
1161 panic!("Expected PossibleTableRename, got {other:?}")
1162 }
1163 }
1164 }
1165
1166 #[test]
1167 fn auto_diff_table_works() {
1168 use crate::migrations::SqliteDialect;
1169 use crate::schema::{ColumnSchema, Table};
1170
1171 struct MyTable;
1172 struct MyRow;
1173
1174 impl Table for MyTable {
1175 type Row = MyRow;
1176 const NAME: &'static str = "items";
1177 const COLUMNS: &'static [&'static str] = &["id", "title"];
1178 const PRIMARY_KEY: Option<&'static str> = Some("id");
1179 }
1180
1181 impl TableSchema for MyTable {
1182 const SCHEMA: &'static [ColumnSchema] = &[
1183 ColumnSchema {
1184 name: "id",
1185 rust_type: "i64",
1186 nullable: false,
1187 primary_key: true,
1188 unique: false,
1189 autoincrement: true,
1190 default_expr: None,
1191 },
1192 ColumnSchema {
1193 name: "title",
1194 rust_type: "String",
1195 nullable: false,
1196 primary_key: false,
1197 unique: false,
1198 autoincrement: false,
1199 default_expr: None,
1200 },
1201 ];
1202 }
1203
1204 let dialect = SqliteDialect::new();
1205 let current = table("items", vec![pk_col("id", DataType::Bigint)]);
1206 let diff = auto_diff_table::<MyTable>(¤t, &dialect);
1207
1208 assert_eq!(diff.operations.len(), 1);
1209 assert!(matches!(
1210 &diff.operations[0],
1211 Operation::AddColumn(op)
1212 if op.column.name == "title"
1213 && op.column.data_type == DataType::Text
1214 ));
1215 }
1216
1217 #[test]
1222 fn unique_change_detected() {
1223 let mut old_col = col("email", DataType::Text, false);
1224 old_col.unique = false;
1225 let old = table("users", vec![old_col]);
1226
1227 let mut new_col = col("email", DataType::Text, false);
1228 new_col.unique = true;
1229 let new = table("users", vec![new_col]);
1230
1231 let diff = diff_table("users", &old, &new);
1232 assert!(diff.operations.iter().any(|op| matches!(
1233 op,
1234 Operation::AlterColumn(a)
1235 if a.column == "email"
1236 && a.change
1237 == AlterColumnChange::SetUnique(true)
1238 )));
1239 }
1240
1241 #[test]
1246 fn primary_key_change_emits_warning() {
1247 let mut old_col = col("email", DataType::Text, false);
1248 old_col.primary_key = false;
1249 let old = table("t", vec![old_col]);
1250
1251 let mut new_col = col("email", DataType::Text, false);
1252 new_col.primary_key = true;
1253 let new = table("t", vec![new_col]);
1254
1255 let diff = diff_table("t", &old, &new);
1256 assert!(diff.warnings.iter().any(|w| matches!(
1257 w,
1258 DiffWarning::PrimaryKeyChange {
1259 column,
1260 new_value: true,
1261 ..
1262 } if column == "email"
1263 )));
1264 }
1265
1266 #[test]
1267 fn autoincrement_change_emits_warning() {
1268 let mut old_col = col("id", DataType::Bigint, false);
1269 old_col.autoincrement = false;
1270 let old = table("t", vec![old_col]);
1271
1272 let mut new_col = col("id", DataType::Bigint, false);
1273 new_col.autoincrement = true;
1274 let new = table("t", vec![new_col]);
1275
1276 let diff = diff_table("t", &old, &new);
1277 assert!(diff.warnings.iter().any(|w| matches!(
1278 w,
1279 DiffWarning::AutoincrementChange {
1280 column,
1281 new_value: true,
1282 ..
1283 } if column == "id"
1284 )));
1285 }
1286
1287 #[test]
1288 fn column_order_change_emits_warning() {
1289 let old = table(
1290 "t",
1291 vec![
1292 col("a", DataType::Text, false),
1293 col("b", DataType::Text, false),
1294 ],
1295 );
1296 let new = table(
1297 "t",
1298 vec![
1299 col("b", DataType::Text, false),
1300 col("a", DataType::Text, false),
1301 ],
1302 );
1303 let diff = diff_table("t", &old, &new);
1304 assert!(
1305 diff.warnings
1306 .iter()
1307 .any(|w| matches!(w, DiffWarning::ColumnOrderChanged { .. }))
1308 );
1309 }
1310
1311 #[test]
1316 fn index_added_detected() {
1317 let old = table("t", vec![col("a", DataType::Text, false)]);
1318 let mut new = table("t", vec![col("a", DataType::Text, false)]);
1319 new.indexes.push(IndexSnapshot {
1320 name: "idx_a".into(),
1321 columns: vec!["a".into()],
1322 unique: false,
1323 index_type: IndexType::BTree,
1324 condition: None,
1325 });
1326 let diff = diff_table("t", &old, &new);
1327 assert!(
1328 diff.operations
1329 .iter()
1330 .any(|op| matches!(op, Operation::CreateIndex(ci) if ci.name == "idx_a"))
1331 );
1332 }
1333
1334 #[test]
1335 fn index_dropped_detected() {
1336 let mut old = table("t", vec![col("a", DataType::Text, false)]);
1337 old.indexes.push(IndexSnapshot {
1338 name: "idx_a".into(),
1339 columns: vec!["a".into()],
1340 unique: false,
1341 index_type: IndexType::BTree,
1342 condition: None,
1343 });
1344 let new = table("t", vec![col("a", DataType::Text, false)]);
1345 let diff = diff_table("t", &old, &new);
1346 assert!(
1347 diff.operations
1348 .iter()
1349 .any(|op| matches!(op, Operation::DropIndex(di) if di.name == "idx_a"))
1350 );
1351 }
1352
1353 #[test]
1358 fn fk_added_detected() {
1359 let old = table("t", vec![col("a", DataType::Bigint, false)]);
1360 let mut new = table("t", vec![col("a", DataType::Bigint, false)]);
1361 new.foreign_keys.push(ForeignKeySnapshot {
1362 name: Some("fk_a".into()),
1363 columns: vec!["a".into()],
1364 references_table: "other".into(),
1365 references_columns: vec!["id".into()],
1366 on_delete: Some(ForeignKeyAction::Cascade),
1367 on_update: None,
1368 });
1369 let diff = diff_table("t", &old, &new);
1370 assert!(diff.operations.iter().any(
1371 |op| matches!(op, Operation::AddForeignKey(fk) if fk.name == Some("fk_a".into()))
1372 ));
1373 }
1374
1375 #[test]
1376 fn fk_dropped_detected() {
1377 let mut old = table("t", vec![col("a", DataType::Bigint, false)]);
1378 old.foreign_keys.push(ForeignKeySnapshot {
1379 name: Some("fk_a".into()),
1380 columns: vec!["a".into()],
1381 references_table: "other".into(),
1382 references_columns: vec!["id".into()],
1383 on_delete: None,
1384 on_update: None,
1385 });
1386 let new = table("t", vec![col("a", DataType::Bigint, false)]);
1387 let diff = diff_table("t", &old, &new);
1388 assert!(
1389 diff.operations
1390 .iter()
1391 .any(|op| matches!(op, Operation::DropForeignKey(fk) if fk.name == "fk_a"))
1392 );
1393 }
1394
1395 #[test]
1400 fn reversible_diff() {
1401 let old = table("t", vec![pk_col("id", DataType::Bigint)]);
1402 let new = table(
1403 "t",
1404 vec![
1405 pk_col("id", DataType::Bigint),
1406 col("email", DataType::Text, true),
1407 ],
1408 );
1409 let diff = diff_table("t", &old, &new);
1410 assert!(diff.is_reversible());
1411
1412 let reversed = diff.reverse().unwrap();
1413 assert_eq!(reversed.operations.len(), 1);
1414 assert!(matches!(
1415 &reversed.operations[0],
1416 Operation::DropColumn(dc)
1417 if dc.column == "email"
1418 ));
1419 }
1420
1421 #[test]
1422 fn non_reversible_diff() {
1423 let old = table(
1424 "t",
1425 vec![
1426 pk_col("id", DataType::Bigint),
1427 col("email", DataType::Text, true),
1428 ],
1429 );
1430 let new = table("t", vec![pk_col("id", DataType::Bigint)]);
1431 let diff = diff_table("t", &old, &new);
1432
1433 assert!(!diff.is_reversible());
1435 assert_eq!(diff.non_reversible_operations().len(), 1);
1436 assert!(diff.reverse().is_none());
1437 }
1438
1439 #[test]
1444 fn to_sql_produces_output() {
1445 use crate::migrations::SqliteDialect;
1446
1447 let old = table("t", vec![pk_col("id", DataType::Bigint)]);
1448 let new = table(
1449 "t",
1450 vec![
1451 pk_col("id", DataType::Bigint),
1452 col("name", DataType::Text, false),
1453 ],
1454 );
1455 let diff = diff_table("t", &old, &new);
1456 let sqls = diff.to_sql(&SqliteDialect::new());
1457 assert_eq!(sqls.len(), 1);
1458 assert!(sqls[0].contains("ADD COLUMN"));
1459 }
1460}