1616logger = 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+
3446def 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 ()
5366foreign_key_parser = build_foreign_key_parser ()
5467attribute_parser = build_attribute_parser ()
5568index_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
161190def 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 :
0 commit comments