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_-4qn2PLbAm6nN9DB7Ve5P.json
Original file line number Diff line number Diff line change
@@ -0,0 +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"}
1 change: 1 addition & 0 deletions .changepacks/changepack_log_lpT0KZP7pkX1dLIT7rA_M.json
Original file line number Diff line number Diff line change
@@ -0,0 +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"}
20 changes: 10 additions & 10 deletions Cargo.lock

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

212 changes: 152 additions & 60 deletions crates/vespertide-core/src/schema/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,87 +100,101 @@ impl TableDef {
}
}

// Process inline unique and index for each column
// Group columns by unique constraint name to create composite unique constraints
// Use same pattern as index grouping
let mut unique_groups: HashMap<String, Vec<String>> = HashMap::new();
let mut unique_order: Vec<String> = Vec::new(); // Preserve order of first occurrence

for col in &self.columns {
// Handle inline unique
if let Some(ref unique_val) = col.unique {
match unique_val {
StrOrBoolOrArray::Str(name) => {
let constraint_name = Some(name.clone());

// Check if this unique constraint already exists
let exists = constraints.iter().any(|c| {
if let TableConstraint::Unique {
name: c_name,
columns,
} = c
{
c_name.as_ref() == Some(name)
&& columns.len() == 1
&& columns[0] == col.name
} else {
false
}
});
// Named unique constraint - group by name for composite constraints
let unique_name = name.clone();

if !exists {
constraints.push(TableConstraint::Unique {
name: constraint_name,
columns: vec![col.name.clone()],
});
if !unique_groups.contains_key(&unique_name) {
unique_order.push(unique_name.clone());
}

unique_groups
.entry(unique_name)
.or_default()
.push(col.name.clone());
}
StrOrBoolOrArray::Bool(true) => {
let exists = constraints.iter().any(|c| {
if let TableConstraint::Unique {
name: None,
columns,
} = c
{
columns.len() == 1 && columns[0] == col.name
} else {
false
}
});
// Use special marker for auto-generated unique constraints (without custom name)
let group_key = format!("__auto_{}", col.name);

if !exists {
constraints.push(TableConstraint::Unique {
name: None,
columns: vec![col.name.clone()],
});
if !unique_groups.contains_key(&group_key) {
unique_order.push(group_key.clone());
}

unique_groups
.entry(group_key)
.or_default()
.push(col.name.clone());
}
StrOrBoolOrArray::Bool(false) => continue,
StrOrBoolOrArray::Array(names) => {
// Array format: each element is a constraint name
// This column will be part of all these named constraints
for constraint_name in names {
// Check if constraint with this name already exists
if let Some(existing) = constraints.iter_mut().find(|c| {
if let TableConstraint::Unique { name: Some(n), .. } = c {
n == constraint_name
} else {
false
}
}) {
// Add this column to existing composite constraint
if let TableConstraint::Unique { columns, .. } = existing
&& !columns.contains(&col.name)
{
columns.push(col.name.clone());
}
} else {
// Create new constraint with this column
constraints.push(TableConstraint::Unique {
name: Some(constraint_name.clone()),
columns: vec![col.name.clone()],
});
for unique_name in names {
if !unique_groups.contains_key(unique_name.as_str()) {
unique_order.push(unique_name.clone());
}

unique_groups
.entry(unique_name.clone())
.or_default()
.push(col.name.clone());
}
}
}
}
}

// Create unique constraints from grouped columns in order
for unique_name in unique_order {
let columns = unique_groups.get(&unique_name).unwrap().clone();

// Determine if this is an auto-generated unique (from unique: true)
// or a named unique (from unique: "name")
let constraint_name = if unique_name.starts_with("__auto_") {
// Auto-generated unique - use None so SQL generation can create the name
None
} else {
// Named unique - preserve the custom name
Some(unique_name.clone())
};

// Check if this unique constraint already exists
let exists = constraints.iter().any(|c| {
if let TableConstraint::Unique {
name,
columns: cols,
} = c
{
// Match by name if both have names, otherwise match by columns
match (&constraint_name, name) {
(Some(n1), Some(n2)) => n1 == n2,
(None, None) => cols == &columns,
_ => false,
}
} else {
false
}
});

if !exists {
constraints.push(TableConstraint::Unique {
name: constraint_name,
columns,
});
}
}

// Process inline foreign_key and index for each column
for col in &self.columns {
// Handle inline foreign_key
if let Some(ref fk_syntax) = col.foreign_key {
// Convert ForeignKeySyntax to ForeignKeyDef
Expand Down Expand Up @@ -539,6 +553,84 @@ mod tests {
));
}

#[test]
fn normalize_composite_unique_from_string_name() {
// Test that multiple columns with the same unique constraint name
// are grouped into a single composite unique constraint
let mut route_col = col("join_route", ColumnType::Simple(SimpleColumnType::Text));
route_col.unique = Some(StrOrBoolOrArray::Str("route_provider_id".into()));

let mut provider_col = col("provider_id", ColumnType::Simple(SimpleColumnType::Text));
provider_col.unique = Some(StrOrBoolOrArray::Str("route_provider_id".into()));

let table = TableDef {
name: "user".into(),
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
route_col,
provider_col,
],
constraints: vec![],
};

let normalized = table.normalize().unwrap();
assert_eq!(normalized.constraints.len(), 1);
assert!(matches!(
&normalized.constraints[0],
TableConstraint::Unique { name: Some(n), columns }
if n == "route_provider_id"
&& columns == &["join_route".to_string(), "provider_id".to_string()]
));
}

#[test]
fn normalize_unique_name_mismatch_creates_both_constraints() {
// Test coverage for line 181: When an inline unique has a name but existing doesn't (or vice versa),
// they should not match and both constraints should be created
let mut email_col = col("email", ColumnType::Simple(SimpleColumnType::Text));
email_col.unique = Some(StrOrBoolOrArray::Str("named_unique".into()));

let table = TableDef {
name: "user".into(),
columns: vec![
col("id", ColumnType::Simple(SimpleColumnType::Integer)),
email_col,
],
constraints: vec![
// Existing unnamed unique constraint on same column
TableConstraint::Unique {
name: None,
columns: vec!["email".into()],
},
],
};

let normalized = table.normalize().unwrap();

// Should have 2 unique constraints: one named, one unnamed
let unique_constraints: Vec<_> = normalized
.constraints
.iter()
.filter(|c| matches!(c, TableConstraint::Unique { .. }))
.collect();
assert_eq!(
unique_constraints.len(),
2,
"Should keep both named and unnamed unique constraints as they don't match"
);

// Verify we have one named and one unnamed
let has_named = unique_constraints.iter().any(
|c| matches!(c, TableConstraint::Unique { name: Some(n), .. } if n == "named_unique"),
);
let has_unnamed = unique_constraints
.iter()
.any(|c| matches!(c, TableConstraint::Unique { name: None, .. }));

assert!(has_named, "Should have named unique constraint");
assert!(has_unnamed, "Should have unnamed unique constraint");
}

#[test]
fn normalize_inline_index_bool() {
let mut name_col = col("name", ColumnType::Simple(SimpleColumnType::Text));
Expand Down
32 changes: 17 additions & 15 deletions crates/vespertide-exporter/src/seaorm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,28 +204,30 @@ fn format_default_value(value: &str, column_type: &ColumnType) -> String {
ColumnType::Complex(ComplexColumnType::Numeric { .. }) => {
format!("default_value = {}", cleaned)
}
// Enum type: use enum variant format
ColumnType::Complex(ComplexColumnType::Enum { name, values }) => {
let enum_name = to_pascal_case(name);
let variant = match values {
// Enum type: use the actual database value (string or number), not Rust enum variant
ColumnType::Complex(ComplexColumnType::Enum { values, .. }) => {
match values {
EnumValues::String(_) => {
// String enum: cleaned is the string value, convert to PascalCase
to_pascal_case(cleaned)
// String enum: use the string value as-is with quotes
format!("default_value = \"{}\"", cleaned)
}
EnumValues::Integer(int_values) => {
// Integer enum: cleaned is a number, find the matching variant name
// Integer enum: can be either a number or a variant name
// Try to parse as number first
if let Ok(num) = cleaned.parse::<i32>() {
int_values
.iter()
.find(|v| v.value == num)
.map(|v| to_pascal_case(&v.name))
.unwrap_or_else(|| to_pascal_case(cleaned))
// Already a number, use as-is
format!("default_value = {}", num)
} else {
to_pascal_case(cleaned)
// It's a variant name, find the corresponding numeric value
let numeric_value = int_values
.iter()
.find(|v| v.name.eq_ignore_ascii_case(cleaned))
.map(|v| v.value)
.unwrap_or(0); // Default to 0 if not found
format!("default_value = {}", numeric_value)
}
}
};
format!("default_value = {}::{}", enum_name, variant)
}
}
// All other types: use quotes
_ => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub enum TaskStatus {
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(default_value = TaskStatus::InProgress)]
#[sea_orm(default_value = 1)]
pub status: TaskStatus,
}

Expand Down
Loading