1use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
2
3use vespertide_core::{MigrationAction, MigrationPlan, TableConstraint, TableDef};
4
5use crate::error::PlannerError;
6
7fn topological_sort_tables<'a>(tables: &[&'a TableDef]) -> Result<Vec<&'a TableDef>, PlannerError> {
11 if tables.is_empty() {
12 return Ok(vec![]);
13 }
14
15 let table_names: HashSet<&str> = tables.iter().map(|t| t.name.as_str()).collect();
17
18 let mut dependencies: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
22 for table in tables {
23 let mut deps_set: BTreeSet<&str> = BTreeSet::new();
24 for constraint in &table.constraints {
25 if let TableConstraint::ForeignKey { ref_table, .. } = constraint {
26 if table_names.contains(ref_table.as_str()) && ref_table != &table.name {
28 deps_set.insert(ref_table.as_str());
29 }
30 }
31 }
32 dependencies.insert(table.name.as_str(), deps_set.into_iter().collect());
33 }
34
35 let mut in_degree: BTreeMap<&str, usize> = BTreeMap::new();
39 for table in tables {
40 in_degree.entry(table.name.as_str()).or_insert(0);
41 }
42
43 for (table_name, deps) in &dependencies {
45 for _dep in deps {
46 }
49 *in_degree.entry(table_name).or_insert(0) += deps.len();
52 }
53
54 let mut queue: VecDeque<&str> = in_degree
57 .iter()
58 .filter(|(_, deg)| **deg == 0)
59 .map(|(name, _)| *name)
60 .collect();
61
62 let mut result: Vec<&TableDef> = Vec::new();
63 let table_map: BTreeMap<&str, &TableDef> =
64 tables.iter().map(|t| (t.name.as_str(), *t)).collect();
65
66 while let Some(table_name) = queue.pop_front() {
67 if let Some(&table) = table_map.get(table_name) {
68 result.push(table);
69 }
70
71 let mut ready_tables: BTreeSet<&str> = BTreeSet::new();
74 for (dependent, deps) in &dependencies {
75 if deps.contains(&table_name)
76 && let Some(degree) = in_degree.get_mut(dependent)
77 {
78 *degree -= 1;
79 if *degree == 0 {
80 ready_tables.insert(dependent);
81 }
82 }
83 }
84 for t in ready_tables {
85 queue.push_back(t);
86 }
87 }
88
89 if result.len() != tables.len() {
91 let remaining: Vec<&str> = tables
92 .iter()
93 .map(|t| t.name.as_str())
94 .filter(|name| !result.iter().any(|t| t.name.as_str() == *name))
95 .collect();
96 return Err(PlannerError::TableValidation(format!(
97 "Circular foreign key dependency detected among tables: {:?}",
98 remaining
99 )));
100 }
101
102 Ok(result)
103}
104
105fn extract_delete_table_name(action: &MigrationAction) -> &str {
110 match action {
111 MigrationAction::DeleteTable { table } => table.as_str(),
112 _ => panic!("Expected DeleteTable action"),
113 }
114}
115
116fn sort_delete_tables(actions: &mut [MigrationAction], all_tables: &BTreeMap<&str, &TableDef>) {
117 let delete_indices: Vec<usize> = actions
119 .iter()
120 .enumerate()
121 .filter_map(|(i, a)| {
122 if matches!(a, MigrationAction::DeleteTable { .. }) {
123 Some(i)
124 } else {
125 None
126 }
127 })
128 .collect();
129
130 if delete_indices.len() <= 1 {
131 return;
132 }
133
134 let delete_table_names: BTreeSet<&str> = delete_indices
137 .iter()
138 .map(|&i| extract_delete_table_name(&actions[i]))
139 .collect();
140
141 let mut dependencies: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
146 for &table_name in &delete_table_names {
147 let mut deps_set: BTreeSet<&str> = BTreeSet::new();
148 if let Some(table_def) = all_tables.get(table_name) {
149 for constraint in &table_def.constraints {
150 if let TableConstraint::ForeignKey { ref_table, .. } = constraint
151 && delete_table_names.contains(ref_table.as_str())
152 && ref_table != table_name
153 {
154 deps_set.insert(ref_table.as_str());
155 }
156 }
157 }
158 dependencies.insert(table_name, deps_set.into_iter().collect());
159 }
160
161 let mut in_degree: BTreeMap<&str, usize> = BTreeMap::new();
165 for &table_name in &delete_table_names {
166 in_degree.insert(
167 table_name,
168 dependencies.get(table_name).map_or(0, |d| d.len()),
169 );
170 }
171
172 let mut queue: VecDeque<&str> = in_degree
175 .iter()
176 .filter(|(_, deg)| **deg == 0)
177 .map(|(name, _)| *name)
178 .collect();
179
180 let mut sorted_tables: Vec<&str> = Vec::new();
181 while let Some(table_name) = queue.pop_front() {
182 sorted_tables.push(table_name);
183
184 let mut ready_tables: BTreeSet<&str> = BTreeSet::new();
187 for (&dependent, deps) in &dependencies {
188 if deps.contains(&table_name)
189 && let Some(degree) = in_degree.get_mut(dependent)
190 {
191 *degree -= 1;
192 if *degree == 0 {
193 ready_tables.insert(dependent);
194 }
195 }
196 }
197 for t in ready_tables {
198 queue.push_back(t);
199 }
200 }
201
202 sorted_tables.reverse();
204
205 let mut delete_actions: Vec<MigrationAction> =
207 delete_indices.iter().map(|&i| actions[i].clone()).collect();
208
209 delete_actions.sort_by(|a, b| {
210 let a_name = extract_delete_table_name(a);
211 let b_name = extract_delete_table_name(b);
212
213 let a_pos = sorted_tables.iter().position(|&t| t == a_name).unwrap_or(0);
214 let b_pos = sorted_tables.iter().position(|&t| t == b_name).unwrap_or(0);
215 a_pos.cmp(&b_pos)
216 });
217
218 for (i, idx) in delete_indices.iter().enumerate() {
220 actions[*idx] = delete_actions[i].clone();
221 }
222}
223
224pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result<MigrationPlan, PlannerError> {
228 let mut actions: Vec<MigrationAction> = Vec::new();
229
230 let from_normalized: Vec<TableDef> = from
232 .iter()
233 .map(|t| {
234 t.normalize().map_err(|e| {
235 PlannerError::TableValidation(format!(
236 "Failed to normalize table '{}': {}",
237 t.name, e
238 ))
239 })
240 })
241 .collect::<Result<Vec<_>, _>>()?;
242 let to_normalized: Vec<TableDef> = to
243 .iter()
244 .map(|t| {
245 t.normalize().map_err(|e| {
246 PlannerError::TableValidation(format!(
247 "Failed to normalize table '{}': {}",
248 t.name, e
249 ))
250 })
251 })
252 .collect::<Result<Vec<_>, _>>()?;
253
254 let from_map: BTreeMap<_, _> = from_normalized
257 .iter()
258 .map(|t| (t.name.as_str(), t))
259 .collect();
260 let to_map: BTreeMap<_, _> = to_normalized.iter().map(|t| (t.name.as_str(), t)).collect();
261
262 let to_original_map: BTreeMap<_, _> = to.iter().map(|t| (t.name.as_str(), t)).collect();
264
265 for name in from_map.keys() {
267 if !to_map.contains_key(name) {
268 actions.push(MigrationAction::DeleteTable {
269 table: name.to_string(),
270 });
271 }
272 }
273
274 for (name, to_tbl) in &to_map {
276 if let Some(from_tbl) = from_map.get(name) {
277 let from_cols: BTreeMap<_, _> = from_tbl
279 .columns
280 .iter()
281 .map(|c| (c.name.as_str(), c))
282 .collect();
283 let to_cols: BTreeMap<_, _> = to_tbl
284 .columns
285 .iter()
286 .map(|c| (c.name.as_str(), c))
287 .collect();
288
289 let deleted_columns: BTreeSet<&str> = from_cols
291 .keys()
292 .filter(|col| !to_cols.contains_key(*col))
293 .copied()
294 .collect();
295
296 for col in &deleted_columns {
297 actions.push(MigrationAction::DeleteColumn {
298 table: name.to_string(),
299 column: col.to_string(),
300 });
301 }
302
303 for (col, to_def) in &to_cols {
305 if let Some(from_def) = from_cols.get(col)
306 && from_def.r#type.requires_migration(&to_def.r#type)
307 {
308 actions.push(MigrationAction::ModifyColumnType {
309 table: name.to_string(),
310 column: col.to_string(),
311 new_type: to_def.r#type.clone(),
312 });
313 }
314 }
315
316 for (col, to_def) in &to_cols {
318 if let Some(from_def) = from_cols.get(col)
319 && from_def.nullable != to_def.nullable
320 {
321 actions.push(MigrationAction::ModifyColumnNullable {
322 table: name.to_string(),
323 column: col.to_string(),
324 nullable: to_def.nullable,
325 fill_with: None,
326 });
327 }
328 }
329
330 for (col, to_def) in &to_cols {
332 if let Some(from_def) = from_cols.get(col) {
333 let from_default = from_def.default.as_ref().map(|d| d.to_sql());
334 let to_default = to_def.default.as_ref().map(|d| d.to_sql());
335 if from_default != to_default {
336 actions.push(MigrationAction::ModifyColumnDefault {
337 table: name.to_string(),
338 column: col.to_string(),
339 new_default: to_default,
340 });
341 }
342 }
343 }
344
345 for (col, to_def) in &to_cols {
347 if let Some(from_def) = from_cols.get(col)
348 && from_def.comment != to_def.comment
349 {
350 actions.push(MigrationAction::ModifyColumnComment {
351 table: name.to_string(),
352 column: col.to_string(),
353 new_comment: to_def.comment.clone(),
354 });
355 }
356 }
357
358 for (col, def) in &to_cols {
362 if !from_cols.contains_key(col) {
363 actions.push(MigrationAction::AddColumn {
364 table: name.to_string(),
365 column: Box::new((*def).clone()),
366 fill_with: None,
367 });
368 }
369 }
370
371 for from_constraint in &from_tbl.constraints {
375 if !to_tbl.constraints.contains(from_constraint) {
376 let constraint_columns = from_constraint.columns();
378
379 let all_columns_deleted = !constraint_columns.is_empty()
381 && constraint_columns
382 .iter()
383 .all(|col| deleted_columns.contains(col.as_str()));
384
385 if !all_columns_deleted {
386 actions.push(MigrationAction::RemoveConstraint {
387 table: name.to_string(),
388 constraint: from_constraint.clone(),
389 });
390 }
391 }
392 }
393 for to_constraint in &to_tbl.constraints {
394 if !from_tbl.constraints.contains(to_constraint) {
395 actions.push(MigrationAction::AddConstraint {
396 table: name.to_string(),
397 constraint: to_constraint.clone(),
398 });
399 }
400 }
401 }
402 }
403
404 let new_tables: Vec<&TableDef> = to_map
408 .iter()
409 .filter(|(name, _)| !from_map.contains_key(*name))
410 .map(|(_, tbl)| *tbl)
411 .collect();
412
413 let sorted_new_tables = topological_sort_tables(&new_tables)?;
414
415 for tbl in sorted_new_tables {
416 let original_tbl = to_original_map.get(tbl.name.as_str()).unwrap();
418 actions.push(MigrationAction::CreateTable {
419 table: original_tbl.name.clone(),
420 columns: original_tbl.columns.clone(),
421 constraints: original_tbl.constraints.clone(),
422 });
423 }
424
425 sort_delete_tables(&mut actions, &from_map);
427
428 Ok(MigrationPlan {
429 comment: None,
430 created_at: None,
431 version: 0,
432 actions,
433 })
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use rstest::rstest;
440 use vespertide_core::{
441 ColumnDef, ColumnType, MigrationAction, SimpleColumnType,
442 schema::{primary_key::PrimaryKeySyntax, str_or_bool::StrOrBoolOrArray},
443 };
444
445 fn col(name: &str, ty: ColumnType) -> ColumnDef {
446 ColumnDef {
447 name: name.to_string(),
448 r#type: ty,
449 nullable: true,
450 default: None,
451 comment: None,
452 primary_key: None,
453 unique: None,
454 index: None,
455 foreign_key: None,
456 }
457 }
458
459 fn table(
460 name: &str,
461 columns: Vec<ColumnDef>,
462 constraints: Vec<vespertide_core::TableConstraint>,
463 ) -> TableDef {
464 TableDef {
465 name: name.to_string(),
466 description: None,
467 columns,
468 constraints,
469 }
470 }
471
472 fn idx(name: &str, columns: Vec<&str>) -> TableConstraint {
473 TableConstraint::Index {
474 name: Some(name.to_string()),
475 columns: columns.into_iter().map(|s| s.to_string()).collect(),
476 }
477 }
478
479 #[rstest]
480 #[case::add_column_and_index(
481 vec![table(
482 "users",
483 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
484 vec![],
485 )],
486 vec![table(
487 "users",
488 vec![
489 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
490 col("name", ColumnType::Simple(SimpleColumnType::Text)),
491 ],
492 vec![idx("ix_users__name", vec!["name"])],
493 )],
494 vec![
495 MigrationAction::AddColumn {
496 table: "users".into(),
497 column: Box::new(col("name", ColumnType::Simple(SimpleColumnType::Text))),
498 fill_with: None,
499 },
500 MigrationAction::AddConstraint {
501 table: "users".into(),
502 constraint: idx("ix_users__name", vec!["name"]),
503 },
504 ]
505 )]
506 #[case::drop_table(
507 vec![table(
508 "users",
509 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
510 vec![],
511 )],
512 vec![],
513 vec![MigrationAction::DeleteTable {
514 table: "users".into()
515 }]
516 )]
517 #[case::add_table_with_index(
518 vec![],
519 vec![table(
520 "users",
521 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
522 vec![idx("idx_users_id", vec!["id"])],
523 )],
524 vec![
525 MigrationAction::CreateTable {
526 table: "users".into(),
527 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
528 constraints: vec![idx("idx_users_id", vec!["id"])],
529 },
530 ]
531 )]
532 #[case::delete_column(
533 vec![table(
534 "users",
535 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("name", ColumnType::Simple(SimpleColumnType::Text))],
536 vec![],
537 )],
538 vec![table(
539 "users",
540 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
541 vec![],
542 )],
543 vec![MigrationAction::DeleteColumn {
544 table: "users".into(),
545 column: "name".into(),
546 }]
547 )]
548 #[case::modify_column_type(
549 vec![table(
550 "users",
551 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
552 vec![],
553 )],
554 vec![table(
555 "users",
556 vec![col("id", ColumnType::Simple(SimpleColumnType::Text))],
557 vec![],
558 )],
559 vec![MigrationAction::ModifyColumnType {
560 table: "users".into(),
561 column: "id".into(),
562 new_type: ColumnType::Simple(SimpleColumnType::Text),
563 }]
564 )]
565 #[case::remove_index(
566 vec![table(
567 "users",
568 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
569 vec![idx("idx_users_id", vec!["id"])],
570 )],
571 vec![table(
572 "users",
573 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
574 vec![],
575 )],
576 vec![MigrationAction::RemoveConstraint {
577 table: "users".into(),
578 constraint: idx("idx_users_id", vec!["id"]),
579 }]
580 )]
581 #[case::add_index_existing_table(
582 vec![table(
583 "users",
584 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
585 vec![],
586 )],
587 vec![table(
588 "users",
589 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
590 vec![idx("idx_users_id", vec!["id"])],
591 )],
592 vec![MigrationAction::AddConstraint {
593 table: "users".into(),
594 constraint: idx("idx_users_id", vec!["id"]),
595 }]
596 )]
597 fn diff_schemas_detects_additions(
598 #[case] from_schema: Vec<TableDef>,
599 #[case] to_schema: Vec<TableDef>,
600 #[case] expected_actions: Vec<MigrationAction>,
601 ) {
602 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
603 assert_eq!(plan.actions, expected_actions);
604 }
605
606 mod integer_enum {
608 use super::*;
609 use vespertide_core::{ComplexColumnType, EnumValues, NumValue};
610
611 #[test]
612 fn integer_enum_values_changed_no_migration() {
613 let from = vec![table(
615 "orders",
616 vec![col(
617 "status",
618 ColumnType::Complex(ComplexColumnType::Enum {
619 name: "order_status".into(),
620 values: EnumValues::Integer(vec![
621 NumValue {
622 name: "Pending".into(),
623 value: 0,
624 },
625 NumValue {
626 name: "Shipped".into(),
627 value: 1,
628 },
629 ]),
630 }),
631 )],
632 vec![],
633 )];
634
635 let to = vec![table(
636 "orders",
637 vec![col(
638 "status",
639 ColumnType::Complex(ComplexColumnType::Enum {
640 name: "order_status".into(),
641 values: EnumValues::Integer(vec![
642 NumValue {
643 name: "Pending".into(),
644 value: 0,
645 },
646 NumValue {
647 name: "Shipped".into(),
648 value: 1,
649 },
650 NumValue {
651 name: "Delivered".into(),
652 value: 2,
653 },
654 NumValue {
655 name: "Cancelled".into(),
656 value: 100,
657 },
658 ]),
659 }),
660 )],
661 vec![],
662 )];
663
664 let plan = diff_schemas(&from, &to).unwrap();
665 assert!(
666 plan.actions.is_empty(),
667 "Expected no actions, got: {:?}",
668 plan.actions
669 );
670 }
671
672 #[test]
673 fn string_enum_values_changed_requires_migration() {
674 let from = vec![table(
676 "orders",
677 vec![col(
678 "status",
679 ColumnType::Complex(ComplexColumnType::Enum {
680 name: "order_status".into(),
681 values: EnumValues::String(vec!["pending".into(), "shipped".into()]),
682 }),
683 )],
684 vec![],
685 )];
686
687 let to = vec![table(
688 "orders",
689 vec![col(
690 "status",
691 ColumnType::Complex(ComplexColumnType::Enum {
692 name: "order_status".into(),
693 values: EnumValues::String(vec![
694 "pending".into(),
695 "shipped".into(),
696 "delivered".into(),
697 ]),
698 }),
699 )],
700 vec![],
701 )];
702
703 let plan = diff_schemas(&from, &to).unwrap();
704 assert_eq!(plan.actions.len(), 1);
705 assert!(matches!(
706 &plan.actions[0],
707 MigrationAction::ModifyColumnType { table, column, .. }
708 if table == "orders" && column == "status"
709 ));
710 }
711 }
712
713 mod inline_constraints {
715 use super::*;
716 use vespertide_core::schema::foreign_key::ForeignKeyDef;
717 use vespertide_core::schema::foreign_key::ForeignKeySyntax;
718 use vespertide_core::schema::primary_key::PrimaryKeySyntax;
719 use vespertide_core::{StrOrBoolOrArray, TableConstraint};
720
721 fn col_with_pk(name: &str, ty: ColumnType) -> ColumnDef {
722 ColumnDef {
723 name: name.to_string(),
724 r#type: ty,
725 nullable: false,
726 default: None,
727 comment: None,
728 primary_key: Some(PrimaryKeySyntax::Bool(true)),
729 unique: None,
730 index: None,
731 foreign_key: None,
732 }
733 }
734
735 fn col_with_unique(name: &str, ty: ColumnType) -> ColumnDef {
736 ColumnDef {
737 name: name.to_string(),
738 r#type: ty,
739 nullable: true,
740 default: None,
741 comment: None,
742 primary_key: None,
743 unique: Some(StrOrBoolOrArray::Bool(true)),
744 index: None,
745 foreign_key: None,
746 }
747 }
748
749 fn col_with_index(name: &str, ty: ColumnType) -> ColumnDef {
750 ColumnDef {
751 name: name.to_string(),
752 r#type: ty,
753 nullable: true,
754 default: None,
755 comment: None,
756 primary_key: None,
757 unique: None,
758 index: Some(StrOrBoolOrArray::Bool(true)),
759 foreign_key: None,
760 }
761 }
762
763 fn col_with_fk(name: &str, ty: ColumnType, ref_table: &str, ref_col: &str) -> ColumnDef {
764 ColumnDef {
765 name: name.to_string(),
766 r#type: ty,
767 nullable: true,
768 default: None,
769 comment: None,
770 primary_key: None,
771 unique: None,
772 index: None,
773 foreign_key: Some(ForeignKeySyntax::Object(ForeignKeyDef {
774 ref_table: ref_table.to_string(),
775 ref_columns: vec![ref_col.to_string()],
776 on_delete: None,
777 on_update: None,
778 })),
779 }
780 }
781
782 #[test]
783 fn create_table_with_inline_pk() {
784 let plan = diff_schemas(
785 &[],
786 &[table(
787 "users",
788 vec![
789 col_with_pk("id", ColumnType::Simple(SimpleColumnType::Integer)),
790 col("name", ColumnType::Simple(SimpleColumnType::Text)),
791 ],
792 vec![],
793 )],
794 )
795 .unwrap();
796
797 assert_eq!(plan.actions.len(), 1);
799 if let MigrationAction::CreateTable {
800 columns,
801 constraints,
802 ..
803 } = &plan.actions[0]
804 {
805 assert_eq!(constraints.len(), 0);
807 let id_col = columns.iter().find(|c| c.name == "id").unwrap();
809 assert!(id_col.primary_key.is_some());
810 } else {
811 panic!("Expected CreateTable action");
812 }
813 }
814
815 #[test]
816 fn create_table_with_inline_unique() {
817 let plan = diff_schemas(
818 &[],
819 &[table(
820 "users",
821 vec![
822 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
823 col_with_unique("email", ColumnType::Simple(SimpleColumnType::Text)),
824 ],
825 vec![],
826 )],
827 )
828 .unwrap();
829
830 assert_eq!(plan.actions.len(), 1);
832 if let MigrationAction::CreateTable {
833 columns,
834 constraints,
835 ..
836 } = &plan.actions[0]
837 {
838 assert_eq!(constraints.len(), 0);
840 let email_col = columns.iter().find(|c| c.name == "email").unwrap();
842 assert!(matches!(
843 email_col.unique,
844 Some(StrOrBoolOrArray::Bool(true))
845 ));
846 } else {
847 panic!("Expected CreateTable action");
848 }
849 }
850
851 #[test]
852 fn create_table_with_inline_index() {
853 let plan = diff_schemas(
854 &[],
855 &[table(
856 "users",
857 vec![
858 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
859 col_with_index("name", ColumnType::Simple(SimpleColumnType::Text)),
860 ],
861 vec![],
862 )],
863 )
864 .unwrap();
865
866 assert_eq!(plan.actions.len(), 1);
868 if let MigrationAction::CreateTable {
869 columns,
870 constraints,
871 ..
872 } = &plan.actions[0]
873 {
874 assert_eq!(constraints.len(), 0);
876 let name_col = columns.iter().find(|c| c.name == "name").unwrap();
878 assert!(matches!(name_col.index, Some(StrOrBoolOrArray::Bool(true))));
879 } else {
880 panic!("Expected CreateTable action");
881 }
882 }
883
884 #[test]
885 fn create_table_with_inline_fk() {
886 let plan = diff_schemas(
887 &[],
888 &[table(
889 "posts",
890 vec![
891 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
892 col_with_fk(
893 "user_id",
894 ColumnType::Simple(SimpleColumnType::Integer),
895 "users",
896 "id",
897 ),
898 ],
899 vec![],
900 )],
901 )
902 .unwrap();
903
904 assert_eq!(plan.actions.len(), 1);
906 if let MigrationAction::CreateTable {
907 columns,
908 constraints,
909 ..
910 } = &plan.actions[0]
911 {
912 assert_eq!(constraints.len(), 0);
914 let user_id_col = columns.iter().find(|c| c.name == "user_id").unwrap();
916 assert!(user_id_col.foreign_key.is_some());
917 } else {
918 panic!("Expected CreateTable action");
919 }
920 }
921
922 #[test]
923 fn add_index_via_inline_constraint() {
924 let plan = diff_schemas(
927 &[table(
928 "users",
929 vec![
930 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
931 col("name", ColumnType::Simple(SimpleColumnType::Text)),
932 ],
933 vec![],
934 )],
935 &[table(
936 "users",
937 vec![
938 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
939 col_with_index("name", ColumnType::Simple(SimpleColumnType::Text)),
940 ],
941 vec![],
942 )],
943 )
944 .unwrap();
945
946 assert_eq!(plan.actions.len(), 1);
948 if let MigrationAction::AddConstraint { table, constraint } = &plan.actions[0] {
949 assert_eq!(table, "users");
950 if let TableConstraint::Index { name, columns } = constraint {
951 assert_eq!(name, &None); assert_eq!(columns, &vec!["name".to_string()]);
953 } else {
954 panic!("Expected Index constraint, got {:?}", constraint);
955 }
956 } else {
957 panic!("Expected AddConstraint action, got {:?}", plan.actions[0]);
958 }
959 }
960
961 #[test]
962 fn create_table_with_all_inline_constraints() {
963 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
964 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
965 id_col.nullable = false;
966
967 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
968 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
969
970 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
971 name_col.index = Some(StrOrBoolOrArray::Bool(true));
972
973 let mut org_id_col = col("org_id", ColumnType::Simple(SimpleColumnType::Integer));
974 org_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
975 ref_table: "orgs".into(),
976 ref_columns: vec!["id".into()],
977 on_delete: None,
978 on_update: None,
979 }));
980
981 let plan = diff_schemas(
982 &[],
983 &[table(
984 "users",
985 vec![id_col, email_col, name_col, org_id_col],
986 vec![],
987 )],
988 )
989 .unwrap();
990
991 assert_eq!(plan.actions.len(), 1);
993
994 if let MigrationAction::CreateTable {
995 columns,
996 constraints,
997 ..
998 } = &plan.actions[0]
999 {
1000 assert_eq!(constraints.len(), 0);
1002
1003 let id_col = columns.iter().find(|c| c.name == "id").unwrap();
1005 assert!(id_col.primary_key.is_some());
1006
1007 let email_col = columns.iter().find(|c| c.name == "email").unwrap();
1008 assert!(matches!(
1009 email_col.unique,
1010 Some(StrOrBoolOrArray::Bool(true))
1011 ));
1012
1013 let name_col = columns.iter().find(|c| c.name == "name").unwrap();
1014 assert!(matches!(name_col.index, Some(StrOrBoolOrArray::Bool(true))));
1015
1016 let org_id_col = columns.iter().find(|c| c.name == "org_id").unwrap();
1017 assert!(org_id_col.foreign_key.is_some());
1018 } else {
1019 panic!("Expected CreateTable action");
1020 }
1021 }
1022
1023 #[test]
1024 fn add_constraint_to_existing_table() {
1025 let from_schema = vec![table(
1027 "users",
1028 vec![
1029 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1030 col("email", ColumnType::Simple(SimpleColumnType::Text)),
1031 ],
1032 vec![],
1033 )];
1034
1035 let to_schema = vec![table(
1036 "users",
1037 vec![
1038 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1039 col("email", ColumnType::Simple(SimpleColumnType::Text)),
1040 ],
1041 vec![vespertide_core::TableConstraint::Unique {
1042 name: Some("uq_users_email".into()),
1043 columns: vec!["email".into()],
1044 }],
1045 )];
1046
1047 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
1048 assert_eq!(plan.actions.len(), 1);
1049 if let MigrationAction::AddConstraint { table, constraint } = &plan.actions[0] {
1050 assert_eq!(table, "users");
1051 assert!(matches!(
1052 constraint,
1053 vespertide_core::TableConstraint::Unique { name: Some(n), columns }
1054 if n == "uq_users_email" && columns == &vec!["email".to_string()]
1055 ));
1056 } else {
1057 panic!("Expected AddConstraint action, got {:?}", plan.actions[0]);
1058 }
1059 }
1060
1061 #[test]
1062 fn remove_constraint_from_existing_table() {
1063 let from_schema = vec![table(
1065 "users",
1066 vec![
1067 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1068 col("email", ColumnType::Simple(SimpleColumnType::Text)),
1069 ],
1070 vec![vespertide_core::TableConstraint::Unique {
1071 name: Some("uq_users_email".into()),
1072 columns: vec!["email".into()],
1073 }],
1074 )];
1075
1076 let to_schema = vec![table(
1077 "users",
1078 vec![
1079 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1080 col("email", ColumnType::Simple(SimpleColumnType::Text)),
1081 ],
1082 vec![],
1083 )];
1084
1085 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
1086 assert_eq!(plan.actions.len(), 1);
1087 if let MigrationAction::RemoveConstraint { table, constraint } = &plan.actions[0] {
1088 assert_eq!(table, "users");
1089 assert!(matches!(
1090 constraint,
1091 vespertide_core::TableConstraint::Unique { name: Some(n), columns }
1092 if n == "uq_users_email" && columns == &vec!["email".to_string()]
1093 ));
1094 } else {
1095 panic!(
1096 "Expected RemoveConstraint action, got {:?}",
1097 plan.actions[0]
1098 );
1099 }
1100 }
1101
1102 #[test]
1103 fn diff_schemas_with_normalize_error() {
1104 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1106 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1107
1108 let table = TableDef {
1109 name: "test".into(),
1110 description: None,
1111 columns: vec![
1112 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1113 col1.clone(),
1114 {
1115 let mut c = col1.clone();
1117 c.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1118 c
1119 },
1120 ],
1121 constraints: vec![],
1122 };
1123
1124 let result = diff_schemas(&[], &[table]);
1125 assert!(result.is_err());
1126 if let Err(PlannerError::TableValidation(msg)) = result {
1127 assert!(msg.contains("Failed to normalize table"));
1128 assert!(msg.contains("Duplicate index"));
1129 } else {
1130 panic!("Expected TableValidation error, got {:?}", result);
1131 }
1132 }
1133
1134 #[test]
1135 fn diff_schemas_with_normalize_error_in_from_schema() {
1136 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1138 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1139
1140 let table = TableDef {
1141 name: "test".into(),
1142 description: None,
1143 columns: vec![
1144 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1145 col1.clone(),
1146 {
1147 let mut c = col1.clone();
1149 c.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1150 c
1151 },
1152 ],
1153 constraints: vec![],
1154 };
1155
1156 let result = diff_schemas(&[table], &[]);
1158 assert!(result.is_err());
1159 if let Err(PlannerError::TableValidation(msg)) = result {
1160 assert!(msg.contains("Failed to normalize table"));
1161 assert!(msg.contains("Duplicate index"));
1162 } else {
1163 panic!("Expected TableValidation error, got {:?}", result);
1164 }
1165 }
1166 }
1167
1168 mod fk_ordering {
1170 use super::*;
1171 use vespertide_core::TableConstraint;
1172
1173 fn table_with_fk(
1174 name: &str,
1175 ref_table: &str,
1176 fk_column: &str,
1177 ref_column: &str,
1178 ) -> TableDef {
1179 TableDef {
1180 name: name.to_string(),
1181 description: None,
1182 columns: vec![
1183 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1184 col(fk_column, ColumnType::Simple(SimpleColumnType::Integer)),
1185 ],
1186 constraints: vec![TableConstraint::ForeignKey {
1187 name: None,
1188 columns: vec![fk_column.to_string()],
1189 ref_table: ref_table.to_string(),
1190 ref_columns: vec![ref_column.to_string()],
1191 on_delete: None,
1192 on_update: None,
1193 }],
1194 }
1195 }
1196
1197 fn simple_table(name: &str) -> TableDef {
1198 TableDef {
1199 name: name.to_string(),
1200 description: None,
1201 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
1202 constraints: vec![],
1203 }
1204 }
1205
1206 #[test]
1207 fn create_tables_respects_fk_order() {
1208 let users = simple_table("users");
1211 let posts = table_with_fk("posts", "users", "user_id", "id");
1212
1213 let plan = diff_schemas(&[], &[posts.clone(), users.clone()]).unwrap();
1214
1215 let create_order: Vec<&str> = plan
1217 .actions
1218 .iter()
1219 .filter_map(|a| {
1220 if let MigrationAction::CreateTable { table, .. } = a {
1221 Some(table.as_str())
1222 } else {
1223 None
1224 }
1225 })
1226 .collect();
1227
1228 assert_eq!(create_order, vec!["users", "posts"]);
1229 }
1230
1231 #[test]
1232 fn create_tables_chain_dependency() {
1233 let users = simple_table("users");
1238 let media = table_with_fk("media", "users", "owner_id", "id");
1239 let articles = table_with_fk("articles", "media", "media_id", "id");
1240
1241 let plan =
1243 diff_schemas(&[], &[articles.clone(), media.clone(), users.clone()]).unwrap();
1244
1245 let create_order: Vec<&str> = plan
1246 .actions
1247 .iter()
1248 .filter_map(|a| {
1249 if let MigrationAction::CreateTable { table, .. } = a {
1250 Some(table.as_str())
1251 } else {
1252 None
1253 }
1254 })
1255 .collect();
1256
1257 assert_eq!(create_order, vec!["users", "media", "articles"]);
1258 }
1259
1260 #[test]
1261 fn create_tables_multiple_independent_branches() {
1262 let users = simple_table("users");
1266 let posts = table_with_fk("posts", "users", "user_id", "id");
1267 let categories = simple_table("categories");
1268 let products = table_with_fk("products", "categories", "category_id", "id");
1269
1270 let plan = diff_schemas(
1271 &[],
1272 &[
1273 products.clone(),
1274 posts.clone(),
1275 categories.clone(),
1276 users.clone(),
1277 ],
1278 )
1279 .unwrap();
1280
1281 let create_order: Vec<&str> = plan
1282 .actions
1283 .iter()
1284 .filter_map(|a| {
1285 if let MigrationAction::CreateTable { table, .. } = a {
1286 Some(table.as_str())
1287 } else {
1288 None
1289 }
1290 })
1291 .collect();
1292
1293 let users_pos = create_order.iter().position(|&t| t == "users").unwrap();
1295 let posts_pos = create_order.iter().position(|&t| t == "posts").unwrap();
1296 assert!(
1297 users_pos < posts_pos,
1298 "users should be created before posts"
1299 );
1300
1301 let categories_pos = create_order
1303 .iter()
1304 .position(|&t| t == "categories")
1305 .unwrap();
1306 let products_pos = create_order.iter().position(|&t| t == "products").unwrap();
1307 assert!(
1308 categories_pos < products_pos,
1309 "categories should be created before products"
1310 );
1311 }
1312
1313 #[test]
1314 fn delete_tables_respects_fk_order() {
1315 let users = simple_table("users");
1318 let posts = table_with_fk("posts", "users", "user_id", "id");
1319
1320 let plan = diff_schemas(&[users.clone(), posts.clone()], &[]).unwrap();
1321
1322 let delete_order: Vec<&str> = plan
1323 .actions
1324 .iter()
1325 .filter_map(|a| {
1326 if let MigrationAction::DeleteTable { table } = a {
1327 Some(table.as_str())
1328 } else {
1329 None
1330 }
1331 })
1332 .collect();
1333
1334 assert_eq!(delete_order, vec!["posts", "users"]);
1335 }
1336
1337 #[test]
1338 fn delete_tables_chain_dependency() {
1339 let users = simple_table("users");
1342 let media = table_with_fk("media", "users", "owner_id", "id");
1343 let articles = table_with_fk("articles", "media", "media_id", "id");
1344
1345 let plan =
1346 diff_schemas(&[users.clone(), media.clone(), articles.clone()], &[]).unwrap();
1347
1348 let delete_order: Vec<&str> = plan
1349 .actions
1350 .iter()
1351 .filter_map(|a| {
1352 if let MigrationAction::DeleteTable { table } = a {
1353 Some(table.as_str())
1354 } else {
1355 None
1356 }
1357 })
1358 .collect();
1359
1360 let articles_pos = delete_order.iter().position(|&t| t == "articles").unwrap();
1362 let media_pos = delete_order.iter().position(|&t| t == "media").unwrap();
1363 assert!(
1364 articles_pos < media_pos,
1365 "articles should be deleted before media"
1366 );
1367
1368 let users_pos = delete_order.iter().position(|&t| t == "users").unwrap();
1370 assert!(
1371 media_pos < users_pos,
1372 "media should be deleted before users"
1373 );
1374 }
1375
1376 #[test]
1377 fn circular_fk_dependency_returns_error() {
1378 let table_a = TableDef {
1380 name: "table_a".to_string(),
1381 description: None,
1382 columns: vec![
1383 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1384 col("b_id", ColumnType::Simple(SimpleColumnType::Integer)),
1385 ],
1386 constraints: vec![TableConstraint::ForeignKey {
1387 name: None,
1388 columns: vec!["b_id".to_string()],
1389 ref_table: "table_b".to_string(),
1390 ref_columns: vec!["id".to_string()],
1391 on_delete: None,
1392 on_update: None,
1393 }],
1394 };
1395
1396 let table_b = TableDef {
1397 name: "table_b".to_string(),
1398 description: None,
1399 columns: vec![
1400 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1401 col("a_id", ColumnType::Simple(SimpleColumnType::Integer)),
1402 ],
1403 constraints: vec![TableConstraint::ForeignKey {
1404 name: None,
1405 columns: vec!["a_id".to_string()],
1406 ref_table: "table_a".to_string(),
1407 ref_columns: vec!["id".to_string()],
1408 on_delete: None,
1409 on_update: None,
1410 }],
1411 };
1412
1413 let result = diff_schemas(&[], &[table_a, table_b]);
1414 assert!(result.is_err());
1415 if let Err(PlannerError::TableValidation(msg)) = result {
1416 assert!(
1417 msg.contains("Circular foreign key dependency"),
1418 "Expected circular dependency error, got: {}",
1419 msg
1420 );
1421 } else {
1422 panic!("Expected TableValidation error, got {:?}", result);
1423 }
1424 }
1425
1426 #[test]
1427 fn fk_to_external_table_is_ignored() {
1428 let posts = table_with_fk("posts", "users", "user_id", "id");
1430 let comments = table_with_fk("comments", "posts", "post_id", "id");
1431
1432 let plan = diff_schemas(&[], &[comments.clone(), posts.clone()]).unwrap();
1434
1435 let create_order: Vec<&str> = plan
1436 .actions
1437 .iter()
1438 .filter_map(|a| {
1439 if let MigrationAction::CreateTable { table, .. } = a {
1440 Some(table.as_str())
1441 } else {
1442 None
1443 }
1444 })
1445 .collect();
1446
1447 let posts_pos = create_order.iter().position(|&t| t == "posts").unwrap();
1449 let comments_pos = create_order.iter().position(|&t| t == "comments").unwrap();
1450 assert!(
1451 posts_pos < comments_pos,
1452 "posts should be created before comments"
1453 );
1454 }
1455
1456 #[test]
1457 fn delete_tables_mixed_with_other_actions() {
1458 use crate::diff::diff_schemas;
1461
1462 let from_schema = vec![
1463 table(
1464 "users",
1465 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
1466 vec![],
1467 ),
1468 table(
1469 "posts",
1470 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
1471 vec![],
1472 ),
1473 ];
1474
1475 let to_schema = vec![
1476 table(
1478 "users",
1479 vec![
1480 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1481 col("name", ColumnType::Simple(SimpleColumnType::Text)),
1482 ],
1483 vec![],
1484 ),
1485 ];
1486
1487 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
1488
1489 assert!(
1491 plan.actions
1492 .iter()
1493 .any(|a| matches!(a, MigrationAction::AddColumn { .. }))
1494 );
1495 assert!(
1496 plan.actions
1497 .iter()
1498 .any(|a| matches!(a, MigrationAction::DeleteTable { .. }))
1499 );
1500
1501 }
1504
1505 #[test]
1506 #[should_panic(expected = "Expected DeleteTable action")]
1507 fn test_extract_delete_table_name_panics_on_non_delete_action() {
1508 use super::extract_delete_table_name;
1510
1511 let action = MigrationAction::AddColumn {
1512 table: "users".into(),
1513 column: Box::new(ColumnDef {
1514 name: "email".into(),
1515 r#type: ColumnType::Simple(SimpleColumnType::Text),
1516 nullable: true,
1517 default: None,
1518 comment: None,
1519 primary_key: None,
1520 unique: None,
1521 index: None,
1522 foreign_key: None,
1523 }),
1524 fill_with: None,
1525 };
1526
1527 extract_delete_table_name(&action);
1529 }
1530
1531 #[test]
1533 fn create_tables_with_inline_fk_chain() {
1534 use super::*;
1535 use vespertide_core::schema::foreign_key::ForeignKeySyntax;
1536 use vespertide_core::schema::primary_key::PrimaryKeySyntax;
1537
1538 fn col_pk(name: &str) -> ColumnDef {
1539 ColumnDef {
1540 name: name.to_string(),
1541 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1542 nullable: false,
1543 default: None,
1544 comment: None,
1545 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1546 unique: None,
1547 index: None,
1548 foreign_key: None,
1549 }
1550 }
1551
1552 fn col_inline_fk(name: &str, ref_table: &str) -> ColumnDef {
1553 ColumnDef {
1554 name: name.to_string(),
1555 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1556 nullable: true,
1557 default: None,
1558 comment: None,
1559 primary_key: None,
1560 unique: None,
1561 index: None,
1562 foreign_key: Some(ForeignKeySyntax::String(format!("{}.id", ref_table))),
1563 }
1564 }
1565
1566 let user = TableDef {
1575 name: "user".to_string(),
1576 description: None,
1577 columns: vec![col_pk("id")],
1578 constraints: vec![],
1579 };
1580
1581 let product = TableDef {
1582 name: "product".to_string(),
1583 description: None,
1584 columns: vec![col_pk("id")],
1585 constraints: vec![],
1586 };
1587
1588 let project = TableDef {
1589 name: "project".to_string(),
1590 description: None,
1591 columns: vec![col_pk("id"), col_inline_fk("user_id", "user")],
1592 constraints: vec![],
1593 };
1594
1595 let code = TableDef {
1596 name: "code".to_string(),
1597 description: None,
1598 columns: vec![
1599 col_pk("id"),
1600 col_inline_fk("product_id", "product"),
1601 col_inline_fk("creator_user_id", "user"),
1602 col_inline_fk("project_id", "project"),
1603 ],
1604 constraints: vec![],
1605 };
1606
1607 let order = TableDef {
1608 name: "order".to_string(),
1609 description: None,
1610 columns: vec![
1611 col_pk("id"),
1612 col_inline_fk("user_id", "user"),
1613 col_inline_fk("project_id", "project"),
1614 col_inline_fk("product_id", "product"),
1615 col_inline_fk("code_id", "code"),
1616 ],
1617 constraints: vec![],
1618 };
1619
1620 let payment = TableDef {
1621 name: "payment".to_string(),
1622 description: None,
1623 columns: vec![col_pk("id"), col_inline_fk("order_id", "order")],
1624 constraints: vec![],
1625 };
1626
1627 let result = diff_schemas(&[], &[payment, order, code, project, product, user]);
1629 assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
1630
1631 let plan = result.unwrap();
1632 let create_order: Vec<&str> = plan
1633 .actions
1634 .iter()
1635 .filter_map(|a| {
1636 if let MigrationAction::CreateTable { table, .. } = a {
1637 Some(table.as_str())
1638 } else {
1639 None
1640 }
1641 })
1642 .collect();
1643
1644 let get_pos = |name: &str| create_order.iter().position(|&t| t == name).unwrap();
1646
1647 assert!(
1650 get_pos("user") < get_pos("project"),
1651 "user must come before project"
1652 );
1653 assert!(
1655 get_pos("product") < get_pos("code"),
1656 "product must come before code"
1657 );
1658 assert!(
1659 get_pos("user") < get_pos("code"),
1660 "user must come before code"
1661 );
1662 assert!(
1663 get_pos("project") < get_pos("code"),
1664 "project must come before code"
1665 );
1666 assert!(
1668 get_pos("code") < get_pos("order"),
1669 "code must come before order"
1670 );
1671 assert!(
1673 get_pos("order") < get_pos("payment"),
1674 "order must come before payment"
1675 );
1676 }
1677
1678 #[test]
1680 fn create_tables_with_duplicate_fk_references() {
1681 use super::*;
1682 use vespertide_core::schema::foreign_key::ForeignKeySyntax;
1683 use vespertide_core::schema::primary_key::PrimaryKeySyntax;
1684
1685 fn col_pk(name: &str) -> ColumnDef {
1686 ColumnDef {
1687 name: name.to_string(),
1688 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1689 nullable: false,
1690 default: None,
1691 comment: None,
1692 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1693 unique: None,
1694 index: None,
1695 foreign_key: None,
1696 }
1697 }
1698
1699 fn col_inline_fk(name: &str, ref_table: &str) -> ColumnDef {
1700 ColumnDef {
1701 name: name.to_string(),
1702 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1703 nullable: true,
1704 default: None,
1705 comment: None,
1706 primary_key: None,
1707 unique: None,
1708 index: None,
1709 foreign_key: Some(ForeignKeySyntax::String(format!("{}.id", ref_table))),
1710 }
1711 }
1712
1713 let user = TableDef {
1715 name: "user".to_string(),
1716 description: None,
1717 columns: vec![col_pk("id")],
1718 constraints: vec![],
1719 };
1720
1721 let code = TableDef {
1722 name: "code".to_string(),
1723 description: None,
1724 columns: vec![
1725 col_pk("id"),
1726 col_inline_fk("creator_user_id", "user"),
1727 col_inline_fk("used_by_user_id", "user"), ],
1729 constraints: vec![],
1730 };
1731
1732 let result = diff_schemas(&[], &[code, user]);
1734 assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
1735
1736 let plan = result.unwrap();
1737 let create_order: Vec<&str> = plan
1738 .actions
1739 .iter()
1740 .filter_map(|a| {
1741 if let MigrationAction::CreateTable { table, .. } = a {
1742 Some(table.as_str())
1743 } else {
1744 None
1745 }
1746 })
1747 .collect();
1748
1749 let user_pos = create_order.iter().position(|&t| t == "user").unwrap();
1751 let code_pos = create_order.iter().position(|&t| t == "code").unwrap();
1752 assert!(user_pos < code_pos, "user must come before code");
1753 }
1754 }
1755
1756 mod primary_key_changes {
1757 use super::*;
1758
1759 fn pk(columns: Vec<&str>) -> TableConstraint {
1760 TableConstraint::PrimaryKey {
1761 auto_increment: false,
1762 columns: columns.into_iter().map(|s| s.to_string()).collect(),
1763 }
1764 }
1765
1766 #[test]
1767 fn add_column_to_composite_pk() {
1768 let from = vec![table(
1770 "users",
1771 vec![
1772 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1773 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
1774 ],
1775 vec![pk(vec!["id"])],
1776 )];
1777
1778 let to = vec![table(
1779 "users",
1780 vec![
1781 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1782 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
1783 ],
1784 vec![pk(vec!["id", "tenant_id"])],
1785 )];
1786
1787 let plan = diff_schemas(&from, &to).unwrap();
1788
1789 assert_eq!(plan.actions.len(), 2);
1791
1792 let has_remove = plan.actions.iter().any(|a| {
1793 matches!(
1794 a,
1795 MigrationAction::RemoveConstraint {
1796 table,
1797 constraint: TableConstraint::PrimaryKey { columns, .. }
1798 } if table == "users" && columns == &vec!["id".to_string()]
1799 )
1800 });
1801 assert!(has_remove, "Should have RemoveConstraint for old PK");
1802
1803 let has_add = plan.actions.iter().any(|a| {
1804 matches!(
1805 a,
1806 MigrationAction::AddConstraint {
1807 table,
1808 constraint: TableConstraint::PrimaryKey { columns, .. }
1809 } if table == "users" && columns == &vec!["id".to_string(), "tenant_id".to_string()]
1810 )
1811 });
1812 assert!(has_add, "Should have AddConstraint for new composite PK");
1813 }
1814
1815 #[test]
1816 fn remove_column_from_composite_pk() {
1817 let from = vec![table(
1819 "users",
1820 vec![
1821 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1822 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
1823 ],
1824 vec![pk(vec!["id", "tenant_id"])],
1825 )];
1826
1827 let to = vec![table(
1828 "users",
1829 vec![
1830 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1831 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
1832 ],
1833 vec![pk(vec!["id"])],
1834 )];
1835
1836 let plan = diff_schemas(&from, &to).unwrap();
1837
1838 assert_eq!(plan.actions.len(), 2);
1840
1841 let has_remove = plan.actions.iter().any(|a| {
1842 matches!(
1843 a,
1844 MigrationAction::RemoveConstraint {
1845 table,
1846 constraint: TableConstraint::PrimaryKey { columns, .. }
1847 } if table == "users" && columns == &vec!["id".to_string(), "tenant_id".to_string()]
1848 )
1849 });
1850 assert!(
1851 has_remove,
1852 "Should have RemoveConstraint for old composite PK"
1853 );
1854
1855 let has_add = plan.actions.iter().any(|a| {
1856 matches!(
1857 a,
1858 MigrationAction::AddConstraint {
1859 table,
1860 constraint: TableConstraint::PrimaryKey { columns, .. }
1861 } if table == "users" && columns == &vec!["id".to_string()]
1862 )
1863 });
1864 assert!(
1865 has_add,
1866 "Should have AddConstraint for new single-column PK"
1867 );
1868 }
1869
1870 #[test]
1871 fn change_pk_columns_entirely() {
1872 let from = vec![table(
1874 "users",
1875 vec![
1876 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1877 col("uuid", ColumnType::Simple(SimpleColumnType::Text)),
1878 ],
1879 vec![pk(vec!["id"])],
1880 )];
1881
1882 let to = vec![table(
1883 "users",
1884 vec![
1885 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1886 col("uuid", ColumnType::Simple(SimpleColumnType::Text)),
1887 ],
1888 vec![pk(vec!["uuid"])],
1889 )];
1890
1891 let plan = diff_schemas(&from, &to).unwrap();
1892
1893 assert_eq!(plan.actions.len(), 2);
1894
1895 let has_remove = plan.actions.iter().any(|a| {
1896 matches!(
1897 a,
1898 MigrationAction::RemoveConstraint {
1899 table,
1900 constraint: TableConstraint::PrimaryKey { columns, .. }
1901 } if table == "users" && columns == &vec!["id".to_string()]
1902 )
1903 });
1904 assert!(has_remove, "Should have RemoveConstraint for old PK");
1905
1906 let has_add = plan.actions.iter().any(|a| {
1907 matches!(
1908 a,
1909 MigrationAction::AddConstraint {
1910 table,
1911 constraint: TableConstraint::PrimaryKey { columns, .. }
1912 } if table == "users" && columns == &vec!["uuid".to_string()]
1913 )
1914 });
1915 assert!(has_add, "Should have AddConstraint for new PK");
1916 }
1917
1918 #[test]
1919 fn add_multiple_columns_to_composite_pk() {
1920 let from = vec![table(
1922 "users",
1923 vec![
1924 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1925 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
1926 col("region_id", ColumnType::Simple(SimpleColumnType::Integer)),
1927 ],
1928 vec![pk(vec!["id"])],
1929 )];
1930
1931 let to = vec![table(
1932 "users",
1933 vec![
1934 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1935 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
1936 col("region_id", ColumnType::Simple(SimpleColumnType::Integer)),
1937 ],
1938 vec![pk(vec!["id", "tenant_id", "region_id"])],
1939 )];
1940
1941 let plan = diff_schemas(&from, &to).unwrap();
1942
1943 assert_eq!(plan.actions.len(), 2);
1944
1945 let has_remove = plan.actions.iter().any(|a| {
1946 matches!(
1947 a,
1948 MigrationAction::RemoveConstraint {
1949 table,
1950 constraint: TableConstraint::PrimaryKey { columns, .. }
1951 } if table == "users" && columns == &vec!["id".to_string()]
1952 )
1953 });
1954 assert!(
1955 has_remove,
1956 "Should have RemoveConstraint for old single-column PK"
1957 );
1958
1959 let has_add = plan.actions.iter().any(|a| {
1960 matches!(
1961 a,
1962 MigrationAction::AddConstraint {
1963 table,
1964 constraint: TableConstraint::PrimaryKey { columns, .. }
1965 } if table == "users" && columns == &vec![
1966 "id".to_string(),
1967 "tenant_id".to_string(),
1968 "region_id".to_string()
1969 ]
1970 )
1971 });
1972 assert!(
1973 has_add,
1974 "Should have AddConstraint for new 3-column composite PK"
1975 );
1976 }
1977
1978 #[test]
1979 fn remove_multiple_columns_from_composite_pk() {
1980 let from = vec![table(
1982 "users",
1983 vec![
1984 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1985 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
1986 col("region_id", ColumnType::Simple(SimpleColumnType::Integer)),
1987 ],
1988 vec![pk(vec!["id", "tenant_id", "region_id"])],
1989 )];
1990
1991 let to = vec![table(
1992 "users",
1993 vec![
1994 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1995 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
1996 col("region_id", ColumnType::Simple(SimpleColumnType::Integer)),
1997 ],
1998 vec![pk(vec!["id"])],
1999 )];
2000
2001 let plan = diff_schemas(&from, &to).unwrap();
2002
2003 assert_eq!(plan.actions.len(), 2);
2004
2005 let has_remove = plan.actions.iter().any(|a| {
2006 matches!(
2007 a,
2008 MigrationAction::RemoveConstraint {
2009 table,
2010 constraint: TableConstraint::PrimaryKey { columns, .. }
2011 } if table == "users" && columns == &vec![
2012 "id".to_string(),
2013 "tenant_id".to_string(),
2014 "region_id".to_string()
2015 ]
2016 )
2017 });
2018 assert!(
2019 has_remove,
2020 "Should have RemoveConstraint for old 3-column composite PK"
2021 );
2022
2023 let has_add = plan.actions.iter().any(|a| {
2024 matches!(
2025 a,
2026 MigrationAction::AddConstraint {
2027 table,
2028 constraint: TableConstraint::PrimaryKey { columns, .. }
2029 } if table == "users" && columns == &vec!["id".to_string()]
2030 )
2031 });
2032 assert!(
2033 has_add,
2034 "Should have AddConstraint for new single-column PK"
2035 );
2036 }
2037
2038 #[test]
2039 fn change_composite_pk_columns_partially() {
2040 let from = vec![table(
2043 "users",
2044 vec![
2045 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2046 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
2047 col("region_id", ColumnType::Simple(SimpleColumnType::Integer)),
2048 ],
2049 vec![pk(vec!["id", "tenant_id"])],
2050 )];
2051
2052 let to = vec![table(
2053 "users",
2054 vec![
2055 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2056 col("tenant_id", ColumnType::Simple(SimpleColumnType::Integer)),
2057 col("region_id", ColumnType::Simple(SimpleColumnType::Integer)),
2058 ],
2059 vec![pk(vec!["id", "region_id"])],
2060 )];
2061
2062 let plan = diff_schemas(&from, &to).unwrap();
2063
2064 assert_eq!(plan.actions.len(), 2);
2065
2066 let has_remove = plan.actions.iter().any(|a| {
2067 matches!(
2068 a,
2069 MigrationAction::RemoveConstraint {
2070 table,
2071 constraint: TableConstraint::PrimaryKey { columns, .. }
2072 } if table == "users" && columns == &vec!["id".to_string(), "tenant_id".to_string()]
2073 )
2074 });
2075 assert!(
2076 has_remove,
2077 "Should have RemoveConstraint for old PK with tenant_id"
2078 );
2079
2080 let has_add = plan.actions.iter().any(|a| {
2081 matches!(
2082 a,
2083 MigrationAction::AddConstraint {
2084 table,
2085 constraint: TableConstraint::PrimaryKey { columns, .. }
2086 } if table == "users" && columns == &vec!["id".to_string(), "region_id".to_string()]
2087 )
2088 });
2089 assert!(
2090 has_add,
2091 "Should have AddConstraint for new PK with region_id"
2092 );
2093 }
2094 }
2095
2096 mod default_changes {
2097 use super::*;
2098
2099 fn col_with_default(name: &str, ty: ColumnType, default: Option<&str>) -> ColumnDef {
2100 ColumnDef {
2101 name: name.to_string(),
2102 r#type: ty,
2103 nullable: true,
2104 default: default.map(|s| s.into()),
2105 comment: None,
2106 primary_key: None,
2107 unique: None,
2108 index: None,
2109 foreign_key: None,
2110 }
2111 }
2112
2113 #[test]
2114 fn add_default_value() {
2115 let from = vec![table(
2117 "users",
2118 vec![
2119 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2120 col_with_default("status", ColumnType::Simple(SimpleColumnType::Text), None),
2121 ],
2122 vec![],
2123 )];
2124
2125 let to = vec![table(
2126 "users",
2127 vec![
2128 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2129 col_with_default(
2130 "status",
2131 ColumnType::Simple(SimpleColumnType::Text),
2132 Some("'active'"),
2133 ),
2134 ],
2135 vec![],
2136 )];
2137
2138 let plan = diff_schemas(&from, &to).unwrap();
2139
2140 assert_eq!(plan.actions.len(), 1);
2141 assert!(matches!(
2142 &plan.actions[0],
2143 MigrationAction::ModifyColumnDefault {
2144 table,
2145 column,
2146 new_default: Some(default),
2147 } if table == "users" && column == "status" && default == "'active'"
2148 ));
2149 }
2150
2151 #[test]
2152 fn remove_default_value() {
2153 let from = vec![table(
2155 "users",
2156 vec![
2157 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2158 col_with_default(
2159 "status",
2160 ColumnType::Simple(SimpleColumnType::Text),
2161 Some("'active'"),
2162 ),
2163 ],
2164 vec![],
2165 )];
2166
2167 let to = vec![table(
2168 "users",
2169 vec![
2170 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2171 col_with_default("status", ColumnType::Simple(SimpleColumnType::Text), None),
2172 ],
2173 vec![],
2174 )];
2175
2176 let plan = diff_schemas(&from, &to).unwrap();
2177
2178 assert_eq!(plan.actions.len(), 1);
2179 assert!(matches!(
2180 &plan.actions[0],
2181 MigrationAction::ModifyColumnDefault {
2182 table,
2183 column,
2184 new_default: None,
2185 } if table == "users" && column == "status"
2186 ));
2187 }
2188
2189 #[test]
2190 fn change_default_value() {
2191 let from = vec![table(
2193 "users",
2194 vec![
2195 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2196 col_with_default(
2197 "status",
2198 ColumnType::Simple(SimpleColumnType::Text),
2199 Some("'active'"),
2200 ),
2201 ],
2202 vec![],
2203 )];
2204
2205 let to = vec![table(
2206 "users",
2207 vec![
2208 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2209 col_with_default(
2210 "status",
2211 ColumnType::Simple(SimpleColumnType::Text),
2212 Some("'pending'"),
2213 ),
2214 ],
2215 vec![],
2216 )];
2217
2218 let plan = diff_schemas(&from, &to).unwrap();
2219
2220 assert_eq!(plan.actions.len(), 1);
2221 assert!(matches!(
2222 &plan.actions[0],
2223 MigrationAction::ModifyColumnDefault {
2224 table,
2225 column,
2226 new_default: Some(default),
2227 } if table == "users" && column == "status" && default == "'pending'"
2228 ));
2229 }
2230
2231 #[test]
2232 fn no_change_same_default() {
2233 let from = vec![table(
2235 "users",
2236 vec![
2237 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2238 col_with_default(
2239 "status",
2240 ColumnType::Simple(SimpleColumnType::Text),
2241 Some("'active'"),
2242 ),
2243 ],
2244 vec![],
2245 )];
2246
2247 let to = vec![table(
2248 "users",
2249 vec![
2250 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2251 col_with_default(
2252 "status",
2253 ColumnType::Simple(SimpleColumnType::Text),
2254 Some("'active'"),
2255 ),
2256 ],
2257 vec![],
2258 )];
2259
2260 let plan = diff_schemas(&from, &to).unwrap();
2261
2262 assert!(plan.actions.is_empty());
2263 }
2264
2265 #[test]
2266 fn multiple_columns_default_changes() {
2267 let from = vec![table(
2269 "users",
2270 vec![
2271 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2272 col_with_default("status", ColumnType::Simple(SimpleColumnType::Text), None),
2273 col_with_default(
2274 "role",
2275 ColumnType::Simple(SimpleColumnType::Text),
2276 Some("'user'"),
2277 ),
2278 col_with_default(
2279 "active",
2280 ColumnType::Simple(SimpleColumnType::Boolean),
2281 Some("true"),
2282 ),
2283 ],
2284 vec![],
2285 )];
2286
2287 let to = vec![table(
2288 "users",
2289 vec![
2290 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2291 col_with_default(
2292 "status",
2293 ColumnType::Simple(SimpleColumnType::Text),
2294 Some("'pending'"),
2295 ), col_with_default("role", ColumnType::Simple(SimpleColumnType::Text), None), col_with_default(
2298 "active",
2299 ColumnType::Simple(SimpleColumnType::Boolean),
2300 Some("true"),
2301 ), ],
2303 vec![],
2304 )];
2305
2306 let plan = diff_schemas(&from, &to).unwrap();
2307
2308 assert_eq!(plan.actions.len(), 2);
2309
2310 let has_status_change = plan.actions.iter().any(|a| {
2311 matches!(
2312 a,
2313 MigrationAction::ModifyColumnDefault {
2314 table,
2315 column,
2316 new_default: Some(default),
2317 } if table == "users" && column == "status" && default == "'pending'"
2318 )
2319 });
2320 assert!(has_status_change, "Should detect status default added");
2321
2322 let has_role_change = plan.actions.iter().any(|a| {
2323 matches!(
2324 a,
2325 MigrationAction::ModifyColumnDefault {
2326 table,
2327 column,
2328 new_default: None,
2329 } if table == "users" && column == "role"
2330 )
2331 });
2332 assert!(has_role_change, "Should detect role default removed");
2333 }
2334
2335 #[test]
2336 fn default_change_with_type_change() {
2337 let from = vec![table(
2339 "users",
2340 vec![
2341 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2342 col_with_default(
2343 "count",
2344 ColumnType::Simple(SimpleColumnType::Integer),
2345 Some("0"),
2346 ),
2347 ],
2348 vec![],
2349 )];
2350
2351 let to = vec![table(
2352 "users",
2353 vec![
2354 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2355 col_with_default(
2356 "count",
2357 ColumnType::Simple(SimpleColumnType::Text),
2358 Some("'0'"),
2359 ),
2360 ],
2361 vec![],
2362 )];
2363
2364 let plan = diff_schemas(&from, &to).unwrap();
2365
2366 assert_eq!(plan.actions.len(), 2);
2368
2369 let has_type_change = plan.actions.iter().any(|a| {
2370 matches!(
2371 a,
2372 MigrationAction::ModifyColumnType { table, column, .. }
2373 if table == "users" && column == "count"
2374 )
2375 });
2376 assert!(has_type_change, "Should detect type change");
2377
2378 let has_default_change = plan.actions.iter().any(|a| {
2379 matches!(
2380 a,
2381 MigrationAction::ModifyColumnDefault {
2382 table,
2383 column,
2384 new_default: Some(default),
2385 } if table == "users" && column == "count" && default == "'0'"
2386 )
2387 });
2388 assert!(has_default_change, "Should detect default change");
2389 }
2390 }
2391
2392 mod comment_changes {
2393 use super::*;
2394
2395 fn col_with_comment(name: &str, ty: ColumnType, comment: Option<&str>) -> ColumnDef {
2396 ColumnDef {
2397 name: name.to_string(),
2398 r#type: ty,
2399 nullable: true,
2400 default: None,
2401 comment: comment.map(|s| s.to_string()),
2402 primary_key: None,
2403 unique: None,
2404 index: None,
2405 foreign_key: None,
2406 }
2407 }
2408
2409 #[test]
2410 fn add_comment() {
2411 let from = vec![table(
2413 "users",
2414 vec![
2415 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2416 col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), None),
2417 ],
2418 vec![],
2419 )];
2420
2421 let to = vec![table(
2422 "users",
2423 vec![
2424 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2425 col_with_comment(
2426 "email",
2427 ColumnType::Simple(SimpleColumnType::Text),
2428 Some("User's email address"),
2429 ),
2430 ],
2431 vec![],
2432 )];
2433
2434 let plan = diff_schemas(&from, &to).unwrap();
2435
2436 assert_eq!(plan.actions.len(), 1);
2437 assert!(matches!(
2438 &plan.actions[0],
2439 MigrationAction::ModifyColumnComment {
2440 table,
2441 column,
2442 new_comment: Some(comment),
2443 } if table == "users" && column == "email" && comment == "User's email address"
2444 ));
2445 }
2446
2447 #[test]
2448 fn remove_comment() {
2449 let from = vec![table(
2451 "users",
2452 vec![
2453 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2454 col_with_comment(
2455 "email",
2456 ColumnType::Simple(SimpleColumnType::Text),
2457 Some("User's email address"),
2458 ),
2459 ],
2460 vec![],
2461 )];
2462
2463 let to = vec![table(
2464 "users",
2465 vec![
2466 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2467 col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), None),
2468 ],
2469 vec![],
2470 )];
2471
2472 let plan = diff_schemas(&from, &to).unwrap();
2473
2474 assert_eq!(plan.actions.len(), 1);
2475 assert!(matches!(
2476 &plan.actions[0],
2477 MigrationAction::ModifyColumnComment {
2478 table,
2479 column,
2480 new_comment: None,
2481 } if table == "users" && column == "email"
2482 ));
2483 }
2484
2485 #[test]
2486 fn change_comment() {
2487 let from = vec![table(
2489 "users",
2490 vec![
2491 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2492 col_with_comment(
2493 "email",
2494 ColumnType::Simple(SimpleColumnType::Text),
2495 Some("Old comment"),
2496 ),
2497 ],
2498 vec![],
2499 )];
2500
2501 let to = vec![table(
2502 "users",
2503 vec![
2504 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2505 col_with_comment(
2506 "email",
2507 ColumnType::Simple(SimpleColumnType::Text),
2508 Some("New comment"),
2509 ),
2510 ],
2511 vec![],
2512 )];
2513
2514 let plan = diff_schemas(&from, &to).unwrap();
2515
2516 assert_eq!(plan.actions.len(), 1);
2517 assert!(matches!(
2518 &plan.actions[0],
2519 MigrationAction::ModifyColumnComment {
2520 table,
2521 column,
2522 new_comment: Some(comment),
2523 } if table == "users" && column == "email" && comment == "New comment"
2524 ));
2525 }
2526
2527 #[test]
2528 fn no_change_same_comment() {
2529 let from = vec![table(
2531 "users",
2532 vec![
2533 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2534 col_with_comment(
2535 "email",
2536 ColumnType::Simple(SimpleColumnType::Text),
2537 Some("Same comment"),
2538 ),
2539 ],
2540 vec![],
2541 )];
2542
2543 let to = vec![table(
2544 "users",
2545 vec![
2546 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2547 col_with_comment(
2548 "email",
2549 ColumnType::Simple(SimpleColumnType::Text),
2550 Some("Same comment"),
2551 ),
2552 ],
2553 vec![],
2554 )];
2555
2556 let plan = diff_schemas(&from, &to).unwrap();
2557
2558 assert!(plan.actions.is_empty());
2559 }
2560
2561 #[test]
2562 fn multiple_columns_comment_changes() {
2563 let from = vec![table(
2565 "users",
2566 vec![
2567 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2568 col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), None),
2569 col_with_comment(
2570 "name",
2571 ColumnType::Simple(SimpleColumnType::Text),
2572 Some("User name"),
2573 ),
2574 col_with_comment(
2575 "phone",
2576 ColumnType::Simple(SimpleColumnType::Text),
2577 Some("Phone number"),
2578 ),
2579 ],
2580 vec![],
2581 )];
2582
2583 let to = vec![table(
2584 "users",
2585 vec![
2586 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2587 col_with_comment(
2588 "email",
2589 ColumnType::Simple(SimpleColumnType::Text),
2590 Some("Email address"),
2591 ), col_with_comment("name", ColumnType::Simple(SimpleColumnType::Text), None), col_with_comment(
2594 "phone",
2595 ColumnType::Simple(SimpleColumnType::Text),
2596 Some("Phone number"),
2597 ), ],
2599 vec![],
2600 )];
2601
2602 let plan = diff_schemas(&from, &to).unwrap();
2603
2604 assert_eq!(plan.actions.len(), 2);
2605
2606 let has_email_change = plan.actions.iter().any(|a| {
2607 matches!(
2608 a,
2609 MigrationAction::ModifyColumnComment {
2610 table,
2611 column,
2612 new_comment: Some(comment),
2613 } if table == "users" && column == "email" && comment == "Email address"
2614 )
2615 });
2616 assert!(has_email_change, "Should detect email comment added");
2617
2618 let has_name_change = plan.actions.iter().any(|a| {
2619 matches!(
2620 a,
2621 MigrationAction::ModifyColumnComment {
2622 table,
2623 column,
2624 new_comment: None,
2625 } if table == "users" && column == "name"
2626 )
2627 });
2628 assert!(has_name_change, "Should detect name comment removed");
2629 }
2630
2631 #[test]
2632 fn comment_change_with_nullable_change() {
2633 let from = vec![table(
2635 "users",
2636 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), {
2637 let mut c =
2638 col_with_comment("email", ColumnType::Simple(SimpleColumnType::Text), None);
2639 c.nullable = true;
2640 c
2641 }],
2642 vec![],
2643 )];
2644
2645 let to = vec![table(
2646 "users",
2647 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), {
2648 let mut c = col_with_comment(
2649 "email",
2650 ColumnType::Simple(SimpleColumnType::Text),
2651 Some("Required email"),
2652 );
2653 c.nullable = false;
2654 c
2655 }],
2656 vec![],
2657 )];
2658
2659 let plan = diff_schemas(&from, &to).unwrap();
2660
2661 assert_eq!(plan.actions.len(), 2);
2663
2664 let has_nullable_change = plan.actions.iter().any(|a| {
2665 matches!(
2666 a,
2667 MigrationAction::ModifyColumnNullable {
2668 table,
2669 column,
2670 nullable: false,
2671 ..
2672 } if table == "users" && column == "email"
2673 )
2674 });
2675 assert!(has_nullable_change, "Should detect nullable change");
2676
2677 let has_comment_change = plan.actions.iter().any(|a| {
2678 matches!(
2679 a,
2680 MigrationAction::ModifyColumnComment {
2681 table,
2682 column,
2683 new_comment: Some(comment),
2684 } if table == "users" && column == "email" && comment == "Required email"
2685 )
2686 });
2687 assert!(has_comment_change, "Should detect comment change");
2688 }
2689 }
2690
2691 mod nullable_changes {
2692 use super::*;
2693
2694 fn col_nullable(name: &str, ty: ColumnType, nullable: bool) -> ColumnDef {
2695 ColumnDef {
2696 name: name.to_string(),
2697 r#type: ty,
2698 nullable,
2699 default: None,
2700 comment: None,
2701 primary_key: None,
2702 unique: None,
2703 index: None,
2704 foreign_key: None,
2705 }
2706 }
2707
2708 #[test]
2709 fn column_nullable_to_non_nullable() {
2710 let from = vec![table(
2712 "users",
2713 vec![
2714 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2715 col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), true),
2716 ],
2717 vec![],
2718 )];
2719
2720 let to = vec![table(
2721 "users",
2722 vec![
2723 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2724 col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), false),
2725 ],
2726 vec![],
2727 )];
2728
2729 let plan = diff_schemas(&from, &to).unwrap();
2730
2731 assert_eq!(plan.actions.len(), 1);
2732 assert!(matches!(
2733 &plan.actions[0],
2734 MigrationAction::ModifyColumnNullable {
2735 table,
2736 column,
2737 nullable: false,
2738 fill_with: None,
2739 } if table == "users" && column == "email"
2740 ));
2741 }
2742
2743 #[test]
2744 fn column_non_nullable_to_nullable() {
2745 let from = vec![table(
2747 "users",
2748 vec![
2749 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2750 col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), false),
2751 ],
2752 vec![],
2753 )];
2754
2755 let to = vec![table(
2756 "users",
2757 vec![
2758 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2759 col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), true),
2760 ],
2761 vec![],
2762 )];
2763
2764 let plan = diff_schemas(&from, &to).unwrap();
2765
2766 assert_eq!(plan.actions.len(), 1);
2767 assert!(matches!(
2768 &plan.actions[0],
2769 MigrationAction::ModifyColumnNullable {
2770 table,
2771 column,
2772 nullable: true,
2773 fill_with: None,
2774 } if table == "users" && column == "email"
2775 ));
2776 }
2777
2778 #[test]
2779 fn multiple_columns_nullable_changes() {
2780 let from = vec![table(
2782 "users",
2783 vec![
2784 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2785 col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), true),
2786 col_nullable("name", ColumnType::Simple(SimpleColumnType::Text), false),
2787 col_nullable("phone", ColumnType::Simple(SimpleColumnType::Text), true),
2788 ],
2789 vec![],
2790 )];
2791
2792 let to = vec![table(
2793 "users",
2794 vec![
2795 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2796 col_nullable("email", ColumnType::Simple(SimpleColumnType::Text), false), col_nullable("name", ColumnType::Simple(SimpleColumnType::Text), true), col_nullable("phone", ColumnType::Simple(SimpleColumnType::Text), true), ],
2800 vec![],
2801 )];
2802
2803 let plan = diff_schemas(&from, &to).unwrap();
2804
2805 assert_eq!(plan.actions.len(), 2);
2806
2807 let has_email_change = plan.actions.iter().any(|a| {
2808 matches!(
2809 a,
2810 MigrationAction::ModifyColumnNullable {
2811 table,
2812 column,
2813 nullable: false,
2814 ..
2815 } if table == "users" && column == "email"
2816 )
2817 });
2818 assert!(
2819 has_email_change,
2820 "Should detect email nullable -> non-nullable"
2821 );
2822
2823 let has_name_change = plan.actions.iter().any(|a| {
2824 matches!(
2825 a,
2826 MigrationAction::ModifyColumnNullable {
2827 table,
2828 column,
2829 nullable: true,
2830 ..
2831 } if table == "users" && column == "name"
2832 )
2833 });
2834 assert!(
2835 has_name_change,
2836 "Should detect name non-nullable -> nullable"
2837 );
2838 }
2839
2840 #[test]
2841 fn nullable_change_with_type_change() {
2842 let from = vec![table(
2844 "users",
2845 vec![
2846 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2847 col_nullable("age", ColumnType::Simple(SimpleColumnType::Integer), true),
2848 ],
2849 vec![],
2850 )];
2851
2852 let to = vec![table(
2853 "users",
2854 vec![
2855 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
2856 col_nullable("age", ColumnType::Simple(SimpleColumnType::Text), false),
2857 ],
2858 vec![],
2859 )];
2860
2861 let plan = diff_schemas(&from, &to).unwrap();
2862
2863 assert_eq!(plan.actions.len(), 2);
2865
2866 let has_type_change = plan.actions.iter().any(|a| {
2867 matches!(
2868 a,
2869 MigrationAction::ModifyColumnType { table, column, .. }
2870 if table == "users" && column == "age"
2871 )
2872 });
2873 assert!(has_type_change, "Should detect type change");
2874
2875 let has_nullable_change = plan.actions.iter().any(|a| {
2876 matches!(
2877 a,
2878 MigrationAction::ModifyColumnNullable {
2879 table,
2880 column,
2881 nullable: false,
2882 ..
2883 } if table == "users" && column == "age"
2884 )
2885 });
2886 assert!(has_nullable_change, "Should detect nullable change");
2887 }
2888 }
2889
2890 mod diff_tables {
2891 use insta::assert_debug_snapshot;
2892
2893 use super::*;
2894
2895 #[test]
2896 fn create_table_with_inline_index() {
2897 let base = [table(
2898 "users",
2899 vec![
2900 ColumnDef {
2901 name: "id".to_string(),
2902 r#type: ColumnType::Simple(SimpleColumnType::Integer),
2903 nullable: false,
2904 default: None,
2905 comment: None,
2906 primary_key: Some(PrimaryKeySyntax::Bool(true)),
2907 unique: None,
2908 index: Some(StrOrBoolOrArray::Bool(false)),
2909 foreign_key: None,
2910 },
2911 ColumnDef {
2912 name: "name".to_string(),
2913 r#type: ColumnType::Simple(SimpleColumnType::Text),
2914 nullable: true,
2915 default: None,
2916 comment: None,
2917 primary_key: None,
2918 unique: Some(StrOrBoolOrArray::Bool(true)),
2919 index: Some(StrOrBoolOrArray::Bool(true)),
2920 foreign_key: None,
2921 },
2922 ],
2923 vec![],
2924 )];
2925 let plan = diff_schemas(&[], &base).unwrap();
2926
2927 assert_eq!(plan.actions.len(), 1);
2928 assert_debug_snapshot!(plan.actions);
2929
2930 let plan = diff_schemas(
2931 &base,
2932 &[table(
2933 "users",
2934 vec![
2935 ColumnDef {
2936 name: "id".to_string(),
2937 r#type: ColumnType::Simple(SimpleColumnType::Integer),
2938 nullable: false,
2939 default: None,
2940 comment: None,
2941 primary_key: Some(PrimaryKeySyntax::Bool(true)),
2942 unique: None,
2943 index: Some(StrOrBoolOrArray::Bool(false)),
2944 foreign_key: None,
2945 },
2946 ColumnDef {
2947 name: "name".to_string(),
2948 r#type: ColumnType::Simple(SimpleColumnType::Text),
2949 nullable: true,
2950 default: None,
2951 comment: None,
2952 primary_key: None,
2953 unique: Some(StrOrBoolOrArray::Bool(true)),
2954 index: Some(StrOrBoolOrArray::Bool(false)),
2955 foreign_key: None,
2956 },
2957 ],
2958 vec![],
2959 )],
2960 )
2961 .unwrap();
2962
2963 assert_eq!(plan.actions.len(), 1);
2964 assert_debug_snapshot!(plan.actions);
2965 }
2966
2967 #[rstest]
2968 #[case(
2969 "add_index",
2970 vec![table(
2971 "users",
2972 vec![
2973 ColumnDef {
2974 name: "id".to_string(),
2975 r#type: ColumnType::Simple(SimpleColumnType::Integer),
2976 nullable: false,
2977 default: None,
2978 comment: None,
2979 primary_key: Some(PrimaryKeySyntax::Bool(true)),
2980 unique: None,
2981 index: None,
2982 foreign_key: None,
2983 },
2984 ],
2985 vec![],
2986 )],
2987 vec![table(
2988 "users",
2989 vec![
2990 ColumnDef {
2991 name: "id".to_string(),
2992 r#type: ColumnType::Simple(SimpleColumnType::Integer),
2993 nullable: false,
2994 default: None,
2995 comment: None,
2996 primary_key: Some(PrimaryKeySyntax::Bool(true)),
2997 unique: None,
2998 index: Some(StrOrBoolOrArray::Bool(true)),
2999 foreign_key: None,
3000 },
3001 ],
3002 vec![],
3003 )],
3004 )]
3005 #[case(
3006 "remove_index",
3007 vec![table(
3008 "users",
3009 vec![
3010 ColumnDef {
3011 name: "id".to_string(),
3012 r#type: ColumnType::Simple(SimpleColumnType::Integer),
3013 nullable: false,
3014 default: None,
3015 comment: None,
3016 primary_key: Some(PrimaryKeySyntax::Bool(true)),
3017 unique: None,
3018 index: Some(StrOrBoolOrArray::Bool(true)),
3019 foreign_key: None,
3020 },
3021 ],
3022 vec![],
3023 )],
3024 vec![table(
3025 "users",
3026 vec![
3027 ColumnDef {
3028 name: "id".to_string(),
3029 r#type: ColumnType::Simple(SimpleColumnType::Integer),
3030 nullable: false,
3031 default: None,
3032 comment: None,
3033 primary_key: Some(PrimaryKeySyntax::Bool(true)),
3034 unique: None,
3035 index: Some(StrOrBoolOrArray::Bool(false)),
3036 foreign_key: None,
3037 },
3038 ],
3039 vec![],
3040 )],
3041 )]
3042 #[case(
3043 "add_named_index",
3044 vec![table(
3045 "users",
3046 vec![
3047 ColumnDef {
3048 name: "id".to_string(),
3049 r#type: ColumnType::Simple(SimpleColumnType::Integer),
3050 nullable: false,
3051 default: None,
3052 comment: None,
3053 primary_key: Some(PrimaryKeySyntax::Bool(true)),
3054 unique: None,
3055 index: None,
3056 foreign_key: None,
3057 },
3058 ],
3059 vec![],
3060 )],
3061 vec![table(
3062 "users",
3063 vec![
3064 ColumnDef {
3065 name: "id".to_string(),
3066 r#type: ColumnType::Simple(SimpleColumnType::Integer),
3067 nullable: false,
3068 default: None,
3069 comment: None,
3070 primary_key: Some(PrimaryKeySyntax::Bool(true)),
3071 unique: None,
3072 index: Some(StrOrBoolOrArray::Str("hello".to_string())),
3073 foreign_key: None,
3074 },
3075 ],
3076 vec![],
3077 )],
3078 )]
3079 #[case(
3080 "remove_named_index",
3081 vec![table(
3082 "users",
3083 vec![
3084 ColumnDef {
3085 name: "id".to_string(),
3086 r#type: ColumnType::Simple(SimpleColumnType::Integer),
3087 nullable: false,
3088 default: None,
3089 comment: None,
3090 primary_key: Some(PrimaryKeySyntax::Bool(true)),
3091 unique: None,
3092 index: Some(StrOrBoolOrArray::Str("hello".to_string())),
3093 foreign_key: None,
3094 },
3095 ],
3096 vec![],
3097 )],
3098 vec![table(
3099 "users",
3100 vec![
3101 ColumnDef {
3102 name: "id".to_string(),
3103 r#type: ColumnType::Simple(SimpleColumnType::Integer),
3104 nullable: false,
3105 default: None,
3106 comment: None,
3107 primary_key: Some(PrimaryKeySyntax::Bool(true)),
3108 unique: None,
3109 index: None,
3110 foreign_key: None,
3111 },
3112 ],
3113 vec![],
3114 )],
3115 )]
3116 fn diff_tables(#[case] name: &str, #[case] base: Vec<TableDef>, #[case] to: Vec<TableDef>) {
3117 use insta::with_settings;
3118
3119 let plan = diff_schemas(&base, &to).unwrap();
3120 with_settings!({ snapshot_suffix => name }, {
3121 assert_debug_snapshot!(plan.actions);
3122 });
3123 }
3124 }
3125
3126 mod coverage_explicit {
3128 use super::*;
3129
3130 #[test]
3131 fn delete_column_explicit() {
3132 let from = vec![table(
3134 "users",
3135 vec![
3136 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
3137 col("name", ColumnType::Simple(SimpleColumnType::Text)),
3138 ],
3139 vec![],
3140 )];
3141
3142 let to = vec![table(
3143 "users",
3144 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
3145 vec![],
3146 )];
3147
3148 let plan = diff_schemas(&from, &to).unwrap();
3149 assert_eq!(plan.actions.len(), 1);
3150 assert!(matches!(
3151 &plan.actions[0],
3152 MigrationAction::DeleteColumn { table, column }
3153 if table == "users" && column == "name"
3154 ));
3155 }
3156
3157 #[test]
3158 fn add_column_explicit() {
3159 let from = vec![table(
3161 "users",
3162 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
3163 vec![],
3164 )];
3165
3166 let to = vec![table(
3167 "users",
3168 vec![
3169 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
3170 col("email", ColumnType::Simple(SimpleColumnType::Text)),
3171 ],
3172 vec![],
3173 )];
3174
3175 let plan = diff_schemas(&from, &to).unwrap();
3176 assert_eq!(plan.actions.len(), 1);
3177 assert!(matches!(
3178 &plan.actions[0],
3179 MigrationAction::AddColumn { table, column, .. }
3180 if table == "users" && column.name == "email"
3181 ));
3182 }
3183
3184 #[test]
3185 fn remove_constraint_explicit() {
3186 let from = vec![table(
3188 "users",
3189 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
3190 vec![idx("idx_users_id", vec!["id"])],
3191 )];
3192
3193 let to = vec![table(
3194 "users",
3195 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
3196 vec![],
3197 )];
3198
3199 let plan = diff_schemas(&from, &to).unwrap();
3200 assert_eq!(plan.actions.len(), 1);
3201 assert!(matches!(
3202 &plan.actions[0],
3203 MigrationAction::RemoveConstraint { table, constraint }
3204 if table == "users" && matches!(constraint, TableConstraint::Index { name: Some(n), .. } if n == "idx_users_id")
3205 ));
3206 }
3207
3208 #[test]
3209 fn add_constraint_explicit() {
3210 let from = vec![table(
3212 "users",
3213 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
3214 vec![],
3215 )];
3216
3217 let to = vec![table(
3218 "users",
3219 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
3220 vec![idx("idx_users_id", vec!["id"])],
3221 )];
3222
3223 let plan = diff_schemas(&from, &to).unwrap();
3224 assert_eq!(plan.actions.len(), 1);
3225 assert!(matches!(
3226 &plan.actions[0],
3227 MigrationAction::AddConstraint { table, constraint }
3228 if table == "users" && matches!(constraint, TableConstraint::Index { name: Some(n), .. } if n == "idx_users_id")
3229 ));
3230 }
3231 }
3232
3233 mod constraint_removal_on_deleted_columns {
3234 use super::*;
3235
3236 fn fk(columns: Vec<&str>, ref_table: &str, ref_columns: Vec<&str>) -> TableConstraint {
3237 TableConstraint::ForeignKey {
3238 name: None,
3239 columns: columns.into_iter().map(|s| s.to_string()).collect(),
3240 ref_table: ref_table.to_string(),
3241 ref_columns: ref_columns.into_iter().map(|s| s.to_string()).collect(),
3242 on_delete: None,
3243 on_update: None,
3244 }
3245 }
3246
3247 #[test]
3248 fn skip_remove_constraint_when_all_columns_deleted() {
3249 let from = vec![table(
3252 "project",
3253 vec![
3254 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
3255 col("template_id", ColumnType::Simple(SimpleColumnType::Integer)),
3256 ],
3257 vec![
3258 fk(vec!["template_id"], "book_template", vec!["id"]),
3259 idx("ix_project__template_id", vec!["template_id"]),
3260 ],
3261 )];
3262
3263 let to = vec![table(
3264 "project",
3265 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
3266 vec![],
3267 )];
3268
3269 let plan = diff_schemas(&from, &to).unwrap();
3270
3271 assert_eq!(plan.actions.len(), 1);
3273 assert!(matches!(
3274 &plan.actions[0],
3275 MigrationAction::DeleteColumn { table, column }
3276 if table == "project" && column == "template_id"
3277 ));
3278
3279 let has_remove_constraint = plan
3281 .actions
3282 .iter()
3283 .any(|a| matches!(a, MigrationAction::RemoveConstraint { .. }));
3284 assert!(
3285 !has_remove_constraint,
3286 "Should NOT have RemoveConstraint when column is deleted"
3287 );
3288 }
3289
3290 #[test]
3291 fn keep_remove_constraint_when_only_some_columns_deleted() {
3292 let from = vec![table(
3294 "orders",
3295 vec![
3296 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
3297 col("user_id", ColumnType::Simple(SimpleColumnType::Integer)),
3298 col("product_id", ColumnType::Simple(SimpleColumnType::Integer)),
3299 ],
3300 vec![idx(
3301 "ix_orders__user_product",
3302 vec!["user_id", "product_id"],
3303 )],
3304 )];
3305
3306 let to = vec![table(
3307 "orders",
3308 vec![
3309 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
3310 col("user_id", ColumnType::Simple(SimpleColumnType::Integer)),
3311 ],
3313 vec![],
3314 )];
3315
3316 let plan = diff_schemas(&from, &to).unwrap();
3317
3318 let has_delete_column = plan.actions.iter().any(|a| {
3321 matches!(
3322 a,
3323 MigrationAction::DeleteColumn { table, column }
3324 if table == "orders" && column == "product_id"
3325 )
3326 });
3327 assert!(has_delete_column, "Should have DeleteColumn for product_id");
3328
3329 let has_remove_constraint = plan.actions.iter().any(|a| {
3330 matches!(
3331 a,
3332 MigrationAction::RemoveConstraint { table, .. }
3333 if table == "orders"
3334 )
3335 });
3336 assert!(
3337 has_remove_constraint,
3338 "Should have RemoveConstraint for composite index when only some columns deleted"
3339 );
3340 }
3341
3342 #[test]
3343 fn skip_remove_constraint_when_all_composite_columns_deleted() {
3344 let from = vec![table(
3346 "orders",
3347 vec![
3348 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
3349 col("user_id", ColumnType::Simple(SimpleColumnType::Integer)),
3350 col("product_id", ColumnType::Simple(SimpleColumnType::Integer)),
3351 ],
3352 vec![idx(
3353 "ix_orders__user_product",
3354 vec!["user_id", "product_id"],
3355 )],
3356 )];
3357
3358 let to = vec![table(
3359 "orders",
3360 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
3361 vec![],
3362 )];
3363
3364 let plan = diff_schemas(&from, &to).unwrap();
3365
3366 let delete_columns: Vec<_> = plan
3368 .actions
3369 .iter()
3370 .filter(|a| matches!(a, MigrationAction::DeleteColumn { .. }))
3371 .collect();
3372 assert_eq!(
3373 delete_columns.len(),
3374 2,
3375 "Should have 2 DeleteColumn actions"
3376 );
3377
3378 let has_remove_constraint = plan
3379 .actions
3380 .iter()
3381 .any(|a| matches!(a, MigrationAction::RemoveConstraint { .. }));
3382 assert!(
3383 !has_remove_constraint,
3384 "Should NOT have RemoveConstraint when all composite columns deleted"
3385 );
3386 }
3387
3388 #[test]
3389 fn keep_remove_constraint_when_no_columns_deleted() {
3390 let from = vec![table(
3392 "users",
3393 vec![
3394 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
3395 col("email", ColumnType::Simple(SimpleColumnType::Text)),
3396 ],
3397 vec![idx("ix_users__email", vec!["email"])],
3398 )];
3399
3400 let to = vec![table(
3401 "users",
3402 vec![
3403 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
3404 col("email", ColumnType::Simple(SimpleColumnType::Text)),
3405 ],
3406 vec![], )];
3408
3409 let plan = diff_schemas(&from, &to).unwrap();
3410
3411 assert_eq!(plan.actions.len(), 1);
3412 assert!(matches!(
3413 &plan.actions[0],
3414 MigrationAction::RemoveConstraint { table, .. }
3415 if table == "users"
3416 ));
3417 }
3418 }
3419}