Skip to content

Commit 2f22e31

Browse files
authored
Merge pull request #506 from dimitri-yatsenko/master
Implement projected dependencies. Fix #436
2 parents 88667d7 + 1d8cd5c commit 2f22e31

File tree

6 files changed

+104
-70
lines changed

6 files changed

+104
-70
lines changed

datajoint/declare.py

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
logger = logging.getLogger(__name__)
1717

1818

19-
def build_foreign_key_parser():
19+
def build_foreign_key_parser_old():
20+
# old-style foreign key parser. Superceded by expression-based syntax. See issue #436
21+
# This will be deprecated in a future release.
2022
left = pp.Literal('(').suppress()
2123
right = pp.Literal(')').suppress()
2224
attribute_name = pp.Word(pp.srange('[a-z]'), pp.srange('[a-z0-9_]'))
@@ -31,6 +33,16 @@ def build_foreign_key_parser():
3133
return new_attrs + arrow + options + ref_table + ref_attrs
3234

3335

36+
def build_foreign_key_parser():
37+
arrow = pp.Literal('->').suppress()
38+
lbracket = pp.Literal('[').suppress()
39+
rbracket = pp.Literal(']').suppress()
40+
option = pp.Word(pp.srange('[a-zA-Z]'))
41+
options = pp.Optional(lbracket + pp.delimitedList(option) + rbracket).setResultsName('options')
42+
ref_table = pp.restOfLine.setResultsName('ref_table')
43+
return arrow + options + ref_table
44+
45+
3446
def build_attribute_parser():
3547
quoted = pp.Or(pp.QuotedString('"'), pp.QuotedString("'"))
3648
colon = pp.Literal(':').suppress()
@@ -50,6 +62,7 @@ def build_index_parser():
5062
return unique + index + left + pp.delimitedList(attribute_name).setResultsName('attr_list') + right
5163

5264

65+
foreign_key_parser_old = build_foreign_key_parser_old()
5366
foreign_key_parser = build_foreign_key_parser()
5467
attribute_parser = build_attribute_parser()
5568
index_parser = build_index_parser()
@@ -77,16 +90,22 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
7790
"""
7891
# Parse and validate
7992
from .table import Table
93+
from .query import Projection
94+
95+
new_style = True # See issue #436. Old style to be deprecated in a future release
8096
try:
8197
result = foreign_key_parser.parseString(line)
82-
except pp.ParseException as err:
83-
raise DataJointError('Parsing error in line "%s". %s.' % (line, err))
98+
except pp.ParseException:
99+
try:
100+
result = foreign_key_parser_old.parseString(line)
101+
except pp.ParseBaseException as err:
102+
raise DataJointError('Parsing error in line "%s". %s.' % (line, err)) from None
103+
else:
104+
new_style = False
84105
try:
85-
referenced_class = eval(result.ref_table, context)
86-
except NameError:
106+
ref = eval(result.ref_table, context)
107+
except Exception if new_style else NameError:
87108
raise DataJointError('Foreign key reference %s could not be resolved' % result.ref_table)
88-
if not issubclass(referenced_class, Table):
89-
raise DataJointError('Foreign key reference %s must be a subclass of UserTable' % result.ref_table)
90109

91110
options = [opt.upper() for opt in result.options]
92111
for opt in options: # check for invalid options
@@ -97,65 +116,75 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
97116
if is_nullable and primary_key is not None:
98117
raise DataJointError('Primary dependencies cannot be nullable in line "{line}"'.format(line=line))
99118

100-
ref = referenced_class()
101-
if not all(r in ref.primary_key for r in result.ref_attrs):
102-
raise DataJointError('Invalid foreign key attributes in "%s"' % line)
103-
104-
try:
105-
raise DataJointError('Duplicate attributes "{attr}" in "{line}"'.format(
106-
attr=next(attr for attr in result.new_attrs if attr in attributes),
107-
line=line))
108-
except StopIteration:
109-
pass # the normal outcome
110-
111-
# Match the primary attributes of the referenced table to local attributes
112-
new_attrs = list(result.new_attrs)
113-
ref_attrs = list(result.ref_attrs)
114-
115-
# special case, the renamed attribute is implicit
116-
if new_attrs and not ref_attrs:
117-
if len(new_attrs) != 1:
118-
raise DataJointError('Renamed foreign key must be mapped to the primary key in "%s"' % line)
119-
if len(ref.primary_key) == 1:
120-
# if the primary key has one attribute, allow implicit renaming
121-
ref_attrs = ref.primary_key
122-
else:
123-
# if only one primary key attribute remains, then allow implicit renaming
124-
ref_attrs = [attr for attr in ref.primary_key if attr not in attributes]
125-
if len(ref_attrs) != 1:
126-
raise DataJointError('Could not resovle which primary key attribute should be referenced in "%s"' % line)
127-
128-
if len(new_attrs) != len(ref_attrs):
129-
raise DataJointError('Mismatched attributes in foreign key "%s"' % line)
130-
131-
# expand the primary key of the referenced table
132-
lookup = dict(zip(ref_attrs, new_attrs)).get # from foreign to local
133-
ref_attrs = [attr for attr in ref.primary_key if lookup(attr, attr) not in attributes]
134-
new_attrs = [lookup(attr, attr) for attr in ref_attrs]
135-
136-
# sanity check
137-
assert len(new_attrs) == len(ref_attrs) and not any(attr in attributes for attr in new_attrs)
119+
if not new_style:
120+
if not isinstance(ref, type) or not issubclass(ref, Table):
121+
raise DataJointError('Foreign key reference %r must be a valid query' % result.ref_table)
122+
123+
if isinstance(ref, type) and issubclass(ref, Table):
124+
ref = ref()
125+
126+
# check that dependency is of supported type
127+
if (not isinstance(ref, (Table, Projection)) or len(ref.restriction) or
128+
(isinstance(ref, Projection) and (not isinstance(ref._arg, Table) or len(ref._arg.restriction)))):
129+
raise DataJointError('Dependency "%s" is not supported (yet). Use a base table or its projection.' %
130+
result.ref_table)
131+
132+
if not new_style:
133+
# for backward compatibility with old-style dependency declarations. See issue #436
134+
if not isinstance(ref, Table):
135+
DataJointError('Dependency "%s" is not supported. Check documentation.' % result.ref_table)
136+
if not all(r in ref.primary_key for r in result.ref_attrs):
137+
raise DataJointError('Invalid foreign key attributes in "%s"' % line)
138+
try:
139+
raise DataJointError('Duplicate attributes "{attr}" in "{line}"'.format(
140+
attr=next(attr for attr in result.new_attrs if attr in attributes),
141+
line=line))
142+
except StopIteration:
143+
pass # the normal outcome
144+
145+
# Match the primary attributes of the referenced table to local attributes
146+
new_attrs = list(result.new_attrs)
147+
ref_attrs = list(result.ref_attrs)
148+
149+
# special case, the renamed attribute is implicit
150+
if new_attrs and not ref_attrs:
151+
if len(new_attrs) != 1:
152+
raise DataJointError('Renamed foreign key must be mapped to the primary key in "%s"' % line)
153+
if len(ref.primary_key) == 1:
154+
# if the primary key has one attribute, allow implicit renaming
155+
ref_attrs = ref.primary_key
156+
else:
157+
# if only one primary key attribute remains, then allow implicit renaming
158+
ref_attrs = [attr for attr in ref.primary_key if attr not in attributes]
159+
if len(ref_attrs) != 1:
160+
raise DataJointError('Could not resovle which primary key attribute should be referenced in "%s"' % line)
161+
162+
if len(new_attrs) != len(ref_attrs):
163+
raise DataJointError('Mismatched attributes in foreign key "%s"' % line)
164+
165+
if ref_attrs:
166+
ref = ref.proj(**dict(zip(new_attrs, ref_attrs)))
138167

139168
# declare new foreign key attributes
140-
for ref_attr in ref_attrs:
141-
new_attr = lookup(ref_attr, ref_attr)
142-
attributes.append(new_attr)
143-
if primary_key is not None:
144-
primary_key.append(new_attr)
145-
attr_sql.append(
146-
ref.heading[ref_attr].sql.replace(ref_attr, new_attr, 1).replace('NOT NULL', '', int(is_nullable)))
169+
base = ref._arg if isinstance(ref, Projection) else ref # base reference table
170+
for attr, ref_attr in zip(ref.primary_key, base.primary_key):
171+
if attr not in attributes:
172+
attributes.append(attr)
173+
if primary_key is not None:
174+
primary_key.append(attr)
175+
attr_sql.append(
176+
base.heading[ref_attr].sql.replace(ref_attr, attr, 1).replace('NOT NULL ', '', int(is_nullable)))
147177

148178
# declare the foreign key
149179
foreign_key_sql.append(
150180
'FOREIGN KEY (`{fk}`) REFERENCES {ref} (`{pk}`) ON UPDATE CASCADE ON DELETE RESTRICT'.format(
151-
fk='`,`'.join(lookup(attr, attr) for attr in ref.primary_key),
152-
pk='`,`'.join(ref.primary_key),
153-
ref=ref.full_table_name))
181+
fk='`,`'.join(ref.primary_key),
182+
pk='`,`'.join(base.primary_key),
183+
ref=base.full_table_name))
154184

155185
# declare unique index
156186
if is_unique:
157-
index_sql.append('UNIQUE INDEX ({attrs})'.format(
158-
attrs='`,`'.join(lookup(attr, attr) for attr in ref.primary_key)))
187+
index_sql.append('UNIQUE INDEX ({attrs})'.format(attrs='`,`'.join(ref.primary_key)))
159188

160189

161190
def declare(full_table_name, definition, context):
@@ -223,7 +252,6 @@ def compile_attribute(line, in_key, foreign_key_sql):
223252
:param foreign_key_sql:
224253
:returns: (name, sql, is_external) -- attribute name and sql code for its declaration
225254
"""
226-
227255
try:
228256
match = attribute_parser.parseString(line+'#', parseAll=True)
229257
except pp.ParseException as err:

datajoint/query.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ def append(self, restriction):
5252
super().append(restriction)
5353

5454

55+
def is_true(restriction):
56+
return restriction is True or isinstance(restriction, AndList) and not len(restriction)
57+
58+
5559
class Query:
5660
"""
5761
Query implements the relational algebra.
@@ -249,6 +253,8 @@ def __iand__(self, restriction):
249253
250254
See query.restrict for more detail.
251255
"""
256+
if is_true(restriction):
257+
return self
252258
return (Subquery.create(self) if self.heading.expressions else self).restrict(restriction)
253259

254260
def __and__(self, restriction):
@@ -258,7 +264,7 @@ def __and__(self, restriction):
258264
See query.restrict for more detail.
259265
"""
260266
return (Subquery.create(self) # the HAVING clause in GroupBy can handle renamed attributes but WHERE cannot
261-
if self.heading.expressions and not isinstance(self, GroupBy)
267+
if not(is_true(restriction)) and self.heading.expressions and not isinstance(self, GroupBy)
262268
else self.__class__(self)).restrict(restriction)
263269

264270
def __isub__(self, restriction):
@@ -324,8 +330,8 @@ def restrict(self, restriction):
324330
:param restriction: a sequence or an array (treated as OR list), another relation, an SQL condition string, or
325331
an AndList.
326332
"""
327-
assert not self.heading.expressions or isinstance(self, GroupBy), "Cannot restrict a projection" \
328-
" with renamed attributes in place."
333+
assert is_true(restriction) or not self.heading.expressions or isinstance(self, GroupBy), \
334+
"Cannot restrict a projection with renamed attributes in place."
329335
self.restriction.append(restriction)
330336
return self
331337

datajoint/schema.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import re
66
import itertools
77
import collections
8-
import types
98
from . import conn, config
109
from .errors import DataJointError
1110
from .jobs import JobTable

datajoint/table.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,15 +488,14 @@ def describe(self, context=None, printout=True):
488488
props=index_props,
489489
class_name=lookup_class_name(parent_name, context) or parent_name)
490490
else:
491-
# expression foreign key
491+
# projected foreign key
492492
parent_name = list(self.connection.dependencies.in_edges(parent_name))[0][0]
493493
lst = [(attr, ref) for attr, ref in fk_props['attr_map'].items() if ref != attr]
494-
definition += '({attr_list}) ->{props} {class_name}{ref_list}\n'.format(
494+
definition += '->{props} {class_name}.proj({proj_list})\n'.format(
495495
attr_list=', '.join(r[0] for r in lst),
496496
props=index_props,
497497
class_name=lookup_class_name(parent_name, context) or parent_name,
498-
ref_list=('' if len(attributes_thus_far) - len(attributes_declared) == 1
499-
else '(%s)' % ','.join(r[1] for r in lst)))
498+
proj_list=','.join('{}="{}"'.format(a,b) for a, b in lst))
500499
attributes_declared.update(fk_props['attr_map'])
501500
if do_include:
502501
attributes_declared.add(attr.name)

tests/schema.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ class SigTermTable(dj.Computed):
260260
-> SimpleSource
261261
"""
262262

263-
def _make_tuples(self, key):
263+
def make(self, key):
264264
os.kill(os.getpid(), signal.SIGTERM)
265265

266266

@@ -271,12 +271,13 @@ class DecimalPrimaryKey(dj.Lookup):
271271
"""
272272
contents = zip((0.1, 0.25, 3.99))
273273

274+
274275
@schema
275276
class IndexRich(dj.Manual):
276277
definition = """
277278
-> Experiment
278279
---
279-
(first) ->[unique, nullable] User
280+
-> [unique, nullable] User.proj(first="username")
280281
first_date : date
281282
value : int
282283
index (first_date, value)

tests/schema_advanced.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class Cell(dj.Manual):
9494
class InputCell(dj.Manual):
9595
definition = """ # a synapse within the slice
9696
-> Cell
97-
(input)-> Cell(cell)
97+
-> Cell.proj(input="cell")
9898
"""
9999

100100

@@ -108,8 +108,9 @@ class LocalSynapse(dj.Manual):
108108

109109
@schema
110110
class GlobalSynapse(dj.Manual):
111+
# Mix old-style and new-style projected foreign keys
111112
definition = """
112113
# a synapse within the slice
113-
(pre_slice, pre_cell) -> Cell(slice, cell)
114+
-> Cell.proj(pre_slice="slice", pre_cell="cell")
114115
(post_slice, post_cell)-> Cell(slice, cell)
115116
"""

0 commit comments

Comments
 (0)