Skip to content

Commit 9094a64

Browse files
fix(#1429): address MilagrosMarin review on #1468
- Drop unused `propagated_edges` parameter from `_propagate_part_to_master` and its call sites. The parameter was vestigial after the design switched from edge-blocking to materialization at the master. - Document two limitations in the docstring: - Single FK path: nx.shortest_path returns one path; non-shortest paths are not applied. - Memory cost of materialization: to_arrays() pulls matching master PKs into Python memory. - Add test_cascade_three_level_part_chain covering PartC → PartB → PartA → Master. Confirms intermediate Parts are restricted at every hop, not just the first. All 36 mysql tests in cascade_delete + cascading_delete + dependencies + semantic_matching pass.
1 parent bbbbadf commit 9094a64

2 files changed

Lines changed: 71 additions & 5 deletions

File tree

src/datajoint/diagram.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ def _propagate_restrictions(self, start_node, mode, part_integrity="enforce"):
471471
seed_master = extract_master(start_node)
472472
if seed_master and seed_master in self.nodes() and seed_master not in visited_masters:
473473
visited_masters.add(seed_master)
474-
if self._propagate_part_to_master(start_node, seed_master, mode, restrictions, propagated_edges):
474+
if self._propagate_part_to_master(start_node, seed_master, mode, restrictions):
475475
allowed_nodes.add(seed_master)
476476
allowed_nodes.update(nx.descendants(self, seed_master))
477477

@@ -542,9 +542,7 @@ def _propagate_restrictions(self, start_node, mode, part_integrity="enforce"):
542542
master_name = extract_master(target)
543543
if master_name and master_name in self.nodes() and master_name not in visited_masters:
544544
visited_masters.add(master_name)
545-
propagated = self._propagate_part_to_master(
546-
target, master_name, mode, restrictions, propagated_edges
547-
)
545+
propagated = self._propagate_part_to_master(target, master_name, mode, restrictions)
548546
if propagated:
549547
allowed_nodes.add(master_name)
550548
allowed_nodes.update(nx.descendants(self, master_name))
@@ -660,7 +658,7 @@ def _apply_propagation_rule_upward(self, child_ft, child_attrs, parent_node, att
660658

661659
self._restriction_attrs.setdefault(parent_node, set()).update(parent_attrs)
662660

663-
def _propagate_part_to_master(self, part_node, master_name, mode, restrictions, propagated_edges):
661+
def _propagate_part_to_master(self, part_node, master_name, mode, restrictions):
664662
"""
665663
Walk the FK graph from `part_node` up to `master_name`, applying
666664
`_apply_propagation_rule_upward` at each real edge along the path.
@@ -681,6 +679,23 @@ def _propagate_part_to_master(self, part_node, master_name, mode, restrictions,
681679
Materializing converts the restriction into a static value set, so
682680
the forward cascade generates ``WHERE ... IN (literal-list)`` rather
683681
than ``WHERE ... IN (SELECT ... FROM <part>)``.
682+
683+
Limitations
684+
-----------
685+
- **Single FK path**: ``nx.shortest_path`` returns *one* path from
686+
``master_name`` to ``part_node``. If a Part is reachable from its
687+
Master through multiple distinct FK chains (e.g. references two
688+
different intermediate Parts), restrictions through the
689+
non-shortest paths are not applied. This pattern is unusual; if a
690+
schema hits it, the user is responsible for restricting the
691+
additional paths explicitly via ``part_integrity="ignore"`` plus
692+
manual ``delete()`` calls.
693+
- **Memory cost of materialization**: ``master_ft.proj().to_arrays()``
694+
pulls the matching master primary keys into Python memory. Cost is
695+
bounded by the count of *distinct* master rows referenced by the
696+
matching parts — typically small for surgical cascades, but can
697+
grow with bulk cascades on tables with many master rows. Cascade
698+
*preview* (``Diagram.cascade(...).counts()``) pays the same cost.
684699
"""
685700
try:
686701
path = nx.shortest_path(self, master_name, part_node)

tests/integration/test_cascade_delete.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,57 @@ class PartB(dj.Part):
392392
assert counts.get(Master.PartB.full_table_name, 0) == 1
393393

394394

395+
def test_cascade_three_level_part_chain(schema_by_backend):
396+
"""
397+
Three-hop chain (#1429 follow-up review): PartC → PartB → PartA → Master.
398+
Verify intermediate Parts (PartA, PartB) are restricted at every hop, not
399+
just the first, and the master cascades back down to all siblings.
400+
"""
401+
402+
@schema_by_backend
403+
class Master(dj.Manual):
404+
definition = """
405+
master_id : int32
406+
"""
407+
408+
class PartA(dj.Part):
409+
definition = """
410+
-> master
411+
part_a_id : int32
412+
"""
413+
414+
class PartB(dj.Part):
415+
definition = """
416+
-> Master.PartA
417+
part_b_id : int32
418+
"""
419+
420+
class PartC(dj.Part):
421+
definition = """
422+
-> Master.PartB
423+
part_c_id : int32
424+
"""
425+
426+
Master.insert([(1,), (2,)])
427+
Master.PartA.insert([(1, 10), (1, 11), (2, 20)])
428+
Master.PartB.insert([(1, 10, 100), (1, 11, 110), (2, 20, 200)])
429+
Master.PartC.insert([(1, 10, 100, 1000), (1, 11, 110, 1100), (2, 20, 200, 2000)])
430+
431+
counts = dj.Diagram.cascade(
432+
Master.PartC & {"master_id": 1, "part_a_id": 10, "part_b_id": 100, "part_c_id": 1000},
433+
part_integrity="cascade",
434+
).counts()
435+
436+
# Master pulled in via the 3-hop upward walk
437+
assert counts.get(Master.full_table_name, 0) == 1, (
438+
"Master restriction lost across 3-hop chain — the per-edge upward walk " "did not reach Master through PartA + PartB."
439+
)
440+
# Master forward-cascades back down to all rows under master_id=1
441+
assert counts.get(Master.PartA.full_table_name, 0) == 2 # both PartA rows under master 1
442+
assert counts.get(Master.PartB.full_table_name, 0) == 2 # both PartB rows under master 1
443+
assert counts.get(Master.PartC.full_table_name, 0) == 2 # both PartC rows under master 1
444+
445+
395446
def test_cascade_part_of_part_actual_delete(schema_by_backend):
396447
"""
397448
End-to-end: actually run delete() with part_integrity="cascade" through

0 commit comments

Comments
 (0)