diff options
Diffstat (limited to 'lib/sqlalchemy/dialects/mysql/reflection.py')
-rw-r--r-- | lib/sqlalchemy/dialects/mysql/reflection.py | 342 |
1 files changed, 190 insertions, 152 deletions
diff --git a/lib/sqlalchemy/dialects/mysql/reflection.py b/lib/sqlalchemy/dialects/mysql/reflection.py index e88bc3f42..d0513eb4d 100644 --- a/lib/sqlalchemy/dialects/mysql/reflection.py +++ b/lib/sqlalchemy/dialects/mysql/reflection.py @@ -36,16 +36,16 @@ class MySQLTableDefinitionParser(object): def parse(self, show_create, charset): state = ReflectedState() state.charset = charset - for line in re.split(r'\r?\n', show_create): - if line.startswith(' ' + self.preparer.initial_quote): + for line in re.split(r"\r?\n", show_create): + if line.startswith(" " + self.preparer.initial_quote): self._parse_column(line, state) # a regular table options line - elif line.startswith(') '): + elif line.startswith(") "): self._parse_table_options(line, state) # an ANSI-mode table options line - elif line == ')': + elif line == ")": pass - elif line.startswith('CREATE '): + elif line.startswith("CREATE "): self._parse_table_name(line, state) # Not present in real reflection, but may be if # loading from a file. @@ -55,11 +55,11 @@ class MySQLTableDefinitionParser(object): type_, spec = self._parse_constraints(line) if type_ is None: util.warn("Unknown schema content: %r" % line) - elif type_ == 'key': + elif type_ == "key": state.keys.append(spec) - elif type_ == 'fk_constraint': + elif type_ == "fk_constraint": state.fk_constraints.append(spec) - elif type_ == 'ck_constraint': + elif type_ == "ck_constraint": state.ck_constraints.append(spec) else: pass @@ -78,39 +78,39 @@ class MySQLTableDefinitionParser(object): # convert columns into name, length pairs # NOTE: we may want to consider SHOW INDEX as the # format of indexes in MySQL becomes more complex - spec['columns'] = self._parse_keyexprs(spec['columns']) - if spec['version_sql']: - m2 = self._re_key_version_sql.match(spec['version_sql']) - if m2 and m2.groupdict()['parser']: - spec['parser'] = m2.groupdict()['parser'] - if spec['parser']: - spec['parser'] = self.preparer.unformat_identifiers( - spec['parser'])[0] - return 'key', spec + spec["columns"] = self._parse_keyexprs(spec["columns"]) + if spec["version_sql"]: + m2 = self._re_key_version_sql.match(spec["version_sql"]) + if m2 and m2.groupdict()["parser"]: + spec["parser"] = m2.groupdict()["parser"] + if spec["parser"]: + spec["parser"] = self.preparer.unformat_identifiers( + spec["parser"] + )[0] + return "key", spec # FOREIGN KEY CONSTRAINT m = self._re_fk_constraint.match(line) if m: spec = m.groupdict() - spec['table'] = \ - self.preparer.unformat_identifiers(spec['table']) - spec['local'] = [c[0] - for c in self._parse_keyexprs(spec['local'])] - spec['foreign'] = [c[0] - for c in self._parse_keyexprs(spec['foreign'])] - return 'fk_constraint', spec + spec["table"] = self.preparer.unformat_identifiers(spec["table"]) + spec["local"] = [c[0] for c in self._parse_keyexprs(spec["local"])] + spec["foreign"] = [ + c[0] for c in self._parse_keyexprs(spec["foreign"]) + ] + return "fk_constraint", spec # CHECK constraint m = self._re_ck_constraint.match(line) if m: spec = m.groupdict() - return 'ck_constraint', spec + return "ck_constraint", spec # PARTITION and SUBPARTITION m = self._re_partition.match(line) if m: # Punt! - return 'partition', line + return "partition", line # No match. return (None, line) @@ -124,7 +124,7 @@ class MySQLTableDefinitionParser(object): regex, cleanup = self._pr_name m = regex.match(line) if m: - state.table_name = cleanup(m.group('name')) + state.table_name = cleanup(m.group("name")) def _parse_table_options(self, line, state): """Build a dictionary of all reflected table-level options. @@ -134,7 +134,7 @@ class MySQLTableDefinitionParser(object): options = {} - if not line or line == ')': + if not line or line == ")": pass else: @@ -143,17 +143,17 @@ class MySQLTableDefinitionParser(object): m = regex.search(rest_of_line) if not m: continue - directive, value = m.group('directive'), m.group('val') + directive, value = m.group("directive"), m.group("val") if cleanup: value = cleanup(value) options[directive.lower()] = value - rest_of_line = regex.sub('', rest_of_line) + rest_of_line = regex.sub("", rest_of_line) - for nope in ('auto_increment', 'data directory', 'index directory'): + for nope in ("auto_increment", "data directory", "index directory"): options.pop(nope, None) for opt, val in options.items(): - state.table_options['%s_%s' % (self.dialect.name, opt)] = val + state.table_options["%s_%s" % (self.dialect.name, opt)] = val def _parse_column(self, line, state): """Extract column details. @@ -167,29 +167,30 @@ class MySQLTableDefinitionParser(object): m = self._re_column.match(line) if m: spec = m.groupdict() - spec['full'] = True + spec["full"] = True else: m = self._re_column_loose.match(line) if m: spec = m.groupdict() - spec['full'] = False + spec["full"] = False if not spec: util.warn("Unknown column definition %r" % line) return - if not spec['full']: + if not spec["full"]: util.warn("Incomplete reflection of column definition %r" % line) - name, type_, args = spec['name'], spec['coltype'], spec['arg'] + name, type_, args = spec["name"], spec["coltype"], spec["arg"] try: col_type = self.dialect.ischema_names[type_] except KeyError: - util.warn("Did not recognize type '%s' of column '%s'" % - (type_, name)) + util.warn( + "Did not recognize type '%s' of column '%s'" % (type_, name) + ) col_type = sqltypes.NullType # Column type positional arguments eg. varchar(32) - if args is None or args == '': + if args is None or args == "": type_args = [] elif args[0] == "'" and args[-1] == "'": type_args = self._re_csv_str.findall(args) @@ -201,50 +202,51 @@ class MySQLTableDefinitionParser(object): if issubclass(col_type, (DATETIME, TIME, TIMESTAMP)): if type_args: - type_kw['fsp'] = type_args.pop(0) + type_kw["fsp"] = type_args.pop(0) - for kw in ('unsigned', 'zerofill'): + for kw in ("unsigned", "zerofill"): if spec.get(kw, False): type_kw[kw] = True - for kw in ('charset', 'collate'): + for kw in ("charset", "collate"): if spec.get(kw, False): type_kw[kw] = spec[kw] if issubclass(col_type, _EnumeratedValues): type_args = _EnumeratedValues._strip_values(type_args) - if issubclass(col_type, SET) and '' in type_args: - type_kw['retrieve_as_bitwise'] = True + if issubclass(col_type, SET) and "" in type_args: + type_kw["retrieve_as_bitwise"] = True type_instance = col_type(*type_args, **type_kw) col_kw = {} # NOT NULL - col_kw['nullable'] = True + col_kw["nullable"] = True # this can be "NULL" in the case of TIMESTAMP - if spec.get('notnull', False) == 'NOT NULL': - col_kw['nullable'] = False + if spec.get("notnull", False) == "NOT NULL": + col_kw["nullable"] = False # AUTO_INCREMENT - if spec.get('autoincr', False): - col_kw['autoincrement'] = True + if spec.get("autoincr", False): + col_kw["autoincrement"] = True elif issubclass(col_type, sqltypes.Integer): - col_kw['autoincrement'] = False + col_kw["autoincrement"] = False # DEFAULT - default = spec.get('default', None) + default = spec.get("default", None) - if default == 'NULL': + if default == "NULL": # eliminates the need to deal with this later. default = None - comment = spec.get('comment', None) + comment = spec.get("comment", None) if comment is not None: comment = comment.replace("\\\\", "\\").replace("''", "'") - col_d = dict(name=name, type=type_instance, default=default, - comment=comment) + col_d = dict( + name=name, type=type_instance, default=default, comment=comment + ) col_d.update(col_kw) state.columns.append(col_d) @@ -262,36 +264,44 @@ class MySQLTableDefinitionParser(object): buffer = [] for row in columns: - (name, col_type, nullable, default, extra) = \ - [row[i] for i in (0, 1, 2, 4, 5)] + (name, col_type, nullable, default, extra) = [ + row[i] for i in (0, 1, 2, 4, 5) + ] - line = [' '] + line = [" "] line.append(self.preparer.quote_identifier(name)) line.append(col_type) if not nullable: - line.append('NOT NULL') + line.append("NOT NULL") if default: - if 'auto_increment' in default: + if "auto_increment" in default: pass - elif (col_type.startswith('timestamp') and - default.startswith('C')): - line.append('DEFAULT') + elif col_type.startswith("timestamp") and default.startswith( + "C" + ): + line.append("DEFAULT") line.append(default) - elif default == 'NULL': - line.append('DEFAULT') + elif default == "NULL": + line.append("DEFAULT") line.append(default) else: - line.append('DEFAULT') + line.append("DEFAULT") line.append("'%s'" % default.replace("'", "''")) if extra: line.append(extra) - buffer.append(' '.join(line)) - - return ''.join([('CREATE TABLE %s (\n' % - self.preparer.quote_identifier(table_name)), - ',\n'.join(buffer), - '\n) ']) + buffer.append(" ".join(line)) + + return "".join( + [ + ( + "CREATE TABLE %s (\n" + % self.preparer.quote_identifier(table_name) + ), + ",\n".join(buffer), + "\n) ", + ] + ) def _parse_keyexprs(self, identifiers): """Unpack '"col"(2),"col" ASC'-ish strings into components.""" @@ -306,29 +316,39 @@ class MySQLTableDefinitionParser(object): _final = self.preparer.final_quote - quotes = dict(zip(('iq', 'fq', 'esc_fq'), - [re.escape(s) for s in - (self.preparer.initial_quote, - _final, - self.preparer._escape_identifier(_final))])) + quotes = dict( + zip( + ("iq", "fq", "esc_fq"), + [ + re.escape(s) + for s in ( + self.preparer.initial_quote, + _final, + self.preparer._escape_identifier(_final), + ) + ], + ) + ) self._pr_name = _pr_compile( - r'^CREATE (?:\w+ +)?TABLE +' - r'%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +\($' % quotes, - self.preparer._unescape_identifier) + r"^CREATE (?:\w+ +)?TABLE +" + r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +\($" % quotes, + self.preparer._unescape_identifier, + ) # `col`,`col2`(32),`col3`(15) DESC # self._re_keyexprs = _re_compile( - r'(?:' - r'(?:%(iq)s((?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)' - r'(?:\((\d+)\))?(?: +(ASC|DESC))?(?=\,|$))+' % quotes) + r"(?:" + r"(?:%(iq)s((?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)" + r"(?:\((\d+)\))?(?: +(ASC|DESC))?(?=\,|$))+" % quotes + ) # 'foo' or 'foo','bar' or 'fo,o','ba''a''r' - self._re_csv_str = _re_compile(r'\x27(?:\x27\x27|[^\x27])*\x27') + self._re_csv_str = _re_compile(r"\x27(?:\x27\x27|[^\x27])*\x27") # 123 or 123,456 - self._re_csv_int = _re_compile(r'\d+') + self._re_csv_int = _re_compile(r"\d+") # `colname` <type> [type opts] # (NOT NULL | NULL) @@ -356,43 +376,39 @@ class MySQLTableDefinitionParser(object): r"(?: +COLUMN_FORMAT +(?P<colfmt>\w+))?" r"(?: +STORAGE +(?P<storage>\w+))?" r"(?: +(?P<extra>.*))?" - r",?$" - % quotes + r",?$" % quotes ) # Fallback, try to parse as little as possible self._re_column_loose = _re_compile( - r' ' - r'%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +' - r'(?P<coltype>\w+)' - r'(?:\((?P<arg>(?:\d+|\d+,\d+|\x27(?:\x27\x27|[^\x27])+\x27))\))?' - r'.*?(?P<notnull>(?:NOT )NULL)?' - % quotes + r" " + r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +" + r"(?P<coltype>\w+)" + r"(?:\((?P<arg>(?:\d+|\d+,\d+|\x27(?:\x27\x27|[^\x27])+\x27))\))?" + r".*?(?P<notnull>(?:NOT )NULL)?" % quotes ) # (PRIMARY|UNIQUE|FULLTEXT|SPATIAL) INDEX `name` (USING (BTREE|HASH))? # (`col` (ASC|DESC)?, `col` (ASC|DESC)?) # KEY_BLOCK_SIZE size | WITH PARSER name /*!50100 WITH PARSER name */ self._re_key = _re_compile( - r' ' - r'(?:(?P<type>\S+) )?KEY' - r'(?: +%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)?' - r'(?: +USING +(?P<using_pre>\S+))?' - r' +\((?P<columns>.+?)\)' - r'(?: +USING +(?P<using_post>\S+))?' - r'(?: +KEY_BLOCK_SIZE *[ =]? *(?P<keyblock>\S+))?' - r'(?: +WITH PARSER +(?P<parser>\S+))?' - r'(?: +COMMENT +(?P<comment>(\x27\x27|\x27([^\x27])*?\x27)+))?' - r'(?: +/\*(?P<version_sql>.+)\*/ +)?' - r',?$' - % quotes + r" " + r"(?:(?P<type>\S+) )?KEY" + r"(?: +%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)?" + r"(?: +USING +(?P<using_pre>\S+))?" + r" +\((?P<columns>.+?)\)" + r"(?: +USING +(?P<using_post>\S+))?" + r"(?: +KEY_BLOCK_SIZE *[ =]? *(?P<keyblock>\S+))?" + r"(?: +WITH PARSER +(?P<parser>\S+))?" + r"(?: +COMMENT +(?P<comment>(\x27\x27|\x27([^\x27])*?\x27)+))?" + r"(?: +/\*(?P<version_sql>.+)\*/ +)?" + r",?$" % quotes ) # https://forums.mysql.com/read.php?20,567102,567111#msg-567111 # It means if the MySQL version >= \d+, execute what's in the comment self._re_key_version_sql = _re_compile( - r'\!\d+ ' - r'(?: *WITH PARSER +(?P<parser>\S+) *)?' + r"\!\d+ " r"(?: *WITH PARSER +(?P<parser>\S+) *)?" ) # CONSTRAINT `name` FOREIGN KEY (`local_col`) @@ -402,20 +418,19 @@ class MySQLTableDefinitionParser(object): # # unique constraints come back as KEYs kw = quotes.copy() - kw['on'] = 'RESTRICT|CASCADE|SET NULL|NOACTION' + kw["on"] = "RESTRICT|CASCADE|SET NULL|NOACTION" self._re_fk_constraint = _re_compile( - r' ' - r'CONSTRAINT +' - r'%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +' - r'FOREIGN KEY +' - r'\((?P<local>[^\)]+?)\) REFERENCES +' - r'(?P<table>%(iq)s[^%(fq)s]+%(fq)s' - r'(?:\.%(iq)s[^%(fq)s]+%(fq)s)?) +' - r'\((?P<foreign>[^\)]+?)\)' - r'(?: +(?P<match>MATCH \w+))?' - r'(?: +ON DELETE (?P<ondelete>%(on)s))?' - r'(?: +ON UPDATE (?P<onupdate>%(on)s))?' - % kw + r" " + r"CONSTRAINT +" + r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +" + r"FOREIGN KEY +" + r"\((?P<local>[^\)]+?)\) REFERENCES +" + r"(?P<table>%(iq)s[^%(fq)s]+%(fq)s" + r"(?:\.%(iq)s[^%(fq)s]+%(fq)s)?) +" + r"\((?P<foreign>[^\)]+?)\)" + r"(?: +(?P<match>MATCH \w+))?" + r"(?: +ON DELETE (?P<ondelete>%(on)s))?" + r"(?: +ON UPDATE (?P<onupdate>%(on)s))?" % kw ) # CONSTRAINT `CONSTRAINT_1` CHECK (`x` > 5)' @@ -423,18 +438,17 @@ class MySQLTableDefinitionParser(object): # is returned on a line by itself, so to match without worrying # about parenthesis in the expresion we go to the end of the line self._re_ck_constraint = _re_compile( - r' ' - r'CONSTRAINT +' - r'%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +' - r'CHECK +' - r'\((?P<sqltext>.+)\),?' - % kw + r" " + r"CONSTRAINT +" + r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +" + r"CHECK +" + r"\((?P<sqltext>.+)\),?" % kw ) # PARTITION # # punt! - self._re_partition = _re_compile(r'(?:.*)(?:SUB)?PARTITION(?:.*)') + self._re_partition = _re_compile(r"(?:.*)(?:SUB)?PARTITION(?:.*)") # Table-level options (COLLATE, ENGINE, etc.) # Do the string options first, since they have quoted @@ -442,44 +456,68 @@ class MySQLTableDefinitionParser(object): for option in _options_of_type_string: self._add_option_string(option) - for option in ('ENGINE', 'TYPE', 'AUTO_INCREMENT', - 'AVG_ROW_LENGTH', 'CHARACTER SET', - 'DEFAULT CHARSET', 'CHECKSUM', - 'COLLATE', 'DELAY_KEY_WRITE', 'INSERT_METHOD', - 'MAX_ROWS', 'MIN_ROWS', 'PACK_KEYS', 'ROW_FORMAT', - 'KEY_BLOCK_SIZE'): + for option in ( + "ENGINE", + "TYPE", + "AUTO_INCREMENT", + "AVG_ROW_LENGTH", + "CHARACTER SET", + "DEFAULT CHARSET", + "CHECKSUM", + "COLLATE", + "DELAY_KEY_WRITE", + "INSERT_METHOD", + "MAX_ROWS", + "MIN_ROWS", + "PACK_KEYS", + "ROW_FORMAT", + "KEY_BLOCK_SIZE", + ): self._add_option_word(option) - self._add_option_regex('UNION', r'\([^\)]+\)') - self._add_option_regex('TABLESPACE', r'.*? STORAGE DISK') + self._add_option_regex("UNION", r"\([^\)]+\)") + self._add_option_regex("TABLESPACE", r".*? STORAGE DISK") self._add_option_regex( - 'RAID_TYPE', - r'\w+\s+RAID_CHUNKS\s*\=\s*\w+RAID_CHUNKSIZE\s*=\s*\w+') + "RAID_TYPE", + r"\w+\s+RAID_CHUNKS\s*\=\s*\w+RAID_CHUNKSIZE\s*=\s*\w+", + ) - _optional_equals = r'(?:\s*(?:=\s*)|\s+)' + _optional_equals = r"(?:\s*(?:=\s*)|\s+)" def _add_option_string(self, directive): - regex = (r'(?P<directive>%s)%s' - r"'(?P<val>(?:[^']|'')*?)'(?!')" % - (re.escape(directive), self._optional_equals)) - self._pr_options.append(_pr_compile( - regex, lambda v: v.replace("\\\\", "\\").replace("''", "'") - )) + regex = r"(?P<directive>%s)%s" r"'(?P<val>(?:[^']|'')*?)'(?!')" % ( + re.escape(directive), + self._optional_equals, + ) + self._pr_options.append( + _pr_compile( + regex, lambda v: v.replace("\\\\", "\\").replace("''", "'") + ) + ) def _add_option_word(self, directive): - regex = (r'(?P<directive>%s)%s' - r'(?P<val>\w+)' % - (re.escape(directive), self._optional_equals)) + regex = r"(?P<directive>%s)%s" r"(?P<val>\w+)" % ( + re.escape(directive), + self._optional_equals, + ) self._pr_options.append(_pr_compile(regex)) def _add_option_regex(self, directive, regex): - regex = (r'(?P<directive>%s)%s' - r'(?P<val>%s)' % - (re.escape(directive), self._optional_equals, regex)) + regex = r"(?P<directive>%s)%s" r"(?P<val>%s)" % ( + re.escape(directive), + self._optional_equals, + regex, + ) self._pr_options.append(_pr_compile(regex)) -_options_of_type_string = ('COMMENT', 'DATA DIRECTORY', 'INDEX DIRECTORY', - 'PASSWORD', 'CONNECTION') + +_options_of_type_string = ( + "COMMENT", + "DATA DIRECTORY", + "INDEX DIRECTORY", + "PASSWORD", + "CONNECTION", +) def _pr_compile(regex, cleanup=None): |