From 60d2b2866cc1543eaca1dbb78e394c1293bc549c Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 11 Feb 2026 00:23:42 +0900 Subject: [PATCH 1/3] Add migration id --- Cargo.lock | 22 +- crates/vespertide-cli/Cargo.toml | 1 + crates/vespertide-cli/src/commands/log.rs | 44 +-- .../vespertide-cli/src/commands/revision.rs | 246 ++++++++------- crates/vespertide-cli/src/commands/sql.rs | 282 +++++++++--------- crates/vespertide-cli/src/commands/status.rs | 51 ++-- crates/vespertide-cli/src/utils.rs | 6 +- crates/vespertide-core/src/action.rs | 5 + crates/vespertide-core/src/migration.rs | 6 + crates/vespertide-macro/src/lib.rs | 75 ++++- crates/vespertide-planner/src/diff.rs | 1 + crates/vespertide-planner/src/plan.rs | 1 + crates/vespertide-planner/src/schema.rs | 8 + crates/vespertide-planner/src/validate.rs | 28 ++ crates/vespertide-query/src/builder.rs | 13 +- .../tests/composite_unique_test.rs | 7 +- .../tests/enum_migration_test.rs | 3 +- .../tests/table_prefixed_enum_test.rs | 3 +- 18 files changed, 480 insertions(+), 322 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a17687e..9b41aa8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3283,6 +3283,7 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ + "getrandom 0.3.4", "js-sys", "serde_core", "wasm-bindgen", @@ -3342,7 +3343,7 @@ dependencies = [ [[package]] name = "vespertide" -version = "0.1.43" +version = "0.1.44" dependencies = [ "vespertide-core", "vespertide-macro", @@ -3350,7 +3351,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "assert_cmd", @@ -3368,6 +3369,7 @@ dependencies = [ "serial_test", "tempfile", "tokio", + "uuid", "vespertide-config", "vespertide-core", "vespertide-exporter", @@ -3378,7 +3380,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.43" +version = "0.1.44" dependencies = [ "clap", "schemars", @@ -3388,7 +3390,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.43" +version = "0.1.44" dependencies = [ "rstest", "schemars", @@ -3400,7 +3402,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.43" +version = "0.1.44" dependencies = [ "insta", "rstest", @@ -3412,7 +3414,7 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "rstest", @@ -3427,7 +3429,7 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.43" +version = "0.1.44" dependencies = [ "proc-macro2", "quote", @@ -3444,11 +3446,11 @@ dependencies = [ [[package]] name = "vespertide-naming" -version = "0.1.43" +version = "0.1.44" [[package]] name = "vespertide-planner" -version = "0.1.43" +version = "0.1.44" dependencies = [ "insta", "rstest", @@ -3459,7 +3461,7 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.43" +version = "0.1.44" dependencies = [ "insta", "rstest", diff --git a/crates/vespertide-cli/Cargo.toml b/crates/vespertide-cli/Cargo.toml index bb91d9b..b367d21 100644 --- a/crates/vespertide-cli/Cargo.toml +++ b/crates/vespertide-cli/Cargo.toml @@ -15,6 +15,7 @@ clap = { version = "4", features = ["derive"] } chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } colored = "3" dialoguer = "0.12" +uuid = { version = "1", features = ["v4"] } serde_json = "1" serde_yaml = "0.9" schemars = "1.2" diff --git a/crates/vespertide-cli/src/commands/log.rs b/crates/vespertide-cli/src/commands/log.rs index f935940..8268f7c 100644 --- a/crates/vespertide-cli/src/commands/log.rs +++ b/crates/vespertide-cli/src/commands/log.rs @@ -133,21 +133,22 @@ mod tests { fs::write("vespertide.json", text).unwrap(); } - fn write_migration(cfg: &VespertideConfig) { - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let plan = MigrationPlan { - comment: Some("init".into()), - created_at: Some("2024-01-01T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![], - constraints: vec![], - }], - }; - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - } + fn write_migration(cfg: &VespertideConfig) { + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let plan = MigrationPlan { + id: String::new(), + comment: Some("init".into()), + created_at: Some("2024-01-01T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![], + constraints: vec![], + }], + }; + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + } #[tokio::test] #[serial_test::serial] @@ -246,12 +247,13 @@ mod tests { write_config(&cfg); fs::create_dir_all(cfg.migrations_dir()).unwrap(); - // Create a migration with ModifyColumnType for SQLite, which generates multiple SQL statements - let plan = MigrationPlan { - comment: Some("modify column type".into()), - created_at: Some("2024-01-01T00:00:00Z".into()), - version: 1, - actions: vec![ + // Create a migration with ModifyColumnType for SQLite, which generates multiple SQL statements + let plan = MigrationPlan { + id: String::new(), + comment: Some("modify column type".into()), + created_at: Some("2024-01-01T00:00:00Z".into()), + version: 1, + actions: vec![ MigrationAction::CreateTable { table: "users".into(), columns: vec![ColumnDef { diff --git a/crates/vespertide-cli/src/commands/revision.rs b/crates/vespertide-cli/src/commands/revision.rs index 6804e68..9d79873 100644 --- a/crates/vespertide-cli/src/commands/revision.rs +++ b/crates/vespertide-cli/src/commands/revision.rs @@ -303,6 +303,7 @@ pub async fn cmd_revision(message: String, fill_with_args: Vec) -> Resul prompt_enum_value, )?; + plan.id = uuid::Uuid::new_v4().to_string(); plan.comment = Some(message); if plan.created_at.is_none() { // Record creation time in RFC3339 (UTC). @@ -517,14 +518,15 @@ mod tests { assert!(has_yaml); } - #[test] - fn check_non_nullable_fk_add_column_fails() { - use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; - let plan = MigrationPlan { - comment: None, - created_at: None, - version: 2, - actions: vec![ + #[test] + fn check_non_nullable_fk_add_column_fails() { + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 2, + actions: vec![ MigrationAction::AddColumn { table: "post".into(), column: Box::new(ColumnDef { @@ -563,14 +565,15 @@ mod tests { assert!(msg.contains("post"), "error should mention table: {msg}"); } - #[test] - fn check_nullable_fk_add_column_ok() { - use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; - let plan = MigrationPlan { - comment: None, - created_at: None, - version: 2, - actions: vec![ + #[test] + fn check_nullable_fk_add_column_ok() { + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 2, + actions: vec![ MigrationAction::AddColumn { table: "post".into(), column: Box::new(ColumnDef { @@ -602,15 +605,16 @@ mod tests { assert!(check_non_nullable_fk_add_columns(&plan).is_ok()); } - #[test] - fn check_non_nullable_no_fk_passes() { - // Regular non-nullable column without FK should NOT be blocked - use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; - let plan = MigrationPlan { - comment: None, - created_at: None, - version: 2, - actions: vec![MigrationAction::AddColumn { + #[test] + fn check_non_nullable_no_fk_passes() { + // Regular non-nullable column without FK should NOT be blocked + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 2, + actions: vec![MigrationAction::AddColumn { table: "post".into(), column: Box::new(ColumnDef { name: "user_id1".into(), @@ -666,15 +670,16 @@ mod tests { ); } - #[test] - fn test_apply_fill_with_to_plan_add_column() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_apply_fill_with_to_plan_add_column() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -707,15 +712,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_modify_column_nullable() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::ModifyColumnNullable { + #[test] + fn test_apply_fill_with_to_plan_modify_column_nullable() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::ModifyColumnNullable { table: "users".into(), column: "status".into(), nullable: false, @@ -739,15 +745,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_skips_existing_fill_with() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_apply_fill_with_to_plan_skips_existing_fill_with() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -781,15 +788,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_no_match() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_apply_fill_with_to_plan_no_match() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -823,15 +831,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_multiple_actions() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![ + #[test] + fn test_apply_fill_with_to_plan_multiple_actions() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![ MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { @@ -883,15 +892,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_other_actions_ignored() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::DeleteColumn { + #[test] + fn test_apply_fill_with_to_plan_other_actions_ignored() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::DeleteColumn { table: "users".into(), column: "old_column".into(), }], @@ -1144,15 +1154,16 @@ mod tests { let _: fn(&str, &str) -> Result = prompt_fill_with_value; } - #[test] - fn test_handle_missing_fill_with_collects_and_applies() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_handle_missing_fill_with_collects_and_applies() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -1200,16 +1211,17 @@ mod tests { ); } - #[test] - fn test_handle_missing_fill_with_no_missing() { - use vespertide_core::MigrationPlan; - - // Plan with no missing fill_with values (nullable column) - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_handle_missing_fill_with_no_missing() { + use vespertide_core::MigrationPlan; + + // Plan with no missing fill_with values (nullable column) + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -1244,15 +1256,16 @@ mod tests { assert!(fill_values.is_empty()); } - #[test] - fn test_handle_missing_fill_with_prompt_error() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_handle_missing_fill_with_prompt_error() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -1294,15 +1307,16 @@ mod tests { } } - #[test] - fn test_handle_missing_fill_with_multiple_columns() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![ + #[test] + fn test_handle_missing_fill_with_multiple_columns() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![ MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { diff --git a/crates/vespertide-cli/src/commands/sql.rs b/crates/vespertide-cli/src/commands/sql.rs index 1889ec6..f79d44b 100644 --- a/crates/vespertide-cli/src/commands/sql.rs +++ b/crates/vespertide-cli/src/commands/sql.rs @@ -226,34 +226,35 @@ mod tests { let cfg = write_config(); write_model("users"); - let plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "id".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }], - }], - }; - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - - let result = cmd_sql(DatabaseBackend::Postgres).await; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + }], + }; + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + + let result = cmd_sql(DatabaseBackend::Postgres).await; assert!(result.is_ok()); } @@ -266,34 +267,35 @@ mod tests { let cfg = write_config(); write_model("users"); - let plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "id".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }], - }], - }; - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - - let result = cmd_sql(DatabaseBackend::MySql).await; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + }], + }; + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + + let result = cmd_sql(DatabaseBackend::MySql).await; assert!(result.is_ok()); } @@ -306,93 +308,98 @@ mod tests { let cfg = write_config(); write_model("users"); - let plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "id".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }], - }], - }; - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - - let result = cmd_sql(DatabaseBackend::Sqlite).await; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + }], + }; + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + + let result = cmd_sql(DatabaseBackend::Sqlite).await; assert!(result.is_ok()); } - #[tokio::test] - #[serial] - async fn emit_sql_prints_created_at_and_comment_postgres() { - let plan = MigrationPlan { - comment: Some("with comment".into()), - created_at: Some("2024-01-02T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::RawSql { - sql: "SELECT 1;".into(), - }], - }; - - let result = emit_sql(&plan, DatabaseBackend::Postgres, &[]); + #[tokio::test] + #[serial] + async fn emit_sql_prints_created_at_and_comment_postgres() { + let plan = MigrationPlan { + id: String::new(), + comment: Some("with comment".into()), + created_at: Some("2024-01-02T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::RawSql { + sql: "SELECT 1;".into(), + }], + }; + + let result = emit_sql(&plan, DatabaseBackend::Postgres, &[]); assert!(result.is_ok()); } - #[tokio::test] - #[serial] - async fn emit_sql_prints_created_at_and_comment_mysql() { - let plan = MigrationPlan { - comment: Some("with comment".into()), - created_at: Some("2024-01-02T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::RawSql { - sql: "SELECT 1;".into(), - }], - }; - - let result = emit_sql(&plan, DatabaseBackend::MySql, &[]); + #[tokio::test] + #[serial] + async fn emit_sql_prints_created_at_and_comment_mysql() { + let plan = MigrationPlan { + id: String::new(), + comment: Some("with comment".into()), + created_at: Some("2024-01-02T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::RawSql { + sql: "SELECT 1;".into(), + }], + }; + + let result = emit_sql(&plan, DatabaseBackend::MySql, &[]); assert!(result.is_ok()); } - #[tokio::test] - #[serial] - async fn emit_sql_prints_created_at_and_comment_sqlite() { - let plan = MigrationPlan { - comment: Some("with comment".into()), - created_at: Some("2024-01-02T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::RawSql { - sql: "SELECT 1;".into(), - }], - }; - - let result = emit_sql(&plan, DatabaseBackend::Sqlite, &[]); + #[tokio::test] + #[serial] + async fn emit_sql_prints_created_at_and_comment_sqlite() { + let plan = MigrationPlan { + id: String::new(), + comment: Some("with comment".into()), + created_at: Some("2024-01-02T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::RawSql { + sql: "SELECT 1;".into(), + }], + }; + + let result = emit_sql(&plan, DatabaseBackend::Sqlite, &[]); assert!(result.is_ok()); } - #[tokio::test] - #[serial] - async fn emit_sql_multiple_queries() { - let plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![ + #[tokio::test] + #[serial] + async fn emit_sql_multiple_queries() { + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![ MigrationAction::CreateTable { table: "users".into(), columns: vec![ColumnDef { @@ -432,12 +439,13 @@ mod tests { let _cfg = write_config(); write_model("users"); - // Create a migration that adds a NOT NULL column in SQLite, which generates multiple queries - let plan = MigrationPlan { - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + // Create a migration that adds a NOT NULL column in SQLite, which generates multiple queries + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "nickname".into(), diff --git a/crates/vespertide-cli/src/commands/status.rs b/crates/vespertide-cli/src/commands/status.rs index 627771b..1145e90 100644 --- a/crates/vespertide-cli/src/commands/status.rs +++ b/crates/vespertide-cli/src/commands/status.rs @@ -222,31 +222,32 @@ mod tests { fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap(); } - fn write_migration(cfg: &VespertideConfig) { - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let plan = MigrationPlan { - comment: Some("init".into()), - created_at: Some("2024-01-01T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "id".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![], - }], - }; - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - } + fn write_migration(cfg: &VespertideConfig) { + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let plan = MigrationPlan { + id: String::new(), + comment: Some("init".into()), + created_at: Some("2024-01-01T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }], + }; + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + } #[tokio::test] #[serial] diff --git a/crates/vespertide-cli/src/utils.rs b/crates/vespertide-cli/src/utils.rs index e78d361..81aa576 100644 --- a/crates/vespertide-cli/src/utils.rs +++ b/crates/vespertide-cli/src/utils.rs @@ -109,8 +109,8 @@ mod tests { use tempfile::tempdir; use vespertide_config::VespertideConfig; use vespertide_core::{ - ColumnDef, ColumnType, MigrationPlan, SimpleColumnType, TableConstraint, TableDef, - schema::foreign_key::ForeignKeySyntax, + schema::foreign_key::ForeignKeySyntax, ColumnDef, ColumnType, MigrationPlan, + SimpleColumnType, TableConstraint, TableDef, }; struct CwdGuard { @@ -226,12 +226,14 @@ mod tests { fs::create_dir_all("migrations").unwrap(); let plan1 = MigrationPlan { + id: String::new(), comment: Some("first".into()), created_at: None, version: 2, actions: vec![], }; let plan0 = MigrationPlan { + id: String::new(), comment: Some("zero".into()), created_at: None, version: 1, diff --git a/crates/vespertide-core/src/action.rs b/crates/vespertide-core/src/action.rs index f8c9e97..0afdebd 100644 --- a/crates/vespertide-core/src/action.rs +++ b/crates/vespertide-core/src/action.rs @@ -6,6 +6,10 @@ use std::fmt; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct MigrationPlan { + /// Unique identifier for this migration (UUID format). + /// Defaults to empty string for backward compatibility with old migration files. + #[serde(default)] + pub id: String, pub comment: Option, #[serde(default)] pub created_at: Option, @@ -810,6 +814,7 @@ mod tests { #[test] fn test_migration_plan_with_prefix() { let plan = MigrationPlan { + id: String::new(), comment: Some("test".into()), created_at: None, version: 1, diff --git a/crates/vespertide-core/src/migration.rs b/crates/vespertide-core/src/migration.rs index f91c111..f3253ae 100644 --- a/crates/vespertide-core/src/migration.rs +++ b/crates/vespertide-core/src/migration.rs @@ -9,4 +9,10 @@ pub enum MigrationError { NotImplemented, #[error("database error: {0}")] DatabaseError(String), + #[error("migration id mismatch for version {version}: expected '{expected}', found '{found}' in database")] + IdMismatch { + version: u32, + expected: String, + found: String, + }, } diff --git a/crates/vespertide-macro/src/lib.rs b/crates/vespertide-macro/src/lib.rs index e85b5b7..3686846 100644 --- a/crates/vespertide-macro/src/lib.rs +++ b/crates/vespertide-macro/src/lib.rs @@ -11,7 +11,7 @@ use vespertide_loader::{ load_config_or_default, load_migrations_at_compile_time, load_models_at_compile_time, }; use vespertide_planner::apply_action; -use vespertide_query::{DatabaseBackend, build_plan_queries}; +use vespertide_query::{build_plan_queries, DatabaseBackend}; struct MacroInput { pool: Expr, @@ -60,6 +60,7 @@ pub(crate) fn build_migration_block( verbose: bool, ) -> Result { let version = migration.version; + let migration_id = &migration.id; // Use the current baseline schema (from all previous migrations) let queries = build_plan_queries(migration, baseline_schema).map_err(|e| { @@ -148,10 +149,22 @@ pub(crate) fn build_migration_block( quote! { if __version < #version { + // Validate migration id against database if version already tracked + if let Some(db_id) = __version_ids.get(&#version) { + let expected_id: &str = #migration_id; + if !expected_id.is_empty() && !db_id.is_empty() && db_id != expected_id { + return Err(::vespertide::MigrationError::IdMismatch { + version: #version, + expected: expected_id.to_string(), + found: db_id.clone(), + }); + } + } + eprintln!("[vespertide] Applying migration {} ({})", #version_str, #comment_str); #(#action_blocks)* - let insert_sql = format!("INSERT INTO {q}{}{q} (version) VALUES ({})", __version_table, #version); + let insert_sql = format!("INSERT INTO {q}{}{q} (version, id) VALUES ({}, '{}')", __version_table, #version, #migration_id); let stmt = sea_orm::Statement::from_string(backend, insert_sql); __txn.execute_raw(stmt).await.map_err(|e| { ::vespertide::MigrationError::DatabaseError(format!("Failed to insert version: {}", e)) @@ -180,6 +193,18 @@ pub(crate) fn build_migration_block( quote! { if __version < #version { + // Validate migration id against database if version already tracked + if let Some(db_id) = __version_ids.get(&#version) { + let expected_id: &str = #migration_id; + if !expected_id.is_empty() && !db_id.is_empty() && db_id != expected_id { + return Err(::vespertide::MigrationError::IdMismatch { + version: #version, + expected: expected_id.to_string(), + found: db_id.clone(), + }); + } + } + let sqls: &[&str] = match backend { sea_orm::DatabaseBackend::Postgres => &[#(#pg_sqls),*], sea_orm::DatabaseBackend::MySql => &[#(#mysql_sqls),*], @@ -196,7 +221,7 @@ pub(crate) fn build_migration_block( } } - let insert_sql = format!("INSERT INTO {q}{}{q} (version) VALUES ({})", __version_table, #version); + let insert_sql = format!("INSERT INTO {q}{}{q} (version, id) VALUES ({}, '{}')", __version_table, #version, #migration_id); let stmt = sea_orm::Statement::from_string(backend, insert_sql); __txn.execute_raw(stmt).await.map_err(|e| { ::vespertide::MigrationError::DatabaseError(format!("Failed to insert version: {}", e)) @@ -232,7 +257,7 @@ fn generate_migration_code( // Create version table if it does not exist (outside transaction) let create_table_sql = format!( - "CREATE TABLE IF NOT EXISTS {q}{}{q} (version INTEGER PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)", + "CREATE TABLE IF NOT EXISTS {q}{}{q} (version INTEGER PRIMARY KEY, id TEXT DEFAULT '', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)", __version_table ); let stmt = sea_orm::Statement::from_string(backend, create_table_sql); @@ -240,6 +265,16 @@ fn generate_migration_code( ::vespertide::MigrationError::DatabaseError(format!("Failed to create version table: {}", e)) })?; + // Add id column for existing tables that don't have it yet (backward compatibility). + // We use a try-and-ignore approach: if the column already exists, the ALTER will fail + // and we simply ignore the error. + let alter_sql = format!( + "ALTER TABLE {q}{}{q} ADD COLUMN id TEXT DEFAULT ''", + __version_table + ); + let stmt = sea_orm::Statement::from_string(backend, alter_sql); + let _ = __pool.execute_raw(stmt).await; + // Single transaction for the entire migration process. // This prevents race conditions when multiple connections exist // (e.g. SQLite with max_connections > 1). @@ -258,6 +293,21 @@ fn generate_migration_code( .and_then(|row| row.try_get::("", "version").ok()) .unwrap_or(0) as u32; + // Load all existing (version, id) pairs for id mismatch validation + let select_ids_sql = format!("SELECT version, id FROM {q}{}{q}", __version_table); + let stmt = sea_orm::Statement::from_string(backend, select_ids_sql); + let id_rows = __txn.query_all_raw(stmt).await.map_err(|e| { + ::vespertide::MigrationError::DatabaseError(format!("Failed to read version ids: {}", e)) + })?; + + let mut __version_ids = std::collections::HashMap::::new(); + for row in &id_rows { + if let Ok(v) = row.try_get::("", "version") { + let id = row.try_get::("", "id").unwrap_or_default(); + __version_ids.insert(v as u32, id); + } + } + #verbose_current_version // Execute each migration block within the same transaction @@ -475,6 +525,7 @@ mod tests { #[test] fn test_build_migration_block_create_table() { let migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -505,6 +556,7 @@ mod tests { fn test_build_migration_block_add_column() { // First create the table let create_migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -520,6 +572,7 @@ mod tests { // Now add a column let add_column_migration = MigrationPlan { + id: String::new(), version: 2, comment: None, created_at: None, @@ -553,6 +606,7 @@ mod tests { #[test] fn test_build_migration_block_multiple_actions() { let migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -584,6 +638,7 @@ mod tests { // Create a simple migration block let migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -628,6 +683,7 @@ mod tests { let mut baseline = Vec::new(); let migration1 = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -640,6 +696,7 @@ mod tests { let block1 = build_migration_block(&migration1, &mut baseline, false).unwrap(); let migration2 = MigrationPlan { + id: String::new(), version: 2, comment: None, created_at: None, @@ -662,6 +719,7 @@ mod tests { #[test] fn test_build_migration_block_generates_all_backends() { let migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -688,6 +746,7 @@ mod tests { fn test_build_migration_block_with_delete_table() { // First create the table let create_migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -704,6 +763,7 @@ mod tests { // Now delete it let delete_migration = MigrationPlan { + id: String::new(), version: 2, comment: None, created_at: None, @@ -724,6 +784,7 @@ mod tests { #[test] fn test_build_migration_block_with_index() { let migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -761,6 +822,7 @@ mod tests { fn test_build_migration_block_error_nonexistent_table() { // Try to add column to a table that doesn't exist - should fail let migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -867,6 +929,7 @@ mod tests { #[test] fn test_build_migration_block_verbose_create_table() { let migration = MigrationPlan { + id: String::new(), version: 1, comment: Some("initial setup".into()), created_at: None, @@ -892,6 +955,7 @@ mod tests { #[test] fn test_build_migration_block_verbose_multiple_actions() { let migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -924,6 +988,7 @@ mod tests { fn test_build_migration_block_verbose_add_column() { // Create table first let create = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, @@ -938,6 +1003,7 @@ mod tests { // Add column in verbose mode let add_col = MigrationPlan { + id: String::new(), version: 2, comment: Some("add email".into()), created_at: None, @@ -971,6 +1037,7 @@ mod tests { let version_table = "test_versions"; let migration = MigrationPlan { + id: String::new(), version: 1, comment: None, created_at: None, diff --git a/crates/vespertide-planner/src/diff.rs b/crates/vespertide-planner/src/diff.rs index d5bfc70..d5d578a 100644 --- a/crates/vespertide-planner/src/diff.rs +++ b/crates/vespertide-planner/src/diff.rs @@ -612,6 +612,7 @@ pub fn diff_schemas(from: &[TableDef], to: &[TableDef]) -> Result Date: Wed, 11 Feb 2026 00:23:56 +0900 Subject: [PATCH 2/3] Add migration id --- .changepacks/changepack_log_kx6-sb-ClLUo-70CrBAMA.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changepacks/changepack_log_kx6-sb-ClLUo-70CrBAMA.json diff --git a/.changepacks/changepack_log_kx6-sb-ClLUo-70CrBAMA.json b/.changepacks/changepack_log_kx6-sb-ClLUo-70CrBAMA.json new file mode 100644 index 0000000..a3d07c0 --- /dev/null +++ b/.changepacks/changepack_log_kx6-sb-ClLUo-70CrBAMA.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch"},"note":"Add migration id","date":"2026-02-10T15:23:54.389369700Z"} \ No newline at end of file From e72827560f6f1c9ec392b64e4362e435500a4c2d Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 11 Feb 2026 00:44:27 +0900 Subject: [PATCH 3/3] Fix lint --- crates/vespertide-cli/src/commands/log.rs | 46 +-- .../vespertide-cli/src/commands/revision.rs | 258 ++++++++-------- crates/vespertide-cli/src/commands/sql.rs | 290 +++++++++--------- crates/vespertide-cli/src/commands/status.rs | 52 ++-- crates/vespertide-cli/src/utils.rs | 4 +- crates/vespertide-core/src/migration.rs | 4 +- crates/vespertide-macro/src/lib.rs | 2 +- crates/vespertide-query/src/builder.rs | 4 +- .../tests/composite_unique_test.rs | 6 +- .../tests/enum_migration_test.rs | 2 +- .../tests/table_prefixed_enum_test.rs | 2 +- 11 files changed, 336 insertions(+), 334 deletions(-) diff --git a/crates/vespertide-cli/src/commands/log.rs b/crates/vespertide-cli/src/commands/log.rs index 8268f7c..35f1d8e 100644 --- a/crates/vespertide-cli/src/commands/log.rs +++ b/crates/vespertide-cli/src/commands/log.rs @@ -133,22 +133,22 @@ mod tests { fs::write("vespertide.json", text).unwrap(); } - fn write_migration(cfg: &VespertideConfig) { - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let plan = MigrationPlan { - id: String::new(), - comment: Some("init".into()), - created_at: Some("2024-01-01T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![], - constraints: vec![], - }], - }; - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - } + fn write_migration(cfg: &VespertideConfig) { + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let plan = MigrationPlan { + id: String::new(), + comment: Some("init".into()), + created_at: Some("2024-01-01T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![], + constraints: vec![], + }], + }; + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + } #[tokio::test] #[serial_test::serial] @@ -247,13 +247,13 @@ mod tests { write_config(&cfg); fs::create_dir_all(cfg.migrations_dir()).unwrap(); - // Create a migration with ModifyColumnType for SQLite, which generates multiple SQL statements - let plan = MigrationPlan { - id: String::new(), - comment: Some("modify column type".into()), - created_at: Some("2024-01-01T00:00:00Z".into()), - version: 1, - actions: vec![ + // Create a migration with ModifyColumnType for SQLite, which generates multiple SQL statements + let plan = MigrationPlan { + id: String::new(), + comment: Some("modify column type".into()), + created_at: Some("2024-01-01T00:00:00Z".into()), + version: 1, + actions: vec![ MigrationAction::CreateTable { table: "users".into(), columns: vec![ColumnDef { diff --git a/crates/vespertide-cli/src/commands/revision.rs b/crates/vespertide-cli/src/commands/revision.rs index 9d79873..954d41d 100644 --- a/crates/vespertide-cli/src/commands/revision.rs +++ b/crates/vespertide-cli/src/commands/revision.rs @@ -518,15 +518,15 @@ mod tests { assert!(has_yaml); } - #[test] - fn check_non_nullable_fk_add_column_fails() { - use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; - let plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 2, - actions: vec![ + #[test] + fn check_non_nullable_fk_add_column_fails() { + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 2, + actions: vec![ MigrationAction::AddColumn { table: "post".into(), column: Box::new(ColumnDef { @@ -565,15 +565,15 @@ mod tests { assert!(msg.contains("post"), "error should mention table: {msg}"); } - #[test] - fn check_nullable_fk_add_column_ok() { - use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; - let plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 2, - actions: vec![ + #[test] + fn check_nullable_fk_add_column_ok() { + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 2, + actions: vec![ MigrationAction::AddColumn { table: "post".into(), column: Box::new(ColumnDef { @@ -605,16 +605,16 @@ mod tests { assert!(check_non_nullable_fk_add_columns(&plan).is_ok()); } - #[test] - fn check_non_nullable_no_fk_passes() { - // Regular non-nullable column without FK should NOT be blocked - use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; - let plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 2, - actions: vec![MigrationAction::AddColumn { + #[test] + fn check_non_nullable_no_fk_passes() { + // Regular non-nullable column without FK should NOT be blocked + use vespertide_core::{ColumnDef, ColumnType, SimpleColumnType}; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 2, + actions: vec![MigrationAction::AddColumn { table: "post".into(), column: Box::new(ColumnDef { name: "user_id1".into(), @@ -670,16 +670,16 @@ mod tests { ); } - #[test] - fn test_apply_fill_with_to_plan_add_column() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_apply_fill_with_to_plan_add_column() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -712,16 +712,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_modify_column_nullable() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::ModifyColumnNullable { + #[test] + fn test_apply_fill_with_to_plan_modify_column_nullable() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::ModifyColumnNullable { table: "users".into(), column: "status".into(), nullable: false, @@ -745,16 +745,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_skips_existing_fill_with() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_apply_fill_with_to_plan_skips_existing_fill_with() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -788,16 +788,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_no_match() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_apply_fill_with_to_plan_no_match() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -831,16 +831,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_multiple_actions() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![ + #[test] + fn test_apply_fill_with_to_plan_multiple_actions() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![ MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { @@ -892,16 +892,16 @@ mod tests { } } - #[test] - fn test_apply_fill_with_to_plan_other_actions_ignored() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::DeleteColumn { + #[test] + fn test_apply_fill_with_to_plan_other_actions_ignored() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::DeleteColumn { table: "users".into(), column: "old_column".into(), }], @@ -1154,16 +1154,16 @@ mod tests { let _: fn(&str, &str) -> Result = prompt_fill_with_value; } - #[test] - fn test_handle_missing_fill_with_collects_and_applies() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_handle_missing_fill_with_collects_and_applies() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -1211,17 +1211,17 @@ mod tests { ); } - #[test] - fn test_handle_missing_fill_with_no_missing() { - use vespertide_core::MigrationPlan; - - // Plan with no missing fill_with values (nullable column) - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_handle_missing_fill_with_no_missing() { + use vespertide_core::MigrationPlan; + + // Plan with no missing fill_with values (nullable column) + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -1256,16 +1256,16 @@ mod tests { assert!(fill_values.is_empty()); } - #[test] - fn test_handle_missing_fill_with_prompt_error() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + #[test] + fn test_handle_missing_fill_with_prompt_error() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "email".into(), @@ -1307,16 +1307,16 @@ mod tests { } } - #[test] - fn test_handle_missing_fill_with_multiple_columns() { - use vespertide_core::MigrationPlan; - - let mut plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![ + #[test] + fn test_handle_missing_fill_with_multiple_columns() { + use vespertide_core::MigrationPlan; + + let mut plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![ MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { diff --git a/crates/vespertide-cli/src/commands/sql.rs b/crates/vespertide-cli/src/commands/sql.rs index f79d44b..b40538a 100644 --- a/crates/vespertide-cli/src/commands/sql.rs +++ b/crates/vespertide-cli/src/commands/sql.rs @@ -226,35 +226,35 @@ mod tests { let cfg = write_config(); write_model("users"); - let plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "id".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }], - }], - }; - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - - let result = cmd_sql(DatabaseBackend::Postgres).await; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + }], + }; + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + + let result = cmd_sql(DatabaseBackend::Postgres).await; assert!(result.is_ok()); } @@ -267,35 +267,35 @@ mod tests { let cfg = write_config(); write_model("users"); - let plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "id".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }], - }], - }; - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - - let result = cmd_sql(DatabaseBackend::MySql).await; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + }], + }; + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + + let result = cmd_sql(DatabaseBackend::MySql).await; assert!(result.is_ok()); } @@ -308,98 +308,98 @@ mod tests { let cfg = write_config(); write_model("users"); - let plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "id".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![TableConstraint::PrimaryKey { - auto_increment: false, - columns: vec!["id".into()], - }], - }], - }; - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - - let result = cmd_sql(DatabaseBackend::Sqlite).await; + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![TableConstraint::PrimaryKey { + auto_increment: false, + columns: vec!["id".into()], + }], + }], + }; + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + + let result = cmd_sql(DatabaseBackend::Sqlite).await; assert!(result.is_ok()); } - #[tokio::test] - #[serial] - async fn emit_sql_prints_created_at_and_comment_postgres() { - let plan = MigrationPlan { - id: String::new(), - comment: Some("with comment".into()), - created_at: Some("2024-01-02T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::RawSql { - sql: "SELECT 1;".into(), - }], - }; - - let result = emit_sql(&plan, DatabaseBackend::Postgres, &[]); + #[tokio::test] + #[serial] + async fn emit_sql_prints_created_at_and_comment_postgres() { + let plan = MigrationPlan { + id: String::new(), + comment: Some("with comment".into()), + created_at: Some("2024-01-02T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::RawSql { + sql: "SELECT 1;".into(), + }], + }; + + let result = emit_sql(&plan, DatabaseBackend::Postgres, &[]); assert!(result.is_ok()); } - #[tokio::test] - #[serial] - async fn emit_sql_prints_created_at_and_comment_mysql() { - let plan = MigrationPlan { - id: String::new(), - comment: Some("with comment".into()), - created_at: Some("2024-01-02T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::RawSql { - sql: "SELECT 1;".into(), - }], - }; - - let result = emit_sql(&plan, DatabaseBackend::MySql, &[]); + #[tokio::test] + #[serial] + async fn emit_sql_prints_created_at_and_comment_mysql() { + let plan = MigrationPlan { + id: String::new(), + comment: Some("with comment".into()), + created_at: Some("2024-01-02T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::RawSql { + sql: "SELECT 1;".into(), + }], + }; + + let result = emit_sql(&plan, DatabaseBackend::MySql, &[]); assert!(result.is_ok()); } - #[tokio::test] - #[serial] - async fn emit_sql_prints_created_at_and_comment_sqlite() { - let plan = MigrationPlan { - id: String::new(), - comment: Some("with comment".into()), - created_at: Some("2024-01-02T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::RawSql { - sql: "SELECT 1;".into(), - }], - }; - - let result = emit_sql(&plan, DatabaseBackend::Sqlite, &[]); + #[tokio::test] + #[serial] + async fn emit_sql_prints_created_at_and_comment_sqlite() { + let plan = MigrationPlan { + id: String::new(), + comment: Some("with comment".into()), + created_at: Some("2024-01-02T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::RawSql { + sql: "SELECT 1;".into(), + }], + }; + + let result = emit_sql(&plan, DatabaseBackend::Sqlite, &[]); assert!(result.is_ok()); } - #[tokio::test] - #[serial] - async fn emit_sql_multiple_queries() { - let plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![ + #[tokio::test] + #[serial] + async fn emit_sql_multiple_queries() { + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![ MigrationAction::CreateTable { table: "users".into(), columns: vec![ColumnDef { @@ -439,13 +439,13 @@ mod tests { let _cfg = write_config(); write_model("users"); - // Create a migration that adds a NOT NULL column in SQLite, which generates multiple queries - let plan = MigrationPlan { - id: String::new(), - comment: None, - created_at: None, - version: 1, - actions: vec![MigrationAction::AddColumn { + // Create a migration that adds a NOT NULL column in SQLite, which generates multiple queries + let plan = MigrationPlan { + id: String::new(), + comment: None, + created_at: None, + version: 1, + actions: vec![MigrationAction::AddColumn { table: "users".into(), column: Box::new(ColumnDef { name: "nickname".into(), diff --git a/crates/vespertide-cli/src/commands/status.rs b/crates/vespertide-cli/src/commands/status.rs index 1145e90..6fc69b6 100644 --- a/crates/vespertide-cli/src/commands/status.rs +++ b/crates/vespertide-cli/src/commands/status.rs @@ -222,32 +222,32 @@ mod tests { fs::write(path, serde_json::to_string_pretty(&table).unwrap()).unwrap(); } - fn write_migration(cfg: &VespertideConfig) { - fs::create_dir_all(cfg.migrations_dir()).unwrap(); - let plan = MigrationPlan { - id: String::new(), - comment: Some("init".into()), - created_at: Some("2024-01-01T00:00:00Z".into()), - version: 1, - actions: vec![MigrationAction::CreateTable { - table: "users".into(), - columns: vec![ColumnDef { - name: "id".into(), - r#type: ColumnType::Simple(SimpleColumnType::Integer), - nullable: false, - default: None, - comment: None, - primary_key: None, - unique: None, - index: None, - foreign_key: None, - }], - constraints: vec![], - }], - }; - let path = cfg.migrations_dir().join("0001_init.json"); - fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); - } + fn write_migration(cfg: &VespertideConfig) { + fs::create_dir_all(cfg.migrations_dir()).unwrap(); + let plan = MigrationPlan { + id: String::new(), + comment: Some("init".into()), + created_at: Some("2024-01-01T00:00:00Z".into()), + version: 1, + actions: vec![MigrationAction::CreateTable { + table: "users".into(), + columns: vec![ColumnDef { + name: "id".into(), + r#type: ColumnType::Simple(SimpleColumnType::Integer), + nullable: false, + default: None, + comment: None, + primary_key: None, + unique: None, + index: None, + foreign_key: None, + }], + constraints: vec![], + }], + }; + let path = cfg.migrations_dir().join("0001_init.json"); + fs::write(path, serde_json::to_string_pretty(&plan).unwrap()).unwrap(); + } #[tokio::test] #[serial] diff --git a/crates/vespertide-cli/src/utils.rs b/crates/vespertide-cli/src/utils.rs index 81aa576..fbc4a48 100644 --- a/crates/vespertide-cli/src/utils.rs +++ b/crates/vespertide-cli/src/utils.rs @@ -109,8 +109,8 @@ mod tests { use tempfile::tempdir; use vespertide_config::VespertideConfig; use vespertide_core::{ - schema::foreign_key::ForeignKeySyntax, ColumnDef, ColumnType, MigrationPlan, - SimpleColumnType, TableConstraint, TableDef, + ColumnDef, ColumnType, MigrationPlan, SimpleColumnType, TableConstraint, TableDef, + schema::foreign_key::ForeignKeySyntax, }; struct CwdGuard { diff --git a/crates/vespertide-core/src/migration.rs b/crates/vespertide-core/src/migration.rs index f3253ae..ad551f2 100644 --- a/crates/vespertide-core/src/migration.rs +++ b/crates/vespertide-core/src/migration.rs @@ -9,7 +9,9 @@ pub enum MigrationError { NotImplemented, #[error("database error: {0}")] DatabaseError(String), - #[error("migration id mismatch for version {version}: expected '{expected}', found '{found}' in database")] + #[error( + "migration id mismatch for version {version}: expected '{expected}', found '{found}' in database" + )] IdMismatch { version: u32, expected: String, diff --git a/crates/vespertide-macro/src/lib.rs b/crates/vespertide-macro/src/lib.rs index 3686846..8c596ad 100644 --- a/crates/vespertide-macro/src/lib.rs +++ b/crates/vespertide-macro/src/lib.rs @@ -11,7 +11,7 @@ use vespertide_loader::{ load_config_or_default, load_migrations_at_compile_time, load_models_at_compile_time, }; use vespertide_planner::apply_action; -use vespertide_query::{build_plan_queries, DatabaseBackend}; +use vespertide_query::{DatabaseBackend, build_plan_queries}; struct MacroInput { pool: Expr, diff --git a/crates/vespertide-query/src/builder.rs b/crates/vespertide-query/src/builder.rs index 8095424..9701a4a 100644 --- a/crates/vespertide-query/src/builder.rs +++ b/crates/vespertide-query/src/builder.rs @@ -1,10 +1,10 @@ use vespertide_core::{MigrationAction, MigrationPlan, TableDef}; use vespertide_planner::apply_action; +use crate::DatabaseBackend; use crate::error::QueryError; -use crate::sql::build_action_queries_with_pending; use crate::sql::BuiltQuery; -use crate::DatabaseBackend; +use crate::sql::build_action_queries_with_pending; pub struct PlanQueries { pub action: MigrationAction, diff --git a/crates/vespertide-query/tests/composite_unique_test.rs b/crates/vespertide-query/tests/composite_unique_test.rs index be6ac90..b0b6cf6 100644 --- a/crates/vespertide-query/tests/composite_unique_test.rs +++ b/crates/vespertide-query/tests/composite_unique_test.rs @@ -1,8 +1,8 @@ use vespertide_core::{ - schema::StrOrBoolOrArray, ColumnDef, ColumnType, MigrationAction, MigrationPlan, - SimpleColumnType, + ColumnDef, ColumnType, MigrationAction, MigrationPlan, SimpleColumnType, + schema::StrOrBoolOrArray, }; -use vespertide_query::{build_plan_queries, DatabaseBackend}; +use vespertide_query::{DatabaseBackend, build_plan_queries}; #[test] fn test_composite_unique_constraint_generates_single_index() { diff --git a/crates/vespertide-query/tests/enum_migration_test.rs b/crates/vespertide-query/tests/enum_migration_test.rs index d0602b1..4b935c8 100644 --- a/crates/vespertide-query/tests/enum_migration_test.rs +++ b/crates/vespertide-query/tests/enum_migration_test.rs @@ -3,7 +3,7 @@ use vespertide_core::{ ColumnDef, ColumnType, ComplexColumnType, EnumValues, MigrationAction, MigrationPlan, SimpleColumnType, TableDef, }; -use vespertide_query::{build_plan_queries, DatabaseBackend}; +use vespertide_query::{DatabaseBackend, build_plan_queries}; #[test] fn test_enum_value_change_generates_correct_sql() { diff --git a/crates/vespertide-query/tests/table_prefixed_enum_test.rs b/crates/vespertide-query/tests/table_prefixed_enum_test.rs index fce9ac1..8633f81 100644 --- a/crates/vespertide-query/tests/table_prefixed_enum_test.rs +++ b/crates/vespertide-query/tests/table_prefixed_enum_test.rs @@ -4,7 +4,7 @@ mod test_utils { ColumnDef, ColumnType, ComplexColumnType, EnumValues, MigrationAction, MigrationPlan, SimpleColumnType, }; - use vespertide_query::{build_plan_queries, DatabaseBackend}; + use vespertide_query::{DatabaseBackend, build_plan_queries}; #[test] fn test_table_prefixed_enum_names() { // Test that enum types are created with table-prefixed names to avoid conflicts