@@ -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