Skip to content

Commit 45a58be

Browse files
committed
Rm duplicate on planner
1 parent e6c09b5 commit 45a58be

File tree

3 files changed

+242
-15
lines changed

3 files changed

+242
-15
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespertide-config/Cargo.toml":"Patch","crates/vespertide-cli/Cargo.toml":"Patch","crates/vespertide-macro/Cargo.toml":"Patch","crates/vespertide/Cargo.toml":"Patch","crates/vespertide-exporter/Cargo.toml":"Patch","crates/vespertide-core/Cargo.toml":"Patch","crates/vespertide-planner/Cargo.toml":"Patch","crates/vespertide-loader/Cargo.toml":"Patch","crates/vespertide-query/Cargo.toml":"Patch"},"note":"Rm duplicate","date":"2025-12-18T16:26:32.685458600Z"}

Cargo.lock

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

crates/vespertide-planner/src/diff.rs

Lines changed: 232 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@ fn topological_sort_tables<'a>(tables: &[&'a TableDef]) -> Result<Vec<&'a TableD
1717

1818
// Build adjacency list: for each table, list the tables it depends on (via FK)
1919
// Use BTreeMap for consistent ordering
20+
// Use BTreeSet to avoid duplicate dependencies (e.g., multiple FKs referencing the same table)
2021
let mut dependencies: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
2122
for table in tables {
22-
let mut deps = Vec::new();
23+
let mut deps_set: BTreeSet<&str> = BTreeSet::new();
2324
for constraint in &table.constraints {
2425
if let TableConstraint::ForeignKey { ref_table, .. } = constraint {
2526
// Only consider dependencies within the set of tables being created
2627
if table_names.contains(ref_table.as_str()) && ref_table != &table.name {
27-
deps.push(ref_table.as_str());
28+
deps_set.insert(ref_table.as_str());
2829
}
2930
}
3031
}
31-
dependencies.insert(table.name.as_str(), deps);
32+
dependencies.insert(table.name.as_str(), deps_set.into_iter().collect());
3233
}
3334

3435
// Kahn's algorithm for topological sort
@@ -140,20 +141,21 @@ fn sort_delete_tables(actions: &mut [MigrationAction], all_tables: &BTreeMap<&st
140141
// Build dependency graph for tables being deleted
141142
// dependencies[A] = [B] means A has FK referencing B
142143
// Use BTreeMap for consistent ordering
144+
// Use BTreeSet to avoid duplicate dependencies (e.g., multiple FKs referencing the same table)
143145
let mut dependencies: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
144146
for &table_name in &delete_table_names {
145-
let mut deps = Vec::new();
147+
let mut deps_set: BTreeSet<&str> = BTreeSet::new();
146148
if let Some(table_def) = all_tables.get(table_name) {
147149
for constraint in &table_def.constraints {
148150
if let TableConstraint::ForeignKey { ref_table, .. } = constraint
149151
&& delete_table_names.contains(ref_table.as_str())
150152
&& ref_table != table_name
151153
{
152-
deps.push(ref_table.as_str());
154+
deps_set.insert(ref_table.as_str());
153155
}
154156
}
155157
}
156-
dependencies.insert(table_name, deps);
158+
dependencies.insert(table_name, deps_set.into_iter().collect());
157159
}
158160

159161
// Use Kahn's algorithm for topological sort
@@ -1501,5 +1503,229 @@ mod tests {
15011503
// This should panic
15021504
extract_delete_table_name(&action);
15031505
}
1506+
1507+
/// Test that inline FK across multiple tables works correctly with topological sort
1508+
#[test]
1509+
fn create_tables_with_inline_fk_chain() {
1510+
use super::*;
1511+
use vespertide_core::schema::foreign_key::ForeignKeySyntax;
1512+
use vespertide_core::schema::primary_key::PrimaryKeySyntax;
1513+
1514+
fn col_pk(name: &str) -> ColumnDef {
1515+
ColumnDef {
1516+
name: name.to_string(),
1517+
r#type: ColumnType::Simple(SimpleColumnType::Integer),
1518+
nullable: false,
1519+
default: None,
1520+
comment: None,
1521+
primary_key: Some(PrimaryKeySyntax::Bool(true)),
1522+
unique: None,
1523+
index: None,
1524+
foreign_key: None,
1525+
}
1526+
}
1527+
1528+
fn col_inline_fk(name: &str, ref_table: &str) -> ColumnDef {
1529+
ColumnDef {
1530+
name: name.to_string(),
1531+
r#type: ColumnType::Simple(SimpleColumnType::Integer),
1532+
nullable: true,
1533+
default: None,
1534+
comment: None,
1535+
primary_key: None,
1536+
unique: None,
1537+
index: None,
1538+
foreign_key: Some(ForeignKeySyntax::String(format!("{}.id", ref_table))),
1539+
}
1540+
}
1541+
1542+
// Reproduce the app example structure:
1543+
// user -> (no deps)
1544+
// product -> (no deps)
1545+
// project -> user
1546+
// code -> product, user, project
1547+
// order -> user, project, product, code
1548+
// payment -> order
1549+
1550+
let user = TableDef {
1551+
name: "user".to_string(),
1552+
columns: vec![col_pk("id")],
1553+
constraints: vec![],
1554+
indexes: vec![],
1555+
};
1556+
1557+
let product = TableDef {
1558+
name: "product".to_string(),
1559+
columns: vec![col_pk("id")],
1560+
constraints: vec![],
1561+
indexes: vec![],
1562+
};
1563+
1564+
let project = TableDef {
1565+
name: "project".to_string(),
1566+
columns: vec![col_pk("id"), col_inline_fk("user_id", "user")],
1567+
constraints: vec![],
1568+
indexes: vec![],
1569+
};
1570+
1571+
let code = TableDef {
1572+
name: "code".to_string(),
1573+
columns: vec![
1574+
col_pk("id"),
1575+
col_inline_fk("product_id", "product"),
1576+
col_inline_fk("creator_user_id", "user"),
1577+
col_inline_fk("project_id", "project"),
1578+
],
1579+
constraints: vec![],
1580+
indexes: vec![],
1581+
};
1582+
1583+
let order = TableDef {
1584+
name: "order".to_string(),
1585+
columns: vec![
1586+
col_pk("id"),
1587+
col_inline_fk("user_id", "user"),
1588+
col_inline_fk("project_id", "project"),
1589+
col_inline_fk("product_id", "product"),
1590+
col_inline_fk("code_id", "code"),
1591+
],
1592+
constraints: vec![],
1593+
indexes: vec![],
1594+
};
1595+
1596+
let payment = TableDef {
1597+
name: "payment".to_string(),
1598+
columns: vec![col_pk("id"), col_inline_fk("order_id", "order")],
1599+
constraints: vec![],
1600+
indexes: vec![],
1601+
};
1602+
1603+
// Pass in arbitrary order - should NOT return circular dependency error
1604+
let result = diff_schemas(&[], &[payment, order, code, project, product, user]);
1605+
assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
1606+
1607+
let plan = result.unwrap();
1608+
let create_order: Vec<&str> = plan
1609+
.actions
1610+
.iter()
1611+
.filter_map(|a| {
1612+
if let MigrationAction::CreateTable { table, .. } = a {
1613+
Some(table.as_str())
1614+
} else {
1615+
None
1616+
}
1617+
})
1618+
.collect();
1619+
1620+
// Verify order respects FK dependencies
1621+
let get_pos = |name: &str| create_order.iter().position(|&t| t == name).unwrap();
1622+
1623+
// user and product have no deps, can be in any order
1624+
// project depends on user
1625+
assert!(
1626+
get_pos("user") < get_pos("project"),
1627+
"user must come before project"
1628+
);
1629+
// code depends on product, user, project
1630+
assert!(
1631+
get_pos("product") < get_pos("code"),
1632+
"product must come before code"
1633+
);
1634+
assert!(
1635+
get_pos("user") < get_pos("code"),
1636+
"user must come before code"
1637+
);
1638+
assert!(
1639+
get_pos("project") < get_pos("code"),
1640+
"project must come before code"
1641+
);
1642+
// order depends on user, project, product, code
1643+
assert!(
1644+
get_pos("code") < get_pos("order"),
1645+
"code must come before order"
1646+
);
1647+
// payment depends on order
1648+
assert!(
1649+
get_pos("order") < get_pos("payment"),
1650+
"order must come before payment"
1651+
);
1652+
}
1653+
1654+
/// Test that multiple FKs to the same table are deduplicated correctly
1655+
#[test]
1656+
fn create_tables_with_duplicate_fk_references() {
1657+
use super::*;
1658+
use vespertide_core::schema::foreign_key::ForeignKeySyntax;
1659+
use vespertide_core::schema::primary_key::PrimaryKeySyntax;
1660+
1661+
fn col_pk(name: &str) -> ColumnDef {
1662+
ColumnDef {
1663+
name: name.to_string(),
1664+
r#type: ColumnType::Simple(SimpleColumnType::Integer),
1665+
nullable: false,
1666+
default: None,
1667+
comment: None,
1668+
primary_key: Some(PrimaryKeySyntax::Bool(true)),
1669+
unique: None,
1670+
index: None,
1671+
foreign_key: None,
1672+
}
1673+
}
1674+
1675+
fn col_inline_fk(name: &str, ref_table: &str) -> ColumnDef {
1676+
ColumnDef {
1677+
name: name.to_string(),
1678+
r#type: ColumnType::Simple(SimpleColumnType::Integer),
1679+
nullable: true,
1680+
default: None,
1681+
comment: None,
1682+
primary_key: None,
1683+
unique: None,
1684+
index: None,
1685+
foreign_key: Some(ForeignKeySyntax::String(format!("{}.id", ref_table))),
1686+
}
1687+
}
1688+
1689+
// Table with multiple FKs referencing the same table (like code.creator_user_id and code.used_by_user_id)
1690+
let user = TableDef {
1691+
name: "user".to_string(),
1692+
columns: vec![col_pk("id")],
1693+
constraints: vec![],
1694+
indexes: vec![],
1695+
};
1696+
1697+
let code = TableDef {
1698+
name: "code".to_string(),
1699+
columns: vec![
1700+
col_pk("id"),
1701+
col_inline_fk("creator_user_id", "user"),
1702+
col_inline_fk("used_by_user_id", "user"), // Second FK to same table
1703+
],
1704+
constraints: vec![],
1705+
indexes: vec![],
1706+
};
1707+
1708+
// This should NOT return circular dependency error even with duplicate FK refs
1709+
let result = diff_schemas(&[], &[code, user]);
1710+
assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
1711+
1712+
let plan = result.unwrap();
1713+
let create_order: Vec<&str> = plan
1714+
.actions
1715+
.iter()
1716+
.filter_map(|a| {
1717+
if let MigrationAction::CreateTable { table, .. } = a {
1718+
Some(table.as_str())
1719+
} else {
1720+
None
1721+
}
1722+
})
1723+
.collect();
1724+
1725+
// user must come before code
1726+
let user_pos = create_order.iter().position(|&t| t == "user").unwrap();
1727+
let code_pos = create_order.iter().position(|&t| t == "code").unwrap();
1728+
assert!(user_pos < code_pos, "user must come before code");
1729+
}
15041730
}
15051731
}

0 commit comments

Comments
 (0)