summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/databases/mysql.py
diff options
context:
space:
mode:
authorJason Kirtland <jek@discorporate.us>2007-08-11 01:03:37 +0000
committerJason Kirtland <jek@discorporate.us>2007-08-11 01:03:37 +0000
commit7a545a886973d76f934e0c234b69be8938cc9b20 (patch)
tree092b542f4228337e3ec383313fa5c2d3ab592bc1 /lib/sqlalchemy/databases/mysql.py
parent1391efea78d552fa81dd056c21e7570fba437bcf (diff)
downloadsqlalchemy-7a545a886973d76f934e0c234b69be8938cc9b20.tar.gz
by popular demand, mysql reflection is now a single round-trip and uses a parse of SHOW CREATE TABLE ddl [ticket:612]
the ANSI_QUOTES mode is now supported halfway there for auto_increment on secondary columns [ticket:649] indexes are now reflected [ticket:663]
Diffstat (limited to 'lib/sqlalchemy/databases/mysql.py')
-rw-r--r--lib/sqlalchemy/databases/mysql.py889
1 files changed, 718 insertions, 171 deletions
diff --git a/lib/sqlalchemy/databases/mysql.py b/lib/sqlalchemy/databases/mysql.py
index 0b9ab3531..f0b18d3ac 100644
--- a/lib/sqlalchemy/databases/mysql.py
+++ b/lib/sqlalchemy/databases/mysql.py
@@ -62,10 +62,28 @@ time::
Table('mytable', metadata, autoload=True,
ForeignKeyConstraint(['other_id'], ['othertable.other_id']))
+MySQL SQL modes are supported. Modes that enable ``ANSI_QUOTE`` (such as
+``ANSI``) require an engine option to modify SQLAlchemy's quoting style.
+When using an ANSI-quoting mode, supply ``use_ansiquotes=True`` when
+creating your ``Engine``::
+
+ create_engine('mysql://localhost/test', use_ansiquotes=True)
+
+This is an engine-wide option and is not toggleable on a per-connection basis.
+SQLAlchemy does not presume to ``SET sql_mode`` for you with this option.
+For the best performance, set the quoting style server-wide in ``my.cnf`` or
+by supplying ``--sql-mode`` to ``mysqld``. You can also use a ``Pool`` hook
+to issue a ``SET SESSION sql_mode='...'`` on connect to configure each
+connection.
+
For normal SQLAlchemy usage, loading this module is unnescesary. It will be
-loaded on-demand when a MySQL connection is needed. If you would like to use
-one of the MySQL-specific or enhanced column types when creating tables with
-your ``Table`` definitions, then you will need to import them from this module::
+loaded on-demand when a MySQL connection is needed. The generic column types
+like ``String`` and ``Integer`` will automatically be adapted to the optimal
+matching MySQL column type.
+
+But if you would like to use one of the MySQL-specific or enhanced column
+types when creating tables with your ``Table`` definitions, then you will
+need to import them from this module::
from sqlalchemy.databases import mysql
@@ -74,6 +92,12 @@ your ``Table`` definitions, then you will need to import them from this module::
Column('ittybittyblob', mysql.MSTinyBlob),
Column('biggy', mysql.MSBigInteger(unsigned=True)))
+All standard MySQL column types are supported. The OpenGIS types are
+available for use via table reflection but have no special support or
+mapping to Python classes. If you're using these types and have opinions
+about how OpenGIS can be smartly integrated into SQLAlchemy please join
+the mailing list!
+
If you have problems that seem server related, first check that you are
using the most recent stable MySQL-Python package available. The Database
Notes page on the wiki at http://sqlalchemy.org is a good resource for timely
@@ -83,11 +107,19 @@ information affecting MySQL in SQLAlchemy.
import re, datetime, inspect, warnings, operator, sys
from array import array as _array
-from sqlalchemy import sql, schema, ansisql
+from sqlalchemy import ansisql, exceptions, logging, schema, sql, util
from sqlalchemy.engine import base as engine_base, default
import sqlalchemy.types as sqltypes
-import sqlalchemy.exceptions as exceptions
-import sqlalchemy.util as util
+
+
+__all__ = (
+ 'MSBigInteger', 'MSBinary', 'MSBit', 'MSBlob', 'MSBoolean',
+ 'MSChar', 'MSDate', 'MSDateTime', 'MSDecimal', 'MSDouble',
+ 'MSEnum', 'MSFloat', 'MSInteger', 'MSLongBlob', 'MSLongText',
+ 'MSMediumBlob', 'MSMediumText', 'MSNChar', 'MSNVarChar',
+ 'MSNumeric', 'MSSet', 'MSSmallInteger', 'MSString', 'MSText',
+ 'MSTime', 'MSTimeStamp', 'MSTinyBlob', 'MSTinyInteger',
+ 'MSTinyText', 'MSVarBinary', 'MSYear' )
RESERVED_WORDS = util.Set(
@@ -207,7 +239,7 @@ class _StringType(object):
class MSNumeric(sqltypes.Numeric, _NumericType):
- """MySQL NUMERIC type"""
+ """MySQL NUMERIC type."""
def __init__(self, precision=10, length=2, asdecimal=True, **kw):
"""Construct a NUMERIC.
@@ -248,7 +280,7 @@ class MSNumeric(sqltypes.Numeric, _NumericType):
class MSDecimal(MSNumeric):
- """MySQL DECIMAL type"""
+ """MySQL DECIMAL type."""
def __init__(self, precision=10, length=2, asdecimal=True, **kw):
"""Construct a DECIMAL.
@@ -281,7 +313,7 @@ class MSDecimal(MSNumeric):
class MSDouble(MSNumeric):
- """MySQL DOUBLE type"""
+ """MySQL DOUBLE type."""
def __init__(self, precision=10, length=2, asdecimal=True, **kw):
"""Construct a DOUBLE.
@@ -317,7 +349,7 @@ class MSDouble(MSNumeric):
class MSFloat(sqltypes.Float, _NumericType):
- """MySQL FLOAT type"""
+ """MySQL FLOAT type."""
def __init__(self, precision=10, length=None, asdecimal=False, **kw):
"""Construct a FLOAT.
@@ -356,7 +388,7 @@ class MSFloat(sqltypes.Float, _NumericType):
class MSInteger(sqltypes.Integer, _NumericType):
- """MySQL INTEGER type"""
+ """MySQL INTEGER type."""
def __init__(self, length=None, **kw):
"""Construct an INTEGER.
@@ -385,7 +417,7 @@ class MSInteger(sqltypes.Integer, _NumericType):
class MSBigInteger(MSInteger):
- """MySQL BIGINTEGER type"""
+ """MySQL BIGINTEGER type."""
def __init__(self, length=None, **kw):
"""Construct a BIGINTEGER.
@@ -412,7 +444,7 @@ class MSBigInteger(MSInteger):
class MSTinyInteger(MSInteger):
- """MySQL TINYINT type"""
+ """MySQL TINYINT type."""
def __init__(self, length=None, **kw):
"""Construct a TINYINT.
@@ -443,7 +475,7 @@ class MSTinyInteger(MSInteger):
class MSSmallInteger(sqltypes.Smallinteger, _NumericType):
- """MySQL SMALLINTEGER type"""
+ """MySQL SMALLINTEGER type."""
def __init__(self, length=None, **kw):
"""Construct a SMALLINTEGER.
@@ -472,7 +504,7 @@ class MSSmallInteger(sqltypes.Smallinteger, _NumericType):
class MSBit(sqltypes.TypeEngine):
- """MySQL BIT type
+ """MySQL BIT type.
This type is for MySQL 5.0.3 or greater for MyISAM, and 5.0.5 or greater for
MyISAM, MEMORY, InnoDB and BDB. For older versions, use a MSTinyInteger(1)
@@ -483,7 +515,7 @@ class MSBit(sqltypes.TypeEngine):
self.length = length
def convert_result_value(self, value, dialect):
- """Converts MySQL's 64 bit, variable length binary string to a long."""
+ """Convert a MySQL's 64 bit, variable length binary string to a long."""
if value is not None:
v = 0L
@@ -500,21 +532,21 @@ class MSBit(sqltypes.TypeEngine):
class MSDateTime(sqltypes.DateTime):
- """MySQL DATETIME type"""
+ """MySQL DATETIME type."""
def get_col_spec(self):
return "DATETIME"
class MSDate(sqltypes.Date):
- """MySQL DATE type"""
+ """MySQL DATE type."""
def get_col_spec(self):
return "DATE"
class MSTime(sqltypes.Time):
- """MySQL TIME type"""
+ """MySQL TIME type."""
def get_col_spec(self):
return "TIME"
@@ -528,10 +560,10 @@ class MSTime(sqltypes.Time):
class MSTimeStamp(sqltypes.TIMESTAMP):
- """MySQL TIMESTAMP type
+ """MySQL TIMESTAMP type.
To signal the orm to automatically re-select modified rows to retrieve
- the timestamp, add a PassiveDefault to your column specification::
+ the updated timestamp, add a PassiveDefault to your column specification::
from sqlalchemy.databases import mysql
Column('updated', mysql.MSTimeStamp,
@@ -549,7 +581,7 @@ class MSTimeStamp(sqltypes.TIMESTAMP):
class MSYear(sqltypes.TypeEngine):
- """MySQL YEAR type, for single byte storage of years 1901-2155"""
+ """MySQL YEAR type, for single byte storage of years 1901-2155."""
def __init__(self, length=None):
self.length = length
@@ -561,7 +593,7 @@ class MSYear(sqltypes.TypeEngine):
return "YEAR(%s)" % self.length
class MSText(_StringType, sqltypes.TEXT):
- """MySQL TEXT type, for text up to 2^16 characters"""
+ """MySQL TEXT type, for text up to 2^16 characters."""
def __init__(self, length=None, **kwargs):
"""Construct a TEXT.
@@ -610,7 +642,7 @@ class MSText(_StringType, sqltypes.TEXT):
class MSTinyText(MSText):
- """MySQL TINYTEXT type, for text up to 2^8 characters"""
+ """MySQL TINYTEXT type, for text up to 2^8 characters."""
def __init__(self, **kwargs):
"""Construct a TINYTEXT.
@@ -649,7 +681,7 @@ class MSTinyText(MSText):
class MSMediumText(MSText):
- """MySQL MEDIUMTEXT type, for text up to 2^24 characters"""
+ """MySQL MEDIUMTEXT type, for text up to 2^24 characters."""
def __init__(self, **kwargs):
"""Construct a MEDIUMTEXT.
@@ -688,7 +720,7 @@ class MSMediumText(MSText):
class MSLongText(MSText):
- """MySQL LONGTEXT type, for text up to 2^32 characters"""
+ """MySQL LONGTEXT type, for text up to 2^32 characters."""
def __init__(self, **kwargs):
"""Construct a LONGTEXT.
@@ -800,8 +832,10 @@ class MSChar(_StringType, sqltypes.CHAR):
class MSNVarChar(_StringType, sqltypes.String):
- """MySQL NVARCHAR type, for variable-length character data in the
- server's configured national character set.
+ """MySQL NVARCHAR type.
+
+ For variable-length character data in the server's configured national
+ character set.
"""
def __init__(self, length=None, **kwargs):
@@ -832,8 +866,10 @@ class MSNVarChar(_StringType, sqltypes.String):
class MSNChar(_StringType, sqltypes.CHAR):
- """MySQL NCHAR type, for fixed-length character data in the
- server's configured national character set.
+ """MySQL NCHAR type.
+
+ For fixed-length character data in the server's configured national
+ character set.
"""
def __init__(self, length=None, **kwargs):
@@ -861,7 +897,7 @@ class MSNChar(_StringType, sqltypes.CHAR):
class _BinaryType(sqltypes.Binary):
- """MySQL binary types"""
+ """Base for MySQL binary types."""
def get_col_spec(self):
if self.length:
@@ -877,7 +913,7 @@ class _BinaryType(sqltypes.Binary):
class MSVarBinary(_BinaryType):
- """MySQL VARBINARY type, for variable length binary data"""
+ """MySQL VARBINARY type, for variable length binary data."""
def __init__(self, length=None, **kw):
"""Construct a VARBINARY. Arguments are:
@@ -953,21 +989,21 @@ class MSBlob(_BinaryType):
class MSTinyBlob(MSBlob):
- """MySQL TINYBLOB type, for binary data up to 2^8 bytes"""
+ """MySQL TINYBLOB type, for binary data up to 2^8 bytes."""
def get_col_spec(self):
return "TINYBLOB"
class MSMediumBlob(MSBlob):
- """MySQL MEDIUMBLOB type, for binary data up to 2^24 bytes"""
+ """MySQL MEDIUMBLOB type, for binary data up to 2^24 bytes."""
def get_col_spec(self):
return "MEDIUMBLOB"
class MSLongBlob(MSBlob):
- """MySQL LONGBLOB type, for binary data up to 2^32 bytes"""
+ """MySQL LONGBLOB type, for binary data up to 2^32 bytes."""
def get_col_spec(self):
return "LONGBLOB"
@@ -1169,6 +1205,7 @@ colspecs = {
_BinaryType: _BinaryType,
}
+# Everything 3.23 through 5.1 excepting OpenGIS types.
ischema_names = {
'bigint': MSBigInteger,
'binary': MSBinary,
@@ -1233,9 +1270,12 @@ class MySQLExecutionContext(default.DefaultExecutionContext):
class MySQLDialect(ansisql.ANSIDialect):
- def __init__(self, **kwargs):
- ansisql.ANSIDialect.__init__(self, default_paramstyle='format',
- **kwargs)
+ """Details of the MySQL dialect. Not used directly in application code."""
+
+ def __init__(self, use_ansiquotes=False, **kwargs):
+ self.use_ansiquotes = use_ansiquotes
+ kwargs.setdefault('default_paramstyle', 'format')
+ ansisql.ANSIDialect.__init__(self, **kwargs)
def dbapi(cls):
import MySQLdb as mysql
@@ -1303,7 +1343,10 @@ class MySQLDialect(ansisql.ANSIDialect):
return MySQLSchemaDropper(self, *args, **kwargs)
def preparer(self):
- return MySQLIdentifierPreparer(self)
+ if self.use_ansiquotes:
+ return MySQLANSIIdentifierPreparer(self)
+ else:
+ return MySQLIdentifierPreparer(self)
def do_executemany(self, cursor, statement, parameters,
context=None, **kwargs):
@@ -1389,9 +1432,8 @@ class MySQLDialect(ansisql.ANSIDialect):
charset = self._detect_charset(connection)
rp = connection.execute("SHOW TABLES FROM %s" %
- self.preparer().quote_identifier(schema))
- return [row[0].decode(charset)
- for row in _compat_fetchall(rp, charset=charset)]
+ self.identifier_preparer.quote_identifier(schema))
+ return [row[0] for row in _compat_fetchall(rp, charset=charset)]
def has_table(self, connection, table_name, schema=None):
# SHOW TABLE STATUS LIKE and SHOW TABLES LIKE do not function properly
@@ -1399,19 +1441,28 @@ class MySQLDialect(ansisql.ANSIDialect):
#
# TODO: if this is not a problem on win, make the strategy swappable
# based on platform. DESCRIBE is slower.
- if schema is not None:
- st = "DESCRIBE `%s`.`%s`" % (schema, table_name)
- else:
- st = "DESCRIBE `%s`" % table_name
+
+ # [ticket:726]
+ # full_name = self.identifier_preparer.format_table(table, use_schema=True)
+
+ full_name = '.'.join(self.identifier_preparer._quote_free_identifiers(
+ schema, table_name))
+
+ st = "DESCRIBE %s" % full_name
+ rs = None
try:
- rs = connection.execute(st)
- have = rs.rowcount > 0
- rs.close()
- return have
- except exceptions.SQLError, e:
- if e.orig.args[0] == 1146:
- return False
- raise
+ try:
+ rs = connection.execute(st)
+ have = rs.rowcount > 0
+ rs.close()
+ return have
+ except exceptions.SQLError, e:
+ if e.orig.args[0] == 1146:
+ return False
+ raise
+ finally:
+ if rs:
+ rs.close()
def server_version_info(self, connection):
"""A tuple of the database server version.
@@ -1459,106 +1510,21 @@ class MySQLDialect(ansisql.ANSIDialect):
def reflecttable(self, connection, table, include_columns):
"""Load column definitions from the server."""
- decode_from = self._detect_charset(connection)
- casing = self._detect_casing(connection, decode_from)
+ charset = self._detect_charset(connection)
+ casing = self._detect_casing(connection, charset)
if casing == 1:
- # fixme: is this really needed?
table.name = table.name.lower()
table.metadata.tables[table.name]= table
- table_name = '.'.join(self.identifier_preparer.format_table_seq(table))
- try:
- rp = connection.execute("DESCRIBE " + table_name)
- except exceptions.SQLError, e:
- if e.orig.args[0] == 1146:
- raise exceptions.NoSuchTableError(table.fullname)
- raise
-
- for row in _compat_fetchall(rp, charset=decode_from):
- (name, type, nullable, primary_key, default) = \
- (row[0], row[1], row[2] == 'YES', row[3] == 'PRI', row[4])
+ sql = self._show_create_table(connection, table, charset)
- # leave column names as unicode
- name = name.decode(decode_from)
-
- if include_columns and name not in include_columns:
- continue
-
- match = re.match(r'(\w+)(\(.*?\))?\s*(\w+)?\s*(\w+)?', type)
- col_type = match.group(1)
- args = match.group(2)
- extra_1 = match.group(3)
- extra_2 = match.group(4)
-
- if col_type == 'tinyint' and args == '(1)':
- col_type = 'boolean'
- args = None
- try:
- coltype = ischema_names[col_type]
- except KeyError:
- warnings.warn(RuntimeWarning(
- "Did not recognize type '%s' of column '%s'" %
- (col_type, name)))
- coltype = sqltypes.NULLTYPE
-
- kw = {}
- if extra_1 is not None:
- kw[extra_1] = True
- if extra_2 is not None:
- kw[extra_2] = True
-
- if args is not None and coltype is not sqltypes.NULLTYPE:
- if col_type in ('enum', 'set'):
- args= args[1:-1]
- argslist = args.split(',')
- coltype = coltype(*argslist, **kw)
- else:
- argslist = re.findall(r'(\d+)', args)
- coltype = coltype(*[int(a) for a in argslist], **kw)
+ try:
+ reflector = self.reflector
+ except AttributeError:
+ self.reflector = reflector = \
+ MySQLSchemaReflector(self.identifier_preparer)
- colargs= []
- if default:
- if col_type == 'timestamp' and default == 'CURRENT_TIMESTAMP':
- default = sql.text(default)
- colargs.append(schema.PassiveDefault(default))
- table.append_column(schema.Column(name, coltype, *colargs,
- **dict(primary_key=primary_key,
- nullable=nullable,
- )))
-
- table_options = self.moretableinfo(connection, table, decode_from)
- table.kwargs.update(table_options)
-
- def moretableinfo(self, connection, table, charset=None):
- """SHOW CREATE TABLE to get foreign key/table options."""
-
- table_name = '.'.join(self.identifier_preparer.format_table_seq(table))
- rp = connection.execute("SHOW CREATE TABLE " + table_name)
- row = _compat_fetchone(rp, charset=charset)
- if not row:
- raise exceptions.NoSuchTableError(table.fullname)
- desc = row[1].strip()
- row.close()
-
- table_options = {}
-
- lastparen = re.search(r'\)[^\)]*\Z', desc)
- if lastparen:
- match = re.search(r'\b(?P<spec>TYPE|ENGINE)=(?P<ttype>.+)\b', desc[lastparen.start():], re.I)
- if match:
- table_options["mysql_%s" % match.group('spec')] = \
- match.group('ttype')
-
- # \x27 == ' (single quote) (avoid xemacs syntax highlighting issue)
- fkpat = r'''CONSTRAINT [`"\x27](?P<name>.+?)[`"\x27] FOREIGN KEY \((?P<columns>.+?)\) REFERENCES [`"\x27](?P<reftable>.+?)[`"\x27] \((?P<refcols>.+?)\)'''
- for match in re.finditer(fkpat, desc):
- columns = re.findall(r'''[`"\x27](.+?)[`"\x27]''', match.group('columns'))
- refcols = [match.group('reftable') + "." + x for x in re.findall(r'''[`"\x27](.+?)[`"\x27]''', match.group('refcols'))]
- schema.Table(match.group('reftable'), table.metadata, autoload=True, autoload_with=connection)
- constraint = schema.ForeignKeyConstraint(columns, refcols, name=match.group('name'))
- table.append_constraint(constraint)
-
- return table_options
+ reflector.reflect(connection, table, sql, charset, only=include_columns)
def _detect_charset(self, connection):
"""Sniff out the character set in use for connection results."""
@@ -1650,19 +1616,36 @@ class MySQLDialect(ansisql.ANSIDialect):
connection.properties['collations'] = collations
return collations
-def _compat_fetchall(rp, charset=None):
- """Proxy result rows to smooth over MySQL-Python driver inconsistencies."""
+ def _show_create_table(self, connection, table, charset=None,
+ full_name=None):
+ """Run SHOW CREATE TABLE for a ``Table``."""
- return [_MySQLPythonRowProxy(row, charset) for row in rp.fetchall()]
+ if full_name is None:
+ full_name = self.identifier_preparer.format_table(table)
+ st = "SHOW CREATE TABLE %s" % full_name
-def _compat_fetchone(rp, charset=None):
- """Proxy a result row to smooth over MySQL-Python driver inconsistencies."""
+ rp = None
+ try:
+ try:
+ rp = connection.execute(st)
+ except exceptions.SQLError, e:
+ if e.orig.args[0] == 1146:
+ raise exceptions.NoSuchTableError(full_name)
+ else:
+ raise
+ row = _compat_fetchone(rp, charset=charset)
+ if not row:
+ raise exceptions.NoSuchTableError(full_name)
+ return row[1].strip()
+ finally:
+ if rp:
+ rp.close()
+
+ return sql
- return _MySQLPythonRowProxy(rp.fetchone(), charset)
-
class _MySQLPythonRowProxy(object):
- """Return consistent column values for all versions of MySQL-python (esp. alphas) and unicode settings."""
+ """Return consistent column values for all versions of MySQL-python (esp. alphas) and Unicode settings."""
# Some MySQL-python versions can return some columns as
# sets.Set(['value']) (seriously) but thankfully that doesn't
@@ -1675,16 +1658,16 @@ class _MySQLPythonRowProxy(object):
item = self.rowproxy[index]
if isinstance(item, _array):
item = item.tostring()
- if self.charset and isinstance(item, unicode):
- return item.encode(self.charset)
+ if self.charset and isinstance(item, str):
+ return item.decode(self.charset)
else:
return item
def __getattr__(self, attr):
item = getattr(self.rowproxy, attr)
if isinstance(item, _array):
item = item.tostring()
- if self.charset and isinstance(item, unicode):
- return item.encode(self.charset)
+ if self.charset and isinstance(item, str):
+ return item.decode(self.charset)
else:
return item
@@ -1746,7 +1729,7 @@ class MySQLSchemaGenerator(ansisql.ANSISchemaGenerator):
if not column.nullable:
colspec.append('NOT NULL')
- # FIXME: #649, also #612 with regard to SHOW CREATE
+ # FIXME: #649 ASAP
if column.primary_key:
if (len(column.foreign_keys)==0
and first_pk
@@ -1786,22 +1769,542 @@ class MySQLSchemaDropper(ansisql.ANSISchemaDropper):
self.execute()
-class MySQLIdentifierPreparer(ansisql.ANSIIdentifierPreparer):
+class MySQLSchemaReflector(object):
+ """Parses SHOW CREATE TABLE output."""
+
+ def __init__(self, identifier_preparer):
+ """Construct a MySQLSchemaReflector.
+
+ identifier_preparer
+ An ANSIIdentifierPreparer type, used to determine the identifier
+ quoting style in effect.
+ """
+
+ self.preparer = identifier_preparer
+ self._prep_regexes()
+
+ def reflect(self, connection, table, show_create, charset, only=None):
+ """Parse MySQL SHOW CREATE TABLE and fill in a ''Table''.
+
+ show_create
+ Unicode output of SHOW CREATE TABLE
+
+ table
+ A ''Table'', to be loaded with Columns, Indexes, etc.
+ table.name will be set if not already
+
+ charset
+ FIXME, some constructed values (like column defaults)
+ currently can't be Unicode. ''charset'' will convert them
+ into the connection character set.
+
+ only
+ An optional sequence of column names. If provided, only
+ these columns will be reflected, and any keys or constraints
+ that include columns outside this set will also be omitted.
+ That means that if ``only`` includes only one column in a
+ 2 part primary key, the entire primary key will be omitted.
+ """
+
+ keys, constraints = [], []
+
+ if only:
+ only = util.Set(only)
+
+ for line in re.split(r'\r?\n', show_create):
+ if line.startswith(' ' + self.preparer.initial_quote):
+ self._add_column(table, line, charset, only)
+ elif line.startswith(') '):
+ self._set_options(table, line)
+ elif line.startswith('CREATE '):
+ self._set_name(table, line)
+ # Not present in real reflection, but may be if loading from a file.
+ elif not line:
+ pass
+ else:
+ type_, spec = self.constraints(line)
+ if type_ is None:
+ warnings.warn(
+ RuntimeWarning("Unknown schema content: %s" % line))
+ elif type_ == 'key':
+ keys.append(spec)
+ elif type_ == 'constraint':
+ constraints.append(spec)
+ else:
+ pass
+
+ self._set_keys(table, keys, only)
+ self._set_constraints(table, constraints, connection, only)
+
+ def _set_name(self, table, line):
+ """Override a Table name with the reflected name.
+
+ table
+ A ``Table``
+
+ line
+ The first line of SHOW CREATE TABLE output.
+ """
+
+ # Don't override by default.
+ if table.name is None:
+ table.name = self.name(line)
+
+ def _add_column(self, table, line, charset, only=None):
+ spec = self.column(line)
+ if not spec:
+ warnings.warn(RuntimeWarning(
+ "Unknown column definition %s" % line))
+ return
+ if not spec['full']:
+ warnings.warn(RuntimeWarning(
+ "Incomplete reflection of column definition %s" % line))
+
+ name, type_, args, notnull = \
+ spec['name'], spec['coltype'], spec['arg'], spec['notnull']
+
+ if only and name.lower() not in only:
+ self.logger.info("Omitting reflected column %s.%s" %
+ (table.name, name))
+ return
+
+ # Convention says that TINYINT(1) columns == BOOLEAN
+ if type_ == 'tinyint' and args == '1':
+ type_ = 'boolean'
+ args = None
+
+ try:
+ col_type = ischema_names[type_]
+ except KeyError:
+ warnings.warn(RuntimeWarning(
+ "Did not recognize type '%s' of column '%s'" %
+ (type_, name)))
+ coltype = sqltypes.NULLTYPE
+
+ # Column type positional arguments eg. varchar(32)
+ if args is None or args == '':
+ type_args = []
+ elif args[0] == "'" and args[-1] == "'":
+ type_args = self._re_csv_str.findall(args)
+ else:
+ type_args = [int(v) for v in self._re_csv_int.findall(args)]
+
+ # Column type keyword options
+ type_kw = {}
+ for kw in ('unsigned', 'zerofill'):
+ if spec.get(kw, False):
+ type_kw[kw] = True
+ for kw in ('charset', 'collate'):
+ if spec.get(kw, False):
+ type_kw[kw] = spec[kw]
+
+ type_instance = col_type(*type_args, **type_kw)
+
+ col_args, col_kw = [], {}
+
+ # NOT NULL
+ if spec.get('notnull', False):
+ col_kw['nullable'] = False
+
+ # AUTO_INCREMENT
+ if spec.get('autoincr', False):
+ col_kw['autoincrement'] = True
+
+ # DEFAULT
+ default = spec.get('default', None)
+ if default is not None and default != 'NULL':
+ # Defaults should be in the native charset for the moment
+ default = default.decode(charset)
+ if type_ == 'timestamp':
+ # can't be NULL for TIMESTAMPs
+ if (default[0], default[-1]) != ("'", "'"):
+ default = sql.text(default)
+ else:
+ default = default[1:-1]
+ col_args.append(schema.PassiveDefault(default))
+
+ table.append_column(schema.Column(name, type_instance,
+ *col_args, **col_kw))
+
+ def _set_keys(self, table, keys, only):
+ """Add ``Index`` and ``PrimaryKeyConstraint`` items to a ``Table``.
+
+ Most of the information gets dropped here- more is reflected than
+ the schema objects can currently represent.
+
+ table
+ A ``Table``
+
+ keys
+ A sequence of key specifications produced by `constraints`
+
+ only
+ Optional `set` of column names. If provided, keys covering
+ columns not in this set will be omitted.
+ """
+
+ for spec in keys:
+ flavor = spec['type']
+ col_names = [s[0] for s in spec['columns']]
+
+ if only and not util.Set(col_names).issubset(only):
+ if flavor is None:
+ flavor = 'index'
+ self.logger.info(
+ "Omitting %s KEY for (%s), key covers ommitted columns." %
+ (flavor, ', '.join(col_names)))
+ continue
+
+ constraint = False
+ if flavor == 'PRIMARY':
+ key = schema.PrimaryKeyConstraint()
+ constraint = True
+ elif flavor == 'UNIQUE':
+ key = schema.Index(spec['name'], unique=True)
+ elif flavor in (None, 'FULLTEXT', 'SPATIAL'):
+ key = schema.Index(spec['name'])
+ else:
+ self.logger.info(
+ "Converting unknown KEY type %s to a plain KEY" % flavor)
+ key = schema.Index(spec['name'])
+
+ for col in [table.c[name] for name in col_names]:
+ key.append_column(col)
+
+ if constraint:
+ table.append_constraint(key)
+
+ def _set_constraints(self, table, constraints, connection, only):
+ """Apply constraints to a ``Table``."""
+
+ for spec in constraints:
+ # only FOREIGN KEYs
+ ref_name = spec['table'][-1]
+ ref_schema = len(spec['table']) > 1 and spec['table'][-2] or None
+
+ loc_names = spec['local']
+ if only and not util.Set(loc_names).issubset(only):
+ self.logger.info(
+ "Omitting FOREIGN KEY for (%s), key covers ommitted "
+ "columns." % (', '.join(loc_names)))
+ continue
+
+ if ref_name in table.metadata.tables:
+ ref_table = table.metadata.tables[ref_name]
+ if ref_table.schema and ref_table.schema != ref_schema:
+ warnings.warn(RuntimeWarning(
+ "Table %s.%s is shadowing %s.%s in this MetaData" %
+ (ref_table.schema, ref_name, table.schema, table.name)))
+ else:
+ ref_table = schema.Table(ref_name, table.metadata,
+ schema=ref_schema,
+ autoload=True, autoload_with=connection)
+
+ ref_names = spec['foreign']
+ if not util.Set(ref_names).issubset(
+ util.Set([c.name for c in ref_table.c])):
+ raise exceptions.InvalidRequestError(
+ "Foreign key columns (%s) are not present on foreign table" %
+ (', '.join(ref_names), ref_table.fullname()))
+ ref_columns = [ref_table.c[name] for name in ref_names]
+
+ con_kw = {}
+ for opt in ('name', 'onupdate', 'ondelete'):
+ if spec.get(opt, False):
+ con_kw[opt] = spec[opt]
+
+ key = schema.ForeignKeyConstraint([], [], **con_kw)
+ table.append_constraint(key)
+ for pair in zip(loc_names, ref_columns):
+ key.append_element(*pair)
+
+ def _set_options(self, table, line):
+ """Apply safe reflected table options to a ``Table``.
+
+ table
+ A ``Table``
+
+ line
+ The final line of SHOW CREATE TABLE output.
+ """
+
+ options = self.table_options(line)
+ for nope in ('auto_increment', 'data_directory', 'index_directory'):
+ options.pop(nope, None)
+
+ for opt, val in options.items():
+ table.kwargs['mysql_%s' % opt] = val
+
+ def _prep_regexes(self):
+ """Pre-compile regular expressions."""
+
+ self._re_columns = []
+ self._pr_options = []
+ self._re_options_util = {}
+
+ _initial, _final = (self.preparer.initial_quote,
+ self.preparer.final_quote)
+
+ quotes = dict(zip(('iq', 'fq', 'esc_fq'),
+ [re.escape(s) for s in
+ (self.preparer.initial_quote,
+ self.preparer.final_quote,
+ self.preparer._escape_identifier(_final))]))
+
+ self._pr_name = _pr_compile(
+ r'^CREATE TABLE +'
+ r'%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +\($' % quotes,
+ self.preparer._unescape_identifier)
+
+ # `col`,`col2`(32),`col3`(15) DESC
+ #
+ # Note: ASC and DESC aren't reflected, so we'll punt...
+ self._re_keyexprs = _re_compile(
+ r'(?:'
+ r'(?:%(iq)s((?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)'
+ r'(?:\((\d+)\))?(?=\,|$))+' % quotes)
+
+ # 'foo' or 'foo','bar' or 'fo,o','ba''a''r'
+ self._re_csv_str = _re_compile(r'\x27(?:\x27\x27|[^\x27])+\x27')
+
+ # 123 or 123,456
+ self._re_csv_int = _re_compile(r'\d+')
+
+
+ # `colname` <type> [type opts]
+ # (NOT NULL | NULL)
+ # DEFAULT ('value' | CURRENT_TIMESTAMP...)
+ # COMMENT 'comment'
+ # COLUMN_FORMAT (FIXED|DYNAMIC|DEFAULT)
+ # STORAGE (DISK|MEMORY)
+ self._re_column = _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+|'
+ r'(?:\x27(?:\x27\x27|[^\x27])+\x27,?)+))\))?'
+ r'(?: +(?P<unsigned>UNSIGNED))?'
+ r'(?: +(?P<zerofill>ZEROFILL))?'
+ r'(?: +CHARACTER SET +(?P<charset>\w+))?'
+ r'(?: +COLLATE +(P<collate>\w+))?'
+ r'(?: +(?P<notnull>NOT NULL))?'
+ r'(?: +DEFAULT +(?P<default>'
+ r'(?:NULL|\x27(?:\x27\x27|[^\x27])+\x27|\w+)'
+ r'(?:ON UPDATE \w+)?'
+ r'))?'
+ r'(?: +(?P<autoincr>AUTO_INCREMENT))?'
+ r'(?: +COMMENT +(P<comment>(?:\x27\x27|[^\x27])+))?'
+ r'(?: +COLUMN_FORMAT +(?P<colfmt>\w+))?'
+ r'(?: +STORAGE +(?P<storage>\w+))?'
+ r'(?: +(?P<extra>.*))?'
+ 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
+ )
+
+ # (PRIMARY|UNIQUE|FULLTEXT|SPATIAL) INDEX `name` (USING (BTREE|HASH))?
+ # (`col` (ASC|DESC)?, `col` (ASC|DESC)?)
+ # KEY_BLOCK_SIZE size | 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>\S+) +)?'
+ r' +\((?P<columns>.+?)\)'
+ r'(?: +KEY_BLOCK_SIZE +(?P<keyblock>\S+))?'
+ r'(?: +WITH PARSER +(?P<parser>\S+))?'
+ r',?$'
+ % quotes
+ )
+
+ # CONSTRAINT `name` FOREIGN KEY (`local_col`)
+ # REFERENCES `remote` (`remote_col`)
+ # MATCH FULL | MATCH PARTIAL | MATCH SIMPLE
+ # ON DELETE CASCADE ON UPDATE RESTRICT
+ #
+ # unique constraints come back as KEYs
+ kw = quotes.copy()
+ kw['on'] = 'RESTRICT|CASCASDE|SET NULL|NOACTION'
+ self._re_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'\((?P<foreign>[^\)]+?)\)'
+ r'(?: +(?P<match>MATCH \w+))?'
+ r'(?: +ON DELETE (?P<ondelete>%(on)s))?'
+ r'(?: +ON UPDATE (?P<onupdate>%(on)s))?'
+ % kw
+ )
+
+ # PARTITION
+ #
+ # punt!
+ self._re_partition = _re_compile(
+ r' '
+ r'(?:SUB)?PARTITION')
+
+ # Table-level options (COLLATE, ENGINE, etc.)
+ 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)
+
+ for option in (('COMMENT', 'DATA_DIRECTORY', 'INDEX_DIRECTORY',
+ 'PASSWORD', 'CONNECTION')):
+ self._add_option_string(option)
+
+ 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+')
+ self._re_options_util['='] = _re_compile(r'\s*=\s*$')
+
+ def _add_option_string(self, directive):
+ regex = (r'(?P<directive>%s\s*(?:=\s*)?)'
+ r'(?:\x27.(?P<val>.*?)\x27(?!\x27)\x27)' %
+ re.escape(directive))
+ self._pr_options.append(
+ _pr_compile(regex, lambda v: v.replace("''", "'")))
+
+ def _add_option_word(self, directive):
+ regex = (r'(?P<directive>%s\s*(?:=\s*)?)'
+ r'(?P<val>\w+)' % re.escape(directive))
+ self._pr_options.append(_pr_compile(regex))
+
+ def _add_option_regex(self, directive, regex):
+ regex = (r'(?P<directive>%s\s*(?:=\s*)?)'
+ r'(?P<val>%s)' % (re.escape(directive), regex))
+ self._pr_options.append(_pr_compile(regex))
+
+
+ def name(self, line):
+ """Extract the table name.
+
+ line
+ The first line of SHOW CREATE TABLE
+ """
+
+ regex, cleanup = self._pr_name
+ m = regex.match(line)
+ if not m:
+ return None
+ return cleanup(m.group('name'))
+
+ def column(self, line):
+ """Extract column details.
+
+ Falls back to a 'minimal support' variant if full parse fails.
+
+ line
+ Any column-bearing line from SHOW CREATE TABLE
+ """
+
+ m = self._re_column.match(line)
+ if m:
+ spec = m.groupdict()
+ spec['full'] = True
+ return spec
+ m = self._re_column_loose.match(line)
+ if m:
+ spec = m.groupdict()
+ spec['full'] = False
+ return spec
+ return None
+
+ def constraints(self, line):
+ """Parse a KEY or CONSTRAINT line.
+
+ line
+ A line of SHOW CREATE TABLE output
+ """
+
+ # KEY
+ m = self._re_key.match(line)
+ if m:
+ spec = m.groupdict()
+ # convert columns into name, length pairs
+ spec['columns'] = self._parse_keyexprs(spec['columns'])
+ return 'key', spec
+
+ # CONSTRAINT
+ m = self._re_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 'constraint', spec
+
+ # PARTITION and SUBPARTITION
+ m = self._re_partition.match(line)
+ if m:
+ # Punt!
+ return 'partition', line
+
+ # No match.
+ return (None, line)
+
+ def table_options(self, line):
+ """Build a dictionary of all reflected table-level options.
+
+ line
+ The final line of SHOW CREATE TABLE output.
+ """
+
+ options = {}
+
+ if not line or line == ')':
+ return options
+
+ r_eq_trim = self._re_options_util['=']
+
+ for regex, cleanup in self._pr_options:
+ m = regex.search(line)
+ if not m:
+ continue
+ directive, value = m.group('directive'), m.group('val')
+ directive = r_eq_trim.sub('', directive).lower()
+ if cleanup:
+ value = cleanup(value)
+ options[directive] = value
+
+ return options
+
+ def _parse_keyexprs(self, identifiers):
+ """Unpack '"col"(2),"col" ASC'-ish strings into components."""
+
+ return self._re_keyexprs.findall(identifiers)
+
+MySQLSchemaReflector.logger = logging.class_logger(MySQLSchemaReflector)
+
+
+class _MySQLIdentifierPreparer(ansisql.ANSIIdentifierPreparer):
"""MySQL-specific schema identifier configuration."""
- def __init__(self, dialect):
- super(MySQLIdentifierPreparer, self).__init__(dialect,
- initial_quote='`')
+ def __init__(self, dialect, **kw):
+ super(_MySQLIdentifierPreparer, self).__init__(dialect, **kw)
def _reserved_words(self):
return RESERVED_WORDS
- def _escape_identifier(self, value):
- return value.replace('`', '``')
-
- def _unescape_identifier(self, value):
- return value.replace('``', '`')
-
def _fold_identifier_case(self, value):
# TODO: determine MySQL's case folding rules
#
@@ -1812,5 +2315,49 @@ class MySQLIdentifierPreparer(ansisql.ANSIIdentifierPreparer):
# just leave everything as-is.
return value
+ def _quote_free_identifiers(self, *ids):
+ """Unilaterally identifier-quote any number of strings."""
+
+ return tuple([self.quote_identifier(id) for id in ids if id is not None])
+
+
+class MySQLIdentifierPreparer(_MySQLIdentifierPreparer):
+ """Traditional MySQL-specific schema identifier configuration."""
+
+ def __init__(self, dialect):
+ super(MySQLIdentifierPreparer, self).__init__(dialect, initial_quote="`")
+
+ def _escape_identifier(self, value):
+ return value.replace('`', '``')
+
+ def _unescape_identifier(self, value):
+ return value.replace('``', '`')
+
+
+class MySQLANSIIdentifierPreparer(_MySQLIdentifierPreparer):
+ """ANSI_QUOTES MySQL schema identifier configuration."""
+
+ pass
+
+
+def _compat_fetchall(rp, charset=None):
+ """Proxy result rows to smooth over MySQL-Python driver inconsistencies."""
+
+ return [_MySQLPythonRowProxy(row, charset) for row in rp.fetchall()]
+
+def _compat_fetchone(rp, charset=None):
+ """Proxy a result row to smooth over MySQL-Python driver inconsistencies."""
+
+ return _MySQLPythonRowProxy(rp.fetchone(), charset)
+
+def _pr_compile(regex, cleanup=None):
+ """Prepare a 2-tuple of compiled regex and callable."""
+
+ return (_re_compile(regex), cleanup)
+
+def _re_compile(regex):
+ """Compile a string to regex, I and UNICODE."""
+
+ return re.compile(regex, re.I | re.UNICODE)
dialect = MySQLDialect