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