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