vespertide_planner/
validate.rs

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
10/// Validate a schema for data integrity issues.
11/// Checks for:
12/// - Duplicate table names
13/// - Foreign keys referencing non-existent tables
14/// - Foreign keys referencing non-existent columns
15/// - Indexes referencing non-existent columns
16/// - Constraints referencing non-existent columns
17/// - Empty constraint column lists
18pub fn validate_schema(schema: &[TableDef]) -> Result<(), PlannerError> {
19    // Check for duplicate table names
20    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    // Build a map of table names to their column names for quick lookup
28    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    // Validate each table
37    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    // Check that the table has a primary key
51    // Primary key can be defined either:
52    // 1. As a table-level constraint (TableConstraint::PrimaryKey)
53    // 2. As an inline column definition (column.primary_key = Some(...))
54    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    // Validate columns (enum types)
65    for column in &table.columns {
66        validate_column(column, &table.name)?;
67    }
68
69    // Validate constraints (including indexes)
70    for constraint in &table.constraints {
71        validate_constraint(constraint, &table.name, &table_columns, table_map)?;
72    }
73
74    Ok(())
75}
76
77/// Extract the unquoted value from a potentially quoted string.
78/// Returns None if the value is a SQL expression (contains parentheses or is a keyword).
79fn extract_enum_value(value: &str) -> Option<&str> {
80    let trimmed = value.trim();
81    if trimmed.is_empty() {
82        return None;
83    }
84    // Check for SQL expressions/keywords that shouldn't be validated
85    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    // Strip quotes if present
94    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    // Unquoted value
101    Some(trimmed)
102}
103
104/// Validate that an enum default/fill_with value is in the allowed enum values.
105fn 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, // "default" or "fill_with"
112) -> Result<(), PlannerError> {
113    let extracted = match extract_enum_value(value) {
114        Some(v) => v,
115        None => return Ok(()), // Skip validation for SQL expressions
116    };
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    // Validate enum types for duplicate names/values
141    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                // Check duplicate names
158                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                // Check duplicate values
170                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        // Validate default value is in enum values
185        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            // Check that referenced table exists
261            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            // Check that all columns in this table exist
270            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            // Check that all referenced columns exist in the referenced table
281            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            // Check that column counts match
293            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            // Check constraints are just expressions, no validation needed
308        }
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
334/// Validate a migration plan for correctness.
335/// Checks for:
336/// - AddColumn actions with NOT NULL columns without default must have fill_with
337/// - ModifyColumnNullable actions changing from nullable to non-nullable must have fill_with
338/// - Enum columns with default/fill_with values must have valid enum values
339pub 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 is NOT NULL and has no default, fill_with is required
348                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                // Validate enum default/fill_with values
356                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 changing from nullable to non-nullable, fill_with is required
382                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/// Information about an action that requires a fill_with value.
393#[derive(Debug, Clone, PartialEq, Eq)]
394pub struct FillWithRequired {
395    /// Index of the action in the migration plan.
396    pub action_index: usize,
397    /// Table name.
398    pub table: String,
399    /// Column name.
400    pub column: String,
401    /// Type of action: "AddColumn" or "ModifyColumnNullable".
402    pub action_type: &'static str,
403    /// Column type (for display purposes).
404    pub column_type: Option<String>,
405}
406
407/// Find all actions in a migration plan that require fill_with values.
408/// Returns a list of actions that need fill_with but don't have one.
409pub 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 is NOT NULL and has no default, fill_with is required
420                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 changing from nullable to non-nullable, fill_with is required
437                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(), // duplicate
911                        ]),
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(), // duplicate name
955                                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, // duplicate value
1002                            },
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        // Changing from non-nullable to nullable does NOT require fill_with
1120        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        // Test that schema validation also catches invalid enum defaults
1329        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()), // Not a valid enum name
1460            }],
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        // NULL values should be allowed and skipped during validation
1480        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        // SQL expressions like function calls should be skipped
1511        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        // Empty string fill_with should be skipped during enum validation
1542        // (converted to '' by to_sql, which is empty after trimming)
1543        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                // Empty string - extract_enum_value returns None for empty trimmed values
1564                fill_with: Some("   ".into()),
1565            }],
1566        };
1567
1568        let result = validate_migration_plan(&plan);
1569        assert!(result.is_ok());
1570    }
1571
1572    // Tests for find_missing_fill_with function
1573    #[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, // nullable, so not missing
1777                        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}