… cycle
A part that references a table which already depends on the part's own
master makes the master-part group depend on itself. topo_sort orders each
master after every table its parts reference, so it could no longer order
such tables, and every delete, drop, Diagram.trace and Schema.list_tables
on the connection failed with networkx.NetworkXUnfeasible, including
dropping the offending part.
- Part._declare_check refuses such a reference before any DDL runs, with
an error that names the part, the referenced table and the cycle. A new
table has no children, so only a part's reference to a table outside its
group can close a cycle, and this check refuses exactly those.
- topo_sort raises a DataJointError that names an existing cycle and the
part that closes it, and takes an optional set of tables to sort. Cascade
and trace propagation, Diagram iteration and Schema.list_tables sort only
their own tables, so an existing cycle affects only the operations that
must order it, and the offending part can be dropped.
- A part that does not inherit its master's primary key, unrenamed and
non-nullable, through a reference to the master or a sibling part gets a
warning at declaration.
- declare() reports every foreign key through a new foreign_keys argument,
which the _declare_check hook now receives.
Closes #1619.
Owned parts
This PR decides which parts belong to their master by a rule instead of by the table name.
A part is owned by its master when its primary key includes a reference to the master, either directly or through another part the master owns. The reference may rename the key attributes. For example:
An owned part and its master form one group: a master row and its owned part rows are one entity. A part that is not owned is ordered as a separate table, like any other table that references the master.
What changes
Declaration refuses a group cycle. Part._declare_check runs after the definition is parsed and before any DDL. It finds out whether the new part is owned. For an owned part, it checks each reference to a table outside the group. If that table already depends on the master, the declaration is refused. Parts the master does not own count as outside the group. For the reproduction in the issue:
When the referenced table is a part that the master does not own, the message says so and explains why.
topo_sort uses owned parts. Only owned parts are kept right after their masters, and a master is ordered after the tables its owned parts reference. Other parts are sorted as separate tables. An existing cycle now raises a DataJointError that prints the cycle and names the part that closes it. It also says how to recover: drop that part with drop(part_integrity='ignore') and declare it without the reference. Before, the error was networkx.exception.NetworkXUnfeasible.
Only the needed tables are sorted. topo_sort(graph, nodes) takes an optional set of tables. It sorts those tables together with the tables on dependency paths between them, so their order still respects every dependency between them, and it returns only the requested tables. Ownership is judged on the whole graph. The callers now pass their own tables:
With an existing cycle, only the operations that must order it now fail. Operations on other tables keep working, and so does dropping the part that closes the cycle.
Warning for a part that is not owned. It is raised at declaration and says that DataJoint sorts the part as a separate table, and that from DataJoint 2.4 delete and trace will also treat it as one. It also says how to make the part owned.
Supporting changes
Why the check is exact
With owned parts as groups and every other table on its own, topo_sort succeeds exactly when the groups do not depend on each other in a cycle. A new table has no children, so it cannot be on a cycle itself. A plain table, a master, or a part that is not owned adds no ordering constraint beyond its own references, so it can never close a cycle. That includes a master declared after a part of its own that does not reference it.
An owned part adds one constraint: its master must come after each table outside the group that the part references. A cycle forms exactly when such a table X already depends on the master. "Depends on" follows references upward and, at a master, continues through the references of its owned parts. group_cycle tests exactly this.
Changes for existing schemas
Why it is safe
Tests
tests/unit/test_group_cycle.py (no database):
Two randomized checks run on random dependency graphs:
tests/integration/test_group_cycle.py (MySQL and PostgreSQL):
Related suites (cascade, trace, diagram, declare, dependencies, schema, alter, foreign keys, virtual modules, autopopulate, unit): 670 passed, 2 skipped.
Full suite: 1218 passed, 14 skipped. On master the suite gives 1158 passed and 14 skipped; the difference is the 60 new tests.
Docs: the master-part specification (datajoint/datajoint-docs#299) can state the ownership rule and the cycle rule.