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 for col in from_cols.keys() {
291 if !to_cols.contains_key(col) {
292 actions.push(MigrationAction::DeleteColumn {
293 table: (*name).to_string(),
294 column: (*col).to_string(),
295 });
296 }
297 }
298
299 for (col, to_def) in &to_cols {
301 if let Some(from_def) = from_cols.get(col)
302 && from_def.r#type.requires_migration(&to_def.r#type)
303 {
304 actions.push(MigrationAction::ModifyColumnType {
305 table: (*name).to_string(),
306 column: (*col).to_string(),
307 new_type: to_def.r#type.clone(),
308 });
309 }
310 }
311
312 for (col, def) in &to_cols {
316 if !from_cols.contains_key(col) {
317 actions.push(MigrationAction::AddColumn {
318 table: (*name).to_string(),
319 column: Box::new((*def).clone()),
320 fill_with: None,
321 });
322 }
323 }
324
325 for from_constraint in &from_tbl.constraints {
327 if !to_tbl.constraints.contains(from_constraint) {
328 actions.push(MigrationAction::RemoveConstraint {
329 table: (*name).to_string(),
330 constraint: from_constraint.clone(),
331 });
332 }
333 }
334 for to_constraint in &to_tbl.constraints {
335 if !from_tbl.constraints.contains(to_constraint) {
336 actions.push(MigrationAction::AddConstraint {
337 table: (*name).to_string(),
338 constraint: to_constraint.clone(),
339 });
340 }
341 }
342 }
343 }
344
345 let new_tables: Vec<&TableDef> = to_map
349 .iter()
350 .filter(|(name, _)| !from_map.contains_key(*name))
351 .map(|(_, tbl)| *tbl)
352 .collect();
353
354 let sorted_new_tables = topological_sort_tables(&new_tables)?;
355
356 for tbl in sorted_new_tables {
357 let original_tbl = to_original_map.get(tbl.name.as_str()).unwrap();
359 actions.push(MigrationAction::CreateTable {
360 table: original_tbl.name.clone(),
361 columns: original_tbl.columns.clone(),
362 constraints: original_tbl.constraints.clone(),
363 });
364 }
365
366 sort_delete_tables(&mut actions, &from_map);
368
369 Ok(MigrationPlan {
370 comment: None,
371 created_at: None,
372 version: 0,
373 actions,
374 })
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use rstest::rstest;
381 use vespertide_core::{
382 ColumnDef, ColumnType, MigrationAction, SimpleColumnType,
383 schema::{primary_key::PrimaryKeySyntax, str_or_bool::StrOrBoolOrArray},
384 };
385
386 fn col(name: &str, ty: ColumnType) -> ColumnDef {
387 ColumnDef {
388 name: name.to_string(),
389 r#type: ty,
390 nullable: true,
391 default: None,
392 comment: None,
393 primary_key: None,
394 unique: None,
395 index: None,
396 foreign_key: None,
397 }
398 }
399
400 fn table(
401 name: &str,
402 columns: Vec<ColumnDef>,
403 constraints: Vec<vespertide_core::TableConstraint>,
404 ) -> TableDef {
405 TableDef {
406 name: name.to_string(),
407 columns,
408 constraints,
409 }
410 }
411
412 fn idx(name: &str, columns: Vec<&str>) -> TableConstraint {
413 TableConstraint::Index {
414 name: Some(name.to_string()),
415 columns: columns.into_iter().map(|s| s.to_string()).collect(),
416 }
417 }
418
419 #[rstest]
420 #[case::add_column_and_index(
421 vec![table(
422 "users",
423 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
424 vec![],
425 )],
426 vec![table(
427 "users",
428 vec![
429 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
430 col("name", ColumnType::Simple(SimpleColumnType::Text)),
431 ],
432 vec![idx("ix_users__name", vec!["name"])],
433 )],
434 vec![
435 MigrationAction::AddColumn {
436 table: "users".into(),
437 column: Box::new(col("name", ColumnType::Simple(SimpleColumnType::Text))),
438 fill_with: None,
439 },
440 MigrationAction::AddConstraint {
441 table: "users".into(),
442 constraint: idx("ix_users__name", vec!["name"]),
443 },
444 ]
445 )]
446 #[case::drop_table(
447 vec![table(
448 "users",
449 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
450 vec![],
451 )],
452 vec![],
453 vec![MigrationAction::DeleteTable {
454 table: "users".into()
455 }]
456 )]
457 #[case::add_table_with_index(
458 vec![],
459 vec![table(
460 "users",
461 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
462 vec![idx("idx_users_id", vec!["id"])],
463 )],
464 vec![
465 MigrationAction::CreateTable {
466 table: "users".into(),
467 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
468 constraints: vec![idx("idx_users_id", vec!["id"])],
469 },
470 ]
471 )]
472 #[case::delete_column(
473 vec![table(
474 "users",
475 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("name", ColumnType::Simple(SimpleColumnType::Text))],
476 vec![],
477 )],
478 vec![table(
479 "users",
480 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
481 vec![],
482 )],
483 vec![MigrationAction::DeleteColumn {
484 table: "users".into(),
485 column: "name".into(),
486 }]
487 )]
488 #[case::modify_column_type(
489 vec![table(
490 "users",
491 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
492 vec![],
493 )],
494 vec![table(
495 "users",
496 vec![col("id", ColumnType::Simple(SimpleColumnType::Text))],
497 vec![],
498 )],
499 vec![MigrationAction::ModifyColumnType {
500 table: "users".into(),
501 column: "id".into(),
502 new_type: ColumnType::Simple(SimpleColumnType::Text),
503 }]
504 )]
505 #[case::remove_index(
506 vec![table(
507 "users",
508 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
509 vec![idx("idx_users_id", vec!["id"])],
510 )],
511 vec![table(
512 "users",
513 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
514 vec![],
515 )],
516 vec![MigrationAction::RemoveConstraint {
517 table: "users".into(),
518 constraint: idx("idx_users_id", vec!["id"]),
519 }]
520 )]
521 #[case::add_index_existing_table(
522 vec![table(
523 "users",
524 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
525 vec![],
526 )],
527 vec![table(
528 "users",
529 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
530 vec![idx("idx_users_id", vec!["id"])],
531 )],
532 vec![MigrationAction::AddConstraint {
533 table: "users".into(),
534 constraint: idx("idx_users_id", vec!["id"]),
535 }]
536 )]
537 fn diff_schemas_detects_additions(
538 #[case] from_schema: Vec<TableDef>,
539 #[case] to_schema: Vec<TableDef>,
540 #[case] expected_actions: Vec<MigrationAction>,
541 ) {
542 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
543 assert_eq!(plan.actions, expected_actions);
544 }
545
546 mod integer_enum {
548 use super::*;
549 use vespertide_core::{ComplexColumnType, EnumValues, NumValue};
550
551 #[test]
552 fn integer_enum_values_changed_no_migration() {
553 let from = vec![table(
555 "orders",
556 vec![col(
557 "status",
558 ColumnType::Complex(ComplexColumnType::Enum {
559 name: "order_status".into(),
560 values: EnumValues::Integer(vec![
561 NumValue {
562 name: "Pending".into(),
563 value: 0,
564 },
565 NumValue {
566 name: "Shipped".into(),
567 value: 1,
568 },
569 ]),
570 }),
571 )],
572 vec![],
573 )];
574
575 let to = vec![table(
576 "orders",
577 vec![col(
578 "status",
579 ColumnType::Complex(ComplexColumnType::Enum {
580 name: "order_status".into(),
581 values: EnumValues::Integer(vec![
582 NumValue {
583 name: "Pending".into(),
584 value: 0,
585 },
586 NumValue {
587 name: "Shipped".into(),
588 value: 1,
589 },
590 NumValue {
591 name: "Delivered".into(),
592 value: 2,
593 },
594 NumValue {
595 name: "Cancelled".into(),
596 value: 100,
597 },
598 ]),
599 }),
600 )],
601 vec![],
602 )];
603
604 let plan = diff_schemas(&from, &to).unwrap();
605 assert!(
606 plan.actions.is_empty(),
607 "Expected no actions, got: {:?}",
608 plan.actions
609 );
610 }
611
612 #[test]
613 fn string_enum_values_changed_requires_migration() {
614 let from = vec![table(
616 "orders",
617 vec![col(
618 "status",
619 ColumnType::Complex(ComplexColumnType::Enum {
620 name: "order_status".into(),
621 values: EnumValues::String(vec!["pending".into(), "shipped".into()]),
622 }),
623 )],
624 vec![],
625 )];
626
627 let to = vec![table(
628 "orders",
629 vec![col(
630 "status",
631 ColumnType::Complex(ComplexColumnType::Enum {
632 name: "order_status".into(),
633 values: EnumValues::String(vec![
634 "pending".into(),
635 "shipped".into(),
636 "delivered".into(),
637 ]),
638 }),
639 )],
640 vec![],
641 )];
642
643 let plan = diff_schemas(&from, &to).unwrap();
644 assert_eq!(plan.actions.len(), 1);
645 assert!(matches!(
646 &plan.actions[0],
647 MigrationAction::ModifyColumnType { table, column, .. }
648 if table == "orders" && column == "status"
649 ));
650 }
651 }
652
653 mod inline_constraints {
655 use super::*;
656 use vespertide_core::schema::foreign_key::ForeignKeyDef;
657 use vespertide_core::schema::foreign_key::ForeignKeySyntax;
658 use vespertide_core::schema::primary_key::PrimaryKeySyntax;
659 use vespertide_core::{StrOrBoolOrArray, TableConstraint};
660
661 fn col_with_pk(name: &str, ty: ColumnType) -> ColumnDef {
662 ColumnDef {
663 name: name.to_string(),
664 r#type: ty,
665 nullable: false,
666 default: None,
667 comment: None,
668 primary_key: Some(PrimaryKeySyntax::Bool(true)),
669 unique: None,
670 index: None,
671 foreign_key: None,
672 }
673 }
674
675 fn col_with_unique(name: &str, ty: ColumnType) -> ColumnDef {
676 ColumnDef {
677 name: name.to_string(),
678 r#type: ty,
679 nullable: true,
680 default: None,
681 comment: None,
682 primary_key: None,
683 unique: Some(StrOrBoolOrArray::Bool(true)),
684 index: None,
685 foreign_key: None,
686 }
687 }
688
689 fn col_with_index(name: &str, ty: ColumnType) -> ColumnDef {
690 ColumnDef {
691 name: name.to_string(),
692 r#type: ty,
693 nullable: true,
694 default: None,
695 comment: None,
696 primary_key: None,
697 unique: None,
698 index: Some(StrOrBoolOrArray::Bool(true)),
699 foreign_key: None,
700 }
701 }
702
703 fn col_with_fk(name: &str, ty: ColumnType, ref_table: &str, ref_col: &str) -> ColumnDef {
704 ColumnDef {
705 name: name.to_string(),
706 r#type: ty,
707 nullable: true,
708 default: None,
709 comment: None,
710 primary_key: None,
711 unique: None,
712 index: None,
713 foreign_key: Some(ForeignKeySyntax::Object(ForeignKeyDef {
714 ref_table: ref_table.to_string(),
715 ref_columns: vec![ref_col.to_string()],
716 on_delete: None,
717 on_update: None,
718 })),
719 }
720 }
721
722 #[test]
723 fn create_table_with_inline_pk() {
724 let plan = diff_schemas(
725 &[],
726 &[table(
727 "users",
728 vec![
729 col_with_pk("id", ColumnType::Simple(SimpleColumnType::Integer)),
730 col("name", ColumnType::Simple(SimpleColumnType::Text)),
731 ],
732 vec![],
733 )],
734 )
735 .unwrap();
736
737 assert_eq!(plan.actions.len(), 1);
739 if let MigrationAction::CreateTable {
740 columns,
741 constraints,
742 ..
743 } = &plan.actions[0]
744 {
745 assert_eq!(constraints.len(), 0);
747 let id_col = columns.iter().find(|c| c.name == "id").unwrap();
749 assert!(id_col.primary_key.is_some());
750 } else {
751 panic!("Expected CreateTable action");
752 }
753 }
754
755 #[test]
756 fn create_table_with_inline_unique() {
757 let plan = diff_schemas(
758 &[],
759 &[table(
760 "users",
761 vec![
762 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
763 col_with_unique("email", ColumnType::Simple(SimpleColumnType::Text)),
764 ],
765 vec![],
766 )],
767 )
768 .unwrap();
769
770 assert_eq!(plan.actions.len(), 1);
772 if let MigrationAction::CreateTable {
773 columns,
774 constraints,
775 ..
776 } = &plan.actions[0]
777 {
778 assert_eq!(constraints.len(), 0);
780 let email_col = columns.iter().find(|c| c.name == "email").unwrap();
782 assert!(matches!(
783 email_col.unique,
784 Some(StrOrBoolOrArray::Bool(true))
785 ));
786 } else {
787 panic!("Expected CreateTable action");
788 }
789 }
790
791 #[test]
792 fn create_table_with_inline_index() {
793 let plan = diff_schemas(
794 &[],
795 &[table(
796 "users",
797 vec![
798 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
799 col_with_index("name", ColumnType::Simple(SimpleColumnType::Text)),
800 ],
801 vec![],
802 )],
803 )
804 .unwrap();
805
806 assert_eq!(plan.actions.len(), 1);
808 if let MigrationAction::CreateTable {
809 columns,
810 constraints,
811 ..
812 } = &plan.actions[0]
813 {
814 assert_eq!(constraints.len(), 0);
816 let name_col = columns.iter().find(|c| c.name == "name").unwrap();
818 assert!(matches!(name_col.index, Some(StrOrBoolOrArray::Bool(true))));
819 } else {
820 panic!("Expected CreateTable action");
821 }
822 }
823
824 #[test]
825 fn create_table_with_inline_fk() {
826 let plan = diff_schemas(
827 &[],
828 &[table(
829 "posts",
830 vec![
831 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
832 col_with_fk(
833 "user_id",
834 ColumnType::Simple(SimpleColumnType::Integer),
835 "users",
836 "id",
837 ),
838 ],
839 vec![],
840 )],
841 )
842 .unwrap();
843
844 assert_eq!(plan.actions.len(), 1);
846 if let MigrationAction::CreateTable {
847 columns,
848 constraints,
849 ..
850 } = &plan.actions[0]
851 {
852 assert_eq!(constraints.len(), 0);
854 let user_id_col = columns.iter().find(|c| c.name == "user_id").unwrap();
856 assert!(user_id_col.foreign_key.is_some());
857 } else {
858 panic!("Expected CreateTable action");
859 }
860 }
861
862 #[test]
863 fn add_index_via_inline_constraint() {
864 let plan = diff_schemas(
867 &[table(
868 "users",
869 vec![
870 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
871 col("name", ColumnType::Simple(SimpleColumnType::Text)),
872 ],
873 vec![],
874 )],
875 &[table(
876 "users",
877 vec![
878 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
879 col_with_index("name", ColumnType::Simple(SimpleColumnType::Text)),
880 ],
881 vec![],
882 )],
883 )
884 .unwrap();
885
886 assert_eq!(plan.actions.len(), 1);
888 if let MigrationAction::AddConstraint { table, constraint } = &plan.actions[0] {
889 assert_eq!(table, "users");
890 if let TableConstraint::Index { name, columns } = constraint {
891 assert_eq!(name, &None); assert_eq!(columns, &vec!["name".to_string()]);
893 } else {
894 panic!("Expected Index constraint, got {:?}", constraint);
895 }
896 } else {
897 panic!("Expected AddConstraint action, got {:?}", plan.actions[0]);
898 }
899 }
900
901 #[test]
902 fn create_table_with_all_inline_constraints() {
903 let mut id_col = col("id", ColumnType::Simple(SimpleColumnType::Integer));
904 id_col.primary_key = Some(PrimaryKeySyntax::Bool(true));
905 id_col.nullable = false;
906
907 let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
908 email_col.unique = Some(StrOrBoolOrArray::Bool(true));
909
910 let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
911 name_col.index = Some(StrOrBoolOrArray::Bool(true));
912
913 let mut org_id_col = col("org_id", ColumnType::Simple(SimpleColumnType::Integer));
914 org_id_col.foreign_key = Some(ForeignKeySyntax::Object(ForeignKeyDef {
915 ref_table: "orgs".into(),
916 ref_columns: vec!["id".into()],
917 on_delete: None,
918 on_update: None,
919 }));
920
921 let plan = diff_schemas(
922 &[],
923 &[table(
924 "users",
925 vec![id_col, email_col, name_col, org_id_col],
926 vec![],
927 )],
928 )
929 .unwrap();
930
931 assert_eq!(plan.actions.len(), 1);
933
934 if let MigrationAction::CreateTable {
935 columns,
936 constraints,
937 ..
938 } = &plan.actions[0]
939 {
940 assert_eq!(constraints.len(), 0);
942
943 let id_col = columns.iter().find(|c| c.name == "id").unwrap();
945 assert!(id_col.primary_key.is_some());
946
947 let email_col = columns.iter().find(|c| c.name == "email").unwrap();
948 assert!(matches!(
949 email_col.unique,
950 Some(StrOrBoolOrArray::Bool(true))
951 ));
952
953 let name_col = columns.iter().find(|c| c.name == "name").unwrap();
954 assert!(matches!(name_col.index, Some(StrOrBoolOrArray::Bool(true))));
955
956 let org_id_col = columns.iter().find(|c| c.name == "org_id").unwrap();
957 assert!(org_id_col.foreign_key.is_some());
958 } else {
959 panic!("Expected CreateTable action");
960 }
961 }
962
963 #[test]
964 fn add_constraint_to_existing_table() {
965 let from_schema = vec![table(
967 "users",
968 vec![
969 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
970 col("email", ColumnType::Simple(SimpleColumnType::Text)),
971 ],
972 vec![],
973 )];
974
975 let to_schema = vec![table(
976 "users",
977 vec![
978 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
979 col("email", ColumnType::Simple(SimpleColumnType::Text)),
980 ],
981 vec![vespertide_core::TableConstraint::Unique {
982 name: Some("uq_users_email".into()),
983 columns: vec!["email".into()],
984 }],
985 )];
986
987 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
988 assert_eq!(plan.actions.len(), 1);
989 if let MigrationAction::AddConstraint { table, constraint } = &plan.actions[0] {
990 assert_eq!(table, "users");
991 assert!(matches!(
992 constraint,
993 vespertide_core::TableConstraint::Unique { name: Some(n), columns }
994 if n == "uq_users_email" && columns == &vec!["email".to_string()]
995 ));
996 } else {
997 panic!("Expected AddConstraint action, got {:?}", plan.actions[0]);
998 }
999 }
1000
1001 #[test]
1002 fn remove_constraint_from_existing_table() {
1003 let from_schema = vec![table(
1005 "users",
1006 vec![
1007 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1008 col("email", ColumnType::Simple(SimpleColumnType::Text)),
1009 ],
1010 vec![vespertide_core::TableConstraint::Unique {
1011 name: Some("uq_users_email".into()),
1012 columns: vec!["email".into()],
1013 }],
1014 )];
1015
1016 let to_schema = vec![table(
1017 "users",
1018 vec![
1019 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1020 col("email", ColumnType::Simple(SimpleColumnType::Text)),
1021 ],
1022 vec![],
1023 )];
1024
1025 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
1026 assert_eq!(plan.actions.len(), 1);
1027 if let MigrationAction::RemoveConstraint { table, constraint } = &plan.actions[0] {
1028 assert_eq!(table, "users");
1029 assert!(matches!(
1030 constraint,
1031 vespertide_core::TableConstraint::Unique { name: Some(n), columns }
1032 if n == "uq_users_email" && columns == &vec!["email".to_string()]
1033 ));
1034 } else {
1035 panic!(
1036 "Expected RemoveConstraint action, got {:?}",
1037 plan.actions[0]
1038 );
1039 }
1040 }
1041
1042 #[test]
1043 fn diff_schemas_with_normalize_error() {
1044 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1046 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1047
1048 let table = TableDef {
1049 name: "test".into(),
1050 columns: vec![
1051 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1052 col1.clone(),
1053 {
1054 let mut c = col1.clone();
1056 c.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1057 c
1058 },
1059 ],
1060 constraints: vec![],
1061 };
1062
1063 let result = diff_schemas(&[], &[table]);
1064 assert!(result.is_err());
1065 if let Err(PlannerError::TableValidation(msg)) = result {
1066 assert!(msg.contains("Failed to normalize table"));
1067 assert!(msg.contains("Duplicate index"));
1068 } else {
1069 panic!("Expected TableValidation error, got {:?}", result);
1070 }
1071 }
1072
1073 #[test]
1074 fn diff_schemas_with_normalize_error_in_from_schema() {
1075 let mut col1 = col("col1", ColumnType::Simple(SimpleColumnType::Text));
1077 col1.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1078
1079 let table = TableDef {
1080 name: "test".into(),
1081 columns: vec![
1082 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1083 col1.clone(),
1084 {
1085 let mut c = col1.clone();
1087 c.index = Some(StrOrBoolOrArray::Str("idx1".into()));
1088 c
1089 },
1090 ],
1091 constraints: vec![],
1092 };
1093
1094 let result = diff_schemas(&[table], &[]);
1096 assert!(result.is_err());
1097 if let Err(PlannerError::TableValidation(msg)) = result {
1098 assert!(msg.contains("Failed to normalize table"));
1099 assert!(msg.contains("Duplicate index"));
1100 } else {
1101 panic!("Expected TableValidation error, got {:?}", result);
1102 }
1103 }
1104 }
1105
1106 mod fk_ordering {
1108 use super::*;
1109 use vespertide_core::TableConstraint;
1110
1111 fn table_with_fk(
1112 name: &str,
1113 ref_table: &str,
1114 fk_column: &str,
1115 ref_column: &str,
1116 ) -> TableDef {
1117 TableDef {
1118 name: name.to_string(),
1119 columns: vec![
1120 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1121 col(fk_column, ColumnType::Simple(SimpleColumnType::Integer)),
1122 ],
1123 constraints: vec![TableConstraint::ForeignKey {
1124 name: None,
1125 columns: vec![fk_column.to_string()],
1126 ref_table: ref_table.to_string(),
1127 ref_columns: vec![ref_column.to_string()],
1128 on_delete: None,
1129 on_update: None,
1130 }],
1131 }
1132 }
1133
1134 fn simple_table(name: &str) -> TableDef {
1135 TableDef {
1136 name: name.to_string(),
1137 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
1138 constraints: vec![],
1139 }
1140 }
1141
1142 #[test]
1143 fn create_tables_respects_fk_order() {
1144 let users = simple_table("users");
1147 let posts = table_with_fk("posts", "users", "user_id", "id");
1148
1149 let plan = diff_schemas(&[], &[posts.clone(), users.clone()]).unwrap();
1150
1151 let create_order: Vec<&str> = plan
1153 .actions
1154 .iter()
1155 .filter_map(|a| {
1156 if let MigrationAction::CreateTable { table, .. } = a {
1157 Some(table.as_str())
1158 } else {
1159 None
1160 }
1161 })
1162 .collect();
1163
1164 assert_eq!(create_order, vec!["users", "posts"]);
1165 }
1166
1167 #[test]
1168 fn create_tables_chain_dependency() {
1169 let users = simple_table("users");
1174 let media = table_with_fk("media", "users", "owner_id", "id");
1175 let articles = table_with_fk("articles", "media", "media_id", "id");
1176
1177 let plan =
1179 diff_schemas(&[], &[articles.clone(), media.clone(), users.clone()]).unwrap();
1180
1181 let create_order: Vec<&str> = plan
1182 .actions
1183 .iter()
1184 .filter_map(|a| {
1185 if let MigrationAction::CreateTable { table, .. } = a {
1186 Some(table.as_str())
1187 } else {
1188 None
1189 }
1190 })
1191 .collect();
1192
1193 assert_eq!(create_order, vec!["users", "media", "articles"]);
1194 }
1195
1196 #[test]
1197 fn create_tables_multiple_independent_branches() {
1198 let users = simple_table("users");
1202 let posts = table_with_fk("posts", "users", "user_id", "id");
1203 let categories = simple_table("categories");
1204 let products = table_with_fk("products", "categories", "category_id", "id");
1205
1206 let plan = diff_schemas(
1207 &[],
1208 &[
1209 products.clone(),
1210 posts.clone(),
1211 categories.clone(),
1212 users.clone(),
1213 ],
1214 )
1215 .unwrap();
1216
1217 let create_order: Vec<&str> = plan
1218 .actions
1219 .iter()
1220 .filter_map(|a| {
1221 if let MigrationAction::CreateTable { table, .. } = a {
1222 Some(table.as_str())
1223 } else {
1224 None
1225 }
1226 })
1227 .collect();
1228
1229 let users_pos = create_order.iter().position(|&t| t == "users").unwrap();
1231 let posts_pos = create_order.iter().position(|&t| t == "posts").unwrap();
1232 assert!(
1233 users_pos < posts_pos,
1234 "users should be created before posts"
1235 );
1236
1237 let categories_pos = create_order
1239 .iter()
1240 .position(|&t| t == "categories")
1241 .unwrap();
1242 let products_pos = create_order.iter().position(|&t| t == "products").unwrap();
1243 assert!(
1244 categories_pos < products_pos,
1245 "categories should be created before products"
1246 );
1247 }
1248
1249 #[test]
1250 fn delete_tables_respects_fk_order() {
1251 let users = simple_table("users");
1254 let posts = table_with_fk("posts", "users", "user_id", "id");
1255
1256 let plan = diff_schemas(&[users.clone(), posts.clone()], &[]).unwrap();
1257
1258 let delete_order: Vec<&str> = plan
1259 .actions
1260 .iter()
1261 .filter_map(|a| {
1262 if let MigrationAction::DeleteTable { table } = a {
1263 Some(table.as_str())
1264 } else {
1265 None
1266 }
1267 })
1268 .collect();
1269
1270 assert_eq!(delete_order, vec!["posts", "users"]);
1271 }
1272
1273 #[test]
1274 fn delete_tables_chain_dependency() {
1275 let users = simple_table("users");
1278 let media = table_with_fk("media", "users", "owner_id", "id");
1279 let articles = table_with_fk("articles", "media", "media_id", "id");
1280
1281 let plan =
1282 diff_schemas(&[users.clone(), media.clone(), articles.clone()], &[]).unwrap();
1283
1284 let delete_order: Vec<&str> = plan
1285 .actions
1286 .iter()
1287 .filter_map(|a| {
1288 if let MigrationAction::DeleteTable { table } = a {
1289 Some(table.as_str())
1290 } else {
1291 None
1292 }
1293 })
1294 .collect();
1295
1296 let articles_pos = delete_order.iter().position(|&t| t == "articles").unwrap();
1298 let media_pos = delete_order.iter().position(|&t| t == "media").unwrap();
1299 assert!(
1300 articles_pos < media_pos,
1301 "articles should be deleted before media"
1302 );
1303
1304 let users_pos = delete_order.iter().position(|&t| t == "users").unwrap();
1306 assert!(
1307 media_pos < users_pos,
1308 "media should be deleted before users"
1309 );
1310 }
1311
1312 #[test]
1313 fn circular_fk_dependency_returns_error() {
1314 let table_a = TableDef {
1316 name: "table_a".to_string(),
1317 columns: vec![
1318 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1319 col("b_id", ColumnType::Simple(SimpleColumnType::Integer)),
1320 ],
1321 constraints: vec![TableConstraint::ForeignKey {
1322 name: None,
1323 columns: vec!["b_id".to_string()],
1324 ref_table: "table_b".to_string(),
1325 ref_columns: vec!["id".to_string()],
1326 on_delete: None,
1327 on_update: None,
1328 }],
1329 };
1330
1331 let table_b = TableDef {
1332 name: "table_b".to_string(),
1333 columns: vec![
1334 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1335 col("a_id", ColumnType::Simple(SimpleColumnType::Integer)),
1336 ],
1337 constraints: vec![TableConstraint::ForeignKey {
1338 name: None,
1339 columns: vec!["a_id".to_string()],
1340 ref_table: "table_a".to_string(),
1341 ref_columns: vec!["id".to_string()],
1342 on_delete: None,
1343 on_update: None,
1344 }],
1345 };
1346
1347 let result = diff_schemas(&[], &[table_a, table_b]);
1348 assert!(result.is_err());
1349 if let Err(PlannerError::TableValidation(msg)) = result {
1350 assert!(
1351 msg.contains("Circular foreign key dependency"),
1352 "Expected circular dependency error, got: {}",
1353 msg
1354 );
1355 } else {
1356 panic!("Expected TableValidation error, got {:?}", result);
1357 }
1358 }
1359
1360 #[test]
1361 fn fk_to_external_table_is_ignored() {
1362 let posts = table_with_fk("posts", "users", "user_id", "id");
1364 let comments = table_with_fk("comments", "posts", "post_id", "id");
1365
1366 let plan = diff_schemas(&[], &[comments.clone(), posts.clone()]).unwrap();
1368
1369 let create_order: Vec<&str> = plan
1370 .actions
1371 .iter()
1372 .filter_map(|a| {
1373 if let MigrationAction::CreateTable { table, .. } = a {
1374 Some(table.as_str())
1375 } else {
1376 None
1377 }
1378 })
1379 .collect();
1380
1381 let posts_pos = create_order.iter().position(|&t| t == "posts").unwrap();
1383 let comments_pos = create_order.iter().position(|&t| t == "comments").unwrap();
1384 assert!(
1385 posts_pos < comments_pos,
1386 "posts should be created before comments"
1387 );
1388 }
1389
1390 #[test]
1391 fn delete_tables_mixed_with_other_actions() {
1392 use crate::diff::diff_schemas;
1395
1396 let from_schema = vec![
1397 table(
1398 "users",
1399 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
1400 vec![],
1401 ),
1402 table(
1403 "posts",
1404 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
1405 vec![],
1406 ),
1407 ];
1408
1409 let to_schema = vec![
1410 table(
1412 "users",
1413 vec![
1414 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1415 col("name", ColumnType::Simple(SimpleColumnType::Text)),
1416 ],
1417 vec![],
1418 ),
1419 ];
1420
1421 let plan = diff_schemas(&from_schema, &to_schema).unwrap();
1422
1423 assert!(
1425 plan.actions
1426 .iter()
1427 .any(|a| matches!(a, MigrationAction::AddColumn { .. }))
1428 );
1429 assert!(
1430 plan.actions
1431 .iter()
1432 .any(|a| matches!(a, MigrationAction::DeleteTable { .. }))
1433 );
1434
1435 }
1438
1439 #[test]
1440 #[should_panic(expected = "Expected DeleteTable action")]
1441 fn test_extract_delete_table_name_panics_on_non_delete_action() {
1442 use super::extract_delete_table_name;
1444
1445 let action = MigrationAction::AddColumn {
1446 table: "users".into(),
1447 column: Box::new(ColumnDef {
1448 name: "email".into(),
1449 r#type: ColumnType::Simple(SimpleColumnType::Text),
1450 nullable: true,
1451 default: None,
1452 comment: None,
1453 primary_key: None,
1454 unique: None,
1455 index: None,
1456 foreign_key: None,
1457 }),
1458 fill_with: None,
1459 };
1460
1461 extract_delete_table_name(&action);
1463 }
1464
1465 #[test]
1467 fn create_tables_with_inline_fk_chain() {
1468 use super::*;
1469 use vespertide_core::schema::foreign_key::ForeignKeySyntax;
1470 use vespertide_core::schema::primary_key::PrimaryKeySyntax;
1471
1472 fn col_pk(name: &str) -> ColumnDef {
1473 ColumnDef {
1474 name: name.to_string(),
1475 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1476 nullable: false,
1477 default: None,
1478 comment: None,
1479 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1480 unique: None,
1481 index: None,
1482 foreign_key: None,
1483 }
1484 }
1485
1486 fn col_inline_fk(name: &str, ref_table: &str) -> ColumnDef {
1487 ColumnDef {
1488 name: name.to_string(),
1489 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1490 nullable: true,
1491 default: None,
1492 comment: None,
1493 primary_key: None,
1494 unique: None,
1495 index: None,
1496 foreign_key: Some(ForeignKeySyntax::String(format!("{}.id", ref_table))),
1497 }
1498 }
1499
1500 let user = TableDef {
1509 name: "user".to_string(),
1510 columns: vec![col_pk("id")],
1511 constraints: vec![],
1512 };
1513
1514 let product = TableDef {
1515 name: "product".to_string(),
1516 columns: vec![col_pk("id")],
1517 constraints: vec![],
1518 };
1519
1520 let project = TableDef {
1521 name: "project".to_string(),
1522 columns: vec![col_pk("id"), col_inline_fk("user_id", "user")],
1523 constraints: vec![],
1524 };
1525
1526 let code = TableDef {
1527 name: "code".to_string(),
1528 columns: vec![
1529 col_pk("id"),
1530 col_inline_fk("product_id", "product"),
1531 col_inline_fk("creator_user_id", "user"),
1532 col_inline_fk("project_id", "project"),
1533 ],
1534 constraints: vec![],
1535 };
1536
1537 let order = TableDef {
1538 name: "order".to_string(),
1539 columns: vec![
1540 col_pk("id"),
1541 col_inline_fk("user_id", "user"),
1542 col_inline_fk("project_id", "project"),
1543 col_inline_fk("product_id", "product"),
1544 col_inline_fk("code_id", "code"),
1545 ],
1546 constraints: vec![],
1547 };
1548
1549 let payment = TableDef {
1550 name: "payment".to_string(),
1551 columns: vec![col_pk("id"), col_inline_fk("order_id", "order")],
1552 constraints: vec![],
1553 };
1554
1555 let result = diff_schemas(&[], &[payment, order, code, project, product, user]);
1557 assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
1558
1559 let plan = result.unwrap();
1560 let create_order: Vec<&str> = plan
1561 .actions
1562 .iter()
1563 .filter_map(|a| {
1564 if let MigrationAction::CreateTable { table, .. } = a {
1565 Some(table.as_str())
1566 } else {
1567 None
1568 }
1569 })
1570 .collect();
1571
1572 let get_pos = |name: &str| create_order.iter().position(|&t| t == name).unwrap();
1574
1575 assert!(
1578 get_pos("user") < get_pos("project"),
1579 "user must come before project"
1580 );
1581 assert!(
1583 get_pos("product") < get_pos("code"),
1584 "product must come before code"
1585 );
1586 assert!(
1587 get_pos("user") < get_pos("code"),
1588 "user must come before code"
1589 );
1590 assert!(
1591 get_pos("project") < get_pos("code"),
1592 "project must come before code"
1593 );
1594 assert!(
1596 get_pos("code") < get_pos("order"),
1597 "code must come before order"
1598 );
1599 assert!(
1601 get_pos("order") < get_pos("payment"),
1602 "order must come before payment"
1603 );
1604 }
1605
1606 #[test]
1608 fn create_tables_with_duplicate_fk_references() {
1609 use super::*;
1610 use vespertide_core::schema::foreign_key::ForeignKeySyntax;
1611 use vespertide_core::schema::primary_key::PrimaryKeySyntax;
1612
1613 fn col_pk(name: &str) -> ColumnDef {
1614 ColumnDef {
1615 name: name.to_string(),
1616 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1617 nullable: false,
1618 default: None,
1619 comment: None,
1620 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1621 unique: None,
1622 index: None,
1623 foreign_key: None,
1624 }
1625 }
1626
1627 fn col_inline_fk(name: &str, ref_table: &str) -> ColumnDef {
1628 ColumnDef {
1629 name: name.to_string(),
1630 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1631 nullable: true,
1632 default: None,
1633 comment: None,
1634 primary_key: None,
1635 unique: None,
1636 index: None,
1637 foreign_key: Some(ForeignKeySyntax::String(format!("{}.id", ref_table))),
1638 }
1639 }
1640
1641 let user = TableDef {
1643 name: "user".to_string(),
1644 columns: vec![col_pk("id")],
1645 constraints: vec![],
1646 };
1647
1648 let code = TableDef {
1649 name: "code".to_string(),
1650 columns: vec![
1651 col_pk("id"),
1652 col_inline_fk("creator_user_id", "user"),
1653 col_inline_fk("used_by_user_id", "user"), ],
1655 constraints: vec![],
1656 };
1657
1658 let result = diff_schemas(&[], &[code, user]);
1660 assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
1661
1662 let plan = result.unwrap();
1663 let create_order: Vec<&str> = plan
1664 .actions
1665 .iter()
1666 .filter_map(|a| {
1667 if let MigrationAction::CreateTable { table, .. } = a {
1668 Some(table.as_str())
1669 } else {
1670 None
1671 }
1672 })
1673 .collect();
1674
1675 let user_pos = create_order.iter().position(|&t| t == "user").unwrap();
1677 let code_pos = create_order.iter().position(|&t| t == "code").unwrap();
1678 assert!(user_pos < code_pos, "user must come before code");
1679 }
1680 }
1681
1682 mod diff_tables {
1683 use insta::assert_debug_snapshot;
1684
1685 use super::*;
1686
1687 #[test]
1688 fn create_table_with_inline_index() {
1689 let base = [table(
1690 "users",
1691 vec![
1692 ColumnDef {
1693 name: "id".to_string(),
1694 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1695 nullable: false,
1696 default: None,
1697 comment: None,
1698 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1699 unique: None,
1700 index: Some(StrOrBoolOrArray::Bool(false)),
1701 foreign_key: None,
1702 },
1703 ColumnDef {
1704 name: "name".to_string(),
1705 r#type: ColumnType::Simple(SimpleColumnType::Text),
1706 nullable: true,
1707 default: None,
1708 comment: None,
1709 primary_key: None,
1710 unique: Some(StrOrBoolOrArray::Bool(true)),
1711 index: Some(StrOrBoolOrArray::Bool(true)),
1712 foreign_key: None,
1713 },
1714 ],
1715 vec![],
1716 )];
1717 let plan = diff_schemas(&[], &base).unwrap();
1718
1719 assert_eq!(plan.actions.len(), 1);
1720 assert_debug_snapshot!(plan.actions);
1721
1722 let plan = diff_schemas(
1723 &base,
1724 &[table(
1725 "users",
1726 vec![
1727 ColumnDef {
1728 name: "id".to_string(),
1729 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1730 nullable: false,
1731 default: None,
1732 comment: None,
1733 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1734 unique: None,
1735 index: Some(StrOrBoolOrArray::Bool(false)),
1736 foreign_key: None,
1737 },
1738 ColumnDef {
1739 name: "name".to_string(),
1740 r#type: ColumnType::Simple(SimpleColumnType::Text),
1741 nullable: true,
1742 default: None,
1743 comment: None,
1744 primary_key: None,
1745 unique: Some(StrOrBoolOrArray::Bool(true)),
1746 index: Some(StrOrBoolOrArray::Bool(false)),
1747 foreign_key: None,
1748 },
1749 ],
1750 vec![],
1751 )],
1752 )
1753 .unwrap();
1754
1755 assert_eq!(plan.actions.len(), 1);
1756 assert_debug_snapshot!(plan.actions);
1757 }
1758
1759 #[rstest]
1760 #[case(
1761 "add_index",
1762 vec![table(
1763 "users",
1764 vec![
1765 ColumnDef {
1766 name: "id".to_string(),
1767 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1768 nullable: false,
1769 default: None,
1770 comment: None,
1771 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1772 unique: None,
1773 index: None,
1774 foreign_key: None,
1775 },
1776 ],
1777 vec![],
1778 )],
1779 vec![table(
1780 "users",
1781 vec![
1782 ColumnDef {
1783 name: "id".to_string(),
1784 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1785 nullable: false,
1786 default: None,
1787 comment: None,
1788 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1789 unique: None,
1790 index: Some(StrOrBoolOrArray::Bool(true)),
1791 foreign_key: None,
1792 },
1793 ],
1794 vec![],
1795 )],
1796 )]
1797 #[case(
1798 "remove_index",
1799 vec![table(
1800 "users",
1801 vec![
1802 ColumnDef {
1803 name: "id".to_string(),
1804 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1805 nullable: false,
1806 default: None,
1807 comment: None,
1808 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1809 unique: None,
1810 index: Some(StrOrBoolOrArray::Bool(true)),
1811 foreign_key: None,
1812 },
1813 ],
1814 vec![],
1815 )],
1816 vec![table(
1817 "users",
1818 vec![
1819 ColumnDef {
1820 name: "id".to_string(),
1821 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1822 nullable: false,
1823 default: None,
1824 comment: None,
1825 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1826 unique: None,
1827 index: Some(StrOrBoolOrArray::Bool(false)),
1828 foreign_key: None,
1829 },
1830 ],
1831 vec![],
1832 )],
1833 )]
1834 #[case(
1835 "add_named_index",
1836 vec![table(
1837 "users",
1838 vec![
1839 ColumnDef {
1840 name: "id".to_string(),
1841 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1842 nullable: false,
1843 default: None,
1844 comment: None,
1845 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1846 unique: None,
1847 index: None,
1848 foreign_key: None,
1849 },
1850 ],
1851 vec![],
1852 )],
1853 vec![table(
1854 "users",
1855 vec![
1856 ColumnDef {
1857 name: "id".to_string(),
1858 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1859 nullable: false,
1860 default: None,
1861 comment: None,
1862 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1863 unique: None,
1864 index: Some(StrOrBoolOrArray::Str("hello".to_string())),
1865 foreign_key: None,
1866 },
1867 ],
1868 vec![],
1869 )],
1870 )]
1871 #[case(
1872 "remove_named_index",
1873 vec![table(
1874 "users",
1875 vec![
1876 ColumnDef {
1877 name: "id".to_string(),
1878 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1879 nullable: false,
1880 default: None,
1881 comment: None,
1882 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1883 unique: None,
1884 index: Some(StrOrBoolOrArray::Str("hello".to_string())),
1885 foreign_key: None,
1886 },
1887 ],
1888 vec![],
1889 )],
1890 vec![table(
1891 "users",
1892 vec![
1893 ColumnDef {
1894 name: "id".to_string(),
1895 r#type: ColumnType::Simple(SimpleColumnType::Integer),
1896 nullable: false,
1897 default: None,
1898 comment: None,
1899 primary_key: Some(PrimaryKeySyntax::Bool(true)),
1900 unique: None,
1901 index: None,
1902 foreign_key: None,
1903 },
1904 ],
1905 vec![],
1906 )],
1907 )]
1908 fn diff_tables(#[case] name: &str, #[case] base: Vec<TableDef>, #[case] to: Vec<TableDef>) {
1909 use insta::with_settings;
1910
1911 let plan = diff_schemas(&base, &to).unwrap();
1912 with_settings!({ snapshot_suffix => name }, {
1913 assert_debug_snapshot!(plan.actions);
1914 });
1915 }
1916 }
1917}