Skip to content

Commit 12a7679

Browse files
committed
Impl num enum
1 parent 470ab73 commit 12a7679

12 files changed

+411
-82
lines changed

crates/vespertide-core/src/lib.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
pub mod action;
2-
pub mod migration;
3-
pub mod schema;
4-
5-
pub use action::{MigrationAction, MigrationPlan};
6-
pub use migration::{MigrationError, MigrationOptions};
7-
pub use schema::{
8-
ColumnDef, ColumnName, ColumnType, ComplexColumnType, IndexDef, IndexName, ReferenceAction,
9-
SimpleColumnType, StrOrBoolOrArray, TableConstraint, TableDef, TableName, TableValidationError,
10-
};
1+
pub mod action;
2+
pub mod migration;
3+
pub mod schema;
4+
5+
pub use action::{MigrationAction, MigrationPlan};
6+
pub use migration::{MigrationError, MigrationOptions};
7+
pub use schema::{
8+
ColumnDef, ColumnName, ColumnType, ComplexColumnType, EnumValue, IndexDef, IndexName,
9+
ReferenceAction, SimpleColumnType, StrOrBoolOrArray, TableConstraint, TableDef, TableName,
10+
TableValidationError,
11+
};

crates/vespertide-core/src/schema/column.rs

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,71 @@ impl SimpleColumnType {
127127
}
128128
}
129129

130+
/// Enum value definition - can be string-based or integer-based
131+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
132+
#[serde(untagged)]
133+
pub enum EnumValue {
134+
/// String enum value: "pending", "active", etc.
135+
String(String),
136+
/// Integer enum value with name and numeric value: { "name": "Black", "value": 0 }
137+
Integer { name: String, value: i32 },
138+
}
139+
140+
impl EnumValue {
141+
/// Get the variant name for this enum value
142+
pub fn variant_name(&self) -> &str {
143+
match self {
144+
EnumValue::String(s) => s.as_str(),
145+
EnumValue::Integer { name, .. } => name.as_str(),
146+
}
147+
}
148+
149+
/// Get the SQL representation for CREATE TYPE ENUM
150+
/// String enums return 'value', Integer enums return the number
151+
pub fn to_sql_value(&self) -> String {
152+
match self {
153+
EnumValue::String(s) => format!("'{}'", s.replace('\'', "''")),
154+
EnumValue::Integer { value, .. } => value.to_string(),
155+
}
156+
}
157+
158+
/// Check if this is a string enum value
159+
pub fn is_string(&self) -> bool {
160+
match self {
161+
EnumValue::String(_) => true,
162+
EnumValue::Integer { .. } => false,
163+
}
164+
}
165+
166+
/// Check if this is an integer enum value
167+
pub fn is_integer(&self) -> bool {
168+
match self {
169+
EnumValue::String(_) => false,
170+
EnumValue::Integer { .. } => true,
171+
}
172+
}
173+
}
174+
175+
impl From<&str> for EnumValue {
176+
fn from(s: &str) -> Self {
177+
EnumValue::String(s.to_string())
178+
}
179+
}
180+
181+
impl From<String> for EnumValue {
182+
fn from(s: String) -> Self {
183+
EnumValue::String(s)
184+
}
185+
}
186+
130187
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
131188
#[serde(rename_all = "snake_case", tag = "kind")]
132189
pub enum ComplexColumnType {
133190
Varchar { length: u32 },
134191
Numeric { precision: u32, scale: u32 },
135192
Char { length: u32 },
136193
Custom { custom_type: String },
137-
Enum { name: String, values: Vec<String> },
194+
Enum { name: String, values: Vec<EnumValue> },
138195
}
139196

140197
#[cfg(test)]
@@ -254,4 +311,74 @@ mod tests {
254311
// Complex types never support auto_increment
255312
assert!(!ColumnType::Complex(column_type).supports_auto_increment());
256313
}
314+
315+
#[test]
316+
fn test_enum_value_integer_variant_name() {
317+
let val = EnumValue::Integer { name: "Black".into(), value: 0 };
318+
assert_eq!(val.variant_name(), "Black");
319+
}
320+
321+
#[test]
322+
fn test_enum_value_integer_to_sql_value() {
323+
let val = EnumValue::Integer { name: "High".into(), value: 10 };
324+
assert_eq!(val.to_sql_value(), "10");
325+
}
326+
327+
#[test]
328+
fn test_enum_value_is_string() {
329+
let string_val = EnumValue::String("active".into());
330+
let int_val = EnumValue::Integer { name: "Active".into(), value: 1 };
331+
assert!(string_val.is_string());
332+
assert!(!int_val.is_string());
333+
}
334+
335+
#[test]
336+
fn test_enum_value_from_string_owned() {
337+
let owned = String::from("pending");
338+
let val: EnumValue = owned.into();
339+
assert_eq!(val, EnumValue::String("pending".into()));
340+
}
341+
342+
#[test]
343+
fn test_enum_value_is_integer() {
344+
let string_val = EnumValue::String("active".into());
345+
let int_val = EnumValue::Integer { name: "Active".into(), value: 1 };
346+
assert!(!string_val.is_integer());
347+
assert!(int_val.is_integer());
348+
}
349+
350+
#[test]
351+
fn test_enum_value_string_variant_name() {
352+
let val = EnumValue::String("pending".into());
353+
assert_eq!(val.variant_name(), "pending");
354+
}
355+
356+
#[test]
357+
fn test_enum_value_string_to_sql_value() {
358+
let val = EnumValue::String("active".into());
359+
assert_eq!(val.to_sql_value(), "'active'");
360+
}
361+
362+
#[rstest]
363+
#[case(SimpleColumnType::SmallInt, true)]
364+
#[case(SimpleColumnType::Integer, true)]
365+
#[case(SimpleColumnType::BigInt, true)]
366+
#[case(SimpleColumnType::Text, false)]
367+
#[case(SimpleColumnType::Boolean, false)]
368+
fn test_simple_column_type_supports_auto_increment(
369+
#[case] ty: SimpleColumnType,
370+
#[case] expected: bool,
371+
) {
372+
assert_eq!(ty.supports_auto_increment(), expected);
373+
}
374+
375+
#[rstest]
376+
#[case(SimpleColumnType::Integer, true)]
377+
#[case(SimpleColumnType::Text, false)]
378+
fn test_column_type_simple_supports_auto_increment(
379+
#[case] ty: SimpleColumnType,
380+
#[case] expected: bool,
381+
) {
382+
assert_eq!(ColumnType::Simple(ty).supports_auto_increment(), expected);
383+
}
257384
}
Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
pub mod column;
2-
pub mod constraint;
3-
pub mod foreign_key;
4-
pub mod index;
5-
pub mod names;
6-
pub mod primary_key;
7-
pub mod reference;
8-
pub mod str_or_bool;
9-
pub mod table;
10-
11-
pub use column::{ColumnDef, ColumnType, ComplexColumnType, SimpleColumnType};
12-
pub use constraint::TableConstraint;
13-
pub use index::IndexDef;
14-
pub use names::{ColumnName, IndexName, TableName};
15-
pub use primary_key::PrimaryKeyDef;
16-
pub use reference::ReferenceAction;
17-
pub use str_or_bool::StrOrBoolOrArray;
18-
pub use table::{TableDef, TableValidationError};
1+
pub mod column;
2+
pub mod constraint;
3+
pub mod foreign_key;
4+
pub mod index;
5+
pub mod names;
6+
pub mod primary_key;
7+
pub mod reference;
8+
pub mod str_or_bool;
9+
pub mod table;
10+
11+
pub use column::{ColumnDef, ColumnType, ComplexColumnType, EnumValue, SimpleColumnType};
12+
pub use constraint::TableConstraint;
13+
pub use index::IndexDef;
14+
pub use names::{ColumnName, IndexName, TableName};
15+
pub use primary_key::PrimaryKeyDef;
16+
pub use reference::ReferenceAction;
17+
pub use str_or_bool::StrOrBoolOrArray;
18+
pub use table::{TableDef, TableValidationError};

crates/vespertide-exporter/src/seaorm/mod.rs

Lines changed: 101 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::HashSet;
22

33
use crate::orm::OrmExporter;
44
use vespertide_core::{
5-
ColumnDef, ColumnType, ComplexColumnType, IndexDef, TableConstraint, TableDef,
5+
ColumnDef, ColumnType, ComplexColumnType, EnumValue, IndexDef, TableConstraint, TableDef,
66
};
77

88
pub struct SeaOrmExporter;
@@ -578,20 +578,44 @@ fn unique_name(base: &str, used: &mut HashSet<String>) -> String {
578578
name
579579
}
580580

581-
fn render_enum(lines: &mut Vec<String>, name: &str, values: &[String]) {
581+
fn render_enum(lines: &mut Vec<String>, name: &str, values: &[EnumValue]) {
582582
let enum_name = to_pascal_case(name);
583583

584-
lines.push("#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]".into());
585-
lines.push(format!(
586-
"#[sea_orm(rs_type = \"String\", db_type = \"Enum\", enum_name = \"{}\")]",
587-
name
588-
));
584+
// Check if this is an integer enum (all values have integer form)
585+
let is_integer_enum = values
586+
.iter()
587+
.all(|v| matches!(v, EnumValue::Integer { .. }));
588+
589+
lines.push(
590+
"#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]"
591+
.into(),
592+
);
593+
594+
if is_integer_enum {
595+
// Integer enum: #[sea_orm(rs_type = "i32", db_type = "Integer")]
596+
lines.push("#[sea_orm(rs_type = \"i32\", db_type = \"Integer\")]".into());
597+
} else {
598+
// String enum: #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "...")]
599+
lines.push(format!(
600+
"#[sea_orm(rs_type = \"String\", db_type = \"Enum\", enum_name = \"{}\")]",
601+
name
602+
));
603+
}
604+
589605
lines.push(format!("pub enum {} {{", enum_name));
590606

591607
for value in values {
592-
let variant_name = enum_variant_name(value);
593-
lines.push(format!(" #[sea_orm(string_value = \"{}\")]", value));
594-
lines.push(format!(" {},", variant_name));
608+
match value {
609+
EnumValue::String(s) => {
610+
let variant_name = enum_variant_name(s);
611+
lines.push(format!(" #[sea_orm(string_value = \"{}\")]", s));
612+
lines.push(format!(" {},", variant_name));
613+
}
614+
EnumValue::Integer { name: var_name, value: num } => {
615+
let variant_name = enum_variant_name(var_name);
616+
lines.push(format!(" {} = {},", variant_name, num));
617+
}
618+
}
595619
}
596620
lines.push("}".into());
597621
lines.push(String::new());
@@ -779,46 +803,79 @@ mod helper_tests {
779803
assert_eq!(enum_variant_name(input), expected);
780804
}
781805

782-
#[test]
783-
fn test_render_enum() {
784-
let mut lines = Vec::new();
785-
render_enum(
786-
&mut lines,
806+
fn string_enum_order_status() -> (&'static str, Vec<EnumValue>) {
807+
(
787808
"order_status",
788-
&["pending".into(), "shipped".into()],
789-
);
809+
vec!["pending".into(), "shipped".into(), "delivered".into()],
810+
)
811+
}
790812

791-
assert!(lines.iter().any(|l| l.contains("pub enum OrderStatus")));
792-
assert!(lines.iter().any(|l| l.contains("Pending")));
793-
assert!(lines.iter().any(|l| l.contains("Shipped")));
794-
assert!(lines.iter().any(|l| l.contains("DeriveActiveEnum")));
795-
assert!(lines.iter().any(|l| l.contains("EnumIter")));
796-
assert!(
797-
lines
798-
.iter()
799-
.any(|l| l.contains("enum_name = \"order_status\""))
800-
);
813+
fn string_enum_numeric_prefix() -> (&'static str, Vec<EnumValue>) {
814+
(
815+
"priority",
816+
vec!["1_high".into(), "2_medium".into(), "3_low".into()],
817+
)
801818
}
802819

803-
#[test]
804-
fn test_render_enum_with_numeric_prefix_value() {
820+
fn integer_enum_color() -> (&'static str, Vec<EnumValue>) {
821+
(
822+
"color",
823+
vec![
824+
EnumValue::Integer {
825+
name: "Black".into(),
826+
value: 0,
827+
},
828+
EnumValue::Integer {
829+
name: "White".into(),
830+
value: 1,
831+
},
832+
EnumValue::Integer {
833+
name: "Red".into(),
834+
value: 2,
835+
},
836+
],
837+
)
838+
}
839+
840+
fn integer_enum_status() -> (&'static str, Vec<EnumValue>) {
841+
(
842+
"task_status",
843+
vec![
844+
EnumValue::Integer {
845+
name: "Pending".into(),
846+
value: 0,
847+
},
848+
EnumValue::Integer {
849+
name: "InProgress".into(),
850+
value: 1,
851+
},
852+
EnumValue::Integer {
853+
name: "Completed".into(),
854+
value: 100,
855+
},
856+
],
857+
)
858+
}
859+
860+
#[rstest]
861+
#[case::string_enum("string_order_status", string_enum_order_status())]
862+
#[case::string_numeric_prefix("string_numeric_prefix", string_enum_numeric_prefix())]
863+
#[case::integer_color("integer_color", integer_enum_color())]
864+
#[case::integer_status("integer_status", integer_enum_status())]
865+
fn test_render_enum_snapshots(
866+
#[case] name: &str,
867+
#[case] input: (&str, Vec<EnumValue>),
868+
) {
869+
use insta::with_settings;
870+
871+
let (enum_name, values) = input;
805872
let mut lines = Vec::new();
806-
render_enum(
807-
&mut lines,
808-
"priority",
809-
&["1_high".into(), "2_medium".into(), "3_low".into()],
810-
);
873+
render_enum(&mut lines, enum_name, &values);
874+
let result = lines.join("\n");
811875

812-
// Numeric prefixed values should be prefixed with 'N'
813-
assert!(lines.iter().any(|l| l.contains("N1High")));
814-
assert!(lines.iter().any(|l| l.contains("N2Medium")));
815-
assert!(lines.iter().any(|l| l.contains("N3Low")));
816-
// But the string_value should remain original
817-
assert!(
818-
lines
819-
.iter()
820-
.any(|l| l.contains("string_value = \"1_high\""))
821-
);
876+
with_settings!({ snapshot_suffix => name }, {
877+
insta::assert_snapshot!(result);
878+
});
822879
}
823880

824881
#[test]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: crates/vespertide-exporter/src/seaorm/mod.rs
3+
expression: result
4+
---
5+
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
6+
#[sea_orm(rs_type = "i32", db_type = "Integer")]
7+
pub enum Color {
8+
Black = 0,
9+
White = 1,
10+
Red = 2,
11+
}

0 commit comments

Comments
 (0)