diff --git a/.changepacks/changepack_log_4sB2YBEV80GvtBNjS_r76.json b/.changepacks/changepack_log_4sB2YBEV80GvtBNjS_r76.json new file mode 100644 index 0000000..f58c0dc --- /dev/null +++ b/.changepacks/changepack_log_4sB2YBEV80GvtBNjS_r76.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch"},"note":"Support enum","date":"2025-12-17T11:30:23.776147Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9dccd7d..3700bff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -523,19 +523,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "der" version = "0.7.10" @@ -901,12 +888,6 @@ dependencies = [ "ahash", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.5" @@ -2251,20 +2232,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "serial_test" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "538c30747ae860d6fb88330addbbd3e0ddbe46d662d032855596d8a8ca260611" -dependencies = [ - "dashmap", - "futures", - "lazy_static", - "log", - "parking_lot", - "serial_test_derive 1.0.0", -] - [[package]] name = "serial_test" version = "3.2.0" @@ -2276,18 +2243,7 @@ dependencies = [ "once_cell", "parking_lot", "scc", - "serial_test_derive 3.2.0", -] - -[[package]] -name = "serial_test_derive" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "079a83df15f85d89a68d64ae1238f142f172b1fa915d0d76b26a7cba1b659a69" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "serial_test_derive", ] [[package]] @@ -2982,7 +2938,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespertide" -version = "0.1.9" +version = "0.1.10" dependencies = [ "vespertide-core", "vespertide-macro", @@ -2990,7 +2946,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.9" +version = "0.1.10" dependencies = [ "anyhow", "assert_cmd", @@ -3002,7 +2958,7 @@ dependencies = [ "schemars", "serde_json", "serde_yaml", - "serial_test 3.2.0", + "serial_test", "tempfile", "vespertide-config", "vespertide-core", @@ -3014,7 +2970,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.9" +version = "0.1.10" dependencies = [ "clap", "serde", @@ -3022,7 +2978,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.9" +version = "0.1.10" dependencies = [ "rstest", "schemars", @@ -3032,7 +2988,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.9" +version = "0.1.10" dependencies = [ "insta", "rstest", @@ -3042,13 +2998,13 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.9" +version = "0.1.10" dependencies = [ "anyhow", "rstest", "serde_json", "serde_yaml", - "serial_test 1.0.0", + "serial_test", "tempfile", "vespertide-config", "vespertide-core", @@ -3057,7 +3013,7 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.9" +version = "0.1.10" dependencies = [ "proc-macro2", "quote", @@ -3067,12 +3023,13 @@ dependencies = [ "vespertide-config", "vespertide-core", "vespertide-loader", + "vespertide-planner", "vespertide-query", ] [[package]] name = "vespertide-planner" -version = "0.1.9" +version = "0.1.10" dependencies = [ "rstest", "thiserror", @@ -3081,13 +3038,14 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.9" +version = "0.1.10" dependencies = [ "insta", "rstest", "sea-query 0.32.7", "thiserror", "vespertide-core", + "vespertide-planner", ] [[package]] diff --git a/crates/vespertide-cli/src/commands/log.rs b/crates/vespertide-cli/src/commands/log.rs index f9db36f..ca519cf 100644 --- a/crates/vespertide-cli/src/commands/log.rs +++ b/crates/vespertide-cli/src/commands/log.rs @@ -1,6 +1,7 @@ use anyhow::Result; use colored::Colorize; -use vespertide_loader::{load_config, load_models}; +use vespertide_loader::load_config; +use vespertide_planner::apply_action; use vespertide_query::{DatabaseBackend, build_plan_queries}; use crate::utils::load_migrations; @@ -21,7 +22,10 @@ pub fn cmd_log(backend: DatabaseBackend) -> Result<()> { plans.len().to_string().bright_yellow().bold() ); println!(); - let current_models = load_models(&config)?; + + // Build baseline schema incrementally as we iterate through migrations + let mut baseline_schema = Vec::new(); + for plan in &plans { println!( "{} {}", @@ -44,9 +48,15 @@ pub fn cmd_log(backend: DatabaseBackend) -> Result<()> { plan.actions.len().to_string().bright_yellow() ); - let plan_queries = build_plan_queries(plan, ¤t_models) + // Use the current baseline schema (from all previous migrations) + let plan_queries = build_plan_queries(plan, &baseline_schema) .map_err(|e| anyhow::anyhow!("query build error for v{}: {}", plan.version, e))?; + // Update baseline schema incrementally by applying each action + for action in &plan.actions { + let _ = apply_action(&mut baseline_schema, action); + } + for (i, pq) in plan_queries.iter().enumerate() { println!( " {}. {}", diff --git a/crates/vespertide-cli/src/commands/sql.rs b/crates/vespertide-cli/src/commands/sql.rs index e7b2a31..465e339 100644 --- a/crates/vespertide-cli/src/commands/sql.rs +++ b/crates/vespertide-cli/src/commands/sql.rs @@ -1,6 +1,6 @@ use anyhow::Result; use colored::Colorize; -use vespertide_planner::plan_next_migration; +use vespertide_planner::{plan_next_migration_with_baseline, schema_from_plans}; use vespertide_query::{DatabaseBackend, build_plan_queries}; use crate::utils::{load_config, load_migrations, load_models}; @@ -10,10 +10,15 @@ pub fn cmd_sql(backend: DatabaseBackend) -> Result<()> { let current_models = load_models(&config)?; let applied_plans = load_migrations(&config)?; - let plan = plan_next_migration(¤t_models, &applied_plans) + // Reconstruct the baseline schema from applied migrations + let baseline_schema = schema_from_plans(&applied_plans) + .map_err(|e| anyhow::anyhow!("failed to reconstruct schema: {}", e))?; + + // Plan next migration using the pre-computed baseline + let plan = plan_next_migration_with_baseline(¤t_models, &applied_plans, &baseline_schema) .map_err(|e| anyhow::anyhow!("planning error: {}", e))?; - emit_sql(&plan, backend, ¤t_models) + emit_sql(&plan, backend, &baseline_schema) } fn emit_sql( diff --git a/crates/vespertide-core/src/schema/column.rs b/crates/vespertide-core/src/schema/column.rs index 8173f5b..23fd5a3 100644 --- a/crates/vespertide-core/src/schema/column.rs +++ b/crates/vespertide-core/src/schema/column.rs @@ -1,306 +1,226 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::schema::{ - foreign_key::ForeignKeySyntax, names::ColumnName, primary_key::PrimaryKeySyntax, - str_or_bool::StrOrBoolOrArray, -}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct ColumnDef { - pub name: ColumnName, - pub r#type: ColumnType, - pub nullable: bool, - pub default: Option, - pub comment: Option, - pub primary_key: Option, - pub unique: Option, - pub index: Option, - pub foreign_key: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case", untagged)] -pub enum ColumnType { - Simple(SimpleColumnType), - Complex(ComplexColumnType), -} - -impl ColumnType { - /// Convert column type to SQL type string - pub fn to_sql(&self) -> String { - match self { - ColumnType::Simple(ty) => match ty { - SimpleColumnType::SmallInt => "SMALLINT".into(), - SimpleColumnType::Integer => "INTEGER".into(), - SimpleColumnType::BigInt => "BIGINT".into(), - SimpleColumnType::Real => "REAL".into(), - SimpleColumnType::DoublePrecision => "DOUBLE PRECISION".into(), - SimpleColumnType::Text => "TEXT".into(), - SimpleColumnType::Boolean => "BOOLEAN".into(), - SimpleColumnType::Date => "DATE".into(), - SimpleColumnType::Time => "TIME".into(), - SimpleColumnType::Timestamp => "TIMESTAMP".into(), - SimpleColumnType::Timestamptz => "TIMESTAMPTZ".into(), - SimpleColumnType::Interval => "INTERVAL".into(), - SimpleColumnType::Bytea => "BYTEA".into(), - SimpleColumnType::Uuid => "UUID".into(), - SimpleColumnType::Json => "JSON".into(), - SimpleColumnType::Jsonb => "JSONB".into(), - SimpleColumnType::Inet => "INET".into(), - SimpleColumnType::Cidr => "CIDR".into(), - SimpleColumnType::Macaddr => "MACADDR".into(), - SimpleColumnType::Xml => "XML".into(), - }, - ColumnType::Complex(ty) => match ty { - ComplexColumnType::Varchar { length } => format!("VARCHAR({})", length), - ComplexColumnType::Numeric { precision, scale } => { - format!("NUMERIC({}, {})", precision, scale) - } - ComplexColumnType::Char { length } => format!("CHAR({})", length), - ComplexColumnType::Custom { custom_type } => custom_type.clone(), - }, - } - } - - /// Convert column type to Rust type string (for SeaORM entity generation) - pub fn to_rust_type(&self, nullable: bool) -> String { - let base = match self { - ColumnType::Simple(ty) => match ty { - SimpleColumnType::SmallInt => "i16".to_string(), - SimpleColumnType::Integer => "i32".to_string(), - SimpleColumnType::BigInt => "i64".to_string(), - SimpleColumnType::Real => "f32".to_string(), - SimpleColumnType::DoublePrecision => "f64".to_string(), - SimpleColumnType::Text => "String".to_string(), - SimpleColumnType::Boolean => "bool".to_string(), - SimpleColumnType::Date => "Date".to_string(), - SimpleColumnType::Time => "Time".to_string(), - SimpleColumnType::Timestamp => "DateTime".to_string(), - SimpleColumnType::Timestamptz => "DateTimeWithTimeZone".to_string(), - SimpleColumnType::Interval => "String".to_string(), - SimpleColumnType::Bytea => "Vec".to_string(), - SimpleColumnType::Uuid => "Uuid".to_string(), - SimpleColumnType::Json | SimpleColumnType::Jsonb => "Json".to_string(), - SimpleColumnType::Inet | SimpleColumnType::Cidr => "String".to_string(), - SimpleColumnType::Macaddr => "String".to_string(), - SimpleColumnType::Xml => "String".to_string(), - }, - ColumnType::Complex(ty) => match ty { - ComplexColumnType::Varchar { .. } => "String".to_string(), - ComplexColumnType::Numeric { .. } => "Decimal".to_string(), - ComplexColumnType::Char { .. } => "String".to_string(), - ComplexColumnType::Custom { .. } => "String".to_string(), // Default for custom types - }, - }; - - if nullable { - format!("Option<{}>", base) - } else { - base - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SimpleColumnType { - SmallInt, - Integer, - BigInt, - Real, - DoublePrecision, - - // Text types - Text, - - // Boolean type - Boolean, - - // Date/Time types - Date, - Time, - Timestamp, - Timestamptz, - Interval, - - // Binary type - Bytea, - - // UUID type - Uuid, - - // JSON types - Json, - Jsonb, - - // Network types - Inet, - Cidr, - Macaddr, - - // XML type - Xml, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case", tag = "kind")] -pub enum ComplexColumnType { - Varchar { length: u32 }, - Numeric { precision: u32, scale: u32 }, - Char { length: u32 }, - Custom { custom_type: String }, -} - -#[cfg(test)] -mod tests { - use super::*; - use rstest::rstest; - - #[rstest] - #[case(SimpleColumnType::SmallInt, "SMALLINT")] - #[case(SimpleColumnType::Integer, "INTEGER")] - #[case(SimpleColumnType::BigInt, "BIGINT")] - #[case(SimpleColumnType::Real, "REAL")] - #[case(SimpleColumnType::DoublePrecision, "DOUBLE PRECISION")] - #[case(SimpleColumnType::Text, "TEXT")] - #[case(SimpleColumnType::Boolean, "BOOLEAN")] - #[case(SimpleColumnType::Date, "DATE")] - #[case(SimpleColumnType::Time, "TIME")] - #[case(SimpleColumnType::Timestamp, "TIMESTAMP")] - #[case(SimpleColumnType::Timestamptz, "TIMESTAMPTZ")] - #[case(SimpleColumnType::Interval, "INTERVAL")] - #[case(SimpleColumnType::Bytea, "BYTEA")] - #[case(SimpleColumnType::Uuid, "UUID")] - #[case(SimpleColumnType::Json, "JSON")] - #[case(SimpleColumnType::Jsonb, "JSONB")] - #[case(SimpleColumnType::Inet, "INET")] - #[case(SimpleColumnType::Cidr, "CIDR")] - #[case(SimpleColumnType::Macaddr, "MACADDR")] - #[case(SimpleColumnType::Xml, "XML")] - fn test_simple_column_type_to_sql( - #[case] column_type: SimpleColumnType, - #[case] expected: &str, - ) { - assert_eq!(ColumnType::Simple(column_type).to_sql(), expected); - } - - #[rstest] - #[case(ComplexColumnType::Varchar { length: 255 }, "VARCHAR(255)")] - #[case(ComplexColumnType::Varchar { length: 50 }, "VARCHAR(50)")] - #[case(ComplexColumnType::Varchar { length: 1 }, "VARCHAR(1)")] - #[case(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "NUMERIC(10, 2)")] - #[case(ComplexColumnType::Numeric { precision: 5, scale: 0 }, "NUMERIC(5, 0)")] - #[case(ComplexColumnType::Numeric { precision: 18, scale: 4 }, "NUMERIC(18, 4)")] - #[case(ComplexColumnType::Char { length: 10 }, "CHAR(10)")] - #[case(ComplexColumnType::Char { length: 1 }, "CHAR(1)")] - #[case(ComplexColumnType::Char { length: 255 }, "CHAR(255)")] - #[case(ComplexColumnType::Custom { custom_type: "MONEY".into() }, "MONEY")] - #[case(ComplexColumnType::Custom { custom_type: "JSONB".into() }, "JSONB")] - #[case(ComplexColumnType::Custom { custom_type: "CUSTOM_TYPE".into() }, "CUSTOM_TYPE")] - fn test_complex_column_type_to_sql( - #[case] column_type: ComplexColumnType, - #[case] expected: &str, - ) { - assert_eq!(ColumnType::Complex(column_type).to_sql(), expected); - } - - #[rstest] - #[case(SimpleColumnType::SmallInt, "i16")] - #[case(SimpleColumnType::Integer, "i32")] - #[case(SimpleColumnType::BigInt, "i64")] - #[case(SimpleColumnType::Real, "f32")] - #[case(SimpleColumnType::DoublePrecision, "f64")] - #[case(SimpleColumnType::Text, "String")] - #[case(SimpleColumnType::Boolean, "bool")] - #[case(SimpleColumnType::Date, "Date")] - #[case(SimpleColumnType::Time, "Time")] - #[case(SimpleColumnType::Timestamp, "DateTime")] - #[case(SimpleColumnType::Timestamptz, "DateTimeWithTimeZone")] - #[case(SimpleColumnType::Interval, "String")] - #[case(SimpleColumnType::Bytea, "Vec")] - #[case(SimpleColumnType::Uuid, "Uuid")] - #[case(SimpleColumnType::Json, "Json")] - #[case(SimpleColumnType::Jsonb, "Json")] - #[case(SimpleColumnType::Inet, "String")] - #[case(SimpleColumnType::Cidr, "String")] - #[case(SimpleColumnType::Macaddr, "String")] - #[case(SimpleColumnType::Xml, "String")] - fn test_simple_column_type_to_rust_type_not_nullable( - #[case] column_type: SimpleColumnType, - #[case] expected: &str, - ) { - assert_eq!( - ColumnType::Simple(column_type).to_rust_type(false), - expected - ); - } - - #[rstest] - #[case(SimpleColumnType::SmallInt, "Option")] - #[case(SimpleColumnType::Integer, "Option")] - #[case(SimpleColumnType::BigInt, "Option")] - #[case(SimpleColumnType::Real, "Option")] - #[case(SimpleColumnType::DoublePrecision, "Option")] - #[case(SimpleColumnType::Text, "Option")] - #[case(SimpleColumnType::Boolean, "Option")] - #[case(SimpleColumnType::Date, "Option")] - #[case(SimpleColumnType::Time, "Option