55import re
66import pyparsing as pp
77import logging
8-
98from .errors import DataJointError
109
10+ from .utils import OrderedDict
11+
1112UUID_DATA_TYPE = 'binary(16)'
1213MAX_TABLE_NAME_LENGTH = 64
1314CONSTANT_LITERALS = {'CURRENT_TIMESTAMP' } # SQL literals to be used without quotes (case insensitive)
@@ -218,20 +219,7 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
218219 index_sql .append ('UNIQUE INDEX ({attrs})' .format (attrs = '`,`' .join (ref .primary_key )))
219220
220221
221- def declare (full_table_name , definition , context ):
222- """
223- Parse declaration and create new SQL table accordingly.
224-
225- :param full_table_name: full name of the table
226- :param definition: DataJoint table definition
227- :param context: dictionary of objects that might be referred to in the table.
228- """
229- table_name = full_table_name .strip ('`' ).split ('.' )[1 ]
230- if len (table_name ) > MAX_TABLE_NAME_LENGTH :
231- raise DataJointError (
232- 'Table name `{name}` exceeds the max length of {max_length}' .format (
233- name = table_name ,
234- max_length = MAX_TABLE_NAME_LENGTH ))
222+ def prepare_declare (definition , context ):
235223 # split definition into lines
236224 definition = re .split (r'\s*\n\s*' , definition .strip ())
237225 # check for optional table comment
@@ -266,7 +254,28 @@ def declare(full_table_name, definition, context):
266254 if name not in attributes :
267255 attributes .append (name )
268256 attribute_sql .append (sql )
269- # compile SQL
257+
258+ return table_comment , primary_key , attribute_sql , foreign_key_sql , index_sql , external_stores
259+
260+
261+ def declare (full_table_name , definition , context ):
262+ """
263+ Parse declaration and generate the SQL CREATE TABLE code
264+ :param full_table_name: full name of the table
265+ :param definition: DataJoint table definition
266+ :param context: dictionary of objects that might be referred to in the table
267+ :return: SQL CREATE TABLE statement, list of external stores used
268+ """
269+ table_name = full_table_name .strip ('`' ).split ('.' )[1 ]
270+ if len (table_name ) > MAX_TABLE_NAME_LENGTH :
271+ raise DataJointError (
272+ 'Table name `{name}` exceeds the max length of {max_length}' .format (
273+ name = table_name ,
274+ max_length = MAX_TABLE_NAME_LENGTH ))
275+
276+ table_comment , primary_key , attribute_sql , foreign_key_sql , index_sql , external_stores = prepare_declare (
277+ definition , context )
278+
270279 if not primary_key :
271280 raise DataJointError ('Table must have a primary key' )
272281
@@ -276,6 +285,94 @@ def declare(full_table_name, definition, context):
276285 '\n ) ENGINE=InnoDB, COMMENT "%s"' % table_comment ), external_stores
277286
278287
288+ def _make_attribute_alter (new , old , primary_key ):
289+ """
290+ :param new: new attribute declarations
291+ :param old: old attribute declarations
292+ :param primary_key: primary key attributes
293+ :return: list of SQL ALTER commands
294+ """
295+
296+ # parse attribute names
297+ name_regexp = re .compile (r"^`(?P<name>\w+)`" )
298+ original_regexp = re .compile (r'COMMENT "\{\s*(?P<name>\w+)\s*\}' )
299+ matched = ((name_regexp .match (d ), original_regexp .search (d )) for d in new )
300+ new_names = OrderedDict ((d .group ('name' ), n and n .group ('name' )) for d , n in matched )
301+ old_names = [name_regexp .search (d ).group ('name' ) for d in old ]
302+
303+ # verify that original names are only used once
304+ renamed = set ()
305+ for v in new_names .values ():
306+ if v :
307+ if v in renamed :
308+ raise DataJointError ('Alter attempted to rename attribute {%s} twice.' % v )
309+ renamed .add (v )
310+
311+ # verify that all renamed attributes existed in the old definition
312+ try :
313+ raise DataJointError (
314+ "Attribute {} does not exist in the original definition" .format (
315+ next (attr for attr in renamed if attr not in old_names )))
316+ except StopIteration :
317+ pass
318+
319+ # dropping attributes
320+ to_drop = [n for n in old_names if n not in renamed and n not in new_names ]
321+ sql = ['DROP `%s`' % n for n in to_drop ]
322+ old_names = [name for name in old_names if name not in to_drop ]
323+
324+ # add or change attributes in order
325+ prev = None
326+ for new_def , (new_name , old_name ) in zip (new , new_names .items ()):
327+ if new_name not in primary_key :
328+ after = None # if None, then must include the AFTER clause
329+ if prev :
330+ try :
331+ idx = old_names .index (old_name or new_name )
332+ except ValueError :
333+ after = prev [0 ]
334+ else :
335+ if idx >= 1 and old_names [idx - 1 ] != (prev [1 ] or prev [0 ]):
336+ after = prev [0 ]
337+ if new_def not in old or after :
338+ sql .append ('{command} {new_def} {after}' .format (
339+ command = ("ADD" if (old_name or new_name ) not in old_names else
340+ "MODIFY" if not old_name else
341+ "CHANGE `%s`" % old_name ),
342+ new_def = new_def ,
343+ after = "" if after is None else "AFTER `%s`" % after ))
344+ prev = new_name , old_name
345+
346+ return sql
347+
348+
349+ def alter (definition , old_definition , context ):
350+ """
351+ :param definition: new table definition
352+ :param old_definition: current table definition
353+ :param context: the context in which to evaluate foreign key definitions
354+ :return: string SQL ALTER command, list of new stores used for external storage
355+ """
356+ table_comment , primary_key , attribute_sql , foreign_key_sql , index_sql , external_stores = prepare_declare (
357+ definition , context )
358+ table_comment_ , primary_key_ , attribute_sql_ , foreign_key_sql_ , index_sql_ , external_stores_ = prepare_declare (
359+ old_definition , context )
360+
361+ # analyze differences between declarations
362+ sql = list ()
363+ if primary_key != primary_key_ :
364+ raise NotImplementedError ('table.alter cannot alter the primary key (yet).' )
365+ if foreign_key_sql != foreign_key_sql_ :
366+ raise NotImplementedError ('table.alter cannot alter foreign keys (yet).' )
367+ if index_sql != index_sql_ :
368+ raise NotImplementedError ('table.alter cannot alter indexes (yet)' )
369+ if attribute_sql != attribute_sql_ :
370+ sql .extend (_make_attribute_alter (attribute_sql , attribute_sql_ , primary_key ))
371+ if table_comment != table_comment_ :
372+ sql .append ('COMMENT="%s"' % table_comment )
373+ return sql , [e for e in external_stores if e not in external_stores_ ]
374+
375+
279376def compile_index (line , index_sql ):
280377 match = index_parser .parseString (line )
281378 index_sql .append ('{unique} index ({attrs})' .format (
0 commit comments