Skip to content

Commit 0a77bcd

Browse files
Fix issue on cascading deletes where MySQL truncates the contraint due to long, complex foreign keys.
1 parent 5217cd4 commit 0a77bcd

File tree

4 files changed

+74
-14
lines changed

4 files changed

+74
-14
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ notebook
2424
.vscode
2525
__main__.py
2626
jupyter_custom.js
27-
apk_requirements.txt
27+
apk_requirements.txt
28+
.eggs

datajoint/table.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@
2020

2121
logger = logging.getLogger(__name__)
2222

23-
foregn_key_error_regexp = re.compile(
23+
foreign_key_full_error_regexp = re.compile(
2424
r"[\w\s:]*\((?P<child>`[^`]+`.`[^`]+`), "
2525
r"CONSTRAINT (?P<name>`[^`]+`) "
2626
r"FOREIGN KEY \((?P<fk_attrs>[^)]+)\) "
27-
r"REFERENCES (?P<parent>`[^`]+`(\.`[^`]+`)?) \((?P<pk_attrs>[^)]+)\)")
27+
r"REFERENCES (?P<parent>`[^`]+`(\.`[^`]+`)?) \((?P<pk_attrs>[^)]+)\)[\s\w]+\)")
28+
29+
foreign_key_partial_error_regexp = re.compile(
30+
r"[\w\s:]*\((?P<child>`[^`]+?`.`[^`]+?`), "
31+
r"CONSTRAINT (?P<name>`[^`]+?`) ")
2832

2933

3034
class _RenameMap(tuple):
@@ -344,23 +348,47 @@ def _delete_cascade(self):
344348
try:
345349
delete_count += self.delete_quick(get_count=True)
346350
except IntegrityError as error:
347-
match = foregn_key_error_regexp.match(error.args[0])
348-
assert match is not None, "foreign key parsing error"
351+
try:
352+
# try to parse error for child info
353+
match = foreign_key_full_error_regexp.match(error.args[0]).groupdict()
354+
if "`.`" not in match['child']: # if schema name is not included, use self
355+
match['child'] = '{}.{}'.format(self.full_table_name.split(".")[0],
356+
match['child'])
357+
match['fk_attrs'] = [k.strip('`') for k in match['fk_attrs'].split(',')]
358+
match['pk_attrs'] = [k.strip('`') for k in match['pk_attrs'].split(',')]
359+
except AttributeError:
360+
# additional query required due to truncated error, trying partial regex
361+
match = foreign_key_partial_error_regexp.match(error.args[0]).groupdict()
362+
if "`.`" not in match['child']: # if schema name is not included, use self
363+
match['child'] = '{}.{}'.format(self.full_table_name.split(".")[0],
364+
match['child'])
365+
match['fk_attrs'], match['parent'], match['pk_attrs'] = list(map(list, zip(
366+
*self.connection.query(
367+
"""
368+
SELECT
369+
COLUMN_NAME as fk_attrs,
370+
CONCAT('`', REFERENCED_TABLE_SCHEMA, '`.`',
371+
REFERENCED_TABLE_NAME, '`') as parent,
372+
REFERENCED_COLUMN_NAME as pk_attrs
373+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
374+
WHERE
375+
CONSTRAINT_NAME = %s AND TABLE_SCHEMA = %s AND TABLE_NAME = %s;
376+
""",
377+
args=(match['name'].strip('`'),
378+
*[_.strip('`') for _ in match['child'].split('`.`')])
379+
).fetchall())))
380+
match['parent'] = match['parent'][0]
349381
# restrict child by self if
350382
# 1. if self's restriction attributes are not in child's primary key
351383
# 2. if child renames any attributes
352384
# otherwise restrict child by self's restriction.
353-
child = match.group('child')
354-
if "`.`" not in child: # if schema name is not included, take it from self
355-
child = self.full_table_name.split("`.")[0] + child
356-
child = FreeTable(self.connection, child)
385+
child = FreeTable(self.connection, match['child'])
357386
if set(self.restriction_attributes) <= set(child.primary_key) and \
358-
match.group('fk_attrs') == match.group('pk_attrs'):
387+
match['fk_attrs'] == match['pk_attrs']:
359388
child._restriction = self._restriction
360-
elif match.group('fk_attrs') != match.group('pk_attrs'):
361-
fk_attrs = [k.strip('`') for k in match.group('fk_attrs').split(',')]
362-
pk_attrs = [k.strip('`') for k in match.group('pk_attrs').split(',')]
363-
child &= self.proj(**dict(zip(fk_attrs, pk_attrs)))
389+
elif match['fk_attrs'] != match['pk_attrs']:
390+
child &= self.proj(**dict(zip(match['fk_attrs'],
391+
match['pk_attrs'])))
364392
else:
365393
child &= self.proj()
366394
delete_count += child._delete_cascade()

tests/schema.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,16 @@ class Child(dj.Lookup):
366366
name: varchar(30)
367367
"""
368368
contents = [(1, 12, 'Dan')]
369+
370+
# Related to issue #886 (8), #883 (5)
371+
@schema
372+
class ComplexParent(dj.Lookup):
373+
definition = '\n'.join(['parent_id_{}: int'.format(i+1) for i in range(8)])
374+
contents = [tuple(i for i in range(8))]
375+
376+
377+
@schema
378+
class ComplexChild(dj.Lookup):
379+
definition = '\n'.join(['-> ComplexParent'] + ['child_id_{}: int'.format(i+1)
380+
for i in range(1)])
381+
contents = [tuple(i for i in range(9))]

tests/test_cascading_delete.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from nose.tools import assert_false, assert_true, assert_equal
22
import datajoint as dj
33
from .schema_simple import A, B, D, E, L
4+
from .schema import ComplexChild, ComplexParent
45

56

67
class TestDelete:
@@ -78,3 +79,20 @@ def test_delete_lookup_restricted():
7879
deleted_count = len(rel)
7980
rel.delete()
8081
assert_true(len(L()) == original_count - deleted_count)
82+
83+
@staticmethod
84+
def test_delete_complex_keys():
85+
# https://github.com/datajoint/datajoint-python/issues/883
86+
# https://github.com/datajoint/datajoint-python/issues/886
87+
assert_false(dj.config['safemode'], 'safemode must be off for testing')
88+
parent_key_count = 8
89+
child_key_count = 1
90+
restriction = dict({'parent_id_{}'.format(i+1): i
91+
for i in range(parent_key_count)},
92+
**{'child_id_{}'.format(i+1): (i + parent_key_count)
93+
for i in range(child_key_count)})
94+
assert len(ComplexParent & restriction) == 1, 'Parent record missing'
95+
assert len(ComplexChild & restriction) == 1, 'Child record missing'
96+
(ComplexParent & restriction).delete()
97+
assert len(ComplexParent & restriction) == 0, 'Parent record was not deleted'
98+
assert len(ComplexChild & restriction) == 0, 'Child record was not deleted'

0 commit comments

Comments
 (0)