Skip to content

Commit f14d68b

Browse files
fix(cascade): restore the walked parts' attributes; test empty and traced Top seeds
The part-to-master walk saves the restrictions of the parts it walks through and restores them once the master is materialized. It saved each part's restricted-attribute set by reference, and the walk then extends that same set in place, so the restore changed nothing. The extra attributes only made later copies stricter, so the effect was an extra subquery, but the code did not do what its comment says. It now saves a copy of the set. Two tests pin down seeds this PR changes: a `Top` over a restriction that matches nothing previews no rows and deletes nothing, and `Diagram.trace` starts from the rows a `Top` seed selects.
1 parent 18e5649 commit f14d68b

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

‎src/datajoint/diagram.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1080,8 +1080,12 @@ def _propagate_part_to_master(self, part_node, master_name, mode, restrictions):
10801080
# the master is materialized. The master then cascades back down to
10811081
# all of its parts.
10821082
intermediates = real_path[1:-1]
1083+
# Copy each node's attribute set too: the walk below extends it in place.
10831084
saved = {
1084-
node: (copy.copy(restrictions[node]) if node in restrictions else None, self._restriction_attrs.get(node))
1085+
node: (
1086+
copy.copy(restrictions[node]) if node in restrictions else None,
1087+
set(self._restriction_attrs[node]) if node in self._restriction_attrs else None,
1088+
)
10851089
for node in intermediates
10861090
}
10871091

‎tests/integration/test_cascade_overdelete.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,60 @@ class Child(dj.Manual):
7575
assert sorted(Child.to_arrays("item_id")) == [3, 4]
7676

7777

78+
def test_top_seed_that_selects_no_rows_deletes_nothing(schema_by_backend):
79+
"""A ``Top`` over a restriction that matches nothing seeds an empty cascade:
80+
the preview counts nothing and the delete removes nothing."""
81+
82+
@schema_by_backend
83+
class Item(dj.Manual):
84+
definition = """
85+
item_id : int32
86+
"""
87+
88+
@schema_by_backend
89+
class Child(dj.Manual):
90+
definition = """
91+
-> Item
92+
child_id : int32
93+
"""
94+
95+
Item.insert([(i,) for i in range(3)])
96+
Child.insert([(i, 0) for i in range(3)])
97+
98+
seed = Item & "item_id > 100" & dj.Top(2, order_by="item_id")
99+
counts = dj.Diagram.cascade(seed).counts()
100+
assert counts[Item.full_table_name] == 0
101+
assert counts[Child.full_table_name] == 0
102+
103+
seed.delete(prompt=False)
104+
assert len(Item()) == 3
105+
assert len(Child()) == 3
106+
107+
108+
def test_top_seed_in_trace(schema_by_backend):
109+
"""``trace`` also starts from the rows a ``Top`` selects."""
110+
111+
@schema_by_backend
112+
class Item(dj.Manual):
113+
definition = """
114+
item_id : int32
115+
"""
116+
117+
@schema_by_backend
118+
class Child(dj.Manual):
119+
definition = """
120+
-> Item
121+
child_id : int32
122+
"""
123+
124+
Item.insert([(i,) for i in range(5)])
125+
Child.insert([(i, 0) for i in range(5)])
126+
127+
traced = dj.Diagram.trace(Child & dj.Top(2, order_by="item_id"))
128+
assert traced.counts()[Child.full_table_name] == 2
129+
assert traced.counts()[Item.full_table_name] == 2
130+
131+
78132
def test_nullable_fk_sharing_a_primary_key_column(schema_by_backend):
79133
"""A child whose nullable foreign key shares a primary-key column with the
80134
parent keeps the rows whose foreign key is NULL: they reference nothing."""

0 commit comments

Comments
 (0)