Skip to content

Commit 5a6077f

Browse files
authored
Merge pull request #1158 from CBroz1/master
Allow including master table in cascading delete
2 parents 701bb0b + c6083a2 commit 5a6077f

File tree

8 files changed

+108
-19
lines changed

8 files changed

+108
-19
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
- Added - Missing tests for set_password - PR [#1106](https://github.com/datajoint/datajoint-python/pull/1106)
1212
- Changed - Returning success count after the .populate() call - PR [#1050](https://github.com/datajoint/datajoint-python/pull/1050)
1313
- Fixed - `Autopopulate.populate` excludes `reserved` jobs in addition to `ignore` and `error` jobs
14-
- Fixed - Issue [#1159]((https://github.com/datajoint/datajoint-python/pull/1159) (cascading delete) - PR [#1160](https://github.com/datajoint/datajoint-python/pull/1160)
14+
- Fixed - Issue [#1159](https://github.com/datajoint/datajoint-python/pull/1159) (cascading delete) - PR [#1160](https://github.com/datajoint/datajoint-python/pull/1160)
1515
- Changed - Minimum Python version for Datajoint-Python is now 3.8 PR #1163
1616
- Fixed - `docker compose` commands in CI [#1164](https://github.com/datajoint/datajoint-python/pull/1164)
17+
- Changed - Default delete behavior now includes masters of part tables - PR [#1158](https://github.com/datajoint/datajoint-python/pull/1158)
1718

1819
### 0.14.1 -- Jun 02, 2023
1920
- Fixed - Fix altering a part table that uses the "master" keyword - PR [#991](https://github.com/datajoint/datajoint-python/pull/991)

datajoint/table.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ def delete(
486486
transaction: bool = True,
487487
safemode: Union[bool, None] = None,
488488
force_parts: bool = False,
489+
force_masters: bool = False,
489490
) -> int:
490491
"""
491492
Deletes the contents of the table and its dependent tables, recursively.
@@ -497,6 +498,8 @@ def delete(
497498
safemode: If `True`, prohibit nested transactions and prompt to confirm. Default
498499
is `dj.config['safemode']`.
499500
force_parts: Delete from parts even when not deleting from their masters.
501+
force_masters: If `True`, include part/master pairs in the cascade.
502+
Default is `False`.
500503
501504
Returns:
502505
Number of deleted rows (excluding those from dependent tables).
@@ -507,6 +510,7 @@ def delete(
507510
DataJointError: Deleting a part table before its master.
508511
"""
509512
deleted = set()
513+
visited_masters = set()
510514

511515
def cascade(table):
512516
"""service function to perform cascading deletes recursively."""
@@ -566,7 +570,27 @@ def cascade(table):
566570
)
567571
else:
568572
child &= table.proj()
569-
cascade(child)
573+
574+
master_name = get_master(child.full_table_name)
575+
if (
576+
force_masters
577+
and master_name
578+
and master_name != table.full_table_name
579+
and master_name not in visited_masters
580+
):
581+
master = FreeTable(table.connection, master_name)
582+
master._restriction_attributes = set()
583+
master._restriction = [
584+
make_condition( # &= may cause in target tables in subquery
585+
master,
586+
(master.proj() & child.proj()).fetch(),
587+
master._restriction_attributes,
588+
)
589+
]
590+
visited_masters.add(master_name)
591+
cascade(master)
592+
else:
593+
cascade(child)
570594
else:
571595
deleted.add(table.full_table_name)
572596
logger.info(

docs/src/manipulation/delete.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ consequence of deleting the master table.
2727

2828
To enforce this workflow, calling `delete` directly on a part table produces an error.
2929
In some cases, it may be necessary to override this behavior.
30-
To remove entities from a part table without calling `delete` master, use the argument `force=True`.
30+
To remove entities from a part table without calling `delete` master, use the argument `force_parts=True`.
31+
To include the corresponding entries in the master table, use the argument `force_masters=True`.

tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ def schema_simp(connection_test, prefix):
339339
schema(schema_simple.E)
340340
schema(schema_simple.F)
341341
schema(schema_simple.F)
342+
schema(schema_simple.G)
342343
schema(schema_simple.DataA)
343344
schema(schema_simple.DataB)
344345
schema(schema_simple.Website)

tests/schema_simple.py

+44-6
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,46 @@ class F(dj.Part):
111111
-> B.C
112112
"""
113113

114+
class G(dj.Part):
115+
definition = """ # test secondary fk reference
116+
-> E
117+
id_g :int
118+
---
119+
-> L
120+
"""
121+
122+
class H(dj.Part):
123+
definition = """ # test no additional fk reference
124+
-> E
125+
id_h :int
126+
"""
127+
128+
class M(dj.Part):
129+
definition = """ # test force_masters revisit
130+
-> E
131+
id_m :int
132+
---
133+
-> E.H
134+
"""
135+
114136
def make(self, key):
115137
random.seed(str(key))
116-
self.insert1(dict(key, **random.choice(list(L().fetch("KEY")))))
117-
sub = E.F()
118-
references = list((B.C() & key).fetch("KEY"))
119-
random.shuffle(references)
120-
sub.insert(
138+
l_contents = list(L().fetch("KEY"))
139+
part_f, part_g, part_h, part_m = E.F(), E.G(), E.H(), E.M()
140+
bc_references = list((B.C() & key).fetch("KEY"))
141+
random.shuffle(bc_references)
142+
143+
self.insert1(dict(key, **random.choice(l_contents)))
144+
part_f.insert(
121145
dict(key, id_f=i, **ref)
122-
for i, ref in enumerate(references)
146+
for i, ref in enumerate(bc_references)
123147
if random.getrandbits(1)
124148
)
149+
g_inserts = [dict(key, id_g=i, **ref) for i, ref in enumerate(l_contents)]
150+
part_g.insert(g_inserts)
151+
h_inserts = [dict(key, id_h=i) for i in range(4)]
152+
part_h.insert(h_inserts)
153+
part_m.insert(dict(key, id_m=m, **random.choice(h_inserts)) for m in range(4))
125154

126155

127156
class F(dj.Manual):
@@ -132,6 +161,15 @@ class F(dj.Manual):
132161
"""
133162

134163

164+
class G(dj.Computed):
165+
definition = """ # test downstream of complex master/parts
166+
-> E
167+
"""
168+
169+
def make(self, key):
170+
self.insert1(key)
171+
172+
135173
class DataA(dj.Lookup):
136174
definition = """
137175
idx : int

tests/test_cascading_delete.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
import datajoint as dj
3-
from .schema_simple import A, B, D, E, L, Website, Profile
3+
from .schema_simple import A, B, D, E, G, L, Website, Profile
44
from .schema import ComplexChild, ComplexParent
55

66

@@ -11,6 +11,7 @@ def schema_simp_pop(schema_simp):
1111
B().populate()
1212
D().populate()
1313
E().populate()
14+
G().populate()
1415
yield schema_simp
1516

1617

@@ -96,7 +97,7 @@ def test_delete_complex_keys(schema_any):
9697
**{
9798
"child_id_{}".format(i + 1): (i + parent_key_count)
9899
for i in range(child_key_count)
99-
}
100+
},
100101
)
101102
assert len(ComplexParent & restriction) == 1, "Parent record missing"
102103
assert len(ComplexChild & restriction) == 1, "Child record missing"
@@ -110,11 +111,24 @@ def test_delete_master(schema_simp_pop):
110111
Profile().delete()
111112

112113

113-
def test_delete_parts(schema_simp_pop):
114+
def test_delete_parts_error(schema_simp_pop):
114115
"""test issue #151"""
115116
with pytest.raises(dj.DataJointError):
116117
Profile().populate_random()
117-
Website().delete()
118+
Website().delete(force_masters=False)
119+
120+
121+
def test_delete_parts(schema_simp_pop):
122+
"""test issue #151"""
123+
Profile().populate_random()
124+
Website().delete(force_masters=True)
125+
126+
127+
def test_delete_parts_complex(schema_simp_pop):
128+
"""test issue #151 with complex master/part. PR #1158."""
129+
prev_len = len(G())
130+
(A() & "id_a=1").delete(force_masters=True)
131+
assert prev_len - len(G()) == 16, "Failed to delete parts"
118132

119133

120134
def test_drop_part(schema_simp_pop):

tests/test_erd.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import datajoint as dj
2-
from .schema_simple import LOCALS_SIMPLE, A, B, D, E, L, OutfitLaunch
2+
from .schema_simple import LOCALS_SIMPLE, A, B, D, E, G, L, OutfitLaunch
33
from .schema_advanced import *
44

55

@@ -20,7 +20,7 @@ def test_dependencies(schema_simp):
2020
assert set(D().parents(primary=True)) == set([A.full_table_name])
2121
assert set(D().parents(primary=False)) == set([L.full_table_name])
2222
assert set(deps.descendants(L.full_table_name)).issubset(
23-
cls.full_table_name for cls in (L, D, E, E.F)
23+
cls.full_table_name for cls in (L, D, E, E.F, E.G, E.H, E.M, G)
2424
)
2525

2626

@@ -38,10 +38,14 @@ def test_erd_algebra(schema_simp):
3838
erd3 = erd1 * erd2
3939
erd4 = (erd0 + E).add_parts() - B - E
4040
assert erd0.nodes_to_show == set(cls.full_table_name for cls in [B])
41-
assert erd1.nodes_to_show == set(cls.full_table_name for cls in (B, B.C, E, E.F))
41+
assert erd1.nodes_to_show == set(
42+
cls.full_table_name for cls in (B, B.C, E, E.F, E.G, E.H, E.M, G)
43+
)
4244
assert erd2.nodes_to_show == set(cls.full_table_name for cls in (A, B, D, E, L))
4345
assert erd3.nodes_to_show == set(cls.full_table_name for cls in (B, E))
44-
assert erd4.nodes_to_show == set(cls.full_table_name for cls in (B.C, E.F))
46+
assert erd4.nodes_to_show == set(
47+
cls.full_table_name for cls in (B.C, E.F, E.G, E.H, E.M)
48+
)
4549

4650

4751
def test_repr_svg(schema_adv):

tests/test_schema.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def test_list_tables(schema_simp):
184184
"""
185185
https://github.com/datajoint/datajoint-python/issues/838
186186
"""
187-
assert set(
187+
expected = set(
188188
[
189189
"reserved_word",
190190
"#l",
@@ -194,6 +194,10 @@ def test_list_tables(schema_simp):
194194
"__b__c",
195195
"__e",
196196
"__e__f",
197+
"__e__g",
198+
"__e__h",
199+
"__e__m",
200+
"__g",
197201
"#outfit_launch",
198202
"#outfit_launch__outfit_piece",
199203
"#i_j",
@@ -207,7 +211,9 @@ def test_list_tables(schema_simp):
207211
"profile",
208212
"profile__website",
209213
]
210-
) == set(schema_simp.list_tables())
214+
)
215+
actual = set(schema_simp.list_tables())
216+
assert actual == expected, f"Missing from list_tables(): {expected - actual}"
211217

212218

213219
def test_schema_save_any(schema_any):

0 commit comments

Comments
 (0)