6
6
import pyparsing as pp
7
7
import logging
8
8
from .errors import DataJointError
9
+ from .attribute_adapter import get_adapter
9
10
10
11
from .utils import OrderedDict
11
12
27
28
INTERNAL_ATTACH = r'attach$' ,
28
29
EXTERNAL_ATTACH = r'attach@(?P<store>[a-z]\w*)$' ,
29
30
FILEPATH = r'filepath@(?P<store>[a-z]\w*)$' ,
30
- UUID = r'uuid$' ).items ()}
31
+ UUID = r'uuid$' ,
32
+ ADAPTED = r'<.+>$'
33
+ ).items ()}
31
34
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
33
38
EXTERNAL_TYPES = {'EXTERNAL_ATTACH' , 'EXTERNAL_BLOB' , 'FILEPATH' } # data referenced by a UUID in external tables
34
39
SERIALIZED_TYPES = {'EXTERNAL_ATTACH' , 'INTERNAL_ATTACH' , 'EXTERNAL_BLOB' , 'INTERNAL_BLOB' } # requires packing data
35
40
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 )
37
42
38
43
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
45
49
46
50
47
51
logger = logging .getLogger (__name__ )
@@ -78,7 +82,8 @@ def build_attribute_parser():
78
82
quoted = pp .QuotedString ('"' ) ^ pp .QuotedString ("'" )
79
83
colon = pp .Literal (':' ).suppress ()
80
84
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' )
82
87
default = pp .Literal ('=' ).suppress () + pp .SkipTo (colon , ignore = quoted ).setResultsName ('default' )
83
88
comment = pp .Literal ('#' ).suppress () + pp .restOfLine .setResultsName ('comment' )
84
89
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
168
173
raise DataJointError ('Invalid foreign key attributes in "%s"' % line )
169
174
try :
170
175
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 ))
173
177
except StopIteration :
174
178
pass # the normal outcome
175
179
@@ -246,7 +250,7 @@ def prepare_declare(definition, context):
246
250
elif re .match (r'^(unique\s+)?index[^:]*$' , line , re .I ): # index
247
251
compile_index (line , index_sql )
248
252
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 )
250
254
if store :
251
255
external_stores .append (store )
252
256
if in_key and name not in primary_key :
@@ -292,10 +296,9 @@ def _make_attribute_alter(new, old, primary_key):
292
296
:param primary_key: primary key attributes
293
297
:return: list of SQL ALTER commands
294
298
"""
295
-
296
299
# parse attribute names
297
300
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*}' )
299
302
matched = ((name_regexp .match (d ), original_regexp .search (d )) for d in new )
300
303
new_names = OrderedDict ((d .group ('name' ), n and n .group ('name' )) for d , n in matched )
301
304
old_names = [name_regexp .search (d ).group ('name' ) for d in old ]
@@ -380,13 +383,41 @@ def compile_index(line, index_sql):
380
383
attrs = ',' .join ('`%s`' % a for a in match .attr_list )))
381
384
382
385
383
- def compile_attribute ( line , in_key , foreign_key_sql ):
386
+ def substitute_special_type ( match , category , foreign_key_sql , context ):
384
387
"""
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
+
386
413
414
+ def compile_attribute (line , in_key , foreign_key_sql , context ):
415
+ """
416
+ Convert attribute definition from DataJoint format to SQL
387
417
:param line: attribution line
388
418
: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
390
421
:returns: (name, sql, is_external) -- attribute name and sql code for its declaration
391
422
"""
392
423
try :
@@ -412,27 +443,18 @@ def compile_attribute(line, in_key, foreign_key_sql):
412
443
match ['default' ] = 'NOT NULL'
413
444
414
445
match ['comment' ] = match ['comment' ].replace ('"' , '\\ "' ) # escape double quotes in comment
415
- category , type_match = match_type (match ['type' ])
416
446
417
447
if match ['comment' ].startswith (':' ):
418
448
raise DataJointError ('An attribute comment must not start with a colon in comment "{comment}"' .format (** match ))
419
449
420
- if category in CUSTOM_TYPES :
450
+ category = match_type (match ['type' ])
451
+ if category in SPECIAL_TYPES :
421
452
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 )
432
454
433
455
if category in SERIALIZED_TYPES and match ['default' ] not in {'DEFAULT NULL' , 'NOT NULL' }:
434
456
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 ) )
436
458
437
459
sql = ('`{name}` {type} {default}' + (' COMMENT "{comment}"' if match ['comment' ] else '' )).format (** match )
438
460
return match ['name' ], sql , match .get ('store' )
0 commit comments