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 pub default_value: Option<String>,
407}
408
409pub fn find_missing_fill_with(plan: &MigrationPlan) -> Vec<FillWithRequired> {
412 let mut missing = Vec::new();
413
414 for (idx, action) in plan.actions.iter().enumerate() {
415 match action {
416 MigrationAction::AddColumn {
417 table,
418 column,
419 fill_with,
420 } => {
421 if !column.nullable && column.default.is_none() && fill_with.is_none() {
423 missing.push(FillWithRequired {
424 action_index: idx,
425 table: table.clone(),
426 column: column.name.clone(),
427 action_type: "AddColumn",
428 column_type: Some(column.r#type.to_display_string()),
429 default_value: column.r#type.default_fill_value().map(String::from),
430 });
431 }
432 }
433 MigrationAction::ModifyColumnNullable {
434 table,
435 column,
436 nullable,
437 fill_with,
438 } => {
439 if !nullable && fill_with.is_none() {
441 missing.push(FillWithRequired {
442 action_index: idx,
443 table: table.clone(),
444 column: column.clone(),
445 action_type: "ModifyColumnNullable",
446 column_type: None,
447 default_value: None,
448 });
449 }
450 }
451 _ => {}
452 }
453 }
454
455 missing
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use rstest::rstest;
462 use vespertide_core::{
463 ColumnDef, ColumnType, ComplexColumnType, EnumValues, NumValue, SimpleColumnType,
464 TableConstraint,
465 };
466
467 fn col(name: &str, ty: ColumnType) -> ColumnDef {
468 ColumnDef {
469 name: name.to_string(),
470 r#type: ty,
471 nullable: true,
472 default: None,
473 comment: None,
474 primary_key: None,
475 unique: None,
476 index: None,
477 foreign_key: None,
478 }
479 }
480
481 fn table(name: &str, columns: Vec<ColumnDef>, constraints: Vec<TableConstraint>) -> TableDef {
482 TableDef {
483 name: name.to_string(),
484 description: None,
485 columns,
486 constraints,
487 }
488 }
489
490 fn idx(name: &str, columns: Vec<&str>) -> TableConstraint {
491 TableConstraint::Index {
492 name: Some(name.to_string()),
493 columns: columns.into_iter().map(|s| s.to_string()).collect(),
494 }
495 }
496
497 fn is_duplicate(err: &PlannerError) -> bool {
498 matches!(err, PlannerError::DuplicateTableName(_))
499 }
500
501 fn is_fk_table(err: &PlannerError) -> bool {
502 matches!(err, PlannerError::ForeignKeyTableNotFound(_, _, _))
503 }
504
505 fn is_fk_column(err: &PlannerError) -> bool {
506 matches!(err, PlannerError::ForeignKeyColumnNotFound(_, _, _, _))
507 }
508
509 fn is_index_column(err: &PlannerError) -> bool {
510 matches!(err, PlannerError::IndexColumnNotFound(_, _, _))
511 }
512
513 fn is_constraint_column(err: &PlannerError) -> bool {
514 matches!(err, PlannerError::ConstraintColumnNotFound(_, _, _))
515 }
516
517 fn is_empty_columns(err: &PlannerError) -> bool {
518 matches!(err, PlannerError::EmptyConstraintColumns(_, _))
519 }
520
521 fn is_missing_pk(err: &PlannerError) -> bool {
522 matches!(err, PlannerError::MissingPrimaryKey(_))
523 }
524
525 fn pk(columns: Vec<&str>) -> TableConstraint {
526 TableConstraint::PrimaryKey {
527 auto_increment: false,
528 columns: columns.into_iter().map(|s| s.to_string()).collect(),
529 }
530 }
531
532 #[rstest]
533 #[case::valid_schema(
534 vec![table(
535 "users",
536 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
537 vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["id".into()] }],
538 )],
539 None
540 )]
541 #[case::duplicate_table(
542 vec![
543 table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![]),
544 table("users", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![]),
545 ],
546 Some(is_duplicate as fn(&PlannerError) -> bool)
547 )]
548 #[case::fk_missing_table(
549 vec![table(
550 "users",
551 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
552 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
553 name: None,
554 columns: vec!["id".into()],
555 ref_table: "nonexistent".into(),
556 ref_columns: vec!["id".into()],
557 on_delete: None,
558 on_update: None,
559 }],
560 )],
561 Some(is_fk_table as fn(&PlannerError) -> bool)
562 )]
563 #[case::fk_missing_column(
564 vec![
565 table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])]),
566 table(
567 "users",
568 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
569 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
570 name: None,
571 columns: vec!["id".into()],
572 ref_table: "posts".into(),
573 ref_columns: vec!["nonexistent".into()],
574 on_delete: None,
575 on_update: None,
576 }],
577 ),
578 ],
579 Some(is_fk_column as fn(&PlannerError) -> bool)
580 )]
581 #[case::fk_local_missing_column(
582 vec![
583 table("posts", vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))], vec![pk(vec!["id"])]),
584 table(
585 "users",
586 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
587 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
588 name: None,
589 columns: vec!["missing".into()],
590 ref_table: "posts".into(),
591 ref_columns: vec!["id".into()],
592 on_delete: None,
593 on_update: None,
594 }],
595 ),
596 ],
597 Some(is_constraint_column as fn(&PlannerError) -> bool)
598 )]
599 #[case::fk_valid(
600 vec![
601 table(
602 "posts",
603 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
604 vec![pk(vec!["id"])],
605 ),
606 table(
607 "users",
608 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("post_id", ColumnType::Simple(SimpleColumnType::Integer))],
609 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
610 name: None,
611 columns: vec!["post_id".into()],
612 ref_table: "posts".into(),
613 ref_columns: vec!["id".into()],
614 on_delete: None,
615 on_update: None,
616 }],
617 ),
618 ],
619 None
620 )]
621 #[case::index_missing_column(
622 vec![table(
623 "users",
624 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
625 vec![pk(vec!["id"]), idx("idx_name", vec!["nonexistent"])],
626 )],
627 Some(is_index_column as fn(&PlannerError) -> bool)
628 )]
629 #[case::constraint_missing_column(
630 vec![table(
631 "users",
632 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
633 vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec!["nonexistent".into()] }],
634 )],
635 Some(is_constraint_column as fn(&PlannerError) -> bool)
636 )]
637 #[case::unique_empty_columns(
638 vec![table(
639 "users",
640 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
641 vec![pk(vec!["id"]), TableConstraint::Unique {
642 name: Some("u".into()),
643 columns: vec![],
644 }],
645 )],
646 Some(is_empty_columns as fn(&PlannerError) -> bool)
647 )]
648 #[case::unique_missing_column(
649 vec![table(
650 "users",
651 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
652 vec![pk(vec!["id"]), TableConstraint::Unique {
653 name: None,
654 columns: vec!["missing".into()],
655 }],
656 )],
657 Some(is_constraint_column as fn(&PlannerError) -> bool)
658 )]
659 #[case::empty_primary_key(
660 vec![table(
661 "users",
662 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
663 vec![TableConstraint::PrimaryKey{ auto_increment: false, columns: vec![] }],
664 )],
665 Some(is_empty_columns as fn(&PlannerError) -> bool)
666 )]
667 #[case::fk_column_count_mismatch(
668 vec![
669 table(
670 "posts",
671 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
672 vec![pk(vec!["id"])],
673 ),
674 table(
675 "users",
676 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("post_id", ColumnType::Simple(SimpleColumnType::Integer))],
677 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
678 name: None,
679 columns: vec!["id".into(), "post_id".into()],
680 ref_table: "posts".into(),
681 ref_columns: vec!["id".into()],
682 on_delete: None,
683 on_update: None,
684 }],
685 ),
686 ],
687 Some(is_fk_column as fn(&PlannerError) -> bool)
688 )]
689 #[case::fk_empty_columns(
690 vec![table(
691 "users",
692 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
693 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
694 name: None,
695 columns: vec![],
696 ref_table: "posts".into(),
697 ref_columns: vec!["id".into()],
698 on_delete: None,
699 on_update: None,
700 }],
701 )],
702 Some(is_empty_columns as fn(&PlannerError) -> bool)
703 )]
704 #[case::fk_empty_ref_columns(
705 vec![
706 table(
707 "posts",
708 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
709 vec![pk(vec!["id"])],
710 ),
711 table(
712 "users",
713 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
714 vec![pk(vec!["id"]), TableConstraint::ForeignKey {
715 name: None,
716 columns: vec!["id".into()],
717 ref_table: "posts".into(),
718 ref_columns: vec![],
719 on_delete: None,
720 on_update: None,
721 }],
722 ),
723 ],
724 Some(is_empty_columns as fn(&PlannerError) -> bool)
725 )]
726 #[case::index_empty_columns(
727 vec![table(
728 "users",
729 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
730 vec![pk(vec!["id"]), TableConstraint::Index {
731 name: Some("idx".into()),
732 columns: vec![],
733 }],
734 )],
735 Some(is_empty_columns as fn(&PlannerError) -> bool)
736 )]
737 #[case::index_valid(
738 vec![table(
739 "users",
740 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), col("name", ColumnType::Simple(SimpleColumnType::Text))],
741 vec![pk(vec!["id"]), idx("idx_name", vec!["name"])],
742 )],
743 None
744 )]
745 #[case::check_constraint_ok(
746 vec![table(
747 "users",
748 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
749 vec![pk(vec!["id"]), TableConstraint::Check {
750 name: "ck".into(),
751 expr: "id > 0".into(),
752 }],
753 )],
754 None
755 )]
756 #[case::missing_primary_key(
757 vec![table(
758 "users",
759 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
760 vec![],
761 )],
762 Some(is_missing_pk as fn(&PlannerError) -> bool)
763 )]
764 fn validate_schema_cases(
765 #[case] schema: Vec<TableDef>,
766 #[case] expected_err: Option<fn(&PlannerError) -> bool>,
767 ) {
768 let result = validate_schema(&schema);
769 match expected_err {
770 None => assert!(result.is_ok()),
771 Some(pred) => {
772 let err = result.unwrap_err();
773 assert!(pred(&err), "unexpected error: {:?}", err);
774 }
775 }
776 }
777
778 #[test]
779 fn validate_migration_plan_missing_fill_with() {
780 use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
781
782 let plan = MigrationPlan {
783 comment: None,
784 created_at: None,
785 version: 1,
786 actions: vec![MigrationAction::AddColumn {
787 table: "users".into(),
788 column: Box::new(ColumnDef {
789 name: "email".into(),
790 r#type: ColumnType::Simple(SimpleColumnType::Text),
791 nullable: false,
792 default: None,
793 comment: None,
794 primary_key: None,
795 unique: None,
796 index: None,
797 foreign_key: None,
798 }),
799 fill_with: None,
800 }],
801 };
802
803 let result = validate_migration_plan(&plan);
804 assert!(result.is_err());
805 match result.unwrap_err() {
806 PlannerError::MissingFillWith(table, column) => {
807 assert_eq!(table, "users");
808 assert_eq!(column, "email");
809 }
810 _ => panic!("expected MissingFillWith error"),
811 }
812 }
813
814 #[test]
815 fn validate_migration_plan_with_fill_with() {
816 use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
817
818 let plan = MigrationPlan {
819 comment: None,
820 created_at: None,
821 version: 1,
822 actions: vec![MigrationAction::AddColumn {
823 table: "users".into(),
824 column: Box::new(ColumnDef {
825 name: "email".into(),
826 r#type: ColumnType::Simple(SimpleColumnType::Text),
827 nullable: false,
828 default: None,
829 comment: None,
830 primary_key: None,
831 unique: None,
832 index: None,
833 foreign_key: None,
834 }),
835 fill_with: Some("default@example.com".into()),
836 }],
837 };
838
839 let result = validate_migration_plan(&plan);
840 assert!(result.is_ok());
841 }
842
843 #[test]
844 fn validate_migration_plan_nullable_column() {
845 use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
846
847 let plan = MigrationPlan {
848 comment: None,
849 created_at: None,
850 version: 1,
851 actions: vec![MigrationAction::AddColumn {
852 table: "users".into(),
853 column: Box::new(ColumnDef {
854 name: "email".into(),
855 r#type: ColumnType::Simple(SimpleColumnType::Text),
856 nullable: true,
857 default: None,
858 comment: None,
859 primary_key: None,
860 unique: None,
861 index: None,
862 foreign_key: None,
863 }),
864 fill_with: None,
865 }],
866 };
867
868 let result = validate_migration_plan(&plan);
869 assert!(result.is_ok());
870 }
871
872 #[test]
873 fn validate_migration_plan_with_default() {
874 use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
875
876 let plan = MigrationPlan {
877 comment: None,
878 created_at: None,
879 version: 1,
880 actions: vec![MigrationAction::AddColumn {
881 table: "users".into(),
882 column: Box::new(ColumnDef {
883 name: "email".into(),
884 r#type: ColumnType::Simple(SimpleColumnType::Text),
885 nullable: false,
886 default: Some("default@example.com".into()),
887 comment: None,
888 primary_key: None,
889 unique: None,
890 index: None,
891 foreign_key: None,
892 }),
893 fill_with: None,
894 }],
895 };
896
897 let result = validate_migration_plan(&plan);
898 assert!(result.is_ok());
899 }
900
901 #[test]
902 fn validate_string_enum_duplicate_variant_name() {
903 let schema = vec![table(
904 "users",
905 vec![
906 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
907 col(
908 "status",
909 ColumnType::Complex(ComplexColumnType::Enum {
910 name: "user_status".into(),
911 values: EnumValues::String(vec![
912 "active".into(),
913 "inactive".into(),
914 "active".into(), ]),
916 }),
917 ),
918 ],
919 vec![TableConstraint::PrimaryKey {
920 auto_increment: false,
921 columns: vec!["id".into()],
922 }],
923 )];
924
925 let result = validate_schema(&schema);
926 assert!(result.is_err());
927 match result.unwrap_err() {
928 PlannerError::DuplicateEnumVariantName(enum_name, table, column, variant) => {
929 assert_eq!(enum_name, "user_status");
930 assert_eq!(table, "users");
931 assert_eq!(column, "status");
932 assert_eq!(variant, "active");
933 }
934 err => panic!("expected DuplicateEnumVariantName, got {:?}", err),
935 }
936 }
937
938 #[test]
939 fn validate_integer_enum_duplicate_variant_name() {
940 let schema = vec![table(
941 "tasks",
942 vec![
943 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
944 col(
945 "priority",
946 ColumnType::Complex(ComplexColumnType::Enum {
947 name: "priority_level".into(),
948 values: EnumValues::Integer(vec![
949 NumValue {
950 name: "Low".into(),
951 value: 0,
952 },
953 NumValue {
954 name: "High".into(),
955 value: 1,
956 },
957 NumValue {
958 name: "Low".into(), value: 2,
960 },
961 ]),
962 }),
963 ),
964 ],
965 vec![TableConstraint::PrimaryKey {
966 auto_increment: false,
967 columns: vec!["id".into()],
968 }],
969 )];
970
971 let result = validate_schema(&schema);
972 assert!(result.is_err());
973 match result.unwrap_err() {
974 PlannerError::DuplicateEnumVariantName(enum_name, table, column, variant) => {
975 assert_eq!(enum_name, "priority_level");
976 assert_eq!(table, "tasks");
977 assert_eq!(column, "priority");
978 assert_eq!(variant, "Low");
979 }
980 err => panic!("expected DuplicateEnumVariantName, got {:?}", err),
981 }
982 }
983
984 #[test]
985 fn validate_integer_enum_duplicate_value() {
986 let schema = vec![table(
987 "tasks",
988 vec![
989 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
990 col(
991 "priority",
992 ColumnType::Complex(ComplexColumnType::Enum {
993 name: "priority_level".into(),
994 values: EnumValues::Integer(vec![
995 NumValue {
996 name: "Low".into(),
997 value: 0,
998 },
999 NumValue {
1000 name: "Medium".into(),
1001 value: 1,
1002 },
1003 NumValue {
1004 name: "High".into(),
1005 value: 0, },
1007 ]),
1008 }),
1009 ),
1010 ],
1011 vec![TableConstraint::PrimaryKey {
1012 auto_increment: false,
1013 columns: vec!["id".into()],
1014 }],
1015 )];
1016
1017 let result = validate_schema(&schema);
1018 assert!(result.is_err());
1019 match result.unwrap_err() {
1020 PlannerError::DuplicateEnumValue(enum_name, table, column, value) => {
1021 assert_eq!(enum_name, "priority_level");
1022 assert_eq!(table, "tasks");
1023 assert_eq!(column, "priority");
1024 assert_eq!(value, 0);
1025 }
1026 err => panic!("expected DuplicateEnumValue, got {:?}", err),
1027 }
1028 }
1029
1030 #[test]
1031 fn validate_enum_valid() {
1032 let schema = vec![table(
1033 "tasks",
1034 vec![
1035 col("id", ColumnType::Simple(SimpleColumnType::Integer)),
1036 col(
1037 "status",
1038 ColumnType::Complex(ComplexColumnType::Enum {
1039 name: "task_status".into(),
1040 values: EnumValues::String(vec![
1041 "pending".into(),
1042 "in_progress".into(),
1043 "completed".into(),
1044 ]),
1045 }),
1046 ),
1047 col(
1048 "priority",
1049 ColumnType::Complex(ComplexColumnType::Enum {
1050 name: "priority_level".into(),
1051 values: EnumValues::Integer(vec![
1052 NumValue {
1053 name: "Low".into(),
1054 value: 0,
1055 },
1056 NumValue {
1057 name: "Medium".into(),
1058 value: 50,
1059 },
1060 NumValue {
1061 name: "High".into(),
1062 value: 100,
1063 },
1064 ]),
1065 }),
1066 ),
1067 ],
1068 vec![TableConstraint::PrimaryKey {
1069 auto_increment: false,
1070 columns: vec!["id".into()],
1071 }],
1072 )];
1073
1074 let result = validate_schema(&schema);
1075 assert!(result.is_ok());
1076 }
1077
1078 #[test]
1079 fn validate_migration_plan_modify_nullable_to_non_nullable_missing_fill_with() {
1080 let plan = MigrationPlan {
1081 comment: None,
1082 created_at: None,
1083 version: 1,
1084 actions: vec![MigrationAction::ModifyColumnNullable {
1085 table: "users".into(),
1086 column: "email".into(),
1087 nullable: false,
1088 fill_with: None,
1089 }],
1090 };
1091
1092 let result = validate_migration_plan(&plan);
1093 assert!(result.is_err());
1094 match result.unwrap_err() {
1095 PlannerError::MissingFillWith(table, column) => {
1096 assert_eq!(table, "users");
1097 assert_eq!(column, "email");
1098 }
1099 _ => panic!("expected MissingFillWith error"),
1100 }
1101 }
1102
1103 #[test]
1104 fn validate_migration_plan_modify_nullable_to_non_nullable_with_fill_with() {
1105 let plan = MigrationPlan {
1106 comment: None,
1107 created_at: None,
1108 version: 1,
1109 actions: vec![MigrationAction::ModifyColumnNullable {
1110 table: "users".into(),
1111 column: "email".into(),
1112 nullable: false,
1113 fill_with: Some("'unknown'".into()),
1114 }],
1115 };
1116
1117 let result = validate_migration_plan(&plan);
1118 assert!(result.is_ok());
1119 }
1120
1121 #[test]
1122 fn validate_migration_plan_modify_non_nullable_to_nullable() {
1123 let plan = MigrationPlan {
1125 comment: None,
1126 created_at: None,
1127 version: 1,
1128 actions: vec![MigrationAction::ModifyColumnNullable {
1129 table: "users".into(),
1130 column: "email".into(),
1131 nullable: true,
1132 fill_with: None,
1133 }],
1134 };
1135
1136 let result = validate_migration_plan(&plan);
1137 assert!(result.is_ok());
1138 }
1139
1140 #[test]
1141 fn validate_enum_add_column_invalid_default() {
1142 let plan = MigrationPlan {
1143 comment: None,
1144 created_at: None,
1145 version: 1,
1146 actions: vec![MigrationAction::AddColumn {
1147 table: "users".into(),
1148 column: Box::new(ColumnDef {
1149 name: "status".into(),
1150 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1151 name: "user_status".into(),
1152 values: EnumValues::String(vec![
1153 "active".into(),
1154 "inactive".into(),
1155 "pending".into(),
1156 ]),
1157 }),
1158 nullable: false,
1159 default: Some("invalid_value".into()),
1160 comment: None,
1161 primary_key: None,
1162 unique: None,
1163 index: None,
1164 foreign_key: None,
1165 }),
1166 fill_with: None,
1167 }],
1168 };
1169
1170 let result = validate_migration_plan(&plan);
1171 assert!(result.is_err());
1172 match result.unwrap_err() {
1173 PlannerError::InvalidEnumDefault(err) => {
1174 assert_eq!(err.enum_name, "user_status");
1175 assert_eq!(err.table_name, "users");
1176 assert_eq!(err.column_name, "status");
1177 assert_eq!(err.value_type, "default");
1178 assert_eq!(err.value, "invalid_value");
1179 }
1180 err => panic!("expected InvalidEnumDefault error, got {:?}", err),
1181 }
1182 }
1183
1184 #[test]
1185 fn validate_enum_add_column_invalid_fill_with() {
1186 let plan = MigrationPlan {
1187 comment: None,
1188 created_at: None,
1189 version: 1,
1190 actions: vec![MigrationAction::AddColumn {
1191 table: "users".into(),
1192 column: Box::new(ColumnDef {
1193 name: "status".into(),
1194 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1195 name: "user_status".into(),
1196 values: EnumValues::String(vec![
1197 "active".into(),
1198 "inactive".into(),
1199 "pending".into(),
1200 ]),
1201 }),
1202 nullable: false,
1203 default: None,
1204 comment: None,
1205 primary_key: None,
1206 unique: None,
1207 index: None,
1208 foreign_key: None,
1209 }),
1210 fill_with: Some("unknown_status".into()),
1211 }],
1212 };
1213
1214 let result = validate_migration_plan(&plan);
1215 assert!(result.is_err());
1216 match result.unwrap_err() {
1217 PlannerError::InvalidEnumDefault(err) => {
1218 assert_eq!(err.enum_name, "user_status");
1219 assert_eq!(err.table_name, "users");
1220 assert_eq!(err.column_name, "status");
1221 assert_eq!(err.value_type, "fill_with");
1222 assert_eq!(err.value, "unknown_status");
1223 }
1224 err => panic!("expected InvalidEnumDefault error, got {:?}", err),
1225 }
1226 }
1227
1228 #[test]
1229 fn validate_enum_add_column_valid_default_quoted() {
1230 let plan = MigrationPlan {
1231 comment: None,
1232 created_at: None,
1233 version: 1,
1234 actions: vec![MigrationAction::AddColumn {
1235 table: "users".into(),
1236 column: Box::new(ColumnDef {
1237 name: "status".into(),
1238 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1239 name: "user_status".into(),
1240 values: EnumValues::String(vec![
1241 "active".into(),
1242 "inactive".into(),
1243 "pending".into(),
1244 ]),
1245 }),
1246 nullable: false,
1247 default: Some("'active'".into()),
1248 comment: None,
1249 primary_key: None,
1250 unique: None,
1251 index: None,
1252 foreign_key: None,
1253 }),
1254 fill_with: None,
1255 }],
1256 };
1257
1258 let result = validate_migration_plan(&plan);
1259 assert!(result.is_ok());
1260 }
1261
1262 #[test]
1263 fn validate_enum_add_column_valid_default_unquoted() {
1264 let plan = MigrationPlan {
1265 comment: None,
1266 created_at: None,
1267 version: 1,
1268 actions: vec![MigrationAction::AddColumn {
1269 table: "users".into(),
1270 column: Box::new(ColumnDef {
1271 name: "status".into(),
1272 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1273 name: "user_status".into(),
1274 values: EnumValues::String(vec![
1275 "active".into(),
1276 "inactive".into(),
1277 "pending".into(),
1278 ]),
1279 }),
1280 nullable: false,
1281 default: Some("active".into()),
1282 comment: None,
1283 primary_key: None,
1284 unique: None,
1285 index: None,
1286 foreign_key: None,
1287 }),
1288 fill_with: None,
1289 }],
1290 };
1291
1292 let result = validate_migration_plan(&plan);
1293 assert!(result.is_ok());
1294 }
1295
1296 #[test]
1297 fn validate_enum_add_column_valid_fill_with() {
1298 let plan = MigrationPlan {
1299 comment: None,
1300 created_at: None,
1301 version: 1,
1302 actions: vec![MigrationAction::AddColumn {
1303 table: "users".into(),
1304 column: Box::new(ColumnDef {
1305 name: "status".into(),
1306 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1307 name: "user_status".into(),
1308 values: EnumValues::String(vec![
1309 "active".into(),
1310 "inactive".into(),
1311 "pending".into(),
1312 ]),
1313 }),
1314 nullable: false,
1315 default: None,
1316 comment: None,
1317 primary_key: None,
1318 unique: None,
1319 index: None,
1320 foreign_key: None,
1321 }),
1322 fill_with: Some("'pending'".into()),
1323 }],
1324 };
1325
1326 let result = validate_migration_plan(&plan);
1327 assert!(result.is_ok());
1328 }
1329
1330 #[test]
1331 fn validate_enum_schema_invalid_default() {
1332 let schema = vec![table(
1334 "users",
1335 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), {
1336 let mut c = col(
1337 "status",
1338 ColumnType::Complex(ComplexColumnType::Enum {
1339 name: "user_status".into(),
1340 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1341 }),
1342 );
1343 c.default = Some("invalid".into());
1344 c
1345 }],
1346 vec![pk(vec!["id"])],
1347 )];
1348
1349 let result = validate_schema(&schema);
1350 assert!(result.is_err());
1351 match result.unwrap_err() {
1352 PlannerError::InvalidEnumDefault(err) => {
1353 assert_eq!(err.enum_name, "user_status");
1354 assert_eq!(err.table_name, "users");
1355 assert_eq!(err.column_name, "status");
1356 assert_eq!(err.value_type, "default");
1357 assert_eq!(err.value, "invalid");
1358 }
1359 err => panic!("expected InvalidEnumDefault error, got {:?}", err),
1360 }
1361 }
1362
1363 #[test]
1364 fn validate_enum_schema_valid_default() {
1365 let schema = vec![table(
1366 "users",
1367 vec![col("id", ColumnType::Simple(SimpleColumnType::Integer)), {
1368 let mut c = col(
1369 "status",
1370 ColumnType::Complex(ComplexColumnType::Enum {
1371 name: "user_status".into(),
1372 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1373 }),
1374 );
1375 c.default = Some("'active'".into());
1376 c
1377 }],
1378 vec![pk(vec!["id"])],
1379 )];
1380
1381 let result = validate_schema(&schema);
1382 assert!(result.is_ok());
1383 }
1384
1385 #[test]
1386 fn validate_enum_integer_add_column_valid() {
1387 let plan = MigrationPlan {
1388 comment: None,
1389 created_at: None,
1390 version: 1,
1391 actions: vec![MigrationAction::AddColumn {
1392 table: "tasks".into(),
1393 column: Box::new(ColumnDef {
1394 name: "priority".into(),
1395 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1396 name: "priority_level".into(),
1397 values: EnumValues::Integer(vec![
1398 NumValue {
1399 name: "Low".into(),
1400 value: 0,
1401 },
1402 NumValue {
1403 name: "Medium".into(),
1404 value: 50,
1405 },
1406 NumValue {
1407 name: "High".into(),
1408 value: 100,
1409 },
1410 ]),
1411 }),
1412 nullable: false,
1413 default: None,
1414 comment: None,
1415 primary_key: None,
1416 unique: None,
1417 index: None,
1418 foreign_key: None,
1419 }),
1420 fill_with: Some("Low".into()),
1421 }],
1422 };
1423
1424 let result = validate_migration_plan(&plan);
1425 assert!(result.is_ok());
1426 }
1427
1428 #[test]
1429 fn validate_enum_integer_add_column_invalid() {
1430 let plan = MigrationPlan {
1431 comment: None,
1432 created_at: None,
1433 version: 1,
1434 actions: vec![MigrationAction::AddColumn {
1435 table: "tasks".into(),
1436 column: Box::new(ColumnDef {
1437 name: "priority".into(),
1438 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1439 name: "priority_level".into(),
1440 values: EnumValues::Integer(vec![
1441 NumValue {
1442 name: "Low".into(),
1443 value: 0,
1444 },
1445 NumValue {
1446 name: "Medium".into(),
1447 value: 50,
1448 },
1449 NumValue {
1450 name: "High".into(),
1451 value: 100,
1452 },
1453 ]),
1454 }),
1455 nullable: false,
1456 default: None,
1457 comment: None,
1458 primary_key: None,
1459 unique: None,
1460 index: None,
1461 foreign_key: None,
1462 }),
1463 fill_with: Some("Critical".into()), }],
1465 };
1466
1467 let result = validate_migration_plan(&plan);
1468 assert!(result.is_err());
1469 match result.unwrap_err() {
1470 PlannerError::InvalidEnumDefault(err) => {
1471 assert_eq!(err.enum_name, "priority_level");
1472 assert_eq!(err.table_name, "tasks");
1473 assert_eq!(err.column_name, "priority");
1474 assert_eq!(err.value_type, "fill_with");
1475 assert_eq!(err.value, "Critical");
1476 }
1477 err => panic!("expected InvalidEnumDefault error, got {:?}", err),
1478 }
1479 }
1480
1481 #[test]
1482 fn validate_enum_null_value_skipped() {
1483 let plan = MigrationPlan {
1485 comment: None,
1486 created_at: None,
1487 version: 1,
1488 actions: vec![MigrationAction::AddColumn {
1489 table: "users".into(),
1490 column: Box::new(ColumnDef {
1491 name: "status".into(),
1492 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1493 name: "user_status".into(),
1494 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1495 }),
1496 nullable: true,
1497 default: Some("NULL".into()),
1498 comment: None,
1499 primary_key: None,
1500 unique: None,
1501 index: None,
1502 foreign_key: None,
1503 }),
1504 fill_with: None,
1505 }],
1506 };
1507
1508 let result = validate_migration_plan(&plan);
1509 assert!(result.is_ok());
1510 }
1511
1512 #[test]
1513 fn validate_enum_sql_expression_skipped() {
1514 let plan = MigrationPlan {
1516 comment: None,
1517 created_at: None,
1518 version: 1,
1519 actions: vec![MigrationAction::AddColumn {
1520 table: "users".into(),
1521 column: Box::new(ColumnDef {
1522 name: "status".into(),
1523 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1524 name: "user_status".into(),
1525 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1526 }),
1527 nullable: true,
1528 default: None,
1529 comment: None,
1530 primary_key: None,
1531 unique: None,
1532 index: None,
1533 foreign_key: None,
1534 }),
1535 fill_with: Some("COALESCE(old_status, 'active')".into()),
1536 }],
1537 };
1538
1539 let result = validate_migration_plan(&plan);
1540 assert!(result.is_ok());
1541 }
1542
1543 #[test]
1544 fn validate_enum_empty_string_fill_with_skipped() {
1545 let plan = MigrationPlan {
1548 comment: None,
1549 created_at: None,
1550 version: 1,
1551 actions: vec![MigrationAction::AddColumn {
1552 table: "users".into(),
1553 column: Box::new(ColumnDef {
1554 name: "status".into(),
1555 r#type: ColumnType::Complex(ComplexColumnType::Enum {
1556 name: "user_status".into(),
1557 values: EnumValues::String(vec!["active".into(), "inactive".into()]),
1558 }),
1559 nullable: true,
1560 default: None,
1561 comment: None,
1562 primary_key: None,
1563 unique: None,
1564 index: None,
1565 foreign_key: None,
1566 }),
1567 fill_with: Some(" ".into()),
1569 }],
1570 };
1571
1572 let result = validate_migration_plan(&plan);
1573 assert!(result.is_ok());
1574 }
1575
1576 #[test]
1578 fn find_missing_fill_with_add_column_not_null_no_default() {
1579 let plan = MigrationPlan {
1580 comment: None,
1581 created_at: None,
1582 version: 1,
1583 actions: vec![MigrationAction::AddColumn {
1584 table: "users".into(),
1585 column: Box::new(ColumnDef {
1586 name: "email".into(),
1587 r#type: ColumnType::Simple(SimpleColumnType::Text),
1588 nullable: false,
1589 default: None,
1590 comment: None,
1591 primary_key: None,
1592 unique: None,
1593 index: None,
1594 foreign_key: None,
1595 }),
1596 fill_with: None,
1597 }],
1598 };
1599
1600 let missing = find_missing_fill_with(&plan);
1601 assert_eq!(missing.len(), 1);
1602 assert_eq!(missing[0].table, "users");
1603 assert_eq!(missing[0].column, "email");
1604 assert_eq!(missing[0].action_type, "AddColumn");
1605 assert!(missing[0].column_type.is_some());
1606 }
1607
1608 #[test]
1609 fn find_missing_fill_with_add_column_with_default() {
1610 let plan = MigrationPlan {
1611 comment: None,
1612 created_at: None,
1613 version: 1,
1614 actions: vec![MigrationAction::AddColumn {
1615 table: "users".into(),
1616 column: Box::new(ColumnDef {
1617 name: "email".into(),
1618 r#type: ColumnType::Simple(SimpleColumnType::Text),
1619 nullable: false,
1620 default: Some("'default@example.com'".into()),
1621 comment: None,
1622 primary_key: None,
1623 unique: None,
1624 index: None,
1625 foreign_key: None,
1626 }),
1627 fill_with: None,
1628 }],
1629 };
1630
1631 let missing = find_missing_fill_with(&plan);
1632 assert!(missing.is_empty());
1633 }
1634
1635 #[test]
1636 fn find_missing_fill_with_add_column_nullable() {
1637 let plan = MigrationPlan {
1638 comment: None,
1639 created_at: None,
1640 version: 1,
1641 actions: vec![MigrationAction::AddColumn {
1642 table: "users".into(),
1643 column: Box::new(ColumnDef {
1644 name: "email".into(),
1645 r#type: ColumnType::Simple(SimpleColumnType::Text),
1646 nullable: true,
1647 default: None,
1648 comment: None,
1649 primary_key: None,
1650 unique: None,
1651 index: None,
1652 foreign_key: None,
1653 }),
1654 fill_with: None,
1655 }],
1656 };
1657
1658 let missing = find_missing_fill_with(&plan);
1659 assert!(missing.is_empty());
1660 }
1661
1662 #[test]
1663 fn find_missing_fill_with_add_column_with_fill_with() {
1664 let plan = MigrationPlan {
1665 comment: None,
1666 created_at: None,
1667 version: 1,
1668 actions: vec![MigrationAction::AddColumn {
1669 table: "users".into(),
1670 column: Box::new(ColumnDef {
1671 name: "email".into(),
1672 r#type: ColumnType::Simple(SimpleColumnType::Text),
1673 nullable: false,
1674 default: None,
1675 comment: None,
1676 primary_key: None,
1677 unique: None,
1678 index: None,
1679 foreign_key: None,
1680 }),
1681 fill_with: Some("'default@example.com'".into()),
1682 }],
1683 };
1684
1685 let missing = find_missing_fill_with(&plan);
1686 assert!(missing.is_empty());
1687 }
1688
1689 #[test]
1690 fn find_missing_fill_with_modify_nullable_to_not_null() {
1691 let plan = MigrationPlan {
1692 comment: None,
1693 created_at: None,
1694 version: 1,
1695 actions: vec![MigrationAction::ModifyColumnNullable {
1696 table: "users".into(),
1697 column: "email".into(),
1698 nullable: false,
1699 fill_with: None,
1700 }],
1701 };
1702
1703 let missing = find_missing_fill_with(&plan);
1704 assert_eq!(missing.len(), 1);
1705 assert_eq!(missing[0].table, "users");
1706 assert_eq!(missing[0].column, "email");
1707 assert_eq!(missing[0].action_type, "ModifyColumnNullable");
1708 assert!(missing[0].column_type.is_none());
1709 }
1710
1711 #[test]
1712 fn find_missing_fill_with_modify_to_nullable() {
1713 let plan = MigrationPlan {
1714 comment: None,
1715 created_at: None,
1716 version: 1,
1717 actions: vec![MigrationAction::ModifyColumnNullable {
1718 table: "users".into(),
1719 column: "email".into(),
1720 nullable: true,
1721 fill_with: None,
1722 }],
1723 };
1724
1725 let missing = find_missing_fill_with(&plan);
1726 assert!(missing.is_empty());
1727 }
1728
1729 #[test]
1730 fn find_missing_fill_with_modify_not_null_with_fill_with() {
1731 let plan = MigrationPlan {
1732 comment: None,
1733 created_at: None,
1734 version: 1,
1735 actions: vec![MigrationAction::ModifyColumnNullable {
1736 table: "users".into(),
1737 column: "email".into(),
1738 nullable: false,
1739 fill_with: Some("'default'".into()),
1740 }],
1741 };
1742
1743 let missing = find_missing_fill_with(&plan);
1744 assert!(missing.is_empty());
1745 }
1746
1747 #[test]
1748 fn find_missing_fill_with_multiple_actions() {
1749 let plan = MigrationPlan {
1750 comment: None,
1751 created_at: None,
1752 version: 1,
1753 actions: vec![
1754 MigrationAction::AddColumn {
1755 table: "users".into(),
1756 column: Box::new(ColumnDef {
1757 name: "email".into(),
1758 r#type: ColumnType::Simple(SimpleColumnType::Text),
1759 nullable: false,
1760 default: None,
1761 comment: None,
1762 primary_key: None,
1763 unique: None,
1764 index: None,
1765 foreign_key: None,
1766 }),
1767 fill_with: None,
1768 },
1769 MigrationAction::ModifyColumnNullable {
1770 table: "orders".into(),
1771 column: "status".into(),
1772 nullable: false,
1773 fill_with: None,
1774 },
1775 MigrationAction::AddColumn {
1776 table: "users".into(),
1777 column: Box::new(ColumnDef {
1778 name: "name".into(),
1779 r#type: ColumnType::Simple(SimpleColumnType::Text),
1780 nullable: true, default: None,
1782 comment: None,
1783 primary_key: None,
1784 unique: None,
1785 index: None,
1786 foreign_key: None,
1787 }),
1788 fill_with: None,
1789 },
1790 ],
1791 };
1792
1793 let missing = find_missing_fill_with(&plan);
1794 assert_eq!(missing.len(), 2);
1795 assert_eq!(missing[0].action_index, 0);
1796 assert_eq!(missing[0].table, "users");
1797 assert_eq!(missing[0].column, "email");
1798 assert_eq!(missing[1].action_index, 1);
1799 assert_eq!(missing[1].table, "orders");
1800 assert_eq!(missing[1].column, "status");
1801 }
1802
1803 #[test]
1804 fn find_missing_fill_with_other_actions_ignored() {
1805 let plan = MigrationPlan {
1806 comment: None,
1807 created_at: None,
1808 version: 1,
1809 actions: vec![
1810 MigrationAction::CreateTable {
1811 table: "users".into(),
1812 columns: vec![col("id", ColumnType::Simple(SimpleColumnType::Integer))],
1813 constraints: vec![pk(vec!["id"])],
1814 },
1815 MigrationAction::DeleteColumn {
1816 table: "orders".into(),
1817 column: "old_column".into(),
1818 },
1819 ],
1820 };
1821
1822 let missing = find_missing_fill_with(&plan);
1823 assert!(missing.is_empty());
1824 }
1825}