]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- break out mysql/base into modules as it's getting huge with more to come
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 4 Jan 2016 22:27:13 +0000 (17:27 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 4 Jan 2016 22:37:23 +0000 (17:37 -0500)
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/mysql/enumerated.py [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/reflection.py [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/types.py [new file with mode: 0644]
test/dialect/mysql/test_reflection.py

index 8830cb0c1990f8213b054ef647f98e7877c7d08e..66ef0d5e24ddd57bc0560e35fdbe08c9ea0103b2 100644 (file)
@@ -536,7 +536,6 @@ output::
 
 """
 
-import datetime
 import re
 import sys
 
@@ -552,6 +551,16 @@ from ...util import topological
 from ...types import DATE, BOOLEAN, \
     BLOB, BINARY, VARBINARY
 
+from . import reflection as _reflection
+from .types import BIGINT, BIT, CHAR, DECIMAL, DATETIME, \
+    DOUBLE, FLOAT, INTEGER, LONGBLOB, LONGTEXT, MEDIUMBLOB, MEDIUMINT, \
+    MEDIUMTEXT, NCHAR, NUMERIC, NVARCHAR, REAL, SMALLINT, TEXT, TIME, \
+    TIMESTAMP, TINYBLOB, TINYINT, TINYTEXT, VARCHAR, YEAR
+from .types import _StringType, _IntegerType, _NumericType, \
+    _FloatType, _MatchType
+from .enumerated import ENUM, SET
+
+
 RESERVED_WORDS = set(
     ['accessible', 'add', 'all', 'alter', 'analyze', 'and', 'as', 'asc',
      'asensitive', 'before', 'between', 'bigint', 'binary', 'blob', 'both',
@@ -614,1056 +623,6 @@ SET_RE = re.compile(
     re.I | re.UNICODE)
 
 
-class _NumericType(object):
-    """Base for MySQL numeric types.
-
-    This is the base both for NUMERIC as well as INTEGER, hence
-    it's a mixin.
-
-    """
-
-    def __init__(self, unsigned=False, zerofill=False, **kw):
-        self.unsigned = unsigned
-        self.zerofill = zerofill
-        super(_NumericType, self).__init__(**kw)
-
-    def __repr__(self):
-        return util.generic_repr(self,
-                                 to_inspect=[_NumericType, sqltypes.Numeric])
-
-
-class _FloatType(_NumericType, sqltypes.Float):
-    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
-        if isinstance(self, (REAL, DOUBLE)) and \
-            (
-                (precision is None and scale is not None) or
-                (precision is not None and scale is None)
-        ):
-            raise exc.ArgumentError(
-                "You must specify both precision and scale or omit "
-                "both altogether.")
-        super(_FloatType, self).__init__(
-            precision=precision, asdecimal=asdecimal, **kw)
-        self.scale = scale
-
-    def __repr__(self):
-        return util.generic_repr(self, to_inspect=[_FloatType,
-                                                   _NumericType,
-                                                   sqltypes.Float])
-
-
-class _IntegerType(_NumericType, sqltypes.Integer):
-    def __init__(self, display_width=None, **kw):
-        self.display_width = display_width
-        super(_IntegerType, self).__init__(**kw)
-
-    def __repr__(self):
-        return util.generic_repr(self, to_inspect=[_IntegerType,
-                                                   _NumericType,
-                                                   sqltypes.Integer])
-
-
-class _StringType(sqltypes.String):
-    """Base for MySQL string types."""
-
-    def __init__(self, charset=None, collation=None,
-                 ascii=False, binary=False, unicode=False,
-                 national=False, **kw):
-        self.charset = charset
-
-        # allow collate= or collation=
-        kw.setdefault('collation', kw.pop('collate', collation))
-
-        self.ascii = ascii
-        self.unicode = unicode
-        self.binary = binary
-        self.national = national
-        super(_StringType, self).__init__(**kw)
-
-    def __repr__(self):
-        return util.generic_repr(self,
-                                 to_inspect=[_StringType, sqltypes.String])
-
-
-class _MatchType(sqltypes.Float, sqltypes.MatchType):
-    def __init__(self, **kw):
-        # TODO: float arguments?
-        sqltypes.Float.__init__(self)
-        sqltypes.MatchType.__init__(self)
-
-
-
-class NUMERIC(_NumericType, sqltypes.NUMERIC):
-    """MySQL NUMERIC type."""
-
-    __visit_name__ = 'NUMERIC'
-
-    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
-        """Construct a NUMERIC.
-
-        :param precision: Total digits in this number.  If scale and precision
-          are both None, values are stored to limits allowed by the server.
-
-        :param scale: The number of digits after the decimal point.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(NUMERIC, self).__init__(precision=precision,
-                                      scale=scale, asdecimal=asdecimal, **kw)
-
-
-class DECIMAL(_NumericType, sqltypes.DECIMAL):
-    """MySQL DECIMAL type."""
-
-    __visit_name__ = 'DECIMAL'
-
-    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
-        """Construct a DECIMAL.
-
-        :param precision: Total digits in this number.  If scale and precision
-          are both None, values are stored to limits allowed by the server.
-
-        :param scale: The number of digits after the decimal point.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(DECIMAL, self).__init__(precision=precision, scale=scale,
-                                      asdecimal=asdecimal, **kw)
-
-
-class DOUBLE(_FloatType):
-    """MySQL DOUBLE type."""
-
-    __visit_name__ = 'DOUBLE'
-
-    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
-        """Construct a DOUBLE.
-
-        .. note::
-
-            The :class:`.DOUBLE` type by default converts from float
-            to Decimal, using a truncation that defaults to 10 digits.
-            Specify either ``scale=n`` or ``decimal_return_scale=n`` in order
-            to change this scale, or ``asdecimal=False`` to return values
-            directly as Python floating points.
-
-        :param precision: Total digits in this number.  If scale and precision
-          are both None, values are stored to limits allowed by the server.
-
-        :param scale: The number of digits after the decimal point.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(DOUBLE, self).__init__(precision=precision, scale=scale,
-                                     asdecimal=asdecimal, **kw)
-
-
-class REAL(_FloatType, sqltypes.REAL):
-    """MySQL REAL type."""
-
-    __visit_name__ = 'REAL'
-
-    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
-        """Construct a REAL.
-
-        .. note::
-
-            The :class:`.REAL` type by default converts from float
-            to Decimal, using a truncation that defaults to 10 digits.
-            Specify either ``scale=n`` or ``decimal_return_scale=n`` in order
-            to change this scale, or ``asdecimal=False`` to return values
-            directly as Python floating points.
-
-        :param precision: Total digits in this number.  If scale and precision
-          are both None, values are stored to limits allowed by the server.
-
-        :param scale: The number of digits after the decimal point.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(REAL, self).__init__(precision=precision, scale=scale,
-                                   asdecimal=asdecimal, **kw)
-
-
-class FLOAT(_FloatType, sqltypes.FLOAT):
-    """MySQL FLOAT type."""
-
-    __visit_name__ = 'FLOAT'
-
-    def __init__(self, precision=None, scale=None, asdecimal=False, **kw):
-        """Construct a FLOAT.
-
-        :param precision: Total digits in this number.  If scale and precision
-          are both None, values are stored to limits allowed by the server.
-
-        :param scale: The number of digits after the decimal point.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(FLOAT, self).__init__(precision=precision, scale=scale,
-                                    asdecimal=asdecimal, **kw)
-
-    def bind_processor(self, dialect):
-        return None
-
-
-class INTEGER(_IntegerType, sqltypes.INTEGER):
-    """MySQL INTEGER type."""
-
-    __visit_name__ = 'INTEGER'
-
-    def __init__(self, display_width=None, **kw):
-        """Construct an INTEGER.
-
-        :param display_width: Optional, maximum display width for this number.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(INTEGER, self).__init__(display_width=display_width, **kw)
-
-
-class BIGINT(_IntegerType, sqltypes.BIGINT):
-    """MySQL BIGINTEGER type."""
-
-    __visit_name__ = 'BIGINT'
-
-    def __init__(self, display_width=None, **kw):
-        """Construct a BIGINTEGER.
-
-        :param display_width: Optional, maximum display width for this number.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(BIGINT, self).__init__(display_width=display_width, **kw)
-
-
-class MEDIUMINT(_IntegerType):
-    """MySQL MEDIUMINTEGER type."""
-
-    __visit_name__ = 'MEDIUMINT'
-
-    def __init__(self, display_width=None, **kw):
-        """Construct a MEDIUMINTEGER
-
-        :param display_width: Optional, maximum display width for this number.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(MEDIUMINT, self).__init__(display_width=display_width, **kw)
-
-
-class TINYINT(_IntegerType):
-    """MySQL TINYINT type."""
-
-    __visit_name__ = 'TINYINT'
-
-    def __init__(self, display_width=None, **kw):
-        """Construct a TINYINT.
-
-        :param display_width: Optional, maximum display width for this number.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(TINYINT, self).__init__(display_width=display_width, **kw)
-
-
-class SMALLINT(_IntegerType, sqltypes.SMALLINT):
-    """MySQL SMALLINTEGER type."""
-
-    __visit_name__ = 'SMALLINT'
-
-    def __init__(self, display_width=None, **kw):
-        """Construct a SMALLINTEGER.
-
-        :param display_width: Optional, maximum display width for this number.
-
-        :param unsigned: a boolean, optional.
-
-        :param zerofill: Optional. If true, values will be stored as strings
-          left-padded with zeros. Note that this does not effect the values
-          returned by the underlying database API, which continue to be
-          numeric.
-
-        """
-        super(SMALLINT, self).__init__(display_width=display_width, **kw)
-
-
-class BIT(sqltypes.TypeEngine):
-    """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() type.
-
-    """
-
-    __visit_name__ = 'BIT'
-
-    def __init__(self, length=None):
-        """Construct a BIT.
-
-        :param length: Optional, number of bits.
-
-        """
-        self.length = length
-
-    def result_processor(self, dialect, coltype):
-        """Convert a MySQL's 64 bit, variable length binary string to a long.
-
-        TODO: this is MySQL-db, pyodbc specific.  OurSQL and mysqlconnector
-        already do this, so this logic should be moved to those dialects.
-
-        """
-
-        def process(value):
-            if value is not None:
-                v = 0
-                for i in value:
-                    if not isinstance(i, int):
-                        i = ord(i)  # convert byte to int on Python 2
-                    v = v << 8 | i
-                return v
-            return value
-        return process
-
-
-class TIME(sqltypes.TIME):
-    """MySQL TIME type. """
-
-    __visit_name__ = 'TIME'
-
-    def __init__(self, timezone=False, fsp=None):
-        """Construct a MySQL TIME type.
-
-        :param timezone: not used by the MySQL dialect.
-        :param fsp: fractional seconds precision value.
-         MySQL 5.6 supports storage of fractional seconds;
-         this parameter will be used when emitting DDL
-         for the TIME type.
-
-         .. note::
-
-            DBAPI driver support for fractional seconds may
-            be limited; current support includes
-            MySQL Connector/Python.
-
-        .. versionadded:: 0.8 The MySQL-specific TIME
-           type as well as fractional seconds support.
-
-        """
-        super(TIME, self).__init__(timezone=timezone)
-        self.fsp = fsp
-
-    def result_processor(self, dialect, coltype):
-        time = datetime.time
-
-        def process(value):
-            # convert from a timedelta value
-            if value is not None:
-                microseconds = value.microseconds
-                seconds = value.seconds
-                minutes = seconds // 60
-                return time(minutes // 60,
-                            minutes % 60,
-                            seconds - minutes * 60,
-                            microsecond=microseconds)
-            else:
-                return None
-        return process
-
-
-class TIMESTAMP(sqltypes.TIMESTAMP):
-    """MySQL TIMESTAMP type.
-
-    """
-
-    __visit_name__ = 'TIMESTAMP'
-
-    def __init__(self, timezone=False, fsp=None):
-        """Construct a MySQL TIMESTAMP type.
-
-        :param timezone: not used by the MySQL dialect.
-        :param fsp: fractional seconds precision value.
-         MySQL 5.6.4 supports storage of fractional seconds;
-         this parameter will be used when emitting DDL
-         for the TIMESTAMP type.
-
-         .. note::
-
-            DBAPI driver support for fractional seconds may
-            be limited; current support includes
-            MySQL Connector/Python.
-
-        .. versionadded:: 0.8.5 Added MySQL-specific :class:`.mysql.TIMESTAMP`
-           with fractional seconds support.
-
-        """
-        super(TIMESTAMP, self).__init__(timezone=timezone)
-        self.fsp = fsp
-
-
-class DATETIME(sqltypes.DATETIME):
-    """MySQL DATETIME type.
-
-    """
-
-    __visit_name__ = 'DATETIME'
-
-    def __init__(self, timezone=False, fsp=None):
-        """Construct a MySQL DATETIME type.
-
-        :param timezone: not used by the MySQL dialect.
-        :param fsp: fractional seconds precision value.
-         MySQL 5.6.4 supports storage of fractional seconds;
-         this parameter will be used when emitting DDL
-         for the DATETIME type.
-
-         .. note::
-
-            DBAPI driver support for fractional seconds may
-            be limited; current support includes
-            MySQL Connector/Python.
-
-        .. versionadded:: 0.8.5 Added MySQL-specific :class:`.mysql.DATETIME`
-           with fractional seconds support.
-
-        """
-        super(DATETIME, self).__init__(timezone=timezone)
-        self.fsp = fsp
-
-
-class YEAR(sqltypes.TypeEngine):
-    """MySQL YEAR type, for single byte storage of years 1901-2155."""
-
-    __visit_name__ = 'YEAR'
-
-    def __init__(self, display_width=None):
-        self.display_width = display_width
-
-
-class TEXT(_StringType, sqltypes.TEXT):
-    """MySQL TEXT type, for text up to 2^16 characters."""
-
-    __visit_name__ = 'TEXT'
-
-    def __init__(self, length=None, **kw):
-        """Construct a TEXT.
-
-        :param length: Optional, if provided the server may optimize storage
-          by substituting the smallest TEXT type sufficient to store
-          ``length`` characters.
-
-        :param charset: Optional, a column-level character set for this string
-          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
-
-        :param collation: Optional, a column-level collation for this string
-          value.  Takes precedence to 'binary' short-hand.
-
-        :param ascii: Defaults to False: short-hand for the ``latin1``
-          character set, generates ASCII in schema.
-
-        :param unicode: Defaults to False: short-hand for the ``ucs2``
-          character set, generates UNICODE in schema.
-
-        :param national: Optional. If true, use the server's configured
-          national character set.
-
-        :param binary: Defaults to False: short-hand, pick the binary
-          collation type that matches the column's character set.  Generates
-          BINARY in schema.  This does not affect the type of data stored,
-          only the collation of character data.
-
-        """
-        super(TEXT, self).__init__(length=length, **kw)
-
-
-class TINYTEXT(_StringType):
-    """MySQL TINYTEXT type, for text up to 2^8 characters."""
-
-    __visit_name__ = 'TINYTEXT'
-
-    def __init__(self, **kwargs):
-        """Construct a TINYTEXT.
-
-        :param charset: Optional, a column-level character set for this string
-          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
-
-        :param collation: Optional, a column-level collation for this string
-          value.  Takes precedence to 'binary' short-hand.
-
-        :param ascii: Defaults to False: short-hand for the ``latin1``
-          character set, generates ASCII in schema.
-
-        :param unicode: Defaults to False: short-hand for the ``ucs2``
-          character set, generates UNICODE in schema.
-
-        :param national: Optional. If true, use the server's configured
-          national character set.
-
-        :param binary: Defaults to False: short-hand, pick the binary
-          collation type that matches the column's character set.  Generates
-          BINARY in schema.  This does not affect the type of data stored,
-          only the collation of character data.
-
-        """
-        super(TINYTEXT, self).__init__(**kwargs)
-
-
-class MEDIUMTEXT(_StringType):
-    """MySQL MEDIUMTEXT type, for text up to 2^24 characters."""
-
-    __visit_name__ = 'MEDIUMTEXT'
-
-    def __init__(self, **kwargs):
-        """Construct a MEDIUMTEXT.
-
-        :param charset: Optional, a column-level character set for this string
-          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
-
-        :param collation: Optional, a column-level collation for this string
-          value.  Takes precedence to 'binary' short-hand.
-
-        :param ascii: Defaults to False: short-hand for the ``latin1``
-          character set, generates ASCII in schema.
-
-        :param unicode: Defaults to False: short-hand for the ``ucs2``
-          character set, generates UNICODE in schema.
-
-        :param national: Optional. If true, use the server's configured
-          national character set.
-
-        :param binary: Defaults to False: short-hand, pick the binary
-          collation type that matches the column's character set.  Generates
-          BINARY in schema.  This does not affect the type of data stored,
-          only the collation of character data.
-
-        """
-        super(MEDIUMTEXT, self).__init__(**kwargs)
-
-
-class LONGTEXT(_StringType):
-    """MySQL LONGTEXT type, for text up to 2^32 characters."""
-
-    __visit_name__ = 'LONGTEXT'
-
-    def __init__(self, **kwargs):
-        """Construct a LONGTEXT.
-
-        :param charset: Optional, a column-level character set for this string
-          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
-
-        :param collation: Optional, a column-level collation for this string
-          value.  Takes precedence to 'binary' short-hand.
-
-        :param ascii: Defaults to False: short-hand for the ``latin1``
-          character set, generates ASCII in schema.
-
-        :param unicode: Defaults to False: short-hand for the ``ucs2``
-          character set, generates UNICODE in schema.
-
-        :param national: Optional. If true, use the server's configured
-          national character set.
-
-        :param binary: Defaults to False: short-hand, pick the binary
-          collation type that matches the column's character set.  Generates
-          BINARY in schema.  This does not affect the type of data stored,
-          only the collation of character data.
-
-        """
-        super(LONGTEXT, self).__init__(**kwargs)
-
-
-class VARCHAR(_StringType, sqltypes.VARCHAR):
-    """MySQL VARCHAR type, for variable-length character data."""
-
-    __visit_name__ = 'VARCHAR'
-
-    def __init__(self, length=None, **kwargs):
-        """Construct a VARCHAR.
-
-        :param charset: Optional, a column-level character set for this string
-          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
-
-        :param collation: Optional, a column-level collation for this string
-          value.  Takes precedence to 'binary' short-hand.
-
-        :param ascii: Defaults to False: short-hand for the ``latin1``
-          character set, generates ASCII in schema.
-
-        :param unicode: Defaults to False: short-hand for the ``ucs2``
-          character set, generates UNICODE in schema.
-
-        :param national: Optional. If true, use the server's configured
-          national character set.
-
-        :param binary: Defaults to False: short-hand, pick the binary
-          collation type that matches the column's character set.  Generates
-          BINARY in schema.  This does not affect the type of data stored,
-          only the collation of character data.
-
-        """
-        super(VARCHAR, self).__init__(length=length, **kwargs)
-
-
-class CHAR(_StringType, sqltypes.CHAR):
-    """MySQL CHAR type, for fixed-length character data."""
-
-    __visit_name__ = 'CHAR'
-
-    def __init__(self, length=None, **kwargs):
-        """Construct a CHAR.
-
-        :param length: Maximum data length, in characters.
-
-        :param binary: Optional, use the default binary collation for the
-          national character set.  This does not affect the type of data
-          stored, use a BINARY type for binary data.
-
-        :param collation: Optional, request a particular collation.  Must be
-          compatible with the national character set.
-
-        """
-        super(CHAR, self).__init__(length=length, **kwargs)
-
-    @classmethod
-    def _adapt_string_for_cast(self, type_):
-        # copy the given string type into a CHAR
-        # for the purposes of rendering a CAST expression
-        type_ = sqltypes.to_instance(type_)
-        if isinstance(type_, sqltypes.CHAR):
-            return type_
-        elif isinstance(type_, _StringType):
-            return CHAR(
-                length=type_.length,
-                charset=type_.charset,
-                collation=type_.collation,
-                ascii=type_.ascii,
-                binary=type_.binary,
-                unicode=type_.unicode,
-                national=False  # not supported in CAST
-            )
-        else:
-            return CHAR(length=type_.length)
-
-
-class NVARCHAR(_StringType, sqltypes.NVARCHAR):
-    """MySQL NVARCHAR type.
-
-    For variable-length character data in the server's configured national
-    character set.
-    """
-
-    __visit_name__ = 'NVARCHAR'
-
-    def __init__(self, length=None, **kwargs):
-        """Construct an NVARCHAR.
-
-        :param length: Maximum data length, in characters.
-
-        :param binary: Optional, use the default binary collation for the
-          national character set.  This does not affect the type of data
-          stored, use a BINARY type for binary data.
-
-        :param collation: Optional, request a particular collation.  Must be
-          compatible with the national character set.
-
-        """
-        kwargs['national'] = True
-        super(NVARCHAR, self).__init__(length=length, **kwargs)
-
-
-class NCHAR(_StringType, sqltypes.NCHAR):
-    """MySQL NCHAR type.
-
-    For fixed-length character data in the server's configured national
-    character set.
-    """
-
-    __visit_name__ = 'NCHAR'
-
-    def __init__(self, length=None, **kwargs):
-        """Construct an NCHAR.
-
-        :param length: Maximum data length, in characters.
-
-        :param binary: Optional, use the default binary collation for the
-          national character set.  This does not affect the type of data
-          stored, use a BINARY type for binary data.
-
-        :param collation: Optional, request a particular collation.  Must be
-          compatible with the national character set.
-
-        """
-        kwargs['national'] = True
-        super(NCHAR, self).__init__(length=length, **kwargs)
-
-
-class TINYBLOB(sqltypes._Binary):
-    """MySQL TINYBLOB type, for binary data up to 2^8 bytes."""
-
-    __visit_name__ = 'TINYBLOB'
-
-
-class MEDIUMBLOB(sqltypes._Binary):
-    """MySQL MEDIUMBLOB type, for binary data up to 2^24 bytes."""
-
-    __visit_name__ = 'MEDIUMBLOB'
-
-
-class LONGBLOB(sqltypes._Binary):
-    """MySQL LONGBLOB type, for binary data up to 2^32 bytes."""
-
-    __visit_name__ = 'LONGBLOB'
-
-
-class _EnumeratedValues(_StringType):
-    def _init_values(self, values, kw):
-        self.quoting = kw.pop('quoting', 'auto')
-
-        if self.quoting == 'auto' and len(values):
-            # What quoting character are we using?
-            q = None
-            for e in values:
-                if len(e) == 0:
-                    self.quoting = 'unquoted'
-                    break
-                elif q is None:
-                    q = e[0]
-
-                if len(e) == 1 or e[0] != q or e[-1] != q:
-                    self.quoting = 'unquoted'
-                    break
-            else:
-                self.quoting = 'quoted'
-
-        if self.quoting == 'quoted':
-            util.warn_deprecated(
-                'Manually quoting %s value literals is deprecated.  Supply '
-                'unquoted values and use the quoting= option in cases of '
-                'ambiguity.' % self.__class__.__name__)
-
-            values = self._strip_values(values)
-
-        self._enumerated_values = values
-        length = max([len(v) for v in values] + [0])
-        return values, length
-
-    @classmethod
-    def _strip_values(cls, values):
-        strip_values = []
-        for a in values:
-            if a[0:1] == '"' or a[0:1] == "'":
-                # strip enclosing quotes and unquote interior
-                a = a[1:-1].replace(a[0] * 2, a[0])
-            strip_values.append(a)
-        return strip_values
-
-
-class ENUM(sqltypes.Enum, _EnumeratedValues):
-    """MySQL ENUM type."""
-
-    __visit_name__ = 'ENUM'
-
-    def __init__(self, *enums, **kw):
-        """Construct an ENUM.
-
-        E.g.::
-
-          Column('myenum', ENUM("foo", "bar", "baz"))
-
-        :param enums: The range of valid values for this ENUM.  Values will be
-          quoted when generating the schema according to the quoting flag (see
-          below).
-
-        :param strict: Defaults to False: ensure that a given value is in this
-          ENUM's range of permissible values when inserting or updating rows.
-          Note that MySQL will not raise a fatal error if you attempt to store
-          an out of range value- an alternate value will be stored instead.
-          (See MySQL ENUM documentation.)
-
-        :param charset: Optional, a column-level character set for this string
-          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
-
-        :param collation: Optional, a column-level collation for this string
-          value.  Takes precedence to 'binary' short-hand.
-
-        :param ascii: Defaults to False: short-hand for the ``latin1``
-          character set, generates ASCII in schema.
-
-        :param unicode: Defaults to False: short-hand for the ``ucs2``
-          character set, generates UNICODE in schema.
-
-        :param binary: Defaults to False: short-hand, pick the binary
-          collation type that matches the column's character set.  Generates
-          BINARY in schema.  This does not affect the type of data stored,
-          only the collation of character data.
-
-        :param quoting: Defaults to 'auto': automatically determine enum value
-          quoting.  If all enum values are surrounded by the same quoting
-          character, then use 'quoted' mode.  Otherwise, use 'unquoted' mode.
-
-          'quoted': values in enums are already quoted, they will be used
-          directly when generating the schema - this usage is deprecated.
-
-          'unquoted': values in enums are not quoted, they will be escaped and
-          surrounded by single quotes when generating the schema.
-
-          Previous versions of this type always required manually quoted
-          values to be supplied; future versions will always quote the string
-          literals for you.  This is a transitional option.
-
-        """
-        values, length = self._init_values(enums, kw)
-        self.strict = kw.pop('strict', False)
-        kw.pop('metadata', None)
-        kw.pop('schema', None)
-        kw.pop('name', None)
-        kw.pop('quote', None)
-        kw.pop('native_enum', None)
-        kw.pop('inherit_schema', None)
-        kw.pop('_create_events', None)
-        _StringType.__init__(self, length=length, **kw)
-        sqltypes.Enum.__init__(self, *values)
-
-    def __repr__(self):
-        return util.generic_repr(
-            self, to_inspect=[ENUM, _StringType, sqltypes.Enum])
-
-    def bind_processor(self, dialect):
-        super_convert = super(ENUM, self).bind_processor(dialect)
-
-        def process(value):
-            if self.strict and value is not None and value not in self.enums:
-                raise exc.InvalidRequestError('"%s" not a valid value for '
-                                              'this enum' % value)
-            if super_convert:
-                return super_convert(value)
-            else:
-                return value
-        return process
-
-    def adapt(self, cls, **kw):
-        if issubclass(cls, ENUM):
-            kw['strict'] = self.strict
-        return sqltypes.Enum.adapt(self, cls, **kw)
-
-
-class SET(_EnumeratedValues):
-    """MySQL SET type."""
-
-    __visit_name__ = 'SET'
-
-    def __init__(self, *values, **kw):
-        """Construct a SET.
-
-        E.g.::
-
-          Column('myset', SET("foo", "bar", "baz"))
-
-
-        The list of potential values is required in the case that this
-        set will be used to generate DDL for a table, or if the
-        :paramref:`.SET.retrieve_as_bitwise` flag is set to True.
-
-        :param values: The range of valid values for this SET.
-
-        :param convert_unicode: Same flag as that of
-         :paramref:`.String.convert_unicode`.
-
-        :param collation: same as that of :paramref:`.String.collation`
-
-        :param charset: same as that of :paramref:`.VARCHAR.charset`.
-
-        :param ascii: same as that of :paramref:`.VARCHAR.ascii`.
-
-        :param unicode: same as that of :paramref:`.VARCHAR.unicode`.
-
-        :param binary: same as that of :paramref:`.VARCHAR.binary`.
-
-        :param quoting: Defaults to 'auto': automatically determine set value
-          quoting.  If all values are surrounded by the same quoting
-          character, then use 'quoted' mode.  Otherwise, use 'unquoted' mode.
-
-          'quoted': values in enums are already quoted, they will be used
-          directly when generating the schema - this usage is deprecated.
-
-          'unquoted': values in enums are not quoted, they will be escaped and
-          surrounded by single quotes when generating the schema.
-
-          Previous versions of this type always required manually quoted
-          values to be supplied; future versions will always quote the string
-          literals for you.  This is a transitional option.
-
-          .. versionadded:: 0.9.0
-
-        :param retrieve_as_bitwise: if True, the data for the set type will be
-          persisted and selected using an integer value, where a set is coerced
-          into a bitwise mask for persistence.  MySQL allows this mode which
-          has the advantage of being able to store values unambiguously,
-          such as the blank string ``''``.   The datatype will appear
-          as the expression ``col + 0`` in a SELECT statement, so that the
-          value is coerced into an integer value in result sets.
-          This flag is required if one wishes
-          to persist a set that can store the blank string ``''`` as a value.
-
-          .. warning::
-
-            When using :paramref:`.mysql.SET.retrieve_as_bitwise`, it is
-            essential that the list of set values is expressed in the
-            **exact same order** as exists on the MySQL database.
-
-          .. versionadded:: 1.0.0
-
-
-        """
-        self.retrieve_as_bitwise = kw.pop('retrieve_as_bitwise', False)
-        values, length = self._init_values(values, kw)
-        self.values = tuple(values)
-        if not self.retrieve_as_bitwise and '' in values:
-            raise exc.ArgumentError(
-                "Can't use the blank value '' in a SET without "
-                "setting retrieve_as_bitwise=True")
-        if self.retrieve_as_bitwise:
-            self._bitmap = dict(
-                (value, 2 ** idx)
-                for idx, value in enumerate(self.values)
-            )
-            self._bitmap.update(
-                (2 ** idx, value)
-                for idx, value in enumerate(self.values)
-            )
-        kw.setdefault('length', length)
-        super(SET, self).__init__(**kw)
-
-    def column_expression(self, colexpr):
-        if self.retrieve_as_bitwise:
-            return sql.type_coerce(
-                sql.type_coerce(colexpr, sqltypes.Integer) + 0,
-                self
-            )
-        else:
-            return colexpr
-
-    def result_processor(self, dialect, coltype):
-        if self.retrieve_as_bitwise:
-            def process(value):
-                if value is not None:
-                    value = int(value)
-
-                    return set(
-                        util.map_bits(self._bitmap.__getitem__, value)
-                    )
-                else:
-                    return None
-        else:
-            super_convert = super(SET, self).result_processor(dialect, coltype)
-
-            def process(value):
-                if isinstance(value, util.string_types):
-                    # MySQLdb returns a string, let's parse
-                    if super_convert:
-                        value = super_convert(value)
-                    return set(re.findall(r'[^,]+', value))
-                else:
-                    # mysql-connector-python does a naive
-                    # split(",") which throws in an empty string
-                    if value is not None:
-                        value.discard('')
-                    return value
-        return process
-
-    def bind_processor(self, dialect):
-        super_convert = super(SET, self).bind_processor(dialect)
-        if self.retrieve_as_bitwise:
-            def process(value):
-                if value is None:
-                    return None
-                elif isinstance(value, util.int_types + util.string_types):
-                    if super_convert:
-                        return super_convert(value)
-                    else:
-                        return value
-                else:
-                    int_value = 0
-                    for v in value:
-                        int_value |= self._bitmap[v]
-                    return int_value
-        else:
-
-            def process(value):
-                # accept strings and int (actually bitflag) values directly
-                if value is not None and not isinstance(
-                        value, util.int_types + util.string_types):
-                    value = ",".join(value)
-
-                if super_convert:
-                    return super_convert(value)
-                else:
-                    return value
-        return process
-
-    def adapt(self, impltype, **kw):
-        kw['retrieve_as_bitwise'] = self.retrieve_as_bitwise
-        return util.constructor_copy(
-            self, impltype,
-            *self.values,
-            **kw
-        )
-
 # old names
 MSTime = TIME
 MSSet = SET
@@ -1974,7 +933,7 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
             ('PARTITION_BY', 'PARTITIONS'),  # only for test consistency
         ], opts):
             arg = opts[opt]
-            if opt in _options_of_type_string:
+            if opt in _reflection._options_of_type_string:
                 arg = "'%s'" % arg.replace("\\", "\\\\").replace("'", "''")
 
             if opt in ('DATA_DIRECTORY', 'INDEX_DIRECTORY',
@@ -2796,7 +1755,7 @@ class MySQLDialect(default.DefaultDialect):
             preparer = self.preparer(self, server_ansiquotes=False)
         else:
             preparer = self.identifier_preparer
-        return MySQLTableDefinitionParser(self, preparer)
+        return _reflection.MySQLTableDefinitionParser(self, preparer)
 
     @reflection.cache
     def _setup_parser(self, connection, table_name, schema=None, **kw):
@@ -2928,430 +1887,6 @@ class MySQLDialect(default.DefaultDialect):
         return rows
 
 
-class ReflectedState(object):
-    """Stores raw information about a SHOW CREATE TABLE statement."""
-
-    def __init__(self):
-        self.columns = []
-        self.table_options = {}
-        self.table_name = None
-        self.keys = []
-        self.constraints = []
-
-
-@log.class_logger
-class MySQLTableDefinitionParser(object):
-    """Parses the results of a SHOW CREATE TABLE statement."""
-
-    def __init__(self, dialect, preparer):
-        self.dialect = dialect
-        self.preparer = preparer
-        self._prep_regexes()
-
-    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):
-                self._parse_column(line, state)
-            # a regular table options line
-            elif line.startswith(') '):
-                self._parse_table_options(line, state)
-            # an ANSI-mode table options line
-            elif line == ')':
-                pass
-            elif line.startswith('CREATE '):
-                self._parse_table_name(line, state)
-            # Not present in real reflection, but may be if
-            # loading from a file.
-            elif not line:
-                pass
-            else:
-                type_, spec = self._parse_constraints(line)
-                if type_ is None:
-                    util.warn("Unknown schema content: %r" % line)
-                elif type_ == 'key':
-                    state.keys.append(spec)
-                elif type_ == 'constraint':
-                    state.constraints.append(spec)
-                else:
-                    pass
-        return state
-
-    def _parse_constraints(self, line):
-        """Parse a KEY or CONSTRAINT line.
-
-        :param 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 _parse_table_name(self, line, state):
-        """Extract the table name.
-
-        :param line: The first line of SHOW CREATE TABLE
-        """
-
-        regex, cleanup = self._pr_name
-        m = regex.match(line)
-        if m:
-            state.table_name = cleanup(m.group('name'))
-
-    def _parse_table_options(self, line, state):
-        """Build a dictionary of all reflected table-level options.
-
-        :param line: The final line of SHOW CREATE TABLE output.
-        """
-
-        options = {}
-
-        if not line or line == ')':
-            pass
-
-        else:
-            rest_of_line = line[:]
-            for regex, cleanup in self._pr_options:
-                m = regex.search(rest_of_line)
-                if not m:
-                    continue
-                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)
-
-        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
-
-    def _parse_column(self, line, state):
-        """Extract column details.
-
-        Falls back to a 'minimal support' variant if full parse fails.
-
-        :param line: Any column-bearing line from SHOW CREATE TABLE
-        """
-
-        spec = None
-        m = self._re_column.match(line)
-        if m:
-            spec = m.groupdict()
-            spec['full'] = True
-        else:
-            m = self._re_column_loose.match(line)
-            if m:
-                spec = m.groupdict()
-                spec['full'] = False
-        if not spec:
-            util.warn("Unknown column definition %r" % line)
-            return
-        if not spec['full']:
-            util.warn("Incomplete reflection of column definition %r" % line)
-
-        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))
-            col_type = 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 = {}
-
-        if issubclass(col_type, (DATETIME, TIME, TIMESTAMP)):
-            if type_args:
-                type_kw['fsp'] = type_args.pop(0)
-
-        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]
-        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
-
-        type_instance = col_type(*type_args, **type_kw)
-
-        col_kw = {}
-
-        # NOT NULL
-        col_kw['nullable'] = True
-        # this can be "NULL" in the case of TIMESTAMP
-        if spec.get('notnull', False) == 'NOT NULL':
-            col_kw['nullable'] = False
-
-        # AUTO_INCREMENT
-        if spec.get('autoincr', False):
-            col_kw['autoincrement'] = True
-        elif issubclass(col_type, sqltypes.Integer):
-            col_kw['autoincrement'] = False
-
-        # DEFAULT
-        default = spec.get('default', None)
-
-        if default == 'NULL':
-            # eliminates the need to deal with this later.
-            default = None
-
-        col_d = dict(name=name, type=type_instance, default=default)
-        col_d.update(col_kw)
-        state.columns.append(col_d)
-
-    def _describe_to_create(self, table_name, columns):
-        """Re-format DESCRIBE output as a SHOW CREATE TABLE string.
-
-        DESCRIBE is a much simpler reflection and is sufficient for
-        reflecting views for runtime use.  This method formats DDL
-        for columns only- keys are omitted.
-
-        :param columns: A sequence of DESCRIBE or SHOW COLUMNS 6-tuples.
-          SHOW FULL COLUMNS FROM rows must be rearranged for use with
-          this function.
-        """
-
-        buffer = []
-        for row in columns:
-            (name, col_type, nullable, default, extra) = \
-                [row[i] for i in (0, 1, 2, 4, 5)]
-
-            line = [' ']
-            line.append(self.preparer.quote_identifier(name))
-            line.append(col_type)
-            if not nullable:
-                line.append('NOT NULL')
-            if default:
-                if 'auto_increment' in default:
-                    pass
-                elif (col_type.startswith('timestamp') and
-                      default.startswith('C')):
-                    line.append('DEFAULT')
-                    line.append(default)
-                elif default == 'NULL':
-                    line.append('DEFAULT')
-                    line.append(default)
-                else:
-                    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) '])
-
-    def _parse_keyexprs(self, identifiers):
-        """Unpack '"col"(2),"col" ASC'-ish strings into components."""
-
-        return self._re_keyexprs.findall(identifiers)
-
-    def _prep_regexes(self):
-        """Pre-compile regular expressions."""
-
-        self._re_columns = []
-        self._pr_options = []
-
-        _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))]))
-
-        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)
-
-        # `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_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',?$'
-            % 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|CASCADE|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'(?:\.%(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'(?:.*)(?:SUB)?PARTITION(?:.*)')
-
-        # Table-level options (COLLATE, ENGINE, etc.)
-        # Do the string options first, since they have quoted
-        # strings we need to get rid of.
-        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'):
-            self._add_option_word(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+')
-
-    _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("''", "'")
-        ))
-
-    def _add_option_word(self, directive):
-        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))
-        self._pr_options.append(_pr_compile(regex))
-
-_options_of_type_string = ('COMMENT', 'DATA DIRECTORY', 'INDEX DIRECTORY',
-                           'PASSWORD', 'CONNECTION')
-
 
 class _DecodingRowProxy(object):
     """Return unicode-decoded values based on type inspection.
@@ -3397,14 +1932,3 @@ class _DecodingRowProxy(object):
         else:
             return item
 
-
-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)
diff --git a/lib/sqlalchemy/dialects/mysql/enumerated.py b/lib/sqlalchemy/dialects/mysql/enumerated.py
new file mode 100644 (file)
index 0000000..53de2b5
--- /dev/null
@@ -0,0 +1,307 @@
+# mysql/enumerated.py
+# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+import re
+
+from .types import _StringType
+from ... import exc, sql, util
+from ... import types as sqltypes
+
+
+class _EnumeratedValues(_StringType):
+    def _init_values(self, values, kw):
+        self.quoting = kw.pop('quoting', 'auto')
+
+        if self.quoting == 'auto' and len(values):
+            # What quoting character are we using?
+            q = None
+            for e in values:
+                if len(e) == 0:
+                    self.quoting = 'unquoted'
+                    break
+                elif q is None:
+                    q = e[0]
+
+                if len(e) == 1 or e[0] != q or e[-1] != q:
+                    self.quoting = 'unquoted'
+                    break
+            else:
+                self.quoting = 'quoted'
+
+        if self.quoting == 'quoted':
+            util.warn_deprecated(
+                'Manually quoting %s value literals is deprecated.  Supply '
+                'unquoted values and use the quoting= option in cases of '
+                'ambiguity.' % self.__class__.__name__)
+
+            values = self._strip_values(values)
+
+        self._enumerated_values = values
+        length = max([len(v) for v in values] + [0])
+        return values, length
+
+    @classmethod
+    def _strip_values(cls, values):
+        strip_values = []
+        for a in values:
+            if a[0:1] == '"' or a[0:1] == "'":
+                # strip enclosing quotes and unquote interior
+                a = a[1:-1].replace(a[0] * 2, a[0])
+            strip_values.append(a)
+        return strip_values
+
+
+class ENUM(sqltypes.Enum, _EnumeratedValues):
+    """MySQL ENUM type."""
+
+    __visit_name__ = 'ENUM'
+
+    def __init__(self, *enums, **kw):
+        """Construct an ENUM.
+
+        E.g.::
+
+          Column('myenum', ENUM("foo", "bar", "baz"))
+
+        :param enums: The range of valid values for this ENUM.  Values will be
+          quoted when generating the schema according to the quoting flag (see
+          below).
+
+        :param strict: Defaults to False: ensure that a given value is in this
+          ENUM's range of permissible values when inserting or updating rows.
+          Note that MySQL will not raise a fatal error if you attempt to store
+          an out of range value- an alternate value will be stored instead.
+          (See MySQL ENUM documentation.)
+
+        :param charset: Optional, a column-level character set for this string
+          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
+
+        :param collation: Optional, a column-level collation for this string
+          value.  Takes precedence to 'binary' short-hand.
+
+        :param ascii: Defaults to False: short-hand for the ``latin1``
+          character set, generates ASCII in schema.
+
+        :param unicode: Defaults to False: short-hand for the ``ucs2``
+          character set, generates UNICODE in schema.
+
+        :param binary: Defaults to False: short-hand, pick the binary
+          collation type that matches the column's character set.  Generates
+          BINARY in schema.  This does not affect the type of data stored,
+          only the collation of character data.
+
+        :param quoting: Defaults to 'auto': automatically determine enum value
+          quoting.  If all enum values are surrounded by the same quoting
+          character, then use 'quoted' mode.  Otherwise, use 'unquoted' mode.
+
+          'quoted': values in enums are already quoted, they will be used
+          directly when generating the schema - this usage is deprecated.
+
+          'unquoted': values in enums are not quoted, they will be escaped and
+          surrounded by single quotes when generating the schema.
+
+          Previous versions of this type always required manually quoted
+          values to be supplied; future versions will always quote the string
+          literals for you.  This is a transitional option.
+
+        """
+        values, length = self._init_values(enums, kw)
+        self.strict = kw.pop('strict', False)
+        kw.pop('metadata', None)
+        kw.pop('schema', None)
+        kw.pop('name', None)
+        kw.pop('quote', None)
+        kw.pop('native_enum', None)
+        kw.pop('inherit_schema', None)
+        kw.pop('_create_events', None)
+        _StringType.__init__(self, length=length, **kw)
+        sqltypes.Enum.__init__(self, *values)
+
+    def __repr__(self):
+        return util.generic_repr(
+            self, to_inspect=[ENUM, _StringType, sqltypes.Enum])
+
+    def bind_processor(self, dialect):
+        super_convert = super(ENUM, self).bind_processor(dialect)
+
+        def process(value):
+            if self.strict and value is not None and value not in self.enums:
+                raise exc.InvalidRequestError('"%s" not a valid value for '
+                                              'this enum' % value)
+            if super_convert:
+                return super_convert(value)
+            else:
+                return value
+        return process
+
+    def adapt(self, cls, **kw):
+        if issubclass(cls, ENUM):
+            kw['strict'] = self.strict
+        return sqltypes.Enum.adapt(self, cls, **kw)
+
+
+class SET(_EnumeratedValues):
+    """MySQL SET type."""
+
+    __visit_name__ = 'SET'
+
+    def __init__(self, *values, **kw):
+        """Construct a SET.
+
+        E.g.::
+
+          Column('myset', SET("foo", "bar", "baz"))
+
+
+        The list of potential values is required in the case that this
+        set will be used to generate DDL for a table, or if the
+        :paramref:`.SET.retrieve_as_bitwise` flag is set to True.
+
+        :param values: The range of valid values for this SET.
+
+        :param convert_unicode: Same flag as that of
+         :paramref:`.String.convert_unicode`.
+
+        :param collation: same as that of :paramref:`.String.collation`
+
+        :param charset: same as that of :paramref:`.VARCHAR.charset`.
+
+        :param ascii: same as that of :paramref:`.VARCHAR.ascii`.
+
+        :param unicode: same as that of :paramref:`.VARCHAR.unicode`.
+
+        :param binary: same as that of :paramref:`.VARCHAR.binary`.
+
+        :param quoting: Defaults to 'auto': automatically determine set value
+          quoting.  If all values are surrounded by the same quoting
+          character, then use 'quoted' mode.  Otherwise, use 'unquoted' mode.
+
+          'quoted': values in enums are already quoted, they will be used
+          directly when generating the schema - this usage is deprecated.
+
+          'unquoted': values in enums are not quoted, they will be escaped and
+          surrounded by single quotes when generating the schema.
+
+          Previous versions of this type always required manually quoted
+          values to be supplied; future versions will always quote the string
+          literals for you.  This is a transitional option.
+
+          .. versionadded:: 0.9.0
+
+        :param retrieve_as_bitwise: if True, the data for the set type will be
+          persisted and selected using an integer value, where a set is coerced
+          into a bitwise mask for persistence.  MySQL allows this mode which
+          has the advantage of being able to store values unambiguously,
+          such as the blank string ``''``.   The datatype will appear
+          as the expression ``col + 0`` in a SELECT statement, so that the
+          value is coerced into an integer value in result sets.
+          This flag is required if one wishes
+          to persist a set that can store the blank string ``''`` as a value.
+
+          .. warning::
+
+            When using :paramref:`.mysql.SET.retrieve_as_bitwise`, it is
+            essential that the list of set values is expressed in the
+            **exact same order** as exists on the MySQL database.
+
+          .. versionadded:: 1.0.0
+
+
+        """
+        self.retrieve_as_bitwise = kw.pop('retrieve_as_bitwise', False)
+        values, length = self._init_values(values, kw)
+        self.values = tuple(values)
+        if not self.retrieve_as_bitwise and '' in values:
+            raise exc.ArgumentError(
+                "Can't use the blank value '' in a SET without "
+                "setting retrieve_as_bitwise=True")
+        if self.retrieve_as_bitwise:
+            self._bitmap = dict(
+                (value, 2 ** idx)
+                for idx, value in enumerate(self.values)
+            )
+            self._bitmap.update(
+                (2 ** idx, value)
+                for idx, value in enumerate(self.values)
+            )
+        kw.setdefault('length', length)
+        super(SET, self).__init__(**kw)
+
+    def column_expression(self, colexpr):
+        if self.retrieve_as_bitwise:
+            return sql.type_coerce(
+                sql.type_coerce(colexpr, sqltypes.Integer) + 0,
+                self
+            )
+        else:
+            return colexpr
+
+    def result_processor(self, dialect, coltype):
+        if self.retrieve_as_bitwise:
+            def process(value):
+                if value is not None:
+                    value = int(value)
+
+                    return set(
+                        util.map_bits(self._bitmap.__getitem__, value)
+                    )
+                else:
+                    return None
+        else:
+            super_convert = super(SET, self).result_processor(dialect, coltype)
+
+            def process(value):
+                if isinstance(value, util.string_types):
+                    # MySQLdb returns a string, let's parse
+                    if super_convert:
+                        value = super_convert(value)
+                    return set(re.findall(r'[^,]+', value))
+                else:
+                    # mysql-connector-python does a naive
+                    # split(",") which throws in an empty string
+                    if value is not None:
+                        value.discard('')
+                    return value
+        return process
+
+    def bind_processor(self, dialect):
+        super_convert = super(SET, self).bind_processor(dialect)
+        if self.retrieve_as_bitwise:
+            def process(value):
+                if value is None:
+                    return None
+                elif isinstance(value, util.int_types + util.string_types):
+                    if super_convert:
+                        return super_convert(value)
+                    else:
+                        return value
+                else:
+                    int_value = 0
+                    for v in value:
+                        int_value |= self._bitmap[v]
+                    return int_value
+        else:
+
+            def process(value):
+                # accept strings and int (actually bitflag) values directly
+                if value is not None and not isinstance(
+                        value, util.int_types + util.string_types):
+                    value = ",".join(value)
+
+                if super_convert:
+                    return super_convert(value)
+                else:
+                    return value
+        return process
+
+    def adapt(self, impltype, **kw):
+        kw['retrieve_as_bitwise'] = self.retrieve_as_bitwise
+        return util.constructor_copy(
+            self, impltype,
+            *self.values,
+            **kw
+        )
diff --git a/lib/sqlalchemy/dialects/mysql/reflection.py b/lib/sqlalchemy/dialects/mysql/reflection.py
new file mode 100644 (file)
index 0000000..cf10782
--- /dev/null
@@ -0,0 +1,449 @@
+# mysql/reflection.py
+# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+import re
+from ... import log, util
+from ... import types as sqltypes
+from .enumerated import _EnumeratedValues, SET
+from .types import DATETIME, TIME, TIMESTAMP
+
+
+class ReflectedState(object):
+    """Stores raw information about a SHOW CREATE TABLE statement."""
+
+    def __init__(self):
+        self.columns = []
+        self.table_options = {}
+        self.table_name = None
+        self.keys = []
+        self.constraints = []
+
+
+@log.class_logger
+class MySQLTableDefinitionParser(object):
+    """Parses the results of a SHOW CREATE TABLE statement."""
+
+    def __init__(self, dialect, preparer):
+        self.dialect = dialect
+        self.preparer = preparer
+        self._prep_regexes()
+
+    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):
+                self._parse_column(line, state)
+            # a regular table options line
+            elif line.startswith(') '):
+                self._parse_table_options(line, state)
+            # an ANSI-mode table options line
+            elif line == ')':
+                pass
+            elif line.startswith('CREATE '):
+                self._parse_table_name(line, state)
+            # Not present in real reflection, but may be if
+            # loading from a file.
+            elif not line:
+                pass
+            else:
+                type_, spec = self._parse_constraints(line)
+                if type_ is None:
+                    util.warn("Unknown schema content: %r" % line)
+                elif type_ == 'key':
+                    state.keys.append(spec)
+                elif type_ == 'constraint':
+                    state.constraints.append(spec)
+                else:
+                    pass
+        return state
+
+    def _parse_constraints(self, line):
+        """Parse a KEY or CONSTRAINT line.
+
+        :param 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 _parse_table_name(self, line, state):
+        """Extract the table name.
+
+        :param line: The first line of SHOW CREATE TABLE
+        """
+
+        regex, cleanup = self._pr_name
+        m = regex.match(line)
+        if m:
+            state.table_name = cleanup(m.group('name'))
+
+    def _parse_table_options(self, line, state):
+        """Build a dictionary of all reflected table-level options.
+
+        :param line: The final line of SHOW CREATE TABLE output.
+        """
+
+        options = {}
+
+        if not line or line == ')':
+            pass
+
+        else:
+            rest_of_line = line[:]
+            for regex, cleanup in self._pr_options:
+                m = regex.search(rest_of_line)
+                if not m:
+                    continue
+                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)
+
+        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
+
+    def _parse_column(self, line, state):
+        """Extract column details.
+
+        Falls back to a 'minimal support' variant if full parse fails.
+
+        :param line: Any column-bearing line from SHOW CREATE TABLE
+        """
+
+        spec = None
+        m = self._re_column.match(line)
+        if m:
+            spec = m.groupdict()
+            spec['full'] = True
+        else:
+            m = self._re_column_loose.match(line)
+            if m:
+                spec = m.groupdict()
+                spec['full'] = False
+        if not spec:
+            util.warn("Unknown column definition %r" % line)
+            return
+        if not spec['full']:
+            util.warn("Incomplete reflection of column definition %r" % line)
+
+        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))
+            col_type = 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 = {}
+
+        if issubclass(col_type, (DATETIME, TIME, TIMESTAMP)):
+            if type_args:
+                type_kw['fsp'] = type_args.pop(0)
+
+        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]
+        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
+
+        type_instance = col_type(*type_args, **type_kw)
+
+        col_kw = {}
+
+        # NOT NULL
+        col_kw['nullable'] = True
+        # this can be "NULL" in the case of TIMESTAMP
+        if spec.get('notnull', False) == 'NOT NULL':
+            col_kw['nullable'] = False
+
+        # AUTO_INCREMENT
+        if spec.get('autoincr', False):
+            col_kw['autoincrement'] = True
+        elif issubclass(col_type, sqltypes.Integer):
+            col_kw['autoincrement'] = False
+
+        # DEFAULT
+        default = spec.get('default', None)
+
+        if default == 'NULL':
+            # eliminates the need to deal with this later.
+            default = None
+
+        col_d = dict(name=name, type=type_instance, default=default)
+        col_d.update(col_kw)
+        state.columns.append(col_d)
+
+    def _describe_to_create(self, table_name, columns):
+        """Re-format DESCRIBE output as a SHOW CREATE TABLE string.
+
+        DESCRIBE is a much simpler reflection and is sufficient for
+        reflecting views for runtime use.  This method formats DDL
+        for columns only- keys are omitted.
+
+        :param columns: A sequence of DESCRIBE or SHOW COLUMNS 6-tuples.
+          SHOW FULL COLUMNS FROM rows must be rearranged for use with
+          this function.
+        """
+
+        buffer = []
+        for row in columns:
+            (name, col_type, nullable, default, extra) = \
+                [row[i] for i in (0, 1, 2, 4, 5)]
+
+            line = [' ']
+            line.append(self.preparer.quote_identifier(name))
+            line.append(col_type)
+            if not nullable:
+                line.append('NOT NULL')
+            if default:
+                if 'auto_increment' in default:
+                    pass
+                elif (col_type.startswith('timestamp') and
+                      default.startswith('C')):
+                    line.append('DEFAULT')
+                    line.append(default)
+                elif default == 'NULL':
+                    line.append('DEFAULT')
+                    line.append(default)
+                else:
+                    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) '])
+
+    def _parse_keyexprs(self, identifiers):
+        """Unpack '"col"(2),"col" ASC'-ish strings into components."""
+
+        return self._re_keyexprs.findall(identifiers)
+
+    def _prep_regexes(self):
+        """Pre-compile regular expressions."""
+
+        self._re_columns = []
+        self._pr_options = []
+
+        _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))]))
+
+        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)
+
+        # `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_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',?$'
+            % 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|CASCADE|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'(?:\.%(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'(?:.*)(?:SUB)?PARTITION(?:.*)')
+
+        # Table-level options (COLLATE, ENGINE, etc.)
+        # Do the string options first, since they have quoted
+        # strings we need to get rid of.
+        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'):
+            self._add_option_word(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+')
+
+    _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("''", "'")
+        ))
+
+    def _add_option_word(self, directive):
+        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))
+        self._pr_options.append(_pr_compile(regex))
+
+_options_of_type_string = ('COMMENT', 'DATA DIRECTORY', 'INDEX DIRECTORY',
+                           'PASSWORD', 'CONNECTION')
+
+
+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)
diff --git a/lib/sqlalchemy/dialects/mysql/types.py b/lib/sqlalchemy/dialects/mysql/types.py
new file mode 100644 (file)
index 0000000..9512982
--- /dev/null
@@ -0,0 +1,766 @@
+# mysql/types.py
+# Copyright (C) 2005-2015 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+import datetime
+from ... import exc, util
+from ... import types as sqltypes
+
+
+class _NumericType(object):
+    """Base for MySQL numeric types.
+
+    This is the base both for NUMERIC as well as INTEGER, hence
+    it's a mixin.
+
+    """
+
+    def __init__(self, unsigned=False, zerofill=False, **kw):
+        self.unsigned = unsigned
+        self.zerofill = zerofill
+        super(_NumericType, self).__init__(**kw)
+
+    def __repr__(self):
+        return util.generic_repr(self,
+                                 to_inspect=[_NumericType, sqltypes.Numeric])
+
+
+class _FloatType(_NumericType, sqltypes.Float):
+    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
+        if isinstance(self, (REAL, DOUBLE)) and \
+            (
+                (precision is None and scale is not None) or
+                (precision is not None and scale is None)
+        ):
+            raise exc.ArgumentError(
+                "You must specify both precision and scale or omit "
+                "both altogether.")
+        super(_FloatType, self).__init__(
+            precision=precision, asdecimal=asdecimal, **kw)
+        self.scale = scale
+
+    def __repr__(self):
+        return util.generic_repr(self, to_inspect=[_FloatType,
+                                                   _NumericType,
+                                                   sqltypes.Float])
+
+
+class _IntegerType(_NumericType, sqltypes.Integer):
+    def __init__(self, display_width=None, **kw):
+        self.display_width = display_width
+        super(_IntegerType, self).__init__(**kw)
+
+    def __repr__(self):
+        return util.generic_repr(self, to_inspect=[_IntegerType,
+                                                   _NumericType,
+                                                   sqltypes.Integer])
+
+
+class _StringType(sqltypes.String):
+    """Base for MySQL string types."""
+
+    def __init__(self, charset=None, collation=None,
+                 ascii=False, binary=False, unicode=False,
+                 national=False, **kw):
+        self.charset = charset
+
+        # allow collate= or collation=
+        kw.setdefault('collation', kw.pop('collate', collation))
+
+        self.ascii = ascii
+        self.unicode = unicode
+        self.binary = binary
+        self.national = national
+        super(_StringType, self).__init__(**kw)
+
+    def __repr__(self):
+        return util.generic_repr(self,
+                                 to_inspect=[_StringType, sqltypes.String])
+
+
+class _MatchType(sqltypes.Float, sqltypes.MatchType):
+    def __init__(self, **kw):
+        # TODO: float arguments?
+        sqltypes.Float.__init__(self)
+        sqltypes.MatchType.__init__(self)
+
+
+
+class NUMERIC(_NumericType, sqltypes.NUMERIC):
+    """MySQL NUMERIC type."""
+
+    __visit_name__ = 'NUMERIC'
+
+    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
+        """Construct a NUMERIC.
+
+        :param precision: Total digits in this number.  If scale and precision
+          are both None, values are stored to limits allowed by the server.
+
+        :param scale: The number of digits after the decimal point.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(NUMERIC, self).__init__(precision=precision,
+                                      scale=scale, asdecimal=asdecimal, **kw)
+
+
+class DECIMAL(_NumericType, sqltypes.DECIMAL):
+    """MySQL DECIMAL type."""
+
+    __visit_name__ = 'DECIMAL'
+
+    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
+        """Construct a DECIMAL.
+
+        :param precision: Total digits in this number.  If scale and precision
+          are both None, values are stored to limits allowed by the server.
+
+        :param scale: The number of digits after the decimal point.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(DECIMAL, self).__init__(precision=precision, scale=scale,
+                                      asdecimal=asdecimal, **kw)
+
+
+class DOUBLE(_FloatType):
+    """MySQL DOUBLE type."""
+
+    __visit_name__ = 'DOUBLE'
+
+    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
+        """Construct a DOUBLE.
+
+        .. note::
+
+            The :class:`.DOUBLE` type by default converts from float
+            to Decimal, using a truncation that defaults to 10 digits.
+            Specify either ``scale=n`` or ``decimal_return_scale=n`` in order
+            to change this scale, or ``asdecimal=False`` to return values
+            directly as Python floating points.
+
+        :param precision: Total digits in this number.  If scale and precision
+          are both None, values are stored to limits allowed by the server.
+
+        :param scale: The number of digits after the decimal point.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(DOUBLE, self).__init__(precision=precision, scale=scale,
+                                     asdecimal=asdecimal, **kw)
+
+
+class REAL(_FloatType, sqltypes.REAL):
+    """MySQL REAL type."""
+
+    __visit_name__ = 'REAL'
+
+    def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
+        """Construct a REAL.
+
+        .. note::
+
+            The :class:`.REAL` type by default converts from float
+            to Decimal, using a truncation that defaults to 10 digits.
+            Specify either ``scale=n`` or ``decimal_return_scale=n`` in order
+            to change this scale, or ``asdecimal=False`` to return values
+            directly as Python floating points.
+
+        :param precision: Total digits in this number.  If scale and precision
+          are both None, values are stored to limits allowed by the server.
+
+        :param scale: The number of digits after the decimal point.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(REAL, self).__init__(precision=precision, scale=scale,
+                                   asdecimal=asdecimal, **kw)
+
+
+class FLOAT(_FloatType, sqltypes.FLOAT):
+    """MySQL FLOAT type."""
+
+    __visit_name__ = 'FLOAT'
+
+    def __init__(self, precision=None, scale=None, asdecimal=False, **kw):
+        """Construct a FLOAT.
+
+        :param precision: Total digits in this number.  If scale and precision
+          are both None, values are stored to limits allowed by the server.
+
+        :param scale: The number of digits after the decimal point.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(FLOAT, self).__init__(precision=precision, scale=scale,
+                                    asdecimal=asdecimal, **kw)
+
+    def bind_processor(self, dialect):
+        return None
+
+
+class INTEGER(_IntegerType, sqltypes.INTEGER):
+    """MySQL INTEGER type."""
+
+    __visit_name__ = 'INTEGER'
+
+    def __init__(self, display_width=None, **kw):
+        """Construct an INTEGER.
+
+        :param display_width: Optional, maximum display width for this number.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(INTEGER, self).__init__(display_width=display_width, **kw)
+
+
+class BIGINT(_IntegerType, sqltypes.BIGINT):
+    """MySQL BIGINTEGER type."""
+
+    __visit_name__ = 'BIGINT'
+
+    def __init__(self, display_width=None, **kw):
+        """Construct a BIGINTEGER.
+
+        :param display_width: Optional, maximum display width for this number.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(BIGINT, self).__init__(display_width=display_width, **kw)
+
+
+class MEDIUMINT(_IntegerType):
+    """MySQL MEDIUMINTEGER type."""
+
+    __visit_name__ = 'MEDIUMINT'
+
+    def __init__(self, display_width=None, **kw):
+        """Construct a MEDIUMINTEGER
+
+        :param display_width: Optional, maximum display width for this number.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(MEDIUMINT, self).__init__(display_width=display_width, **kw)
+
+
+class TINYINT(_IntegerType):
+    """MySQL TINYINT type."""
+
+    __visit_name__ = 'TINYINT'
+
+    def __init__(self, display_width=None, **kw):
+        """Construct a TINYINT.
+
+        :param display_width: Optional, maximum display width for this number.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(TINYINT, self).__init__(display_width=display_width, **kw)
+
+
+class SMALLINT(_IntegerType, sqltypes.SMALLINT):
+    """MySQL SMALLINTEGER type."""
+
+    __visit_name__ = 'SMALLINT'
+
+    def __init__(self, display_width=None, **kw):
+        """Construct a SMALLINTEGER.
+
+        :param display_width: Optional, maximum display width for this number.
+
+        :param unsigned: a boolean, optional.
+
+        :param zerofill: Optional. If true, values will be stored as strings
+          left-padded with zeros. Note that this does not effect the values
+          returned by the underlying database API, which continue to be
+          numeric.
+
+        """
+        super(SMALLINT, self).__init__(display_width=display_width, **kw)
+
+
+class BIT(sqltypes.TypeEngine):
+    """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() type.
+
+    """
+
+    __visit_name__ = 'BIT'
+
+    def __init__(self, length=None):
+        """Construct a BIT.
+
+        :param length: Optional, number of bits.
+
+        """
+        self.length = length
+
+    def result_processor(self, dialect, coltype):
+        """Convert a MySQL's 64 bit, variable length binary string to a long.
+
+        TODO: this is MySQL-db, pyodbc specific.  OurSQL and mysqlconnector
+        already do this, so this logic should be moved to those dialects.
+
+        """
+
+        def process(value):
+            if value is not None:
+                v = 0
+                for i in value:
+                    if not isinstance(i, int):
+                        i = ord(i)  # convert byte to int on Python 2
+                    v = v << 8 | i
+                return v
+            return value
+        return process
+
+
+class TIME(sqltypes.TIME):
+    """MySQL TIME type. """
+
+    __visit_name__ = 'TIME'
+
+    def __init__(self, timezone=False, fsp=None):
+        """Construct a MySQL TIME type.
+
+        :param timezone: not used by the MySQL dialect.
+        :param fsp: fractional seconds precision value.
+         MySQL 5.6 supports storage of fractional seconds;
+         this parameter will be used when emitting DDL
+         for the TIME type.
+
+         .. note::
+
+            DBAPI driver support for fractional seconds may
+            be limited; current support includes
+            MySQL Connector/Python.
+
+        .. versionadded:: 0.8 The MySQL-specific TIME
+           type as well as fractional seconds support.
+
+        """
+        super(TIME, self).__init__(timezone=timezone)
+        self.fsp = fsp
+
+    def result_processor(self, dialect, coltype):
+        time = datetime.time
+
+        def process(value):
+            # convert from a timedelta value
+            if value is not None:
+                microseconds = value.microseconds
+                seconds = value.seconds
+                minutes = seconds // 60
+                return time(minutes // 60,
+                            minutes % 60,
+                            seconds - minutes * 60,
+                            microsecond=microseconds)
+            else:
+                return None
+        return process
+
+
+class TIMESTAMP(sqltypes.TIMESTAMP):
+    """MySQL TIMESTAMP type.
+
+    """
+
+    __visit_name__ = 'TIMESTAMP'
+
+    def __init__(self, timezone=False, fsp=None):
+        """Construct a MySQL TIMESTAMP type.
+
+        :param timezone: not used by the MySQL dialect.
+        :param fsp: fractional seconds precision value.
+         MySQL 5.6.4 supports storage of fractional seconds;
+         this parameter will be used when emitting DDL
+         for the TIMESTAMP type.
+
+         .. note::
+
+            DBAPI driver support for fractional seconds may
+            be limited; current support includes
+            MySQL Connector/Python.
+
+        .. versionadded:: 0.8.5 Added MySQL-specific :class:`.mysql.TIMESTAMP`
+           with fractional seconds support.
+
+        """
+        super(TIMESTAMP, self).__init__(timezone=timezone)
+        self.fsp = fsp
+
+
+class DATETIME(sqltypes.DATETIME):
+    """MySQL DATETIME type.
+
+    """
+
+    __visit_name__ = 'DATETIME'
+
+    def __init__(self, timezone=False, fsp=None):
+        """Construct a MySQL DATETIME type.
+
+        :param timezone: not used by the MySQL dialect.
+        :param fsp: fractional seconds precision value.
+         MySQL 5.6.4 supports storage of fractional seconds;
+         this parameter will be used when emitting DDL
+         for the DATETIME type.
+
+         .. note::
+
+            DBAPI driver support for fractional seconds may
+            be limited; current support includes
+            MySQL Connector/Python.
+
+        .. versionadded:: 0.8.5 Added MySQL-specific :class:`.mysql.DATETIME`
+           with fractional seconds support.
+
+        """
+        super(DATETIME, self).__init__(timezone=timezone)
+        self.fsp = fsp
+
+
+class YEAR(sqltypes.TypeEngine):
+    """MySQL YEAR type, for single byte storage of years 1901-2155."""
+
+    __visit_name__ = 'YEAR'
+
+    def __init__(self, display_width=None):
+        self.display_width = display_width
+
+
+class TEXT(_StringType, sqltypes.TEXT):
+    """MySQL TEXT type, for text up to 2^16 characters."""
+
+    __visit_name__ = 'TEXT'
+
+    def __init__(self, length=None, **kw):
+        """Construct a TEXT.
+
+        :param length: Optional, if provided the server may optimize storage
+          by substituting the smallest TEXT type sufficient to store
+          ``length`` characters.
+
+        :param charset: Optional, a column-level character set for this string
+          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
+
+        :param collation: Optional, a column-level collation for this string
+          value.  Takes precedence to 'binary' short-hand.
+
+        :param ascii: Defaults to False: short-hand for the ``latin1``
+          character set, generates ASCII in schema.
+
+        :param unicode: Defaults to False: short-hand for the ``ucs2``
+          character set, generates UNICODE in schema.
+
+        :param national: Optional. If true, use the server's configured
+          national character set.
+
+        :param binary: Defaults to False: short-hand, pick the binary
+          collation type that matches the column's character set.  Generates
+          BINARY in schema.  This does not affect the type of data stored,
+          only the collation of character data.
+
+        """
+        super(TEXT, self).__init__(length=length, **kw)
+
+
+class TINYTEXT(_StringType):
+    """MySQL TINYTEXT type, for text up to 2^8 characters."""
+
+    __visit_name__ = 'TINYTEXT'
+
+    def __init__(self, **kwargs):
+        """Construct a TINYTEXT.
+
+        :param charset: Optional, a column-level character set for this string
+          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
+
+        :param collation: Optional, a column-level collation for this string
+          value.  Takes precedence to 'binary' short-hand.
+
+        :param ascii: Defaults to False: short-hand for the ``latin1``
+          character set, generates ASCII in schema.
+
+        :param unicode: Defaults to False: short-hand for the ``ucs2``
+          character set, generates UNICODE in schema.
+
+        :param national: Optional. If true, use the server's configured
+          national character set.
+
+        :param binary: Defaults to False: short-hand, pick the binary
+          collation type that matches the column's character set.  Generates
+          BINARY in schema.  This does not affect the type of data stored,
+          only the collation of character data.
+
+        """
+        super(TINYTEXT, self).__init__(**kwargs)
+
+
+class MEDIUMTEXT(_StringType):
+    """MySQL MEDIUMTEXT type, for text up to 2^24 characters."""
+
+    __visit_name__ = 'MEDIUMTEXT'
+
+    def __init__(self, **kwargs):
+        """Construct a MEDIUMTEXT.
+
+        :param charset: Optional, a column-level character set for this string
+          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
+
+        :param collation: Optional, a column-level collation for this string
+          value.  Takes precedence to 'binary' short-hand.
+
+        :param ascii: Defaults to False: short-hand for the ``latin1``
+          character set, generates ASCII in schema.
+
+        :param unicode: Defaults to False: short-hand for the ``ucs2``
+          character set, generates UNICODE in schema.
+
+        :param national: Optional. If true, use the server's configured
+          national character set.
+
+        :param binary: Defaults to False: short-hand, pick the binary
+          collation type that matches the column's character set.  Generates
+          BINARY in schema.  This does not affect the type of data stored,
+          only the collation of character data.
+
+        """
+        super(MEDIUMTEXT, self).__init__(**kwargs)
+
+
+class LONGTEXT(_StringType):
+    """MySQL LONGTEXT type, for text up to 2^32 characters."""
+
+    __visit_name__ = 'LONGTEXT'
+
+    def __init__(self, **kwargs):
+        """Construct a LONGTEXT.
+
+        :param charset: Optional, a column-level character set for this string
+          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
+
+        :param collation: Optional, a column-level collation for this string
+          value.  Takes precedence to 'binary' short-hand.
+
+        :param ascii: Defaults to False: short-hand for the ``latin1``
+          character set, generates ASCII in schema.
+
+        :param unicode: Defaults to False: short-hand for the ``ucs2``
+          character set, generates UNICODE in schema.
+
+        :param national: Optional. If true, use the server's configured
+          national character set.
+
+        :param binary: Defaults to False: short-hand, pick the binary
+          collation type that matches the column's character set.  Generates
+          BINARY in schema.  This does not affect the type of data stored,
+          only the collation of character data.
+
+        """
+        super(LONGTEXT, self).__init__(**kwargs)
+
+
+class VARCHAR(_StringType, sqltypes.VARCHAR):
+    """MySQL VARCHAR type, for variable-length character data."""
+
+    __visit_name__ = 'VARCHAR'
+
+    def __init__(self, length=None, **kwargs):
+        """Construct a VARCHAR.
+
+        :param charset: Optional, a column-level character set for this string
+          value.  Takes precedence to 'ascii' or 'unicode' short-hand.
+
+        :param collation: Optional, a column-level collation for this string
+          value.  Takes precedence to 'binary' short-hand.
+
+        :param ascii: Defaults to False: short-hand for the ``latin1``
+          character set, generates ASCII in schema.
+
+        :param unicode: Defaults to False: short-hand for the ``ucs2``
+          character set, generates UNICODE in schema.
+
+        :param national: Optional. If true, use the server's configured
+          national character set.
+
+        :param binary: Defaults to False: short-hand, pick the binary
+          collation type that matches the column's character set.  Generates
+          BINARY in schema.  This does not affect the type of data stored,
+          only the collation of character data.
+
+        """
+        super(VARCHAR, self).__init__(length=length, **kwargs)
+
+
+class CHAR(_StringType, sqltypes.CHAR):
+    """MySQL CHAR type, for fixed-length character data."""
+
+    __visit_name__ = 'CHAR'
+
+    def __init__(self, length=None, **kwargs):
+        """Construct a CHAR.
+
+        :param length: Maximum data length, in characters.
+
+        :param binary: Optional, use the default binary collation for the
+          national character set.  This does not affect the type of data
+          stored, use a BINARY type for binary data.
+
+        :param collation: Optional, request a particular collation.  Must be
+          compatible with the national character set.
+
+        """
+        super(CHAR, self).__init__(length=length, **kwargs)
+
+    @classmethod
+    def _adapt_string_for_cast(self, type_):
+        # copy the given string type into a CHAR
+        # for the purposes of rendering a CAST expression
+        type_ = sqltypes.to_instance(type_)
+        if isinstance(type_, sqltypes.CHAR):
+            return type_
+        elif isinstance(type_, _StringType):
+            return CHAR(
+                length=type_.length,
+                charset=type_.charset,
+                collation=type_.collation,
+                ascii=type_.ascii,
+                binary=type_.binary,
+                unicode=type_.unicode,
+                national=False  # not supported in CAST
+            )
+        else:
+            return CHAR(length=type_.length)
+
+
+class NVARCHAR(_StringType, sqltypes.NVARCHAR):
+    """MySQL NVARCHAR type.
+
+    For variable-length character data in the server's configured national
+    character set.
+    """
+
+    __visit_name__ = 'NVARCHAR'
+
+    def __init__(self, length=None, **kwargs):
+        """Construct an NVARCHAR.
+
+        :param length: Maximum data length, in characters.
+
+        :param binary: Optional, use the default binary collation for the
+          national character set.  This does not affect the type of data
+          stored, use a BINARY type for binary data.
+
+        :param collation: Optional, request a particular collation.  Must be
+          compatible with the national character set.
+
+        """
+        kwargs['national'] = True
+        super(NVARCHAR, self).__init__(length=length, **kwargs)
+
+
+class NCHAR(_StringType, sqltypes.NCHAR):
+    """MySQL NCHAR type.
+
+    For fixed-length character data in the server's configured national
+    character set.
+    """
+
+    __visit_name__ = 'NCHAR'
+
+    def __init__(self, length=None, **kwargs):
+        """Construct an NCHAR.
+
+        :param length: Maximum data length, in characters.
+
+        :param binary: Optional, use the default binary collation for the
+          national character set.  This does not affect the type of data
+          stored, use a BINARY type for binary data.
+
+        :param collation: Optional, request a particular collation.  Must be
+          compatible with the national character set.
+
+        """
+        kwargs['national'] = True
+        super(NCHAR, self).__init__(length=length, **kwargs)
+
+
+class TINYBLOB(sqltypes._Binary):
+    """MySQL TINYBLOB type, for binary data up to 2^8 bytes."""
+
+    __visit_name__ = 'TINYBLOB'
+
+
+class MEDIUMBLOB(sqltypes._Binary):
+    """MySQL MEDIUMBLOB type, for binary data up to 2^24 bytes."""
+
+    __visit_name__ = 'MEDIUMBLOB'
+
+
+class LONGBLOB(sqltypes._Binary):
+    """MySQL LONGBLOB type, for binary data up to 2^32 bytes."""
+
+    __visit_name__ = 'LONGBLOB'
index b8cbea819a363c2ac20d7b90232649d438f10835..44880c36b98dee58d76d6f853b9df74d1ff57ff3 100644 (file)
@@ -9,6 +9,7 @@ from sqlalchemy import event
 from sqlalchemy import sql
 from sqlalchemy import inspect
 from sqlalchemy.dialects.mysql import base as mysql
+from sqlalchemy.dialects.mysql import reflection as _reflection
 from sqlalchemy.testing import fixtures, AssertsExecutionResults
 from sqlalchemy import testing
 
@@ -532,7 +533,7 @@ class ReflectionTest(fixtures.TestBase, AssertsExecutionResults):
 class RawReflectionTest(fixtures.TestBase):
     def setup(self):
         dialect = mysql.dialect()
-        self.parser = mysql.MySQLTableDefinitionParser(
+        self.parser = _reflection.MySQLTableDefinitionParser(
             dialect, dialect.identifier_preparer)
 
     def test_key_reflection(self):