66import pyparsing as pp
77import logging
88from .errors import DataJointError
9+ from .attribute_adapter import get_adapter
910
1011from .utils import OrderedDict
1112
2728 INTERNAL_ATTACH = r'attach$' ,
2829 EXTERNAL_ATTACH = r'attach@(?P<store>[a-z]\w*)$' ,
2930 FILEPATH = r'filepath@(?P<store>[a-z]\w*)$' ,
30- UUID = r'uuid$' ).items ()}
31+ UUID = r'uuid$' ,
32+ ADAPTED = r'<.+>$'
33+ ).items ()}
3134
32- CUSTOM_TYPES = {'UUID' , 'INTERNAL_ATTACH' , 'EXTERNAL_ATTACH' , 'EXTERNAL_BLOB' , 'FILEPATH' } # types stored in attribute comment
35+ # custom types are stored in attribute comment
36+ SPECIAL_TYPES = {'UUID' , 'INTERNAL_ATTACH' , 'EXTERNAL_ATTACH' , 'EXTERNAL_BLOB' , 'FILEPATH' , 'ADAPTED' }
37+ NATIVE_TYPES = set (TYPE_PATTERN ) - SPECIAL_TYPES
3338EXTERNAL_TYPES = {'EXTERNAL_ATTACH' , 'EXTERNAL_BLOB' , 'FILEPATH' } # data referenced by a UUID in external tables
3439SERIALIZED_TYPES = {'EXTERNAL_ATTACH' , 'INTERNAL_ATTACH' , 'EXTERNAL_BLOB' , 'INTERNAL_BLOB' } # requires packing data
3540
36- assert set ().union (CUSTOM_TYPES , EXTERNAL_TYPES , SERIALIZED_TYPES ) <= set (TYPE_PATTERN )
41+ assert set ().union (SPECIAL_TYPES , EXTERNAL_TYPES , SERIALIZED_TYPES ) <= set (TYPE_PATTERN )
3742
3843
39- def match_type (datatype ):
40- for category , pattern in TYPE_PATTERN .items ():
41- match = pattern .match (datatype )
42- if match :
43- return category , match
44- raise DataJointError ('Unsupported data types "%s"' % datatype )
44+ def match_type (attribute_type ):
45+ try :
46+ return next (category for category , pattern in TYPE_PATTERN .items () if pattern .match (attribute_type ))
47+ except StopIteration :
48+ raise DataJointError ("Unsupported attribute type {type}" .format (type = attribute_type )) from None
4549
4650
4751logger = logging .getLogger (__name__ )
@@ -78,7 +82,8 @@ def build_attribute_parser():
7882 quoted = pp .QuotedString ('"' ) ^ pp .QuotedString ("'" )
7983 colon = pp .Literal (':' ).suppress ()
8084 attribute_name = pp .Word (pp .srange ('[a-z]' ), pp .srange ('[a-z0-9_]' )).setResultsName ('name' )
81- data_type = pp .Combine (pp .Word (pp .alphas ) + pp .SkipTo ("#" , ignore = quoted )).setResultsName ('type' )
85+ data_type = (pp .Combine (pp .Word (pp .alphas ) + pp .SkipTo ("#" , ignore = quoted ))
86+ ^ pp .QuotedString ('<' , endQuoteChar = '>' , unquoteResults = False )).setResultsName ('type' )
8287 default = pp .Literal ('=' ).suppress () + pp .SkipTo (colon , ignore = quoted ).setResultsName ('default' )
8388 comment = pp .Literal ('#' ).suppress () + pp .restOfLine .setResultsName ('comment' )
8489 return attribute_name + pp .Optional (default ) + colon + data_type + comment
@@ -168,8 +173,7 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
168173 raise DataJointError ('Invalid foreign key attributes in "%s"' % line )
169174 try :
170175 raise DataJointError ('Duplicate attributes "{attr}" in "{line}"' .format (
171- attr = next (attr for attr in result .new_attrs if attr in attributes ),
172- line = line ))
176+ attr = next (attr for attr in result .new_attrs if attr in attributes ), line = line ))
173177 except StopIteration :
174178 pass # the normal outcome
175179
@@ -246,7 +250,7 @@ def prepare_declare(definition, context):
246250 elif re .match (r'^(unique\s+)?index[^:]*$' , line , re .I ): # index
247251 compile_index (line , index_sql )
248252 else :
249- name , sql , store = compile_attribute (line , in_key , foreign_key_sql )
253+ name , sql , store = compile_attribute (line , in_key , foreign_key_sql , context )
250254 if store :
251255 external_stores .append (store )
252256 if in_key and name not in primary_key :
@@ -292,10 +296,9 @@ def _make_attribute_alter(new, old, primary_key):
292296 :param primary_key: primary key attributes
293297 :return: list of SQL ALTER commands
294298 """
295-
296299 # parse attribute names
297300 name_regexp = re .compile (r"^`(?P<name>\w+)`" )
298- original_regexp = re .compile (r'COMMENT "\ {\s*(?P<name>\w+)\s*\ }' )
301+ original_regexp = re .compile (r'COMMENT "{\s*(?P<name>\w+)\s*}' )
299302 matched = ((name_regexp .match (d ), original_regexp .search (d )) for d in new )
300303 new_names = OrderedDict ((d .group ('name' ), n and n .group ('name' )) for d , n in matched )
301304 old_names = [name_regexp .search (d ).group ('name' ) for d in old ]
@@ -380,13 +383,41 @@ def compile_index(line, index_sql):
380383 attrs = ',' .join ('`%s`' % a for a in match .attr_list )))
381384
382385
383- def compile_attribute ( line , in_key , foreign_key_sql ):
386+ def substitute_special_type ( match , category , foreign_key_sql , context ):
384387 """
385- Convert attribute definition from DataJoint format to SQL
388+ :param match: dict containing with keys "type" and "comment" -- will be modified in place
389+ :param category: attribute type category from TYPE_PATTERN
390+ :param foreign_key_sql: list of foreign key declarations to add to
391+ :param context: context for looking up user-defined attribute_type adapters
392+ """
393+ if category == 'UUID' :
394+ match ['type' ] = UUID_DATA_TYPE
395+ elif category == 'INTERNAL_ATTACH' :
396+ match ['type' ] = 'LONGBLOB'
397+ elif category in EXTERNAL_TYPES :
398+ match ['store' ] = match ['type' ].split ('@' , 1 )[1 ]
399+ match ['type' ] = UUID_DATA_TYPE
400+ foreign_key_sql .append (
401+ "FOREIGN KEY (`{name}`) REFERENCES `{{database}}`.`{external_table_root}_{store}` (`hash`) "
402+ "ON UPDATE RESTRICT ON DELETE RESTRICT" .format (external_table_root = EXTERNAL_TABLE_ROOT , ** match ))
403+ elif category == 'ADAPTED' :
404+ adapter = get_adapter (context , match ['type' ])
405+ match ['type' ] = adapter .attribute_type
406+ category = match_type (match ['type' ])
407+ if category in SPECIAL_TYPES :
408+ # recursive redefinition from user-defined datatypes.
409+ substitute_special_type (match , category , foreign_key_sql , context )
410+ else :
411+ assert False , 'Unknown special type'
412+
386413
414+ def compile_attribute (line , in_key , foreign_key_sql , context ):
415+ """
416+ Convert attribute definition from DataJoint format to SQL
387417 :param line: attribution line
388418 :param in_key: set to True if attribute is in primary key set
389- :param foreign_key_sql:
419+ :param foreign_key_sql: the list of foreign key declarations to add to
420+ :param context: context in which to look up user-defined attribute type adapterss
390421 :returns: (name, sql, is_external) -- attribute name and sql code for its declaration
391422 """
392423 try :
@@ -412,27 +443,18 @@ def compile_attribute(line, in_key, foreign_key_sql):
412443 match ['default' ] = 'NOT NULL'
413444
414445 match ['comment' ] = match ['comment' ].replace ('"' , '\\ "' ) # escape double quotes in comment
415- category , type_match = match_type (match ['type' ])
416446
417447 if match ['comment' ].startswith (':' ):
418448 raise DataJointError ('An attribute comment must not start with a colon in comment "{comment}"' .format (** match ))
419449
420- if category in CUSTOM_TYPES :
450+ category = match_type (match ['type' ])
451+ if category in SPECIAL_TYPES :
421452 match ['comment' ] = ':{type}:{comment}' .format (** match ) # insert custom type into comment
422- if category == 'UUID' :
423- match ['type' ] = UUID_DATA_TYPE
424- elif category == 'INTERNAL_ATTACH' :
425- match ['type' ] = 'LONGBLOB'
426- elif category in EXTERNAL_TYPES :
427- match ['store' ] = match ['type' ].split ('@' , 1 )[1 ]
428- match ['type' ] = UUID_DATA_TYPE
429- foreign_key_sql .append (
430- "FOREIGN KEY (`{name}`) REFERENCES `{{database}}`.`{external_table_root}_{store}` (`hash`) "
431- "ON UPDATE RESTRICT ON DELETE RESTRICT" .format (external_table_root = EXTERNAL_TABLE_ROOT , ** match ))
453+ substitute_special_type (match , category , foreign_key_sql , context )
432454
433455 if category in SERIALIZED_TYPES and match ['default' ] not in {'DEFAULT NULL' , 'NOT NULL' }:
434456 raise DataJointError (
435- 'The default value for a blob or attachment attributes can only be NULL in:\n %s' % line )
457+ 'The default value for a blob or attachment attributes can only be NULL in:\n {line}' . format ( line = line ) )
436458
437459 sql = ('`{name}` {type} {default}' + (' COMMENT "{comment}"' if match ['comment' ] else '' )).format (** match )
438460 return match ['name' ], sql , match .get ('store' )
0 commit comments