Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changepacks/changepack_log_T_aD8tn8X0SBCzdcSr7WV.json
Original file line number Diff line number Diff line change
@@ -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"}
40 changes: 20 additions & 20 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
98 changes: 96 additions & 2 deletions crates/vespertide-exporter/src/seaorm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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));
}
Expand All @@ -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();
Expand All @@ -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;
Expand Down Expand Up @@ -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")]
Expand All @@ -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",
Expand Down
15 changes: 6 additions & 9 deletions crates/vespertide-planner/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand All @@ -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);

Expand Down Expand Up @@ -542,7 +540,6 @@ pub fn find_missing_fill_with(
has_foreign_key,
});
}
}
_ => {}
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"