1use std::collections::HashSet;
2
3use vespertide_core::{
4 ColumnDef, ColumnType, ComplexColumnType, EnumValues, MigrationAction, MigrationPlan,
5 TableConstraint, TableDef,
6};
7
8use crate::error::{InvalidEnumDefaultError, PlannerError};
9
10pub fn validate_schema(schema: &[TableDef]) -> Result<(), PlannerError> {
19 let mut table_names = HashSet::new();
21 for table in schema {
22 if !table_names.insert(&table.name) {
23 return Err(PlannerError::DuplicateTableName(table.name.clone()));
24 }
25 }
26
27 let table_map: std::collections::HashMap<_, _> = schema
29 .iter()
30 .map(|t| {
31 let columns: HashSet<_> = t.columns.iter().map(|c| c.name.as_str()).collect();
32 (t.name.as_str(), columns)
33 })
34 .collect();
35
36 for table in schema {
38 validate_table(table, &table_map)?;
39 }
40
41 Ok(())
42}
43
44fn validate_table(
45 table: &TableDef,
46 table_map: &std::collections::HashMap<&str, HashSet<&str>>,
47) -> Result<(), PlannerError> {
48 let table_columns: HashSet<_> = table.columns.iter().map(|c| c.name.as_str()).collect();
49
50 let has_table_pk = table
55 .constraints
56 .iter()
57 .any(|c| matches!(c, TableConstraint::PrimaryKey { .. }));
58 let has_inline_pk = table.columns.iter().any(|c| c.primary_key.is_some());
59
60 if !has_table_pk && !has_inline_pk {
61 return Err(PlannerError::MissingPrimaryKey(table.name.clone()));
62 }
63
64 for column in &table.columns {
66 validate_column(column, &table.name)?;
67 }
68
69 for constraint in &table.constraints {
71 validate_constraint(constraint, &table.name, &table_columns, table_map)?;
72 }
73
74 Ok(())
75}
76
77fn extract_enum_value(value: &str) -> Option<&str> {
80 let trimmed = value.trim();
81 if trimmed.is_empty() {
82 return None;
83 }
84 if trimmed.contains('(')
86 || trimmed.contains(')')
87 || trimmed.eq_ignore_ascii_case("null")
88 || trimmed.eq_ignore_ascii_case("current_timestamp")
89 || trimmed.eq_ignore_ascii_case("now")
90 {
91 return None;
92 }
93 if ((trimmed.starts_with('\'') && trimmed.ends_with('\''))
95 || (trimmed.starts_with('"') && trimmed.ends_with('"')))
96 && trimmed.len() >= 2
97 {
98 return Some(&trimmed[1..trimmed.len() - 1]);
99 }
100 Some(trimmed)
102}
103
104fn validate_enum_value(
106 value: &str,
107 enum_name: &str,
108 enum_values: &EnumValues,
109 table_name: &str,
110 column_name: &str,
111 value_type: &str, ) -> Result<(), PlannerError> {
113 let extracted = match extract_enum_value(value) {
114 Some(v) => v,
115 None => return Ok(()), };
117
118 let is_valid = match enum_values {
119 EnumValues::String(variants) => variants.iter().any(|v| v == extracted),
120 EnumValues::Integer(variants) => variants.iter().any(|v| v.name == extracted),
121 };
122
123 if !is_valid {
124 let allowed = enum_values.variant_names().join(", ");
125 return Err(Box::new(InvalidEnumDefaultError {
126 enum_name: enum_name.to_string(),
127 table_name: table_name.to_string(),
128 column_name: column_name.to_string(),
129 value_type: value_type.to_string(),
130 value: extracted.to_string(),
131 allowed,
132 })
133 .into());
134 }
135
136 Ok(())
137}
138
139fn validate_column(column: &ColumnDef, table_name: &str) -> Result<(), PlannerError> {
140 if let ColumnType::Complex(ComplexColumnType::Enum { name, values }) = &column.r#type {
142 match values {
143 EnumValues::String(variants) => {
144 let mut seen = HashSet::new();
145 for variant in variants {
146 if !seen.insert(variant.as_str()) {
147 return Err(PlannerError::DuplicateEnumVariantName(
148 name.clone(),
149 table_name.to_string(),
150 column.name.clone(),
151 variant.clone(),
152 ));
153 }
154 }
155 }
156 EnumValues::Integer(variants) => {
157 let mut seen_names = HashSet::new();
159 for variant in variants {
160 if !seen_names.insert(variant.name.as_str()) {
161 return Err(PlannerError::DuplicateEnumVariantName(
162 name.clone(),
163 table_name.to_string(),
164 column.name.clone(),
165 variant.name.clone(),
166 ));
167 }
168 }
169 let mut seen_values = HashSet::new();
171 for variant in variants {
172 if !seen_values.insert(variant.value) {
173 return Err(PlannerError::DuplicateEnumValue(
174 name.clone(),
175 table_name.to_string(),
176 column.name.clone(),
177 variant.value,
178 ));
179 }
180 }
181 }
182 }
183
184 if let Some(default) = &column.default {
186 let default_str = default.to_sql();
187 validate_enum_value(
188 &default_str,
189 name,
190 values,
191 table_name,
192 &column.name,
193 "default",
194 )?;
195 }
196 }
197 Ok(())
198}
199
200fn validate_constraint(
201 constraint: &TableConstraint,
202 table_name: &str,
203 table_columns: &HashSet<&str>,
204 table_map: &std::collections::HashMap<&str, HashSet<&str>>,
205) -> Result<(), PlannerError> {
206 match constraint {
207 TableConstraint::PrimaryKey { columns, .. } => {
208 if columns.is_empty() {
209 return Err(PlannerError::EmptyConstraintColumns(
210 table_name.to_string(),
211 "PrimaryKey".to_string(),
212 ));
213 }
214 for col in columns {
215 if !table_columns.contains(col.as_str()) {
216 return Err(PlannerError::ConstraintColumnNotFound(
217 table_name.to_string(),
218 "PrimaryKey".to_string(),
219 col.clone(),
220 ));
221 }
222 }
223 }
224 TableConstraint::Unique { columns, .. } => {
225 if columns.is_empty() {
226 return Err(PlannerError::EmptyConstraintColumns(
227 table_name.to_string(),
228 "Unique".to_string(),
229 ));
230 }
231 for col in columns {
232 if !table_columns.contains(col.as_str()) {
233 return Err(PlannerError::ConstraintColumnNotFound(
234 table_name.to_string(),
235 "Unique".to_string(),
236 col.clone(),
237 ));
238 }
239 }
240 }
241 TableConstraint::ForeignKey {
242 columns,
243 ref_table,
244 ref_columns,
245 ..
246 } => {
247 if columns.is_empty() {
248 return Err(PlannerError::EmptyConstraintColumns(
249 table_name.to_string(),
250 "ForeignKey".to_string(),
251 ));
252 }
253 if ref_columns.is_empty() {
254 return Err(PlannerError::EmptyConstraintColumns(
255 ref_table.clone(),
256 "ForeignKey (ref_columns)".to_string(),
257 ));
258 }
259
260 let ref_table_columns = table_map.get(ref_table.as_str()).ok_or_else(|| {
262 PlannerError::ForeignKeyTableNotFound(
263 table_name.to_string(),
264 columns.join(", "),
265 ref_table.clone(),
266 )
267 })?;
268
269 for col in columns {
271 if !table_columns.contains(col.as_str()) {
272 return Err(PlannerError::ConstraintColumnNotFound(
273 table_name.to_string(),
274 "ForeignKey".to_string(),
275 col.clone(),
276 ));
277 }
278 }
279
280 for ref_col in ref_columns {
282 if !ref_table_columns.contains(ref_col.as_str()) {
283 return Err(PlannerError::ForeignKeyColumnNotFound(
284 table_name.to_string(),
285 columns.join(", "),
286 ref_table.clone(),
287 ref_col.clone(),
288 ));
289 }
290 }
291
292 if columns.len() != ref_columns.len() {
294 return Err(PlannerError::ForeignKeyColumnNotFound(
295 table_name.to_string(),
296 format!(
297 "column count mismatch: {} != {}",
298 columns.len(),
299 ref_columns.len()
300 ),
301 ref_table.clone(),
302 "".to_string(),
303 ));
304 }
305 }
306 TableConstraint::Check { .. } => {
307 }
309 TableConstraint::Index { name, columns } => {
310 if columns.is_empty() {
311 let index_name = name.clone().unwrap_or_else(|| "(unnamed)".to_string());
312 return Err(PlannerError::EmptyConstraintColumns(
313 table_name.to_string(),
314 format!("Index({})", index_name),
315 ));
316 }
317
318 for col in columns {
319 if !table_columns.contains(col.as_str()) {
320 let index_name = name.clone().unwrap_or_else(|| "(unnamed)".to_string());
321 return Err(PlannerError::IndexColumnNotFound(
322 table_name.to_string(),
323 index_name,
324 col.clone(),
325 ));
326 }
327 }
328 }
329 }
330
331 Ok(())
332}
333
334pub fn validate_migration_plan(plan: &MigrationPlan) -> Result<(), PlannerError> {
340 for action in &plan.actions {
341 match action {
342 MigrationAction::AddColumn {
343 table,
344 column,
345 fill_with,
346 } => {
347 if !column.nullable && column.default.is_none() && fill_with.is_none() {
349 return Err(PlannerError::MissingFillWith(
350 table.clone(),
351 column.name.clone(),
352 ));
353 }
354
355 if let ColumnType::Complex(ComplexColumnType::Enum { name, values }) =
357 &column.r#type
358 {
359 if let Some(fill) = fill_with {
360 validate_enum_value(fill, name, values, table, &column.name, "fill_with")?;
361 }
362 if let Some(default) = &column.default {
363 let default_str = default.to_sql();
364 validate_enum_value(
365 &default_str,
366 name,
367 values,
368 table,
369 &column.name,
370 "default",
371 )?;
372 }
373 }
374 }
375 MigrationAction::ModifyColumnNullable {
376 table,
377 column,
378 nullable,
379 fill_with,
380 } => {
381 if !nullable && fill_with.is_none() {
383 return Err(PlannerError::MissingFillWith(table.clone(), column.clone()));
384 }
385 }
386 _ => {}
387 }
388 }
389 Ok(())
390}
391
392#[derive(Debug, Clone, PartialEq, Eq)]
394pub struct FillWithRequired {
395 pub action_index: usize,
397 pub table: String,
399 pub column: String,
401 pub action_type: &'static str,
403 pub column_type: Option<String>,
405}
406
407pub fn find_missing_fill_with(plan: &MigrationPlan) -> Vec<FillWithRequired> {
410 let mut missing = Vec::new();
411
412 for (idx, action) in plan.actions.iter().enumerate() {
413 match action {
414 MigrationAction::AddColumn {
415 table,
416 column,
417 fill_with,
418 } => {
419 if !column.nullable && column.default.is_none() && fill_with.is_none() {
421 missing.push(FillWithRequired {
422 action_index: idx,
423 table: table.clone(),
424 column: column.name.clone(),
425 action_type: "AddColumn",
426 column_type: Some(format!("{:?}", column.r#type)),
427 });
428 }
429 }
430 MigrationAction::ModifyColumnNullable {
431 table,
432 column,
433 nullable,
434 fill_with,
435 } => {
436 if !nullable && fill_with.is_none() {
438 missing.push(FillWithRequired {
439 action_index: idx,
440 table: table.clone(),
441 column: column.clone(),
442 action_type: "ModifyColumnNullable",
443 column_type: None,
444 });
445 }
446 }
447 _ => {}
448 }
449 }
450
451 missing
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use rstest::rstest;
458 use vespertide_core::{
459 ColumnDef, ColumnType, ComplexColumnType, EnumValues, NumValue, SimpleColumnType,
460 TableConstraint,
461 };
462
463 fn col(name: &str, ty: ColumnType) -> ColumnDef {
464 ColumnDef {
465 name: name.to_string(),
466 r#type: ty,
467 nullable: true,
468 default: None,
469 comment: None,
470 primary_key: None,
471 unique: None,
472 index: None,
473 foreign_key: None,
474 }
475 }
476
477 fn table(name: &str, columns: Vec<ColumnDef>, constraints: Vec<TableConstraint>) -> TableDef {
478 TableDef {
479 name: name.to_string(),
480 columns,
481 constraints,
482 }
483 }
484
485 fn idx(name: &str, columns: Vec<&str>) -> TableConstraint {
486 TableConstraint::Index {
487 name: Some(name.to_string()),
488 columns: columns.into_iter().map(|s| s.to_string()).collect(),
489 }
490 }
491
492 fn is_duplicate(err: &PlannerError) -> bool {
493 matches!(err, PlannerError::DuplicateTableName(_))
494 }
495
496 fn is_fk_table(err: &PlannerError) -> bool {
497 matches!(err, PlannerError::ForeignKeyTableNotFound(_, _, _))
498 }
499
500 fn is_fk_column(err: &PlannerError) -> bool {
501 matches!(err, PlannerError::ForeignKeyColumnNotFound(_, _, _, _))
502 }
503
504 fn is_index_column(err: &PlannerError) -> bool {
505 matches!(err, PlannerError::IndexColumnNotFound(_, _, _))
506 }
507
508 fn is_constraint_column(err: &PlannerError) -> bool {
509 matches!(err, PlannerError::ConstraintColumnNotFound(_, _, _))
510 }
511
512 fn is_empty_columns(err: &PlannerError) -> bool {
513 matches!(err, PlannerError::EmptyConstraintColumns(_, _))
514 }
515
516 fn is_missing_pk(err: &PlannerError) -> bool {
517 matches!(err, PlannerError::MissingPrimaryKey(_))
518 }
519
520 fn pk(columns: Vec<&str>) -> TableConstraint {
521 TableConstraint::PrimaryKey {
522 auto_increment: false,
523 columns: columns.into_iter().map(|s| s.to_string()).collect(),
524 }
525 }
526
527 #[rstest]
528 #[case::valid_schema(
529 vec![table(
530 "users",
531 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
532 vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }],
533 )],
534 None
535 )]
536 #[case::duplicate_table(
537 vec![
538 table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![]),
539 table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![]),
540 ],
541 Some(is_duplicate as fn(&PlannerError) -> bool)
542 )]
543 #[case::fk_missing_table(
544 vec![table(
545 "users",
546 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
547 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
548 name: None,
549 columns: vec!["id".into()],
550 ref_table: "nonexistent".into(),
551 ref_columns: vec!["id".into()],
552 on_delete: None,
553 on_update: None,
554 }],
555 )],
556 Some(is_fk_table as fn(&PlannerError) -> bool)
557 )]
558 #[case::fk_missing_column(
559 vec![
560 table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])]),
561 table(
562 "users",
563 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
564 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
565 name: None,
566 columns: vec!["id".into()],
567 ref_table: "posts".into(),
568 ref_columns: vec!["nonexistent".into()],
569 on_delete: None,
570 on_update: None,
571 }],
572 ),
573 ],
574 Some(is_fk_column as fn(&PlannerError) -> bool)
575 )]
576 #[case::fk_local_missing_column(
577 vec![
578 table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])]),
579 table(
580 "users",
581 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
582 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
583 name: None,
584 columns: vec!["missing".into()],
585 ref_table: "posts".into(),
586 ref_columns: vec!["id".into()],
587 on_delete: None,
588 on_update: None,
589 }],
590 ),
591 ],
592 Some(is_constraint_column as fn(&PlannerError) -> bool)
593 )]
594 #[case::fk_valid(
595 vec![
596 table(
597 "posts",
598 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
599 vec![pk(vec!["id"])],
600 ),
601 table(
602 "users",
603 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("post_id", ColumnType::Simple(SimpleColumnType::Integer))],
604 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
605 name: None,
606 columns: vec!["post_id".into()],
607 ref_table: "posts".into(),
608 ref_columns: vec!["id".into()],
609 on_delete: None,
610 on_update: None,
611 }],
612 ),
613 ],
614 None
615 )]
616 #[case::index_missing_column(
617 vec![table(
618 "users",
619 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
620 vec![pk(vec!["id"]), idx("idx_name", vec!["nonexistent"])],
621 )],
622 Some(is_index_column as fn(&PlannerError) -> bool)
623 )]
624 #[case::constraint_missing_column(
625 vec![table(
626 "users",
627 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
628 vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["nonexistent".into()] }],
629 )],
630 Some(is_constraint_column as fn(&PlannerError) -> bool)
631 )]
632 #[case::unique_empty_columns(
633 vec![table(
634 "users",
635 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
636 vec![pk(vec!["id"]), TableConstraint::Unique {
637 name: Some("u".into()),
638 columns: vec![],
639 }],
640 )],
641 Some(is_empty_columns as fn(&PlannerError) -> bool)
642 )]
643 #[case::unique_missing_column(
644 vec![table(
645 "users",
646 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
647 vec![pk(vec!["id"]), TableConstraint::Unique {
648 name: None,
649 columns: vec!["missing".into()],
650 }],
651 )],
652 Some(is_constraint_column as fn(&PlannerError) -> bool)
653 )]
654 #[case::empty_primary_key(
655 vec![table(
656 "users",
657 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
658 vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec![] }],
659 )],
660 Some(is_empty_columns as fn(&PlannerError) -> bool)
661 )]
662 #[case::fk_column_count_mismatch(
663 vec![
664 table(
665 "posts",
666 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
667 vec![pk(vec!["id"])],
668 ),
669 table(
670 "users",
671 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("post_id", ColumnType::Simple(SimpleColumnType::Integer))],
672 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
673 name: None,
674 columns: vec!["id".into(), "post_id".into()],
675 ref_table: "posts".into(),
676 ref_columns: vec!["id".into()],
677 on_delete: None,
678 on_update: None,
679 }],
680 ),
681 ],
682 Some(is_fk_column as fn(&PlannerError) -> bool)
683 )]
684 #[case::fk_empty_columns(
685 vec![table(
686 "users",
687 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
688 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
689 name: None,
690 columns: vec![],
691 ref_table: "posts".into(),
692 ref_columns: vec!["id".into()],
693 on_delete: None,
694 on_update: None,
695 }],
696 )],
697 Some(is_empty_columns as fn(&PlannerError) -> bool)
698 )]
699 #[case::fk_empty_ref_columns(
700 vec![
701 table(
702 "posts",
703 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
704 vec![pk(vec!["id"])],
705 ),
706 table(
707 "users",
708 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
709 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
710 name: None,
711 columns: vec!["id".into()],
712 ref_table: "posts".into(),
713 ref_columns: vec![],
714 on_delete: None,
715 on_update: None,
716 }],
717 ),
718 ],
719 Some(is_empty_columns as fn(&PlannerError) -> bool)
720 )]
721 #[case::index_empty_columns(
722 vec![table(
723 "users",
724 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
725 vec![pk(vec!["id"]), TableConstraint::Index {
726 name: Some("idx".into()),
727 columns: vec![],
728 }],
729 )],
730 Some(is_empty_columns as fn(&PlannerError) -> bool)
731 )]
732 #[case::index_valid(
733 vec![table(
734 "users",
735 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("name", ColumnType::Simple(SimpleColumnType::Text))],
736 vec![pk(vec!["id"]), idx("idx_name", vec!["name"])],
737 )],
738 None
739 )]
740 #[case::check_constraint_ok(
741 vec![table(
742 "users",
743 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
744 vec![pk(vec!["id"]), TableConstraint::Check {
745 name: "ck".into(),
746 expr: "id > 0".into(),
747 }],
748 )],
749 None
750 )]
751 #[case::missing_primary_key(
752 vec![table(
753 "users",
754 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
755 vec![],
756 )],
757 Some(is_missing_pk as fn(&PlannerError) -> bool)
758 )]
759 fn validate_schema_cases(
760 #[case] schema: Vec<TableDef>,
761 #[case] expected_err: Option<fn(&PlannerError) -> bool>,
762 ) {
763 let result = validate_schema(&schema);
764 match expected_err {
765 None => assert!(result.is_ok()),
766 Some(pred) => {
767 let err = result.unwrap_err();
768 assert!(pred(&err), "unexpected error: {:?}", err);
769 }
770 }
771 }
772
773 #[test]
774 fn validate_migration_plan_missing_fill_with() {
775 use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
776
777 let plan = MigrationPlan {
778 comment: None,
779 created_at: None,
780 version: 1,
781 actions: vec![MigrationAction::AddColumn {
782 table: "users".into(),
783 column: Box::new(ColumnDef {
784 name: "email".into(),
785 r#type: ColumnType::Simple(SimpleColumnType::Text),
786 nullable: false,
787 default: None,
788 comment: None,
789 primary_key: None,
790 unique: None,
791 index: None,
792 foreign_key: None,
793 }),
794 fill_with: None,
795 }],
796 };
797
798 let result = validate_migration_plan(&plan);
799 assert!(result.is_err());
800 match result.unwrap_err() {
801 PlannerError::MissingFillWith(table, column) => {
802 assert_eq!(table, "users");
803 assert_eq!(column, "email");
804 }
805 _ => panic!("expected MissingFillWith error"),
806 }
807 }
808
809 #[test]
810 fn validate_migration_plan_with_fill_with() {
811 use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
812
813 let plan = MigrationPlan {
814 comment: None,
815 created_at: None,
816 version: 1,
817 actions: vec![MigrationAction::AddColumn {
818 table: "users".into(),
819 column: Box::new(ColumnDef {
820 name: "email".into(),
821 r#type: ColumnType::Simple(SimpleColumnType::Text),
822 nullable: false,
823 default: None,
824 comment: None,
825 primary_key: None,
826 unique: None,
827 index: None,
828 foreign_key: None,
829 }),
830 fill_with: Some("default@example.com".into()),
831 }],
832 };
833
834 let result = validate_migration_plan(&plan);
835 assert!(result.is_ok());
836 }
837
838 #[test]
839 fn validate_migration_plan_nullable_column() {
840 use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
841
842 let plan = MigrationPlan {
843 comment: None,
844 created_at: None,
845 version: 1,
846 actions: vec![MigrationAction::AddColumn {
847 table: "users".into(),
848 column: Box::new(ColumnDef {
849 name: "email".into(),
850 r#type: ColumnType::Simple(SimpleColumnType::Text),
851 nullable: true,
852 default: None,
853 comment: None,
854 primary_key: None,
855 unique: None,
856 index: None,
857 foreign_key: None,
858 }),
859 fill_with: None,
860 }],
861 };
862
863 let result = validate_migration_plan(&plan);
864 assert!(result.is_ok());
865 }
866
867 #[test]
868 fn validate_migration_plan_with_default() {
869 use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
870
871 let plan = MigrationPlan {
872 comment: None,
873 created_at: None,
874 version: 1,
875 actions: vec![MigrationAction::AddColumn {
876 table: "users".into(),
877 column: Box::new(ColumnDef {
878 name: "email".into(),
879 r#type: ColumnType::Simple(SimpleColumnType::Text),
880 nullable: false,
881 default: Some("default@example.com".into()),
882 comment: None,
883 primary_key: None,
884 unique: None,
885 index: None,
886 foreign_key: None,
887 }),
888 fill_with: None,
889 }],
890 };
891
892 let result = validate_migration_plan(&plan);
893 assert!(result.is_ok());
894 }
895
896 #[test]
897 fn validate_string_enum_duplicate_variant_name() {
898 let schema = vec![table(
899 "users",
900 vec![
901 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
902 col(
903 "status",
904 ColumnType::Complex(ComplexColumnType::Enum {
905 name: "user_status".into(),
906 values: EnumValues::String(vec![
907 "active".into(),
908 "inactive".into(),
909 "active".into(), ]),
911 }),
912 ),
913 ],
914 vec![TableConstraint::PrimaryKey {
915 auto_increment: false,
916 columns: vec!["id".into()],
917 }],
918 )];
919
920 let result = validate_schema(&schema);
921 assert!(result.is_err());
922 match result.unwrap_err() {
923 PlannerError::DuplicateEnumVariantName(enum_name, table, column, variant) => {
924 assert_eq!(enum_name, "user_status");
925 assert_eq!(table, "users");
926 assert_eq!(column, "status");
927 assert_eq!(variant, "active");
928 }
929 err => panic!("expected DuplicateEnumVariantName, got {:?}", err),
930 }
931 }
932
933 #[test]
934 fn validate_integer_enum_duplicate_variant_name() {
935 let schema = vec![table(
936 "tasks",
937 vec![
938 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
939 col(
940 "priority",
941 ColumnType::Complex(ComplexColumnType::Enum {
942 name: "priority_level".into(),
943 values: EnumValues::Integer(vec![
944 NumValue {
945 name: "Low".into(),
946 value: 0,
947 },
948 NumValue {
949 name: "High".into(),
950 value: 1,
951 },
952 NumValue {
953 name: "Low".into(), value: 2,
955 },
956 ]),
957 }),
958 ),
959 ],
960 vec![TableConstraint::PrimaryKey {
961 auto_increment: false,
962 columns: vec!["id".into()],
963 }],
964 )];
965
966 let result = validate_schema(&schema);
967 assert!(result.is_err());
968 match result.unwrap_err() {
969 PlannerError::DuplicateEnumVariantName(enum_name, table, column, variant) => {
970 assert_eq!(enum_name, "priority_level");
971 assert_eq!(table, "tasks");
972 assert_eq!(column, "priority");
973 assert_eq!(variant, "Low");
974 }
975 err => panic!("expected DuplicateEnumVariantName, got {:?}", err),
976 }
977 }
978
979 #[test]
980 fn validate_integer_enum_duplicate_value() {
981 let schema = vec![table(
982 "tasks",
983 vec![
984 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
985 col(
986 "priority",
987 ColumnType::Complex(ComplexColumnType::Enum {
988 name: "priority_level".into(),
989 values: EnumValues::Integer(vec![
990 NumValue {
991 name: "Low".into(),
992 value: 0,
993 },
994 NumValue {
995 name: "Medium".into(),
996 value: 1,
997 },
998 NumValue {
999 name: "High".into(),
1000 value: 0, },
1002 ]),
1003 }),
1004 ),
1005 ],
1006 vec![TableConstraint::PrimaryKey {
1007 auto_increment: false,
1008 columns: vec!["id".into()],
1009 }],
1010 )];
1011
1012 let result = validate_schema(&schema);
1013 assert!(result.is_err());
1014 match result.unwrap_err() {
1015 PlannerError::DuplicateEnumValue(enum_name, table, column, value) => {
1016 assert_eq!(enum_name, "priority_level");
1017 assert_eq!(table, "tasks");
1018 assert_eq!(column, "priority");
1019 assert_eq!(value, 0);
1020 }
1021 err => panic!("expected DuplicateEnumValue, got {:?}", err),
1022 }
1023 }
1024
1025 #[test]
1026 fn validate_enum_valid() {
1027 let schema = vec![table(
1028 "tasks",
1029 vec![
1030 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1031 col(
1032 "status",
1033 ColumnType::Complex(ComplexColumnType::Enum {
1034 name: "task_status".into(),
1035 values: EnumValues::String(vec![
1036 "pending".into(),
1037 "in_progress".into(),
1038 "completed".into(),
1039 ]),
1040 }),
1041 ),
1042 col(
1043 "priority",
1044 ColumnType::Complex(ComplexColumnType::Enum {
1045 name: "priority_level".into(),
1046 values: EnumValues::Integer(vec![
1047 NumValue {
1048 name: "Low".into(),
1049 value: 0,
1050 },
1051 NumValue {
1052 name: "Medium".into(),
1053 value: 50,
1054 },
1055 NumValue {
1056 name: "High".into(),
1057 value: 100,
1058 },
1059 ]),
1060 }),
1061 ),
1062 ],
1063 vec![TableConstraint::PrimaryKey {
1064 auto_increment: false,
1065 columns: vec!["id".into()],
1066 }],
1067 )];
1068
1069 let result = validate_schema(&schema);
1070 assert!(result.is_ok());
1071 }
1072
1073 #[test]
1074 fn validate_migration_plan_modify_nullable_to_non_nullable_missing_fill_with() {
1075 let plan = MigrationPlan {
1076 comment: None,
1077 created_at: None,
1078 version: 1,
1079 actions: vec![MigrationAction::ModifyColumnNullable {
1080 table: "users".into(),
1081 column: "email".into(),
1082 nullable: false,
1083 fill_with: None,
1084 }],
1085 };
1086
1087 let result = validate_migration_plan(&plan);
1088 assert!(result.is_err());
1089 match result.unwrap_err() {
1090 PlannerError::MissingFillWith(table, column) => {
1091 assert_eq!(table, "users");
1092 assert_eq!(column, "email");
1093 }
1094 _ => panic!("expected MissingFillWith error"),
1095 }
1096 }
1097
1098 #[test]
1099 fn validate_migration_plan_modify_nullable_to_non_nullable_with_fill_with() {
1100 let plan = MigrationPlan {
1101 comment: None,
1102 created_at: None,
1103 version: 1,
1104 actions: vec![MigrationAction::ModifyColumnNullable {
1105 table: "users".into(),
1106 column: "email".into(),
1107 nullable: false,
1108 fill_with: Some("'unknown'".into()),
1109 }],
1110 };
1111
1112 let result = validate_migration_plan(&plan);
1113 assert!(result.is_ok());
1114 }
1115
1116 #[test]
1117 fn validate_migration_plan_modify_non_nullable_to_nullable() {
1118 let plan = MigrationPlan {
1120 comment: None,
1121 created_at: None,
1122 version: 1,
1123 actions: vec![MigrationAction::ModifyColumnNullable {
1124 table: "users".into(),
1125 column: "email".into(),
1126 nullable: true,
1127 fill_with: None,
1128 }],
1129 };
1130
1131 let result = validate_migration_plan(&plan);
1132 assert!(result.is_ok());
1133 }
1134
1135 #[test]
1136 fn validate_enum_add_column_invalid_default() {
1137 let plan = MigrationPlan {
1138 comment: None,
1139 created_at: None,
1140 version: 1,
1141 actions: vec![MigrationAction::AddColumn {
1142 table: "users".into(),
1143 column: Box::new(ColumnDef {
1144 name: "status".into(),
1145 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1146 name: "user_status".into(),
1147 values: EnumValues::String(vec![
1148 "active".into(),
1149 "inactive".into(),
1150 "pending".into(),
1151 ]),
1152 }),
1153 nullable: false,
1154 default: Some("invalid_value".into()),
1155 comment: None,
1156 primary_key: None,
1157 unique: None,
1158 index: None,
1159 foreign_key: None,
1160 }),
1161 fill_with: None,
1162 }],
1163 };
1164
1165 let result = validate_migration_plan(&plan);
1166 assert!(result.is_err());
1167 match result.unwrap_err() {
1168 PlannerError::InvalidEnumDefault(err) => {
1169 assert_eq!(err.enum_name, "user_status");
1170 assert_eq!(err.table_name, "users");
1171 assert_eq!(err.column_name, "status");
1172 assert_eq!(err.value_type, "default");
1173 assert_eq!(err.value, "invalid_value");
1174 }
1175 err => panic!("expected InvalidEnumDefault error, got {:?}", err),
1176 }
1177 }
1178
1179 #[test]
1180 fn validate_enum_add_column_invalid_fill_with() {
1181 let plan = MigrationPlan {
1182 comment: None,
1183 created_at: None,
1184 version: 1,
1185 actions: vec![MigrationAction::AddColumn {
1186 table: "users".into(),
1187 column: Box::new(ColumnDef {
1188 name: "status".into(),
1189 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1190 name: "user_status".into(),
1191 values: EnumValues::String(vec![
1192 "active".into(),
1193 "inactive".into(),
1194 "pending".into(),
1195 ]),
1196 }),
1197 nullable: false,
1198 default: None,
1199 comment: None,
1200 primary_key: None,
1201 unique: None,
1202 index: None,
1203 foreign_key: None,
1204 }),
1205 fill_with: Some("unknown_status".into()),
1206 }],
1207 };
1208
1209 let result = validate_migration_plan(&plan);
1210 assert!(result.is_err());
1211 match result.unwrap_err() {
1212 PlannerError::InvalidEnumDefault(err) => {
1213 assert_eq!(err.enum_name, "user_status");
1214 assert_eq!(err.table_name, "users");
1215 assert_eq!(err.column_name, "status");
1216 assert_eq!(err.value_type, "fill_with");
1217 assert_eq!(err.value, "unknown_status");
1218 }
1219 err => panic!("expected InvalidEnumDefault error, got {:?}", err),
1220 }
1221 }
1222
1223 #[test]
1224 fn validate_enum_add_column_valid_default_quoted() {
1225 let plan = MigrationPlan {
1226 comment: None,
1227 created_at: None,
1228 version: 1,
1229 actions: vec![MigrationAction::AddColumn {
1230 table: "users".into(),
1231 column: Box::new(ColumnDef {
1232 name: "status".into(),
1233 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1234 name: "user_status".into(),
1235 values: EnumValues::String(vec![
1236 "active".into(),
1237 "inactive".into(),
1238 "pending".into(),
1239 ]),
1240 }),
1241 nullable: false,
1242 default: Some("'active'".into()),
1243 comment: None,
1244 primary_key: None,
1245 unique: None,
1246 index: None,
1247 foreign_key: None,
1248 }),
1249 fill_with: None,
1250 }],
1251 };
1252
1253 let result = validate_migration_plan(&plan);
1254 assert!(result.is_ok());
1255 }
1256
1257 #[test]
1258 fn validate_enum_add_column_valid_default_unquoted() {
1259 let plan = MigrationPlan {
1260 comment: None,
1261 created_at: None,
1262 version: 1,
1263 actions: vec![MigrationAction::AddColumn {
1264 table: "users".into(),
1265 column: Box::new(ColumnDef {
1266 name: "status".into(),
1267 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1268 name: "user_status".into(),
1269 values: EnumValues::String(vec![
1270 "active".into(),
1271 "inactive".into(),
1272 "pending".into(),
1273 ]),
1274 }),
1275 nullable: false,
1276 default: Some("active".into()),
1277 comment: None,
1278 primary_key: None,
1279 unique: None,
1280 index: None,
1281 foreign_key: None,
1282 }),
1283 fill_with: None,
1284 }],
1285 };
1286
1287 let result = validate_migration_plan(&plan);
1288 assert!(result.is_ok());
1289 }
1290
1291 #[test]
1292 fn validate_enum_add_column_valid_fill_with() {
1293 let plan = MigrationPlan {
1294 comment: None,
1295 created_at: None,
1296 version: 1,
1297 actions: vec![MigrationAction::AddColumn {
1298 table: "users".into(),
1299 column: Box::new(ColumnDef {
1300 name: "status".into(),
1301 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1302 name: "user_status".into(),
1303 values: EnumValues::String(vec![
1304 "active".into(),
1305 "inactive".into(),
1306 "pending".into(),
1307 ]),
1308 }),
1309 nullable: false,
1310 default: None,
1311 comment: None,
1312 primary_key: None,
1313 unique: None,
1314 index: None,
1315 foreign_key: None,
1316 }),
1317 fill_with: Some("'pending'".into()),
1318 }],
1319 };
1320
1321 let result = validate_migration_plan(&plan);
1322 assert!(result.is_ok());
1323 }
1324
1325 #[test]
1326 fn validate_enum_schema_invalid_default() {
1327 let schema = vec![table(
1329 "users",
1330 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), {
1331 let mut c = col(
1332 "status",
1333 ColumnType::Complex(ComplexColumnType::Enum {
1334 name: "user_status".into(),
1335 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1336 }),
1337 );
1338 c.default = Some("invalid".into());
1339 c
1340 }],
1341 vec![pk(vec!["id"])],
1342 )];
1343
1344 let result = validate_schema(&schema);
1345 assert!(result.is_err());
1346 match result.unwrap_err() {
1347 PlannerError::InvalidEnumDefault(err) => {
1348 assert_eq!(err.enum_name, "user_status");
1349 assert_eq!(err.table_name, "users");
1350 assert_eq!(err.column_name, "status");
1351 assert_eq!(err.value_type, "default");
1352 assert_eq!(err.value, "invalid");
1353 }
1354 err => panic!("expected InvalidEnumDefault error, got {:?}", err),
1355 }
1356 }
1357
1358 #[test]
1359 fn validate_enum_schema_valid_default() {
1360 let schema = vec![table(
1361 "users",
1362 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), {
1363 let mut c = col(
1364 "status",
1365 ColumnType::Complex(ComplexColumnType::Enum {
1366 name: "user_status".into(),
1367 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1368 }),
1369 );
1370 c.default = Some("'active'".into());
1371 c
1372 }],
1373 vec![pk(vec!["id"])],
1374 )];
1375
1376 let result = validate_schema(&schema);
1377 assert!(result.is_ok());
1378 }
1379
1380 #[test]
1381 fn validate_enum_integer_add_column_valid() {
1382 let plan = MigrationPlan {
1383 comment: None,
1384 created_at: None,
1385 version: 1,
1386 actions: vec![MigrationAction::AddColumn {
1387 table: "tasks".into(),
1388 column: Box::new(ColumnDef {
1389 name: "priority".into(),
1390 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1391 name: "priority_level".into(),
1392 values: EnumValues::Integer(vec![
1393 NumValue {
1394 name: "Low".into(),
1395 value: 0,
1396 },
1397 NumValue {
1398 name: "Medium".into(),
1399 value: 50,
1400 },
1401 NumValue {
1402 name: "High".into(),
1403 value: 100,
1404 },
1405 ]),
1406 }),
1407 nullable: false,
1408 default: None,
1409 comment: None,
1410 primary_key: None,
1411 unique: None,
1412 index: None,
1413 foreign_key: None,
1414 }),
1415 fill_with: Some("Low".into()),
1416 }],
1417 };
1418
1419 let result = validate_migration_plan(&plan);
1420 assert!(result.is_ok());
1421 }
1422
1423 #[test]
1424 fn validate_enum_integer_add_column_invalid() {
1425 let plan = MigrationPlan {
1426 comment: None,
1427 created_at: None,
1428 version: 1,
1429 actions: vec![MigrationAction::AddColumn {
1430 table: "tasks".into(),
1431 column: Box::new(ColumnDef {
1432 name: "priority".into(),
1433 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1434 name: "priority_level".into(),
1435 values: EnumValues::Integer(vec![
1436 NumValue {
1437 name: "Low".into(),
1438 value: 0,
1439 },
1440 NumValue {
1441 name: "Medium".into(),
1442 value: 50,
1443 },
1444 NumValue {
1445 name: "High".into(),
1446 value: 100,
1447 },
1448 ]),
1449 }),
1450 nullable: false,
1451 default: None,
1452 comment: None,
1453 primary_key: None,
1454 unique: None,
1455 index: None,
1456 foreign_key: None,
1457 }),
1458 fill_with: Some("Critical".into()), }],
1460 };
1461
1462 let result = validate_migration_plan(&plan);
1463 assert!(result.is_err());
1464 match result.unwrap_err() {
1465 PlannerError::InvalidEnumDefault(err) => {
1466 assert_eq!(err.enum_name, "priority_level");
1467 assert_eq!(err.table_name, "tasks");
1468 assert_eq!(err.column_name, "priority");
1469 assert_eq!(err.value_type, "fill_with");
1470 assert_eq!(err.value, "Critical");
1471 }
1472 err => panic!("expected InvalidEnumDefault error, got {:?}", err),
1473 }
1474 }
1475
1476 #[test]
1477 fn validate_enum_null_value_skipped() {
1478 let plan = MigrationPlan {
1480 comment: None,
1481 created_at: None,
1482 version: 1,
1483 actions: vec![MigrationAction::AddColumn {
1484 table: "users".into(),
1485 column: Box::new(ColumnDef {
1486 name: "status".into(),
1487 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1488 name: "user_status".into(),
1489 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1490 }),
1491 nullable: true,
1492 default: Some("NULL".into()),
1493 comment: None,
1494 primary_key: None,
1495 unique: None,
1496 index: None,
1497 foreign_key: None,
1498 }),
1499 fill_with: None,
1500 }],
1501 };
1502
1503 let result = validate_migration_plan(&plan);
1504 assert!(result.is_ok());
1505 }
1506
1507 #[test]
1508 fn validate_enum_sql_expression_skipped() {
1509 let plan = MigrationPlan {
1511 comment: None,
1512 created_at: None,
1513 version: 1,
1514 actions: vec![MigrationAction::AddColumn {
1515 table: "users".into(),
1516 column: Box::new(ColumnDef {
1517 name: "status".into(),
1518 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1519 name: "user_status".into(),
1520 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1521 }),
1522 nullable: true,
1523 default: None,
1524 comment: None,
1525 primary_key: None,
1526 unique: None,
1527 index: None,
1528 foreign_key: None,
1529 }),
1530 fill_with: Some("COALESCE(old_status, 'active')".into()),
1531 }],
1532 };
1533
1534 let result = validate_migration_plan(&plan);
1535 assert!(result.is_ok());
1536 }
1537
1538 #[test]
1539 fn validate_enum_empty_string_fill_with_skipped() {
1540 let plan = MigrationPlan {
1543 comment: None,
1544 created_at: None,
1545 version: 1,
1546 actions: vec![MigrationAction::AddColumn {
1547 table: "users".into(),
1548 column: Box::new(ColumnDef {
1549 name: "status".into(),
1550 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1551 name: "user_status".into(),
1552 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1553 }),
1554 nullable: true,
1555 default: None,
1556 comment: None,
1557 primary_key: None,
1558 unique: None,
1559 index: None,
1560 foreign_key: None,
1561 }),
1562 fill_with: Some(" ".into()),
1564 }],
1565 };
1566
1567 let result = validate_migration_plan(&plan);
1568 assert!(result.is_ok());
1569 }
1570
1571 #[test]
1573 fn find_missing_fill_with_add_column_not_null_no_default() {
1574 let plan = MigrationPlan {
1575 comment: None,
1576 created_at: None,
1577 version: 1,
1578 actions: vec![MigrationAction::AddColumn {
1579 table: "users".into(),
1580 column: Box::new(ColumnDef {
1581 name: "email".into(),
1582 r#type: ColumnType::Simple(SimpleColumnType::Text),
1583 nullable: false,
1584 default: None,
1585 comment: None,
1586 primary_key: None,
1587 unique: None,
1588 index: None,
1589 foreign_key: None,
1590 }),
1591 fill_with: None,
1592 }],
1593 };
1594
1595 let missing = find_missing_fill_with(&plan);
1596 assert_eq!(missing.len(), 1);
1597 assert_eq!(missing[0].table, "users");
1598 assert_eq!(missing[0].column, "email");
1599 assert_eq!(missing[0].action_type, "AddColumn");
1600 assert!(missing[0].column_type.is_some());
1601 }
1602
1603 #[test]
1604 fn find_missing_fill_with_add_column_with_default() {
1605 let plan = MigrationPlan {
1606 comment: None,
1607 created_at: None,
1608 version: 1,
1609 actions: vec![MigrationAction::AddColumn {
1610 table: "users".into(),
1611 column: Box::new(ColumnDef {
1612 name: "email".into(),
1613 r#type: ColumnType::Simple(SimpleColumnType::Text),
1614 nullable: false,
1615 default: Some("'default@example.com'".into()),
1616 comment: None,
1617 primary_key: None,
1618 unique: None,
1619 index: None,
1620 foreign_key: None,
1621 }),
1622 fill_with: None,
1623 }],
1624 };
1625
1626 let missing = find_missing_fill_with(&plan);
1627 assert!(missing.is_empty());
1628 }
1629
1630 #[test]
1631 fn find_missing_fill_with_add_column_nullable() {
1632 let plan = MigrationPlan {
1633 comment: None,
1634 created_at: None,
1635 version: 1,
1636 actions: vec![MigrationAction::AddColumn {
1637 table: "users".into(),
1638 column: Box::new(ColumnDef {
1639 name: "email".into(),
1640 r#type: ColumnType::Simple(SimpleColumnType::Text),
1641 nullable: true,
1642 default: None,
1643 comment: None,
1644 primary_key: None,
1645 unique: None,
1646 index: None,
1647 foreign_key: None,
1648 }),
1649 fill_with: None,
1650 }],
1651 };
1652
1653 let missing = find_missing_fill_with(&plan);
1654 assert!(missing.is_empty());
1655 }
1656
1657 #[test]
1658 fn find_missing_fill_with_add_column_with_fill_with() {
1659 let plan = MigrationPlan {
1660 comment: None,
1661 created_at: None,
1662 version: 1,
1663 actions: vec![MigrationAction::AddColumn {
1664 table: "users".into(),
1665 column: Box::new(ColumnDef {
1666 name: "email".into(),
1667 r#type: ColumnType::Simple(SimpleColumnType::Text),
1668 nullable: false,
1669 default: None,
1670 comment: None,
1671 primary_key: None,
1672 unique: None,
1673 index: None,
1674 foreign_key: None,
1675 }),
1676 fill_with: Some("'default@example.com'".into()),
1677 }],
1678 };
1679
1680 let missing = find_missing_fill_with(&plan);
1681 assert!(missing.is_empty());
1682 }
1683
1684 #[test]
1685 fn find_missing_fill_with_modify_nullable_to_not_null() {
1686 let plan = MigrationPlan {
1687 comment: None,
1688 created_at: None,
1689 version: 1,
1690 actions: vec![MigrationAction::ModifyColumnNullable {
1691 table: "users".into(),
1692 column: "email".into(),
1693 nullable: false,
1694 fill_with: None,
1695 }],
1696 };
1697
1698 let missing = find_missing_fill_with(&plan);
1699 assert_eq!(missing.len(), 1);
1700 assert_eq!(missing[0].table, "users");
1701 assert_eq!(missing[0].column, "email");
1702 assert_eq!(missing[0].action_type, "ModifyColumnNullable");
1703 assert!(missing[0].column_type.is_none());
1704 }
1705
1706 #[test]
1707 fn find_missing_fill_with_modify_to_nullable() {
1708 let plan = MigrationPlan {
1709 comment: None,
1710 created_at: None,
1711 version: 1,
1712 actions: vec![MigrationAction::ModifyColumnNullable {
1713 table: "users".into(),
1714 column: "email".into(),
1715 nullable: true,
1716 fill_with: None,
1717 }],
1718 };
1719
1720 let missing = find_missing_fill_with(&plan);
1721 assert!(missing.is_empty());
1722 }
1723
1724 #[test]
1725 fn find_missing_fill_with_modify_not_null_with_fill_with() {
1726 let plan = MigrationPlan {
1727 comment: None,
1728 created_at: None,
1729 version: 1,
1730 actions: vec![MigrationAction::ModifyColumnNullable {
1731 table: "users".into(),
1732 column: "email".into(),
1733 nullable: false,
1734 fill_with: Some("'default'".into()),
1735 }],
1736 };
1737
1738 let missing = find_missing_fill_with(&plan);
1739 assert!(missing.is_empty());
1740 }
1741
1742 #[test]
1743 fn find_missing_fill_with_multiple_actions() {
1744 let plan = MigrationPlan {
1745 comment: None,
1746 created_at: None,
1747 version: 1,
1748 actions: vec![
1749 MigrationAction::AddColumn {
1750 table: "users".into(),
1751 column: Box::new(ColumnDef {
1752 name: "email".into(),
1753 r#type: ColumnType::Simple(SimpleColumnType::Text),
1754 nullable: false,
1755 default: None,
1756 comment: None,
1757 primary_key: None,
1758 unique: None,
1759 index: None,
1760 foreign_key: None,
1761 }),
1762 fill_with: None,
1763 },
1764 MigrationAction::ModifyColumnNullable {
1765 table: "orders".into(),
1766 column: "status".into(),
1767 nullable: false,
1768 fill_with: None,
1769 },
1770 MigrationAction::AddColumn {
1771 table: "users".into(),
1772 column: Box::new(ColumnDef {
1773 name: "name".into(),
1774 r#type: ColumnType::Simple(SimpleColumnType::Text),
1775 nullable: true, default: None,
1777 comment: None,
1778 primary_key: None,
1779 unique: None,
1780 index: None,
1781 foreign_key: None,
1782 }),
1783 fill_with: None,
1784 },
1785 ],
1786 };
1787
1788 let missing = find_missing_fill_with(&plan);
1789 assert_eq!(missing.len(), 2);
1790 assert_eq!(missing[0].action_index, 0);
1791 assert_eq!(missing[0].table, "users");
1792 assert_eq!(missing[0].column, "email");
1793 assert_eq!(missing[1].action_index, 1);
1794 assert_eq!(missing[1].table, "orders");
1795 assert_eq!(missing[1].column, "status");
1796 }
1797
1798 #[test]
1799 fn find_missing_fill_with_other_actions_ignored() {
1800 let plan = MigrationPlan {
1801 comment: None,
1802 created_at: None,
1803 version: 1,
1804 actions: vec![
1805 MigrationAction::CreateTable {
1806 table: "users".into(),
1807 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
1808 constraints: vec![pk(vec!["id"])],
1809 },
1810 MigrationAction::DeleteColumn {
1811 table: "orders".into(),
1812 column: "old_column".into(),
1813 },
1814 ],
1815 };
1816
1817 let missing = find_missing_fill_with(&plan);
1818 assert!(missing.is_empty());
1819 }
1820}