Skip to content

Commit 5a2f6b6

Browse files
fix issue 151 - cascades to part tables should be from parent tables only.
1 parent 5b81247 commit 5a2f6b6

File tree

1 file changed

+61
-33
lines changed

1 file changed

+61
-33
lines changed

datajoint/table.py

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,17 @@ class _RenameMap(tuple):
4646

4747
class Table(QueryExpression):
4848
"""
49-
Table is an abstract class that represents a base relation, i.e. a table in the schema.
49+
Table is an abstract class that represents a table in the schema.
50+
It implements insert and delete methods and inherits query functionality.
5051
To make it a concrete class, override the abstract properties specifying the connection,
5152
table name, database, and definition.
52-
A Relation implements insert and delete methods in addition to inherited relational operators.
5353
"""
5454

5555
_table_name = None # must be defined in subclass
5656
_log_ = None # placeholder for the Log table object
5757

58-
# These properties must be set by the schema decorator (schemas.py) at class level or by FreeTable at instance level
58+
# These properties must be set by the schema decorator (schemas.py) at class level
59+
# or by FreeTable at instance level
5960
database = None
6061
declaration_context = None
6162

@@ -65,7 +66,8 @@ def table_name(self):
6566

6667
@property
6768
def definition(self):
68-
raise NotImplementedError('Subclasses of Table must implement the `definition` property')
69+
raise NotImplementedError(
70+
'Subclasses of Table must implement the `definition` property')
6971

7072
def declare(self, context=None):
7173
"""
@@ -95,7 +97,8 @@ def alter(self, prompt=True, context=None):
9597
"""
9698
if self.connection.in_transaction:
9799
raise DataJointError(
98-
'Cannot update table declaration inside a transaction, e.g. from inside a populate/make call')
100+
'Cannot update table declaration inside a transaction, '
101+
'e.g. from inside a populate/make call')
99102
if context is None:
100103
frame = inspect.currentframe().f_back
101104
context = dict(frame.f_globals, **frame.f_locals)
@@ -117,7 +120,8 @@ def alter(self, prompt=True, context=None):
117120
# skip if no create privilege
118121
pass
119122
else:
120-
self.__class__._heading = Heading(table_info=self.heading.table_info) # reset heading
123+
# reset heading
124+
self.__class__._heading = Heading(table_info=self.heading.table_info)
121125
if prompt:
122126
print('Table altered')
123127
self._log('Altered ' + self.full_table_name)
@@ -226,9 +230,11 @@ def external(self):
226230
def update1(self, row):
227231
"""
228232
update1 updates one existing entry in the table.
229-
Caution: Updates are not part of the DataJoint data manipulation model. For strict data integrity,
230-
use delete and insert.
231-
:param row: a dict containing the primary key and the attributes to update.
233+
Caution: In DataJoint the primary modes for data manipulation is to insert and delete
234+
entire records since referential integrity works on the level of records, not fields.
235+
Therefore, updates are reserved for corrective operations outside of main workflow.
236+
Use UPDATE methods sparingly with full awareness of potential violations of assumptions.
237+
:param row: a dict containing the primary key values and the attributes to update.
232238
Setting an attribute value to None will reset it to the default value (if any)
233239
The primary key attributes must always be provided.
234240
Examples:
@@ -241,7 +247,8 @@ def update1(self, row):
241247
if not set(row).issuperset(self.primary_key):
242248
raise DataJointError('The argument of update1 must supply all primary key values.')
243249
try:
244-
raise DataJointError('Attribute `%s` not found.' % next(k for k in row if k not in self.heading.names))
250+
raise DataJointError('Attribute `%s` not found.' %
251+
next(k for k in row if k not in self.heading.names))
245252
except StopIteration:
246253
pass # ok
247254
if len(self.restriction):
@@ -250,7 +257,8 @@ def update1(self, row):
250257
if len(self & key) != 1:
251258
raise DataJointError('Update entry must exist.')
252259
# UPDATE query
253-
row = [self.__make_placeholder(k, v) for k, v in row.items() if k not in self.primary_key]
260+
row = [self.__make_placeholder(k, v) for k, v in row.items()
261+
if k not in self.primary_key]
254262
query = "UPDATE {table} SET {assignments} WHERE {where}".format(
255263
table=self.full_table_name,
256264
assignments=",".join('`%s`=%s' % r[:2] for r in row),
@@ -259,22 +267,25 @@ def update1(self, row):
259267

260268
def insert1(self, row, **kwargs):
261269
"""
262-
Insert one data record or one Mapping (like a dict).
263-
:param row: a numpy record, a dict-like object, or an ordered sequence to be inserted as one row.
270+
Insert one data record into the table..
271+
:param row: a numpy record, a dict-like object, or an ordered sequence to be inserted
272+
as one row.
264273
For kwargs, see insert()
265274
"""
266275
self.insert((row,), **kwargs)
267276

268-
def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields=False, allow_direct_insert=None):
277+
def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields=False,
278+
allow_direct_insert=None):
269279
"""
270280
Insert a collection of rows.
271-
:param rows: An iterable where an element is a numpy record, a dict-like object, a pandas.DataFrame, a sequence,
272-
or a query expression with the same heading as table self.
281+
:param rows: An iterable where an element is a numpy record, a dict-like object, a
282+
pandas.DataFrame, a sequence, or a query expression with the same heading as self.
273283
:param replace: If True, replaces the existing tuple.
274284
:param skip_duplicates: If True, silently skip duplicate inserts.
275285
:param ignore_extra_fields: If False, fields that are not in the heading raise error.
276286
:param allow_direct_insert: applies only in auto-populated tables.
277-
If False (default), insert are allowed only from inside the make callback.
287+
If False (default), insert are allowed only from inside
288+
the make callback.
278289
Example::
279290
>>> relation.insert([
280291
>>> dict(subject_id=7, species="mouse", date_of_birth="2014-09-01"),
@@ -288,20 +299,22 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields
288299
).to_records(index=False)
289300

290301
# prohibit direct inserts into auto-populated tables
291-
if not allow_direct_insert and not getattr(self, '_allow_insert', True): # allow_insert is only used in AutoPopulate
302+
if not allow_direct_insert and not getattr(self, '_allow_insert', True):
292303
raise DataJointError(
293-
'Inserts into an auto-populated table can only done inside its make method during a populate call.'
304+
'Inserts into an auto-populated table can only done inside '
305+
'its make method during a populate call.'
294306
' To override, set keyword argument allow_direct_insert=True.')
295307

296-
if inspect.isclass(rows) and issubclass(rows, QueryExpression): # instantiate if a class
297-
rows = rows()
308+
if inspect.isclass(rows) and issubclass(rows, QueryExpression):
309+
rows = rows() # instantiate if a class
298310
if isinstance(rows, QueryExpression):
299311
# insert from select
300312
if not ignore_extra_fields:
301313
try:
302314
raise DataJointError(
303-
"Attribute %s not found. To ignore extra attributes in insert, set ignore_extra_fields=True." %
304-
next(name for name in rows.heading if name not in self.heading))
315+
"Attribute %s not found. To ignore extra attributes in insert, "
316+
"set ignore_extra_fields=True." % next(
317+
name for name in rows.heading if name not in self.heading))
305318
except StopIteration:
306319
pass
307320
fields = list(name for name in rows.heading if name in self.heading)
@@ -369,10 +382,21 @@ def _delete_cascade(self):
369382
*[_.strip('`') for _ in match['child'].split('`.`')]
370383
)).fetchall())))
371384
match['parent'] = match['parent'][0]
372-
# restrict child by self if
373-
# 1. if self's restriction attributes are not in child's primary key
374-
# 2. if child renames any attributes
375-
# otherwise restrict child by self's restriction.
385+
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('`') + '__'):
389+
raise DataJointError(
390+
'Attempt to delete from part table {part} before deleting from '
391+
'its master. Delete from {master} first.'.format(
392+
part=match['child'],
393+
master=match['child'].split('__')[0] + '`'
394+
))
395+
396+
# Restrict child by self if
397+
# 1. if self's restriction attributes are not in child's primary key
398+
# 2. if child renames any attributes
399+
# Otherwise restrict child by self's restriction.
376400
child = FreeTable(self.connection, match['child'])
377401
if set(self.restriction_attributes) <= set(child.primary_key) and \
378402
match['fk_attrs'] == match['pk_attrs']:
@@ -396,6 +420,8 @@ def delete(self, transaction=True, safemode=None):
396420
Deletes the contents of the table and its dependent tables, recursively.
397421
398422
:param transaction: if True, use the entire delete becomes an atomic transaction.
423+
This is the default and recommended behavior. Set to False if this delete is nested
424+
within another transaction.
399425
:param safemode: If True, prohibit nested transactions and prompt to confirm. Default
400426
is dj.config['safemode'].
401427
:return: number of deleted rows (excluding those from dependent tables)
@@ -460,7 +486,7 @@ def drop(self):
460486
User is prompted for confirmation if config['safemode'] is set to True.
461487
"""
462488
if self.restriction:
463-
raise DataJointError('A relation with an applied restriction condition cannot be dropped.'
489+
raise DataJointError('A table with an applied restriction cannot be dropped.'
464490
' Call drop() on the unrestricted Table.')
465491
self.connection.dependencies.load()
466492
do_drop = True
@@ -555,12 +581,14 @@ def describe(self, context=None, printout=True):
555581

556582
def _update(self, attrname, value=None):
557583
"""
558-
This is a deprecated function to be removed in datajoint 0.14. Use .update1 instead.
584+
This is a deprecated function to be removed in datajoint 0.14.
585+
Use .update1 instead.
559586
560-
Updates a field in an existing tuple. This is not a datajoyous operation and should not be used
561-
routinely. Relational database maintain referential integrity on the level of a tuple. Therefore,
562-
the UPDATE operator can violate referential integrity. The datajoyous way to update information is
563-
to delete the entire tuple and insert the entire update tuple.
587+
Updates a field in one existing tuple. self must be restricted to exactly one entry.
588+
In DataJoint the principal way of updating data is to delete and re-insert the
589+
entire record and updates are reserved for corrective actions.
590+
This is because referential integrity is observed on the level of entire
591+
records rather than individual attributes.
564592
565593
Safety constraints:
566594
1. self must be restricted to exactly one tuple

0 commit comments

Comments
 (0)