Skip to content

Commit 4a2e1af

Browse files
Fix issue #374
1 parent 5a2f6b6 commit 4a2e1af

File tree

4 files changed

+36
-10
lines changed

4 files changed

+36
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### 0.13.3 -- TBD
44
* Bugfix - Dependencies not properly loaded on populate. (#902) PR #919
55
* Bugfix - Replace use of numpy aliases of built-in types with built-in type. (#938) PR #939
6+
* Bugfix - Deletes and drops can cascade to part from master only. (#151 and #374) PR #957
67

78
### 0.13.2 -- May 7, 2021
89
* Update `setuptools_certificate` dependency to new name `otumat`

datajoint/table.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .condition import make_condition
1515
from .expression import QueryExpression
1616
from . import blob
17-
from .utils import user_choice
17+
from .utils import user_choice, get_master
1818
from .heading import Heading
1919
from .errors import (DuplicateError, AccessError, DataJointError, UnknownAttributeError,
2020
IntegrityError)
@@ -383,15 +383,13 @@ def _delete_cascade(self):
383383
)).fetchall())))
384384
match['parent'] = match['parent'][0]
385385

386-
# If child is a part table of another table, do not recurse (See issue #151)
387-
if '__' in match['child'] and (not match['child'].strip('`').startswith(
388-
self.full_table_name).strip('`') + '__'):
386+
# Avoid deleting from child before master (See issue #151)
387+
master = get_master(match['child'])
388+
if master and self.full_table_name != master:
389389
raise DataJointError(
390390
'Attempt to delete from part table {part} before deleting from '
391391
'its master. Delete from {master} first.'.format(
392-
part=match['child'],
393-
master=match['child'].split('__')[0] + '`'
394-
))
392+
part=match['child'], master=master))
395393

396394
# Restrict child by self if
397395
# 1. if self's restriction attributes are not in child's primary key
@@ -490,8 +488,17 @@ def drop(self):
490488
' Call drop() on the unrestricted Table.')
491489
self.connection.dependencies.load()
492490
do_drop = True
493-
tables = [table for table in self.connection.dependencies.descendants(self.full_table_name)
494-
if not table.isdigit()]
491+
tables = [table for table in self.connection.dependencies.descendants(
492+
self.full_table_name) if not table.isdigit()]
493+
494+
# avoid dropping part tables without their masters: See issue #374
495+
for part in tables:
496+
master = get_master(part)
497+
if master and master not in tables:
498+
raise DataJointError(
499+
'Attempt to drop part table {part} before dropping '
500+
'its master. Drop {master} first.'.format(part=part, master=master))
501+
495502
if config['safemode']:
496503
for table in tables:
497504
print(table, '(%d tuples)' % len(FreeTable(self.connection, table)))
@@ -692,7 +699,7 @@ def check_fields(fields):
692699
if field not in self.heading:
693700
raise KeyError(u'`{0:s}` is not in the table heading'.format(field))
694701
elif set(field_list) != set(fields).intersection(self.heading.names):
695-
raise DataJointError('Attempt to insert rows with different fields')
702+
raise DataJointError('Attempt to insert rows with different fields.')
696703

697704
if isinstance(row, np.void): # np.array
698705
check_fields(row.dtype.fields)

datajoint/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ def user_choice(prompt, choices=("yes", "no"), default=None):
3131
return response
3232

3333

34+
def get_master(full_table_name):
35+
"""
36+
If the table name is that of a part table, then return what the master table name would be.
37+
:param full_table_name:
38+
:return: Supposed master full table name or empty string if not a part table name.
39+
40+
This follows DataJoint's table naming convention where a master and a part must be in the
41+
same schema and the part table is prefixed with the mater table name + '__'.
42+
43+
Example:
44+
`ephys`.`session` -- master
45+
`ephys`.`session__recording` -- part
46+
"""
47+
master, *part = full_table_name.split('__')
48+
return master + '`' if part else ""
49+
50+
3451
def to_camel_case(s):
3552
"""
3653
Convert names with under score (_) separation into camel case names.

docs-parts/intro/Releases_lang1.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
----------------------
33
* Bugfix - Dependencies not properly loaded on populate. (#902) PR #919
44
* Bugfix - Replace use of numpy aliases of built-in types with built-in type. (#938) PR #939
5+
* Bugfix - Deletes and drops can cascade to part from master only. (#151 and #374) PR #957
56

67
0.13.2 -- May 7, 2021
78
----------------------

0 commit comments

Comments
 (0)