Skip to content

Commit 5cfdf21

Browse files
Merge pull request #10 from guzman-raphael/delete-parsing
Fix error when cascading delete on tables with many, complex keys
2 parents 9955fe2 + b8e98ef commit 5cfdf21

File tree

9 files changed

+77
-22
lines changed

9 files changed

+77
-22
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

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## Release notes
22

3-
### 0.13.0 -- Mar 19, 2021
3+
### 0.13.0 -- Mar 24, 2021
44
* Re-implement query transpilation into SQL, fixing issues (#386, #449, #450, #484). PR #754
55
* Re-implement cascading deletes for better performance. PR #839.
66
* Add table method `.update1` to update a row in the table with new values PR #763
@@ -11,6 +11,7 @@
1111
* Default enable_python_native_blobs to True
1212
* Bugfix - Regression error on joins with same attribute name (#857) PR #878
1313
* Bugfix - Error when `fetch1('KEY')` when `dj.config['fetch_format']='frame'` set (#876) PR #880, #878
14+
* Bugfix - Error when cascading deletes in tables with many, complex keys (#883, #886) PR #839
1415
* Drop support for Python 3.5
1516

1617
### 0.12.8 -- Jan 12, 2021

LNX-docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ services:
3232
interval: 1s
3333
fakeservices.datajoint.io:
3434
<<: *net
35-
image: datajoint/nginx:v0.0.15
35+
image: datajoint/nginx:v0.0.16
3636
environment:
3737
- ADD_db_TYPE=DATABASE
3838
- ADD_db_ENDPOINT=db:3306

datajoint/table.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,27 @@
1515
from . import blob
1616
from .utils import user_choice
1717
from .heading import Heading
18-
from .errors import DuplicateError, AccessError, DataJointError, UnknownAttributeError, IntegrityError
18+
from .errors import (DuplicateError, AccessError, DataJointError, UnknownAttributeError,
19+
IntegrityError)
1920
from .version import __version__ as version
2021

2122
logger = logging.getLogger(__name__)
2223

23-
foregn_key_error_regexp = re.compile(
24+
foreign_key_error_regexp = re.compile(
2425
r"[\w\s:]*\((?P<child>`[^`]+`.`[^`]+`), "
2526
r"CONSTRAINT (?P<name>`[^`]+`) "
26-
r"FOREIGN KEY \((?P<fk_attrs>[^)]+)\) "
27-
r"REFERENCES (?P<parent>`[^`]+`(\.`[^`]+`)?) \((?P<pk_attrs>[^)]+)\)")
27+
r"(FOREIGN KEY \((?P<fk_attrs>[^)]+)\) "
28+
r"REFERENCES (?P<parent>`[^`]+`(\.`[^`]+`)?) \((?P<pk_attrs>[^)]+)\)[\s\w]+\))?")
29+
30+
constraint_info_query = ' '.join("""
31+
SELECT
32+
COLUMN_NAME as fk_attrs,
33+
CONCAT('`', REFERENCED_TABLE_SCHEMA, '`.`', REFERENCED_TABLE_NAME, '`') as parent,
34+
REFERENCED_COLUMN_NAME as pk_attrs
35+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
36+
WHERE
37+
CONSTRAINT_NAME = %s AND TABLE_SCHEMA = %s AND TABLE_NAME = %s;
38+
""".split())
2839

2940

3041
class _RenameMap(tuple):
@@ -344,23 +355,31 @@ def _delete_cascade(self):
344355
try:
345356
delete_count += self.delete_quick(get_count=True)
346357
except IntegrityError as error:
347-
match = foregn_key_error_regexp.match(error.args[0])
348-
assert match is not None, "foreign key parsing error"
358+
match = foreign_key_error_regexp.match(error.args[0]).groupdict()
359+
if "`.`" not in match['child']: # if schema name missing, use self
360+
match['child'] = '{}.{}'.format(self.full_table_name.split(".")[0],
361+
match['child'])
362+
if match['pk_attrs'] is not None: # fully matched, adjusting the keys
363+
match['fk_attrs'] = [k.strip('`') for k in match['fk_attrs'].split(',')]
364+
match['pk_attrs'] = [k.strip('`') for k in match['pk_attrs'].split(',')]
365+
else: # only partially matched, querying with constraint to determine keys
366+
match['fk_attrs'], match['parent'], match['pk_attrs'] = list(map(
367+
list, zip(*self.connection.query(constraint_info_query, args=(
368+
match['name'].strip('`'),
369+
*[_.strip('`') for _ in match['child'].split('`.`')]
370+
)).fetchall())))
371+
match['parent'] = match['parent'][0]
349372
# restrict child by self if
350373
# 1. if self's restriction attributes are not in child's primary key
351374
# 2. if child renames any attributes
352375
# 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)
376+
child = FreeTable(self.connection, match['child'])
357377
if set(self.restriction_attributes) <= set(child.primary_key) and \
358-
match.group('fk_attrs') == match.group('pk_attrs'):
378+
match['fk_attrs'] == match['pk_attrs']:
359379
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)))
380+
elif match['fk_attrs'] != match['pk_attrs']:
381+
child &= self.proj(**dict(zip(match['fk_attrs'],
382+
match['pk_attrs'])))
364383
else:
365384
child &= self.proj()
366385
delete_count += child._delete_cascade()
@@ -375,8 +394,10 @@ def _delete_cascade(self):
375394
def delete(self, transaction=True, safemode=None):
376395
"""
377396
Deletes the contents of the table and its dependent tables, recursively.
397+
378398
:param transaction: if True, use the entire delete becomes an atomic transaction.
379-
:param safemode: If True, prohibit nested transactions and prompt to confirm. Default is dj.config['safemode'].
399+
:param safemode: If True, prohibit nested transactions and prompt to confirm. Default
400+
is dj.config['safemode'].
380401
"""
381402
safemode = config['safemode'] if safemode is None else safemode
382403

datajoint/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__version__ = "0.13.dev6"
1+
__version__ = "0.13.dev7"
22

33
assert len(__version__) <= 10 # The log table limits version to the 10 characters

docs-parts/intro/Releases_lang1.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
0.13.0 -- Mar 19, 2021
1+
0.13.0 -- Mar 24, 2021
22
----------------------
33
* Re-implement query transpilation into SQL, fixing issues (#386, #449, #450, #484). PR #754
44
* Re-implement cascading deletes for better performance. PR #839.
@@ -10,6 +10,7 @@
1010
* Default enable_python_native_blobs to True
1111
* Bugfix - Regression error on joins with same attribute name (#857) PR #878
1212
* Bugfix - Error when `fetch1('KEY')` when `dj.config['fetch_format']='frame'` set (#876) PR #880, #878
13+
* Bugfix - Error when cascading deletes in tables with many, complex keys (#883, #886) PR #839
1314
* Drop support for Python 3.5
1415

1516
0.12.8 -- Jan 12, 2021

local-docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ services:
3434
interval: 1s
3535
fakeservices.datajoint.io:
3636
<<: *net
37-
image: datajoint/nginx:v0.0.15
37+
image: datajoint/nginx:v0.0.16
3838
environment:
3939
- ADD_db_TYPE=DATABASE
4040
- ADD_db_ENDPOINT=db:3306

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)