Skip to content

Commit 25b2fec

Browse files
authored
Merge pull request #507 from dimitri-yatsenko/dev
Fix #300, #345 -- correct handling of renamed foreign key attributes in populate and cascading deletes.
2 parents 2f22e31 + b5bd423 commit 25b2fec

11 files changed

+104
-70
lines changed

CHANGELOG.md

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
## Release notes
2-
### 0.10.1
2+
3+
### 0.11.0 -- Oct 25, 2018
4+
* Full support of dependencies with renamed attributes using projection syntax (#300, #345, #436, #506, #507)
5+
* Rename internal class and module names to comply with terminology in documentation (#494, #500)
6+
* Full support of secondary indexes (#498, 500)
7+
* ERD no longer shows numbers in nodes corresponding to derived dependencies (#478, #500)
8+
* Full support of unique and nullable dependencies (#254, #301, #493, #495, #500)
9+
* Improve memory management in `populate` (#461, #486)
10+
* Fix query errors and redundancies (#456, #463, #482)
11+
12+
### 0.10.1 -- Aug 28, 2018
313
* Fix ERD Tooltip message (#431)
414
* Networkx 2.0 support (#443)
515
* Fix insert from query with skip_duplicates=True (#451)
616
* Sped up queries (#458)
717
* Bugfix in restriction of the form (A & B) * B (#463)
818
* Improved error messages (#466)
919

10-
### 0.10.0 -- January 10, 2018
20+
### 0.10.0 -- Jan 10, 2018
1121
* Deletes are more efficient (#424)
1222
* ERD shows table definition on tooltip hover in Jupyter (#422)
1323
* S3 external storage
@@ -17,18 +27,13 @@
1727
* Compatibility with pymysql 0.8.0+
1828
* More efficient loading of dependencies (#403)
1929

20-
### 0.9.0 -- November 17, 2017
30+
### 0.9.0 -- Nov 17, 2017
2131
* Made graphviz installation optional
2232
* Implement file-based external storage
2333
* Implement union operator +
24-
25-
26-
### 0.9.0 -- November 17, 2017
27-
* Bug fixes
28-
* Made graphviz installation optional
2934
* Implement file-based external storage
3035

31-
### 0.8.0 -- July 26, 2017
36+
### 0.8.0 -- Jul 26, 2017
3237
Documentation and tutorials available at https://docs.datajoint.io and https://tutorials.datajoint.io
3338
* improved the ERD graphics and features using the graphviz libraries (#207, #333)
3439
* improved password handling logic (#322, #321)
@@ -42,18 +47,18 @@ Documentation and tutorials available at https://docs.datajoint.io and https://t
4247
* simplified the `fetch` and `fetch1` syntax, deprecating the `fetch[...]` syntax (#319)
4348
* the jobs tables now store the connection ids to allow identifying abandoned jobs (#288, #317)
4449

45-
### 0.5.0 (#298) -- March 8, 2017
50+
### 0.5.0 (#298) -- Mar 8, 2017
4651
* All fetched integers are now 64-bit long and all fetched floats are double precision.
4752
* Added `dj.create_virtual_module`
4853

49-
### 0.4.10 (#286) -- February 6, 2017
54+
### 0.4.10 (#286) -- Feb 6, 2017
5055
* Removed Vagrant and Readthedocs support
5156
* Explicit saving of configuration (issue #284)
5257

53-
### 0.4.9 (#285) -- February 2, 2017
58+
### 0.4.9 (#285) -- Feb 2, 2017
5459
* Fixed setup.py for pip install
5560

56-
### 0.4.7 (#281) -- January 24, 2017
61+
### 0.4.7 (#281) -- Jan 24, 2017
5762
* Fixed issues related to order of attributes in projection.
5863

5964
### 0.4.6 (#277) -- Dec 22, 2016
@@ -62,32 +67,32 @@ Documentation and tutorials available at https://docs.datajoint.io and https://t
6267
### 0.4.5 (#274) -- Dec 20, 2016
6368
* Populate reports how many keys remain to be populated at the start.
6469

65-
### 0.4.3 (#271) -- December 6, 2016
70+
### 0.4.3 (#271) -- Dec 6, 2016
6671
* Fixed aggregation issues (#270)
6772
* datajoint no longer attempts to connect to server at import time
6873
* dropped support of view (reversed #257)
6974
* more elegant handling of insufficient privileges (#268)
7075

71-
### 0.4.2 (#267) -- December 6, 2016
76+
### 0.4.2 (#267) -- Dec 6, 2016
7277
* improved table appearance in Jupyter
7378

74-
### 0.4.1 (#266) -- October 28, 2016
79+
### 0.4.1 (#266) -- Oct 28, 2016
7580
* bugfix for very long error messages
7681

77-
### 0.3.9 -- September 27, 2016
82+
### 0.3.9 -- Sep 27, 2016
7883
* Added support for datatype `YEAR`
7984
* Fixed issues with `dj.U` and the `aggr` operator (#246, #247)
8085

81-
### 0.3.8 -- August 2, 2016
86+
### 0.3.8 -- Aug 2, 2016
8287
* added the `_update` method in `base_relation`. It allows updating values in existing tuples.
8388
* bugfix in reading values of type double. Previously it was cast as float32.
8489

85-
### 0.3.7 -- July 31, 2016
90+
### 0.3.7 -- Jul 31, 2016
8691
* added parameter `ignore_extra_fields` in `insert`
8792
* `insert(..., skip_duplicates=True)` now relies on `SELECT IGNORE`. Previously it explicitly checked if tuple already exists.
8893
* table previews now include blob attributes displaying the string <BLOB>
8994

90-
### 0.3.6 -- July 30, 2016
95+
### 0.3.6 -- Jul 30, 2016
9196
* bugfix in `schema.spawn_missing_classes`. Previously, spawned part classes would not show in ERDs.
9297
* dj.key now causes fetch to return as a list of dicts. Previously it was a recarray.
9398

datajoint/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .version import __version__
2020

2121
__author__ = "Dimitri Yatsenko, Edgar Y. Walker, and Fabian Sinz at Baylor College of Medicine"
22-
__date__ = "October 15, 2018"
22+
__date__ = "Oct 25, 2018"
2323
__all__ = ['__author__', '__version__',
2424
'config', 'conn', 'kill', 'Table',
2525
'Connection', 'Heading', 'FreeTable', 'Not', 'schema',

datajoint/autopopulate.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,25 @@ def key_source(self):
3131
The default value is the join of the parent relations.
3232
Users may override to change the granularity or the scope of populate() calls.
3333
"""
34-
if self._key_source is None:
34+
def parent_gen(self):
3535
if self.target.full_table_name not in self.connection.dependencies:
3636
self.connection.dependencies.load()
37-
parents = list(self.target.parents(primary=True))
38-
if not parents:
39-
raise DataJointError('A relation must have parent relations to be able to be populated')
40-
self._key_source = FreeTable(self.connection, parents.pop(0)).proj()
41-
while parents:
42-
self._key_source *= FreeTable(self.connection, parents.pop(0)).proj()
37+
for parent_name, fk_props in self.target.parents(primary=True).items():
38+
if not parent_name.isdigit(): # simple foreign key
39+
yield FreeTable(self.connection, parent_name).proj()
40+
else:
41+
grandparent = list(self.connection.dependencies.in_edges(parent_name))[0][0]
42+
yield FreeTable(self.connection, grandparent).proj(**{
43+
attr: ref for attr, ref in fk_props['attr_map'].items() if ref != attr})
44+
45+
if self._key_source is None:
46+
parents = parent_gen(self)
47+
try:
48+
self._key_source = next(parents)
49+
except StopIteration:
50+
raise DataJointError('A relation must have primary dependencies for auto-populate to work') from None
51+
for q in parents:
52+
self._key_source *= q
4353
return self._key_source
4454

4555
def make(self, key):

datajoint/declare.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
163163
raise DataJointError('Mismatched attributes in foreign key "%s"' % line)
164164

165165
if ref_attrs:
166+
# convert to projected dependency
166167
ref = ref.proj(**dict(zip(new_attrs, ref_attrs)))
167168

168169
# declare new foreign key attributes

datajoint/dependencies.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,19 @@ def descendants(self, full_table_name):
105105
:param full_table_name: In form `schema`.`table_name`
106106
:return: all dependent tables sorted in topological order. Self is included.
107107
"""
108-
109108
nodes = self.subgraph(
110109
nx.algorithms.dag.descendants(self, full_table_name))
111110

112111
return [full_table_name] + list(
113112
nx.algorithms.dag.topological_sort(nodes))
113+
114+
def ancestors(self, full_table_name):
115+
"""
116+
:param full_table_name: In form `schema`.`table_name`
117+
:return: all dependent tables sorted in topological order. Self is included.
118+
"""
119+
nodes = self.subgraph(
120+
nx.algorithms.dag.ancestors(self, full_table_name))
121+
return [full_table_name] + list(reversed(list(
122+
nx.algorithms.dag.topological_sort(nodes))))
123+

datajoint/table.py

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
logger = logging.getLogger(__name__)
2020

2121

22+
class _rename_map(tuple):
23+
""" for internal use """
24+
pass
25+
26+
2227
class Table(Query):
2328
"""
2429
Table is an abstract class that represents a base relation, i.e. a table in the schema.
@@ -105,6 +110,12 @@ def children(self, primary=None):
105110
"""
106111
return self.connection.dependencies.children(self.full_table_name, primary)
107112

113+
def descendants(self):
114+
return self. connection.dependencies.descendants(self.full_table_name)
115+
116+
def ancestors(self):
117+
return self. connection.dependencies.ancestors(self.full_table_name)
118+
108119
@property
109120
def is_declared(self):
110121
"""
@@ -315,62 +326,60 @@ def delete(self, verbose=True):
315326
Deletes the contents of the table and its dependent tables, recursively.
316327
User is prompted for confirmation if config['safemode'] is set to True.
317328
"""
318-
already_in_transaction = self.connection.in_transaction
329+
conn = self.connection
330+
already_in_transaction = conn.in_transaction
319331
safe = config['safemode']
320332
if already_in_transaction and safe:
321333
raise DataJointError('Cannot delete within a transaction in safemode. '
322334
'Set dj.config["safemode"] = False or complete the ongoing transaction first.')
323-
graph = self.connection.dependencies
335+
graph = conn.dependencies
324336
graph.load()
325-
delete_list = collections.OrderedDict()
326-
for table in graph.descendants(self.full_table_name):
327-
if not table.isdigit():
328-
delete_list[table] = FreeTable(self.connection, table)
329-
else:
330-
raise DataJointError('Cascading deletes across renamed foreign keys is not supported. See issue #300.')
331-
parent, edge = next(iter(graph.parents(table).items()))
332-
delete_list[table] = FreeTable(self.connection, parent).proj(
333-
**{new_name: old_name
334-
for new_name, old_name in edge['attr_map'].items() if new_name != old_name})
337+
delete_list = collections.OrderedDict(
338+
(name, _rename_map(next(iter(graph.parents(name).items()))) if name.isdigit() else FreeTable(conn, name))
339+
for name in graph.descendants(self.full_table_name))
335340

336341
# construct restrictions for each relation
337342
restrict_by_me = set()
343+
# restrictions: Or-Lists of restriction conditions for each table.
344+
# Uncharacteristically of Or-Lists, an empty entry denotes "delete everything".
338345
restrictions = collections.defaultdict(list)
339346
# restrict by self
340347
if self.restriction:
341348
restrict_by_me.add(self.full_table_name)
342349
restrictions[self.full_table_name].append(self.restriction) # copy own restrictions
343350
# restrict by renamed nodes
344351
restrict_by_me.update(table for table in delete_list if table.isdigit()) # restrict by all renamed nodes
345-
# restrict by tables restricted by a non-primary semijoin
352+
# restrict by secondary dependencies
346353
for table in delete_list:
347354
restrict_by_me.update(graph.children(table, primary=False)) # restrict by any non-primary dependents
348355

349356
# compile restriction lists
350-
for table, rel in delete_list.items():
351-
for dep in graph.children(table):
352-
if table in restrict_by_me:
353-
restrictions[dep].append(rel) # if restrict by me, then restrict by the entire relation
354-
else:
355-
restrictions[dep].extend(restrictions[table]) # or re-apply the same restrictions
357+
for name, table in delete_list.items():
358+
for dep in graph.children(name):
359+
# if restrict by me, then restrict by the entire relation otherwise copy restrictions
360+
restrictions[dep].extend([table] if name in restrict_by_me else restrictions[name])
356361

357362
# apply restrictions
358-
for name, r in delete_list.items():
359-
if restrictions[name]: # do not restrict by an empty list
360-
r.restrict([r.proj() if isinstance(r, Query) else r
361-
for r in restrictions[name]])
363+
for name, table in delete_list.items():
364+
if not name.isdigit() and restrictions[name]: # do not restrict by an empty list
365+
table.restrict([
366+
r.proj() if isinstance(r, FreeTable) else (
367+
delete_list[r[0]].proj(**{a: b for a, b in r[1]['attr_map'].items()})
368+
if isinstance(r, _rename_map) else r)
369+
for r in restrictions[name]])
362370
if safe:
363371
print('About to delete:')
364372

365373
if not already_in_transaction:
366374
self.connection.start_transaction()
367375
total = 0
368376
try:
369-
for r in reversed(list(delete_list.values())):
370-
count = r.delete_quick(get_count=True)
371-
total += count
372-
if (verbose or safe) and count:
373-
print('{table}: {count} items'.format(table=r.full_table_name, count=count))
377+
for name, table in reversed(list(delete_list.items())):
378+
if not name.isdigit():
379+
count = table.delete_quick(get_count=True)
380+
total += count
381+
if (verbose or safe) and count:
382+
print('{table}: {count} items'.format(table=name, count=count))
374383
except:
375384
# Delete failed, perhaps due to insufficient privileges. Cancel transaction.
376385
if not already_in_transaction:

datajoint/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.10.2"
1+
__version__ = "0.11.0"

tests/schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def make(self, key):
142142
@schema
143143
class Trial(dj.Imported):
144144
definition = """ # a trial within an experiment
145-
-> Experiment
145+
-> Experiment.proj(exp='experiment_id')
146146
trial_id :smallint # trial number
147147
---
148148
start_time :double # (s)
@@ -182,7 +182,7 @@ class Ephys(dj.Imported):
182182

183183
class Channel(dj.Part):
184184
definition = """ # subtable containing individual channels
185-
-> Ephys
185+
-> master
186186
channel :tinyint unsigned # channel number within Ephys
187187
----
188188
voltage : longblob

tests/test_declare.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,19 @@ def test_attributes():
8585
['subject_id', 'experiment_id'])
8686

8787
assert_list_equal(trial.heading.names,
88-
['subject_id', 'experiment_id', 'trial_id', 'start_time'])
88+
['subject_id', 'exp', 'trial_id', 'start_time'])
8989
assert_list_equal(trial.primary_key,
90-
['subject_id', 'experiment_id', 'trial_id'])
90+
['subject_id', 'exp', 'trial_id'])
9191

9292
assert_list_equal(ephys.heading.names,
93-
['subject_id', 'experiment_id', 'trial_id', 'sampling_frequency', 'duration'])
93+
['subject_id', 'exp', 'trial_id', 'sampling_frequency', 'duration'])
9494
assert_list_equal(ephys.primary_key,
95-
['subject_id', 'experiment_id', 'trial_id'])
95+
['subject_id', 'exp', 'trial_id'])
9696

9797
assert_list_equal(channel.heading.names,
98-
['subject_id', 'experiment_id', 'trial_id', 'channel', 'voltage', 'current'])
98+
['subject_id', 'exp', 'trial_id', 'channel', 'voltage', 'current'])
9999
assert_list_equal(channel.primary_key,
100-
['subject_id', 'experiment_id', 'trial_id', 'channel'])
100+
['subject_id', 'exp', 'trial_id', 'channel'])
101101
assert_true(channel.heading.attributes['voltage'].is_blob)
102102

103103
@staticmethod
@@ -108,8 +108,8 @@ def test_dependencies():
108108
assert_equal(set(subject.children(primary=True)), {experiment.full_table_name})
109109
assert_equal(set(experiment.parents(primary=True)), {subject.full_table_name})
110110

111-
assert_true(trial.full_table_name in set(experiment.children(primary=True)))
112-
assert_equal(set(trial.parents(primary=True)), {experiment.full_table_name})
111+
assert_true(trial.full_table_name in experiment.descendants())
112+
assert_true(experiment.full_table_name in trial.ancestors())
113113

114114
assert_equal(set(trial.children(primary=True)),
115115
{ephys.full_table_name, trial.Condition.full_table_name})

tests/test_foreign_keys.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from . import schema_advanced
66

77

8-
@raises(DataJointError) # TODO: remove after fixing issue #300
98
def test_aliased_fk():
109
person = schema_advanced.Person()
1110
parent = schema_advanced.Parent()
@@ -33,7 +32,6 @@ def test_describe():
3332
assert_equal(c1, c2)
3433

3534

36-
@raises(DataJointError) # TODO: remove after fixing issue #300
3735
def test_delete():
3836
person = schema_advanced.Person()
3937
parent = schema_advanced.Parent()

tests/test_jobs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def test_sigint():
7676
assert_equals(error_message, 'KeyboardInterrupt')
7777
schema.schema.jobs.delete()
7878

79+
7980
def test_key_pack_testing():
8081
jobs = schema.schema.jobs
8182
key = dict(a='string', b=int, c=Decimal())

0 commit comments

Comments
 (0)