16
16
logger = logging .getLogger (__name__ )
17
17
18
18
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.
20
22
left = pp .Literal ('(' ).suppress ()
21
23
right = pp .Literal (')' ).suppress ()
22
24
attribute_name = pp .Word (pp .srange ('[a-z]' ), pp .srange ('[a-z0-9_]' ))
@@ -31,6 +33,16 @@ def build_foreign_key_parser():
31
33
return new_attrs + arrow + options + ref_table + ref_attrs
32
34
33
35
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
+
34
46
def build_attribute_parser ():
35
47
quoted = pp .Or (pp .QuotedString ('"' ), pp .QuotedString ("'" ))
36
48
colon = pp .Literal (':' ).suppress ()
@@ -50,6 +62,7 @@ def build_index_parser():
50
62
return unique + index + left + pp .delimitedList (attribute_name ).setResultsName ('attr_list' ) + right
51
63
52
64
65
+ foreign_key_parser_old = build_foreign_key_parser_old ()
53
66
foreign_key_parser = build_foreign_key_parser ()
54
67
attribute_parser = build_attribute_parser ()
55
68
index_parser = build_index_parser ()
@@ -77,16 +90,22 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
77
90
"""
78
91
# Parse and validate
79
92
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
80
96
try :
81
97
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
84
105
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 :
87
108
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 )
90
109
91
110
options = [opt .upper () for opt in result .options ]
92
111
for opt in options : # check for invalid options
@@ -97,65 +116,75 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
97
116
if is_nullable and primary_key is not None :
98
117
raise DataJointError ('Primary dependencies cannot be nullable in line "{line}"' .format (line = line ))
99
118
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 )))
138
167
139
168
# 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 )))
147
177
148
178
# declare the foreign key
149
179
foreign_key_sql .append (
150
180
'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 ))
154
184
155
185
# declare unique index
156
186
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 )))
159
188
160
189
161
190
def declare (full_table_name , definition , context ):
@@ -223,7 +252,6 @@ def compile_attribute(line, in_key, foreign_key_sql):
223
252
:param foreign_key_sql:
224
253
:returns: (name, sql, is_external) -- attribute name and sql code for its declaration
225
254
"""
226
-
227
255
try :
228
256
match = attribute_parser .parseString (line + '#' , parseAll = True )
229
257
except pp .ParseException as err :
0 commit comments