Skip to content

Commit 281e390

Browse files
authored
Merge pull request #34 from dev-five-git/unique-issue
Unique issue
2 parents cb09317 + 5828dcf commit 281e390

File tree

37 files changed

+745
-317
lines changed

37 files changed

+745
-317
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch"},"note":"Fix multiple unique","date":"2025-12-19T08:56:40.862336800Z"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-naming/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch"},"note":"Fix enum export issue","date":"2025-12-19T08:56:26.871529700Z"}

Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 152 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -100,87 +100,101 @@ impl TableDef {
100100
}
101101
}
102102

103-
// Process inline unique and index for each column
103+
// Group columns by unique constraint name to create composite unique constraints
104+
// Use same pattern as index grouping
105+
let mut unique_groups: HashMap<String, Vec<String>> = HashMap::new();
106+
let mut unique_order: Vec<String> = Vec::new(); // Preserve order of first occurrence
107+
104108
for col in &self.columns {
105-
// Handle inline unique
106109
if let Some(ref unique_val) = col.unique {
107110
match unique_val {
108111
StrOrBoolOrArray::Str(name) => {
109-
let constraint_name = Some(name.clone());
110-
111-
// Check if this unique constraint already exists
112-
let exists = constraints.iter().any(|c| {
113-
if let TableConstraint::Unique {
114-
name: c_name,
115-
columns,
116-
} = c
117-
{
118-
c_name.as_ref() == Some(name)
119-
&& columns.len() == 1
120-
&& columns[0] == col.name
121-
} else {
122-
false
123-
}
124-
});
112+
// Named unique constraint - group by name for composite constraints
113+
let unique_name = name.clone();
125114

126-
if !exists {
127-
constraints.push(TableConstraint::Unique {
128-
name: constraint_name,
129-
columns: vec![col.name.clone()],
130-
});
115+
if !unique_groups.contains_key(&unique_name) {
116+
unique_order.push(unique_name.clone());
131117
}
118+
119+
unique_groups
120+
.entry(unique_name)
121+
.or_default()
122+
.push(col.name.clone());
132123
}
133124
StrOrBoolOrArray::Bool(true) => {
134-
let exists = constraints.iter().any(|c| {
135-
if let TableConstraint::Unique {
136-
name: None,
137-
columns,
138-
} = c
139-
{
140-
columns.len() == 1 && columns[0] == col.name
141-
} else {
142-
false
143-
}
144-
});
125+
// Use special marker for auto-generated unique constraints (without custom name)
126+
let group_key = format!("__auto_{}", col.name);
145127

146-
if !exists {
147-
constraints.push(TableConstraint::Unique {
148-
name: None,
149-
columns: vec![col.name.clone()],
150-
});
128+
if !unique_groups.contains_key(&group_key) {
129+
unique_order.push(group_key.clone());
151130
}
131+
132+
unique_groups
133+
.entry(group_key)
134+
.or_default()
135+
.push(col.name.clone());
152136
}
153137
StrOrBoolOrArray::Bool(false) => continue,
154138
StrOrBoolOrArray::Array(names) => {
155139
// Array format: each element is a constraint name
156140
// This column will be part of all these named constraints
157-
for constraint_name in names {
158-
// Check if constraint with this name already exists
159-
if let Some(existing) = constraints.iter_mut().find(|c| {
160-
if let TableConstraint::Unique { name: Some(n), .. } = c {
161-
n == constraint_name
162-
} else {
163-
false
164-
}
165-
}) {
166-
// Add this column to existing composite constraint
167-
if let TableConstraint::Unique { columns, .. } = existing
168-
&& !columns.contains(&col.name)
169-
{
170-
columns.push(col.name.clone());
171-
}
172-
} else {
173-
// Create new constraint with this column
174-
constraints.push(TableConstraint::Unique {
175-
name: Some(constraint_name.clone()),
176-
columns: vec![col.name.clone()],
177-
});
141+
for unique_name in names {
142+
if !unique_groups.contains_key(unique_name.as_str()) {
143+
unique_order.push(unique_name.clone());
178144
}
145+
146+
unique_groups
147+
.entry(unique_name.clone())
148+
.or_default()
149+
.push(col.name.clone());
179150
}
180151
}
181152
}
182153
}
154+
}
155+
156+
// Create unique constraints from grouped columns in order
157+
for unique_name in unique_order {
158+
let columns = unique_groups.get(&unique_name).unwrap().clone();
159+
160+
// Determine if this is an auto-generated unique (from unique: true)
161+
// or a named unique (from unique: "name")
162+
let constraint_name = if unique_name.starts_with("__auto_") {
163+
// Auto-generated unique - use None so SQL generation can create the name
164+
None
165+
} else {
166+
// Named unique - preserve the custom name
167+
Some(unique_name.clone())
168+
};
169+
170+
// Check if this unique constraint already exists
171+
let exists = constraints.iter().any(|c| {
172+
if let TableConstraint::Unique {
173+
name,
174+
columns: cols,
175+
} = c
176+
{
177+
// Match by name if both have names, otherwise match by columns
178+
match (&constraint_name, name) {
179+
(Some(n1), Some(n2)) => n1 == n2,
180+
(None, None) => cols == &columns,
181+
_ => false,
182+
}
183+
} else {
184+
false
185+
}
186+
});
187+
188+
if !exists {
189+
constraints.push(TableConstraint::Unique {
190+
name: constraint_name,
191+
columns,
192+
});
193+
}
194+
}
183195

196+
// Process inline foreign_key and index for each column
197+
for col in &self.columns {
184198
// Handle inline foreign_key
185199
if let Some(ref fk_syntax) = col.foreign_key {
186200
// Convert ForeignKeySyntax to ForeignKeyDef
@@ -539,6 +553,84 @@ mod tests {
539553
));
540554
}
541555

556+
#[test]
557+
fn normalize_composite_unique_from_string_name() {
558+
// Test that multiple columns with the same unique constraint name
559+
// are grouped into a single composite unique constraint
560+
let mut route_col = col("join_route", ColumnType::Simple(SimpleColumnType::Text));
561+
route_col.unique = Some(StrOrBoolOrArray::Str("route_provider_id".into()));
562+
563+
let mut provider_col = col("provider_id", ColumnType::Simple(SimpleColumnType::Text));
564+
provider_col.unique = Some(StrOrBoolOrArray::Str("route_provider_id".into()));
565+
566+
let table = TableDef {
567+
name: "user".into(),
568+
columns: vec![
569+
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
570+
route_col,
571+
provider_col,
572+
],
573+
constraints: vec![],
574+
};
575+
576+
let normalized = table.normalize().unwrap();
577+
assert_eq!(normalized.constraints.len(), 1);
578+
assert!(matches!(
579+
&normalized.constraints[0],
580+
TableConstraint::Unique { name: Some(n), columns }
581+
if n == "route_provider_id"
582+
&& columns == &["join_route".to_string(), "provider_id".to_string()]
583+
));
584+
}
585+
586+
#[test]
587+
fn normalize_unique_name_mismatch_creates_both_constraints() {
588+
// Test coverage for line 181: When an inline unique has a name but existing doesn't (or vice versa),
589+
// they should not match and both constraints should be created
590+
let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
591+
email_col.unique = Some(StrOrBoolOrArray::Str("named_unique".into()));
592+
593+
let table = TableDef {
594+
name: "user".into(),
595+
columns: vec![
596+
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
597+
email_col,
598+
],
599+
constraints: vec![
600+
// Existing unnamed unique constraint on same column
601+
TableConstraint::Unique {
602+
name: None,
603+
columns: vec!["email".into()],
604+
},
605+
],
606+
};
607+
608+
let normalized = table.normalize().unwrap();
609+
610+
// Should have 2 unique constraints: one named, one unnamed
611+
let unique_constraints: Vec<_> = normalized
612+
.constraints
613+
.iter()
614+
.filter(|c| matches!(c, TableConstraint::Unique { .. }))
615+
.collect();
616+
assert_eq!(
617+
unique_constraints.len(),
618+
2,
619+
"Should keep both named and unnamed unique constraints as they don't match"
620+
);
621+
622+
// Verify we have one named and one unnamed
623+
let has_named = unique_constraints.iter().any(
624+
|c| matches!(c, TableConstraint::Unique { name: Some(n), .. } if n == "named_unique"),
625+
);
626+
let has_unnamed = unique_constraints
627+
.iter()
628+
.any(|c| matches!(c, TableConstraint::Unique { name: None, .. }));
629+
630+
assert!(has_named, "Should have named unique constraint");
631+
assert!(has_unnamed, "Should have unnamed unique constraint");
632+
}
633+
542634
#[test]
543635
fn normalize_inline_index_bool() {
544636
let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));

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

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -204,28 +204,30 @@ fn format_default_value(value: &str, column_type: &ColumnType) -> String {
204204
ColumnType::Complex(ComplexColumnType::Numeric { .. }) => {
205205
format!("default_value = {}", cleaned)
206206
}
207-
// Enum type: use enum variant format
208-
ColumnType::Complex(ComplexColumnType::Enum { name, values }) => {
209-
let enum_name = to_pascal_case(name);
210-
let variant = match values {
207+
// Enum type: use the actual database value (string or number), not Rust enum variant
208+
ColumnType::Complex(ComplexColumnType::Enum { values, .. }) => {
209+
match values {
211210
EnumValues::String(_) => {
212-
// String enum: cleaned is the string value, convert to PascalCase
213-
to_pascal_case(cleaned)
211+
// String enum: use the string value as-is with quotes
212+
format!("default_value = \"{}\"", cleaned)
214213
}
215214
EnumValues::Integer(int_values) => {
216-
// Integer enum: cleaned is a number, find the matching variant name
215+
// Integer enum: can be either a number or a variant name
216+
// Try to parse as number first
217217
if let Ok(num) = cleaned.parse::<i32>() {
218-
int_values
219-
.iter()
220-
.find(|v| v.value == num)
221-
.map(|v| to_pascal_case(&v.name))
222-
.unwrap_or_else(|| to_pascal_case(cleaned))
218+
// Already a number, use as-is
219+
format!("default_value = {}", num)
223220
} else {
224-
to_pascal_case(cleaned)
221+
// It's a variant name, find the corresponding numeric value
222+
let numeric_value = int_values
223+
.iter()
224+
.find(|v| v.name.eq_ignore_ascii_case(cleaned))
225+
.map(|v| v.value)
226+
.unwrap_or(0); // Default to 0 if not found
227+
format!("default_value = {}", numeric_value)
225228
}
226229
}
227-
};
228-
format!("default_value = {}::{}", enum_name, variant)
230+
}
229231
}
230232
// All other types: use quotes
231233
_ => {

crates/vespertide-exporter/src/seaorm/snapshots/vespertide_exporter__seaorm__tests__integer_enum_default_value_snapshots@1.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub enum TaskStatus {
1919
pub struct Model {
2020
#[sea_orm(primary_key)]
2121
pub id: i32,
22-
#[sea_orm(default_value = TaskStatus::InProgress)]
22+
#[sea_orm(default_value = 1)]
2323
pub status: TaskStatus,
2424
}
2525

0 commit comments

Comments
 (0)