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