Skip to content

Commit 88c3194

Browse files
authored
Merge pull request #6 from dev-five-git/mig-validation
Add migration validation
2 parents fa75c4d + b7bbc4a commit 88c3194

File tree

6 files changed

+143
-10
lines changed

6 files changed

+143
-10
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespertide/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch"},"note":"Add migration validation","date":"2025-12-10T16:31:52.974551200Z"}

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vespertide-cli/src/utils.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::path::PathBuf;
44
use anyhow::{Context, Result};
55
use vespertide_config::{FileFormat, VespertideConfig};
66
use vespertide_core::{MigrationPlan, TableDef};
7-
use vespertide_planner::validate_schema;
7+
use vespertide_planner::{validate_migration_plan, validate_schema};
88

99
/// Load vespertide.json config from current directory.
1010
pub fn load_config() -> Result<VespertideConfig> {
@@ -86,6 +86,10 @@ pub fn load_migrations(config: &VespertideConfig) -> Result<Vec<MigrationPlan>>
8686
.with_context(|| format!("parse migration: {}", path.display()))?
8787
};
8888

89+
// Validate the migration plan
90+
validate_migration_plan(&plan)
91+
.with_context(|| format!("validate migration: {}", path.display()))?;
92+
8993
plans.push(plan);
9094
}
9195
}

crates/vespertide-planner/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ pub enum PlannerError {
2424
ConstraintColumnNotFound(String, String, String),
2525
#[error("constraint has empty column list: {0}.{1}")]
2626
EmptyConstraintColumns(String, String),
27+
#[error("AddColumn requires fill_with when column is NOT NULL without default: {0}.{1}")]
28+
MissingFillWith(String, String),
2729
}

crates/vespertide-planner/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ pub use diff::diff_schemas;
1010
pub use error::PlannerError;
1111
pub use plan::plan_next_migration;
1212
pub use schema::schema_from_plans;
13-
pub use validate::validate_schema;
13+
pub use validate::{validate_migration_plan, validate_schema};

crates/vespertide-planner/src/validate.rs

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::collections::HashSet;
22

3-
use vespertide_core::{IndexDef, TableConstraint, TableDef};
3+
use vespertide_core::{IndexDef, MigrationAction, MigrationPlan, TableConstraint, TableDef};
44

55
use crate::error::PlannerError;
66

@@ -514,4 +514,130 @@ mod tests {
514514
}
515515
}
516516
}
517+
518+
#[test]
519+
fn validate_migration_plan_missing_fill_with() {
520+
use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
521+
522+
let plan = MigrationPlan {
523+
comment: None,
524+
created_at: None,
525+
version: 1,
526+
actions: vec![MigrationAction::AddColumn {
527+
table: "users".into(),
528+
column: ColumnDef {
529+
name: "email".into(),
530+
r#type: ColumnType::Text,
531+
nullable: false,
532+
default: None,
533+
},
534+
fill_with: None,
535+
}],
536+
};
537+
538+
let result = validate_migration_plan(&plan);
539+
assert!(result.is_err());
540+
match result.unwrap_err() {
541+
PlannerError::MissingFillWith(table, column) => {
542+
assert_eq!(table, "users");
543+
assert_eq!(column, "email");
544+
}
545+
_ => panic!("expected MissingFillWith error"),
546+
}
547+
}
548+
549+
#[test]
550+
fn validate_migration_plan_with_fill_with() {
551+
use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
552+
553+
let plan = MigrationPlan {
554+
comment: None,
555+
created_at: None,
556+
version: 1,
557+
actions: vec![MigrationAction::AddColumn {
558+
table: "users".into(),
559+
column: ColumnDef {
560+
name: "email".into(),
561+
r#type: ColumnType::Text,
562+
nullable: false,
563+
default: None,
564+
},
565+
fill_with: Some("[email protected]".into()),
566+
}],
567+
};
568+
569+
let result = validate_migration_plan(&plan);
570+
assert!(result.is_ok());
571+
}
572+
573+
#[test]
574+
fn validate_migration_plan_nullable_column() {
575+
use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
576+
577+
let plan = MigrationPlan {
578+
comment: None,
579+
created_at: None,
580+
version: 1,
581+
actions: vec![MigrationAction::AddColumn {
582+
table: "users".into(),
583+
column: ColumnDef {
584+
name: "email".into(),
585+
r#type: ColumnType::Text,
586+
nullable: true,
587+
default: None,
588+
},
589+
fill_with: None,
590+
}],
591+
};
592+
593+
let result = validate_migration_plan(&plan);
594+
assert!(result.is_ok());
595+
}
596+
597+
#[test]
598+
fn validate_migration_plan_with_default() {
599+
use vespertide_core::{ColumnDef, ColumnType, MigrationAction, MigrationPlan};
600+
601+
let plan = MigrationPlan {
602+
comment: None,
603+
created_at: None,
604+
version: 1,
605+
actions: vec![MigrationAction::AddColumn {
606+
table: "users".into(),
607+
column: ColumnDef {
608+
name: "email".into(),
609+
r#type: ColumnType::Text,
610+
nullable: false,
611+
default: Some("[email protected]".into()),
612+
},
613+
fill_with: None,
614+
}],
615+
};
616+
617+
let result = validate_migration_plan(&plan);
618+
assert!(result.is_ok());
619+
}
620+
}
621+
622+
/// Validate a migration plan for correctness.
623+
/// Checks for:
624+
/// - AddColumn actions with NOT NULL columns without default must have fill_with
625+
pub fn validate_migration_plan(plan: &MigrationPlan) -> Result<(), PlannerError> {
626+
for action in &plan.actions {
627+
if let MigrationAction::AddColumn {
628+
table,
629+
column,
630+
fill_with,
631+
} = action
632+
{
633+
// If column is NOT NULL and has no default, fill_with is required
634+
if !column.nullable && column.default.is_none() && fill_with.is_none() {
635+
return Err(PlannerError::MissingFillWith(
636+
table.clone(),
637+
column.name.clone(),
638+
));
639+
}
640+
}
641+
}
642+
Ok(())
517643
}

0 commit comments

Comments
 (0)