diff --git a/.changepacks/changepack_log_T_aD8tn8X0SBCzdcSr7WV.json b/.changepacks/changepack_log_T_aD8tn8X0SBCzdcSr7WV.json new file mode 100644 index 00000000..ee32ebd8 --- /dev/null +++ b/.changepacks/changepack_log_T_aD8tn8X0SBCzdcSr7WV.json @@ -0,0 +1 @@ +{"changes":{"crates/vespertide/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch"},"note":"Fix enum SCREAMING_SNAKE_CASE","date":"2026-04-17T07:02:13.191508900Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 175f3453..0056084c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -969,7 +969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2541,7 +2541,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3345,10 +3345,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3672,9 +3672,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespera" -version = "0.1.48" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b3bac41bd5e069d7f1b31eb6a3fd0225402a97f4845f8b4a6d77a10e39be0d" +checksum = "4e53fae5b7dfbc9c6358a7ed2ab03309bafda94edff6f0ed137aacbbf4673290" dependencies = [ "axum", "axum-extra", @@ -3689,9 +3689,9 @@ dependencies = [ [[package]] name = "vespera_core" -version = "0.1.48" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1198fde536e67801f3a091a9c353d223095be6182328d70a0036e97b09b52533" +checksum = "a7907610cd10b5404764392d01a9ed1cbc9f77e65fbdab3d29d7e725a69fef2a" dependencies = [ "serde", "serde_json", @@ -3699,9 +3699,9 @@ dependencies = [ [[package]] name = "vespera_macro" -version = "0.1.48" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf76c9614257c8c16d4d37b828da36aa32ebe14e5be22a7b0669244c21b83ab" +checksum = "8516ec0107927abe8dfa91ea4c7db0c03a63c8b1e5273a6013e3de665b5ff029" dependencies = [ "proc-macro2", "quote", @@ -3713,7 +3713,7 @@ dependencies = [ [[package]] name = "vespertide" -version = "0.1.57" +version = "0.1.58" dependencies = [ "sea-orm", "tokio", @@ -3723,7 +3723,7 @@ dependencies = [ [[package]] name = "vespertide-cli" -version = "0.1.57" +version = "0.1.58" dependencies = [ "anyhow", "assert_cmd", @@ -3752,7 +3752,7 @@ dependencies = [ [[package]] name = "vespertide-config" -version = "0.1.57" +version = "0.1.58" dependencies = [ "clap", "schemars", @@ -3762,7 +3762,7 @@ dependencies = [ [[package]] name = "vespertide-core" -version = "0.1.57" +version = "0.1.58" dependencies = [ "rstest", "schemars", @@ -3774,7 +3774,7 @@ dependencies = [ [[package]] name = "vespertide-exporter" -version = "0.1.57" +version = "0.1.58" dependencies = [ "insta", "rstest", @@ -3786,7 +3786,7 @@ dependencies = [ [[package]] name = "vespertide-loader" -version = "0.1.57" +version = "0.1.58" dependencies = [ "anyhow", "rstest", @@ -3801,7 +3801,7 @@ dependencies = [ [[package]] name = "vespertide-macro" -version = "0.1.57" +version = "0.1.58" dependencies = [ "proc-macro2", "runtime-macros", @@ -3816,11 +3816,11 @@ dependencies = [ [[package]] name = "vespertide-naming" -version = "0.1.57" +version = "0.1.58" [[package]] name = "vespertide-planner" -version = "0.1.57" +version = "0.1.58" dependencies = [ "insta", "rstest", @@ -3831,7 +3831,7 @@ dependencies = [ [[package]] name = "vespertide-query" -version = "0.1.57" +version = "0.1.58" dependencies = [ "insta", "rstest", diff --git a/Cargo.toml b/Cargo.toml index a9515cc7..2d0f3873 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,14 @@ documentation = "https://docs.rs/vespertide" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } [workspace.dependencies] -vespertide-core = { path = "crates/vespertide-core", version = "0.1.58", default-features = false } -vespertide-config = { path = "crates/vespertide-config", version = "0.1.58", default-features = false } -vespertide-loader = { path = "crates/vespertide-loader", version = "0.1.58", default-features = false } -vespertide-macro = { path = "crates/vespertide-macro", version = "0.1.58" } -vespertide-naming = { path = "crates/vespertide-naming", version = "0.1.58" } -vespertide-planner = { path = "crates/vespertide-planner", version = "0.1.58" } -vespertide-query = { path = "crates/vespertide-query", version = "0.1.58" } -vespertide-exporter = { path = "crates/vespertide-exporter", version = "0.1.58" } +vespertide-core = { path = "crates/vespertide-core", default-features = false } +vespertide-config = { path = "crates/vespertide-config", default-features = false } +vespertide-loader = { path = "crates/vespertide-loader", default-features = false } +vespertide-macro = { path = "crates/vespertide-macro" } +vespertide-naming = { path = "crates/vespertide-naming" } +vespertide-planner = { path = "crates/vespertide-planner" } +vespertide-query = { path = "crates/vespertide-query" } +vespertide-exporter = { path = "crates/vespertide-exporter" } [profile.dev] debug = 1 # Line tables only — faster DWARF generation for large codegen output diff --git a/crates/vespertide-exporter/src/seaorm/mod.rs b/crates/vespertide-exporter/src/seaorm/mod.rs index b6a63ca9..d8250926 100644 --- a/crates/vespertide-exporter/src/seaorm/mod.rs +++ b/crates/vespertide-exporter/src/seaorm/mod.rs @@ -1584,7 +1584,7 @@ fn render_enum( lines.push(format!("#[derive({})]", derives.join(", "))); lines.push(format!( "#[serde(rename_all = \"{}\")]", - config.enum_naming_case().serde_rename_all() + enum_serde_rename_all(values, config) )); match values { @@ -1605,8 +1605,9 @@ fn render_enum( match values { EnumValues::String(string_values) => { + let use_screaming_snake_variants = uses_screaming_snake_variants(string_values); for s in string_values { - let variant_name = enum_variant_name(s); + let variant_name = enum_string_variant_name(s, use_screaming_snake_variants); lines.push(format!(" #[sea_orm(string_value = \"{}\")]", s)); lines.push(format!(" {},", variant_name)); } @@ -1631,6 +1632,20 @@ fn render_enum( fn enum_variant_name(s: &str) -> String { let pascal = to_pascal_case(s); + finalize_enum_variant_name(pascal) +} + +fn enum_string_variant_name(s: &str, use_screaming_snake_variants: bool) -> String { + let pascal = if use_screaming_snake_variants { + screaming_snake_to_pascal_case(s) + } else { + to_pascal_case(s) + }; + + finalize_enum_variant_name(pascal) +} + +fn finalize_enum_variant_name(pascal: String) -> String { // Handle empty string if pascal.is_empty() { return "Value".to_string(); @@ -1649,6 +1664,56 @@ fn enum_variant_name(s: &str) -> String { pascal } +fn enum_serde_rename_all(values: &EnumValues, config: &SeaOrmConfig) -> &'static str { + match values { + EnumValues::String(string_values) if uses_screaming_snake_variants(string_values) => { + "SCREAMING_SNAKE_CASE" + } + _ => config.enum_naming_case().serde_rename_all(), + } +} + +fn uses_screaming_snake_variants(values: &[String]) -> bool { + !values.is_empty() && values.iter().all(|value| is_screaming_snake_value(value)) +} + +fn is_screaming_snake_value(value: &str) -> bool { + let mut has_ascii_upper = false; + + for ch in value.chars() { + if ch.is_ascii_lowercase() { + return false; + } + if ch.is_ascii_uppercase() { + has_ascii_upper = true; + continue; + } + if ch.is_ascii_digit() || ch == '_' { + continue; + } + return false; + } + + has_ascii_upper +} + +fn screaming_snake_to_pascal_case(value: &str) -> String { + value + .split('_') + .filter(|segment| !segment.is_empty()) + .map(|segment| { + let mut chars = segment.chars(); + let first = chars + .next() + .expect("empty segments are filtered before PascalCase conversion"); + let mut out = String::new(); + out.push(first.to_ascii_uppercase()); + out.extend(chars.map(|ch| ch.to_ascii_lowercase())); + out + }) + .collect() +} + fn to_pascal_case(s: &str) -> String { let mut result = String::new(); let mut capitalize = true; @@ -2182,6 +2247,8 @@ mod helper_tests { #[case("pending", "Pending")] #[case("in_stock", "InStock")] #[case("info-level", "InfoLevel")] + #[case("ACTIVE", "ACTIVE")] + #[case("ERROR_LEVEL", "ERRORLEVEL")] #[case("1critical", "N1critical")] #[case("123abc", "N123abc")] #[case("1_critical", "N1Critical")] @@ -2190,6 +2257,33 @@ mod helper_tests { assert_eq!(enum_variant_name(input), expected); } + #[test] + fn test_render_enum_uses_screaming_snake_serde_for_uppercase_values() { + let mut lines = Vec::new(); + let config = SeaOrmConfig::default(); + let values = EnumValues::String(vec!["PENDING".into(), "IN_PROGRESS".into()]); + + render_enum(&mut lines, "orders", "order_status", &values, &config); + + let result = lines.join("\n"); + assert!(result.contains("#[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]")); + assert!(result.contains(" #[sea_orm(string_value = \"PENDING\")]\n Pending,")); + assert!(result.contains(" #[sea_orm(string_value = \"IN_PROGRESS\")]\n InProgress,")); + } + + #[test] + fn test_is_screaming_snake_value_rejects_invalid_symbol() { + assert!(!is_screaming_snake_value("PENDING-REVIEW")); + } + + #[test] + fn test_screaming_snake_to_pascal_case_ignores_empty_segments() { + assert_eq!( + screaming_snake_to_pascal_case("PENDING__REVIEW"), + "PendingReview" + ); + } + fn string_enum_order_status() -> (&'static str, EnumValues) { ( "order_status", diff --git a/crates/vespertide-planner/src/validate.rs b/crates/vespertide-planner/src/validate.rs index 6c48273f..c2933e6e 100644 --- a/crates/vespertide-planner/src/validate.rs +++ b/crates/vespertide-planner/src/validate.rs @@ -417,12 +417,11 @@ pub fn validate_migration_plan(plan: &MigrationPlan) -> Result<(), PlannerError> nullable, fill_with, delete_null_rows, - } => { + } // If changing from nullable to non-nullable, fill_with is required - if !nullable && fill_with.is_none() && !delete_null_rows.unwrap_or(false) { + if !nullable && fill_with.is_none() && !delete_null_rows.unwrap_or(false) => { return Err(PlannerError::MissingFillWith(table.clone(), column.clone())); } - } MigrationAction::ModifyColumnType { table, column, @@ -484,9 +483,9 @@ pub fn find_missing_fill_with( table, column, fill_with, - } => { + } // If column is NOT NULL and has no default, fill_with is required - if !column.nullable && column.default.is_none() && fill_with.is_none() { + if !column.nullable && column.default.is_none() && fill_with.is_none() => { missing.push(FillWithRequired { action_index: idx, table: table.clone(), @@ -498,17 +497,16 @@ pub fn find_missing_fill_with( has_foreign_key: false, }); } - } MigrationAction::ModifyColumnNullable { table, column, nullable, fill_with, delete_null_rows, - } => { + } // If changing from nullable to non-nullable, fill_with is required // UNLESS the column already has a default value (which will be used) - if !nullable && fill_with.is_none() && !delete_null_rows.unwrap_or(false) { + if !nullable && fill_with.is_none() && !delete_null_rows.unwrap_or(false) => { // Look up column from the current schema let table_def = current_schema.iter().find(|t| t.name == *table); @@ -542,7 +540,6 @@ pub fn find_missing_fill_with( has_foreign_key, }); } - } _ => {} } } diff --git a/examples/app/Cargo.toml b/examples/app/Cargo.toml index f4fe8df9..24b9e13c 100644 --- a/examples/app/Cargo.toml +++ b/examples/app/Cargo.toml @@ -10,4 +10,4 @@ tokio = { version = "1", features = ["full"] } sea-orm = { version = "2.0.0-rc.37", features = ["sqlx-sqlite", "sqlx-postgres", "runtime-tokio-native-tls", "macros"] } anyhow = "1" serde = { version = "1", features = ["derive"] } -vespera = "0.1.48" +vespera = "0.1.50"