diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2009-08-06 21:11:27 +0000 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2009-08-06 21:11:27 +0000 |
commit | 8fc5005dfe3eb66a46470ad8a8c7b95fc4d6bdca (patch) | |
tree | ae9e27d12c9fbf8297bb90469509e1cb6a206242 /lib/sqlalchemy/databases/mysql.py | |
parent | 7638aa7f242c6ea3d743aa9100e32be2052546a6 (diff) | |
download | sqlalchemy-8fc5005dfe3eb66a46470ad8a8c7b95fc4d6bdca.tar.gz |
merge 0.6 series to trunk.
Diffstat (limited to 'lib/sqlalchemy/databases/mysql.py')
-rw-r--r-- | lib/sqlalchemy/databases/mysql.py | 2732 |
1 files changed, 0 insertions, 2732 deletions
diff --git a/lib/sqlalchemy/databases/mysql.py b/lib/sqlalchemy/databases/mysql.py deleted file mode 100644 index ba6b026ea..000000000 --- a/lib/sqlalchemy/databases/mysql.py +++ /dev/null @@ -1,2732 +0,0 @@ -# -*- fill-column: 78 -*- -# mysql.py -# Copyright (C) 2005, 2006, 2007, 2008, 2009 Michael Bayer mike_mp@zzzcomputing.com -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php - -"""Support for the MySQL database. - -Overview --------- - -For normal SQLAlchemy usage, importing this module is unnecessary. It will be -loaded on-demand when a MySQL connection is needed. The generic column types -like :class:`~sqlalchemy.String` and :class:`~sqlalchemy.Integer` will -automatically be adapted to the optimal matching MySQL column type. - -But if you would like to use one of the MySQL-specific or enhanced column -types when creating tables with your :class:`~sqlalchemy.Table` definitions, -then you will need to import them from this module:: - - from sqlalchemy.databases import mysql - - Table('mytable', metadata, - Column('id', Integer, primary_key=True), - Column('ittybittyblob', mysql.MSTinyBlob), - Column('biggy', mysql.MSBigInteger(unsigned=True))) - -All standard MySQL column types are supported. The OpenGIS types are -available for use via table reflection but have no special support or mapping -to Python classes. If you're using these types and have opinions about how -OpenGIS can be smartly integrated into SQLAlchemy please join the mailing -list! - -Supported Versions and Features -------------------------------- - -SQLAlchemy supports 6 major MySQL versions: 3.23, 4.0, 4.1, 5.0, 5.1 and 6.0, -with capabilities increasing with more modern servers. - -Versions 4.1 and higher support the basic SQL functionality that SQLAlchemy -uses in the ORM and SQL expressions. These versions pass the applicable tests -in the suite 100%. No heroic measures are taken to work around major missing -SQL features- if your server version does not support sub-selects, for -example, they won't work in SQLAlchemy either. - -Currently, the only DB-API driver supported is `MySQL-Python` (also referred to -as `MySQLdb`). Either 1.2.1 or 1.2.2 are recommended. The alpha, beta and -gamma releases of 1.2.1 and 1.2.2 should be avoided. Support for Jython and -IronPython is planned. - -===================================== =============== -Feature Minimum Version -===================================== =============== -sqlalchemy.orm 4.1.1 -Table Reflection 3.23.x -DDL Generation 4.1.1 -utf8/Full Unicode Connections 4.1.1 -Transactions 3.23.15 -Two-Phase Transactions 5.0.3 -Nested Transactions 5.0.3 -===================================== =============== - -See the official MySQL documentation for detailed information about features -supported in any given server release. - -Character Sets --------------- - -Many MySQL server installations default to a ``latin1`` encoding for client -connections. All data sent through the connection will be converted into -``latin1``, even if you have ``utf8`` or another character set on your tables -and columns. With versions 4.1 and higher, you can change the connection -character set either through server configuration or by including the -``charset`` parameter in the URL used for ``create_engine``. The ``charset`` -option is passed through to MySQL-Python and has the side-effect of also -enabling ``use_unicode`` in the driver by default. For regular encoded -strings, also pass ``use_unicode=0`` in the connection arguments:: - - # set client encoding to utf8; all strings come back as unicode - create_engine('mysql:///mydb?charset=utf8') - - # set client encoding to utf8; all strings come back as utf8 str - create_engine('mysql:///mydb?charset=utf8&use_unicode=0') - -Storage Engines ---------------- - -Most MySQL server installations have a default table type of ``MyISAM``, a -non-transactional table type. During a transaction, non-transactional storage -engines do not participate and continue to store table changes in autocommit -mode. For fully atomic transactions, all participating tables must use a -transactional engine such as ``InnoDB``, ``Falcon``, ``SolidDB``, `PBXT`, etc. - -Storage engines can be elected when creating tables in SQLAlchemy by supplying -a ``mysql_engine='whatever'`` to the ``Table`` constructor. Any MySQL table -creation option can be specified in this syntax:: - - Table('mytable', metadata, - Column('data', String(32)), - mysql_engine='InnoDB', - mysql_charset='utf8' - ) - -Keys ----- - -Not all MySQL storage engines support foreign keys. For ``MyISAM`` and -similar engines, the information loaded by table reflection will not include -foreign keys. For these tables, you may supply a -:class:`~sqlalchemy.ForeignKeyConstraint` at reflection time:: - - Table('mytable', metadata, - ForeignKeyConstraint(['other_id'], ['othertable.other_id']), - autoload=True - ) - -When creating tables, SQLAlchemy will automatically set ``AUTO_INCREMENT``` on -an integer primary key column:: - - >>> t = Table('mytable', metadata, - ... Column('mytable_id', Integer, primary_key=True) - ... ) - >>> t.create() - CREATE TABLE mytable ( - id INTEGER NOT NULL AUTO_INCREMENT, - PRIMARY KEY (id) - ) - -You can disable this behavior by supplying ``autoincrement=False`` to the -:class:`~sqlalchemy.Column`. This flag can also be used to enable -auto-increment on a secondary column in a multi-column key for some storage -engines:: - - Table('mytable', metadata, - Column('gid', Integer, primary_key=True, autoincrement=False), - Column('id', Integer, primary_key=True) - ) - -SQL Mode --------- - -MySQL SQL modes are supported. Modes that enable ``ANSI_QUOTES`` (such as -``ANSI``) require an engine option to modify SQLAlchemy's quoting style. -When using an ANSI-quoting mode, supply ``use_ansiquotes=True`` when -creating your ``Engine``:: - - create_engine('mysql://localhost/test', use_ansiquotes=True) - -This is an engine-wide option and is not toggleable on a per-connection basis. -SQLAlchemy does not presume to ``SET sql_mode`` for you with this option. For -the best performance, set the quoting style server-wide in ``my.cnf`` or by -supplying ``--sql-mode`` to ``mysqld``. You can also use a -:class:`sqlalchemy.pool.Pool` listener hook to issue a ``SET SESSION -sql_mode='...'`` on connect to configure each connection. - -If you do not specify ``use_ansiquotes``, the regular MySQL quoting style is -used by default. - -If you do issue a ``SET sql_mode`` through SQLAlchemy, the dialect must be -updated if the quoting style is changed. Again, this change will affect all -connections:: - - connection.execute('SET sql_mode="ansi"') - connection.dialect.use_ansiquotes = True - -MySQL SQL Extensions --------------------- - -Many of the MySQL SQL extensions are handled through SQLAlchemy's generic -function and operator support:: - - table.select(table.c.password==func.md5('plaintext')) - table.select(table.c.username.op('regexp')('^[a-d]')) - -And of course any valid MySQL statement can be executed as a string as well. - -Some limited direct support for MySQL extensions to SQL is currently -available. - - * SELECT pragma:: - - select(..., prefixes=['HIGH_PRIORITY', 'SQL_SMALL_RESULT']) - - * UPDATE with LIMIT:: - - update(..., mysql_limit=10) - -Troubleshooting ---------------- - -If you have problems that seem server related, first check that you are -using the most recent stable MySQL-Python package available. The Database -Notes page on the wiki at http://www.sqlalchemy.org is a good resource for -timely information affecting MySQL in SQLAlchemy. - -""" - -import datetime, decimal, inspect, re, sys -from array import array as _array - -from sqlalchemy import exc, log, schema, sql, util -from sqlalchemy.sql import operators as sql_operators -from sqlalchemy.sql import functions as sql_functions -from sqlalchemy.sql import compiler - -from sqlalchemy.engine import base as engine_base, default -from sqlalchemy import types as sqltypes - - -__all__ = ( - 'MSBigInteger', 'MSMediumInteger', 'MSBinary', 'MSBit', 'MSBlob', 'MSBoolean', - 'MSChar', 'MSDate', 'MSDateTime', 'MSDecimal', 'MSDouble', - 'MSEnum', 'MSFloat', 'MSInteger', 'MSLongBlob', 'MSLongText', - 'MSMediumBlob', 'MSMediumText', 'MSNChar', 'MSNVarChar', - 'MSNumeric', 'MSSet', 'MSSmallInteger', 'MSString', 'MSText', - 'MSTime', 'MSTimeStamp', 'MSTinyBlob', 'MSTinyInteger', - 'MSTinyText', 'MSVarBinary', 'MSYear' ) - - -RESERVED_WORDS = set( - ['accessible', 'add', 'all', 'alter', 'analyze','and', 'as', 'asc', - 'asensitive', 'before', 'between', 'bigint', 'binary', 'blob', 'both', - 'by', 'call', 'cascade', 'case', 'change', 'char', 'character', 'check', - 'collate', 'column', 'condition', 'constraint', 'continue', 'convert', - 'create', 'cross', 'current_date', 'current_time', 'current_timestamp', - 'current_user', 'cursor', 'database', 'databases', 'day_hour', - 'day_microsecond', 'day_minute', 'day_second', 'dec', 'decimal', - 'declare', 'default', 'delayed', 'delete', 'desc', 'describe', - 'deterministic', 'distinct', 'distinctrow', 'div', 'double', 'drop', - 'dual', 'each', 'else', 'elseif', 'enclosed', 'escaped', 'exists', - 'exit', 'explain', 'false', 'fetch', 'float', 'float4', 'float8', - 'for', 'force', 'foreign', 'from', 'fulltext', 'grant', 'group', 'having', - 'high_priority', 'hour_microsecond', 'hour_minute', 'hour_second', 'if', - 'ignore', 'in', 'index', 'infile', 'inner', 'inout', 'insensitive', - 'insert', 'int', 'int1', 'int2', 'int3', 'int4', 'int8', 'integer', - 'interval', 'into', 'is', 'iterate', 'join', 'key', 'keys', 'kill', - 'leading', 'leave', 'left', 'like', 'limit', 'linear', 'lines', 'load', - 'localtime', 'localtimestamp', 'lock', 'long', 'longblob', 'longtext', - 'loop', 'low_priority', 'master_ssl_verify_server_cert', 'match', - 'mediumblob', 'mediumint', 'mediumtext', 'middleint', - 'minute_microsecond', 'minute_second', 'mod', 'modifies', 'natural', - 'not', 'no_write_to_binlog', 'null', 'numeric', 'on', 'optimize', - 'option', 'optionally', 'or', 'order', 'out', 'outer', 'outfile', - 'precision', 'primary', 'procedure', 'purge', 'range', 'read', 'reads', - 'read_only', 'read_write', 'real', 'references', 'regexp', 'release', - 'rename', 'repeat', 'replace', 'require', 'restrict', 'return', - 'revoke', 'right', 'rlike', 'schema', 'schemas', 'second_microsecond', - 'select', 'sensitive', 'separator', 'set', 'show', 'smallint', 'spatial', - 'specific', 'sql', 'sqlexception', 'sqlstate', 'sqlwarning', - 'sql_big_result', 'sql_calc_found_rows', 'sql_small_result', 'ssl', - 'starting', 'straight_join', 'table', 'terminated', 'then', 'tinyblob', - 'tinyint', 'tinytext', 'to', 'trailing', 'trigger', 'true', 'undo', - 'union', 'unique', 'unlock', 'unsigned', 'update', 'usage', 'use', - 'using', 'utc_date', 'utc_time', 'utc_timestamp', 'values', 'varbinary', - 'varchar', 'varcharacter', 'varying', 'when', 'where', 'while', 'with', - 'write', 'x509', 'xor', 'year_month', 'zerofill', # 5.0 - 'columns', 'fields', 'privileges', 'soname', 'tables', # 4.1 - 'accessible', 'linear', 'master_ssl_verify_server_cert', 'range', - 'read_only', 'read_write', # 5.1 - ]) - -AUTOCOMMIT_RE = re.compile( - r'\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER|LOAD +DATA|REPLACE)', - re.I | re.UNICODE) -SET_RE = re.compile( - r'\s*SET\s+(?:(?:GLOBAL|SESSION)\s+)?\w', - re.I | re.UNICODE) - - -class _NumericType(object): - """Base for MySQL numeric types.""" - - def __init__(self, kw): - self.unsigned = kw.pop('unsigned', False) - self.zerofill = kw.pop('zerofill', False) - - def _extend(self, spec): - "Extend a numeric-type declaration with MySQL specific extensions." - - if self.unsigned: - spec += ' UNSIGNED' - if self.zerofill: - spec += ' ZEROFILL' - return spec - - -class _StringType(object): - """Base for MySQL string types.""" - - def __init__(self, charset=None, collation=None, - ascii=False, unicode=False, binary=False, - national=False, **kwargs): - self.charset = charset - # allow collate= or collation= - self.collation = kwargs.get('collate', collation) - self.ascii = ascii - self.unicode = unicode - self.binary = binary - self.national = national - - def _extend(self, spec): - """Extend a string-type declaration with standard SQL CHARACTER SET / - COLLATE annotations and MySQL specific extensions. - """ - - if self.charset: - charset = 'CHARACTER SET %s' % self.charset - elif self.ascii: - charset = 'ASCII' - elif self.unicode: - charset = 'UNICODE' - else: - charset = None - - if self.collation: - collation = 'COLLATE %s' % self.collation - elif self.binary: - collation = 'BINARY' - else: - collation = None - - if self.national: - # NATIONAL (aka NCHAR/NVARCHAR) trumps charsets. - return ' '.join([c for c in ('NATIONAL', spec, collation) - if c is not None]) - return ' '.join([c for c in (spec, charset, collation) - if c is not None]) - - def __repr__(self): - attributes = inspect.getargspec(self.__init__)[0][1:] - attributes.extend(inspect.getargspec(_StringType.__init__)[0][1:]) - - params = {} - for attr in attributes: - val = getattr(self, attr) - if val is not None and val is not False: - params[attr] = val - - return "%s(%s)" % (self.__class__.__name__, - ', '.join(['%s=%r' % (k, params[k]) for k in params])) - - -class MSNumeric(sqltypes.Numeric, _NumericType): - """MySQL NUMERIC type.""" - - def __init__(self, precision=10, scale=2, 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. - - """ - _NumericType.__init__(self, kw) - sqltypes.Numeric.__init__(self, precision, scale, asdecimal=asdecimal, **kw) - - def get_col_spec(self): - if self.precision is None: - return self._extend("NUMERIC") - else: - return self._extend("NUMERIC(%(precision)s, %(scale)s)" % {'precision': self.precision, 'scale' : self.scale}) - - def bind_processor(self, dialect): - return None - - def result_processor(self, dialect): - if not self.asdecimal: - def process(value): - if isinstance(value, decimal.Decimal): - return float(value) - else: - return value - return process - else: - return None - - -class MSDecimal(MSNumeric): - """MySQL DECIMAL type.""" - - def __init__(self, precision=10, scale=2, 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(MSDecimal, self).__init__(precision, scale, asdecimal=asdecimal, **kw) - - def get_col_spec(self): - if self.precision is None: - return self._extend("DECIMAL") - elif self.scale is None: - return self._extend("DECIMAL(%(precision)s)" % {'precision': self.precision}) - else: - return self._extend("DECIMAL(%(precision)s, %(scale)s)" % {'precision': self.precision, 'scale' : self.scale}) - - -class MSDouble(sqltypes.Float, _NumericType): - """MySQL DOUBLE type.""" - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a DOUBLE. - - :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. - - """ - if ((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.") - - _NumericType.__init__(self, kw) - sqltypes.Float.__init__(self, asdecimal=asdecimal, **kw) - self.scale = scale - self.precision = precision - - def get_col_spec(self): - if self.precision is not None and self.scale is not None: - return self._extend("DOUBLE(%(precision)s, %(scale)s)" % - {'precision': self.precision, - 'scale' : self.scale}) - else: - return self._extend('DOUBLE') - - -class MSReal(MSDouble): - """MySQL REAL type.""" - - def __init__(self, precision=None, scale=None, asdecimal=True, **kw): - """Construct a REAL. - - :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. - - """ - MSDouble.__init__(self, precision, scale, asdecimal, **kw) - - def get_col_spec(self): - if self.precision is not None and self.scale is not None: - return self._extend("REAL(%(precision)s, %(scale)s)" % - {'precision': self.precision, - 'scale' : self.scale}) - else: - return self._extend('REAL') - - -class MSFloat(sqltypes.Float, _NumericType): - """MySQL FLOAT type.""" - - 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. - - """ - _NumericType.__init__(self, kw) - sqltypes.Float.__init__(self, asdecimal=asdecimal, **kw) - self.scale = scale - self.precision = precision - - def get_col_spec(self): - if self.scale is not None and self.precision is not None: - return self._extend("FLOAT(%s, %s)" % (self.precision, self.scale)) - elif self.precision is not None: - return self._extend("FLOAT(%s)" % (self.precision,)) - else: - return self._extend("FLOAT") - - def bind_processor(self, dialect): - return None - - -class MSInteger(sqltypes.Integer, _NumericType): - """MySQL INTEGER type.""" - - 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. - - """ - if 'length' in kw: - util.warn_deprecated("'length' is deprecated for MSInteger and subclasses. Use 'display_width'.") - self.display_width = kw.pop('length') - else: - self.display_width = display_width - _NumericType.__init__(self, kw) - sqltypes.Integer.__init__(self, **kw) - - def get_col_spec(self): - if self.display_width is not None: - return self._extend("INTEGER(%(display_width)s)" % {'display_width': self.display_width}) - else: - return self._extend("INTEGER") - - -class MSBigInteger(MSInteger): - """MySQL BIGINTEGER type.""" - - 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(MSBigInteger, self).__init__(display_width, **kw) - - def get_col_spec(self): - if self.display_width is not None: - return self._extend("BIGINT(%(display_width)s)" % {'display_width': self.display_width}) - else: - return self._extend("BIGINT") - - -class MSMediumInteger(MSInteger): - """MySQL MEDIUMINTEGER type.""" - - 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(MSMediumInteger, self).__init__(display_width, **kw) - - def get_col_spec(self): - if self.display_width is not None: - return self._extend("MEDIUMINT(%(display_width)s)" % {'display_width': self.display_width}) - else: - return self._extend("MEDIUMINT") - - - -class MSTinyInteger(MSInteger): - """MySQL TINYINT type.""" - - def __init__(self, display_width=None, **kw): - """Construct a TINYINT. - - Note: following the usual MySQL conventions, TINYINT(1) columns - reflected during Table(..., autoload=True) are treated as - Boolean columns. - - :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(MSTinyInteger, self).__init__(display_width, **kw) - - def get_col_spec(self): - if self.display_width is not None: - return self._extend("TINYINT(%s)" % self.display_width) - else: - return self._extend("TINYINT") - - -class MSSmallInteger(sqltypes.Smallinteger, MSInteger): - """MySQL SMALLINTEGER type.""" - - 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. - - """ - self.display_width = display_width - _NumericType.__init__(self, kw) - sqltypes.SmallInteger.__init__(self, **kw) - - def get_col_spec(self): - if self.display_width is not None: - return self._extend("SMALLINT(%(display_width)s)" % {'display_width': self.display_width}) - else: - return self._extend("SMALLINT") - - -class MSBit(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. - - """ - - def __init__(self, length=None): - """Construct a BIT. - - :param length: Optional, number of bits. - - """ - self.length = length - - def result_processor(self, dialect): - """Convert a MySQL's 64 bit, variable length binary string to a long.""" - def process(value): - if value is not None: - v = 0L - for i in map(ord, value): - v = v << 8 | i - value = v - return value - return process - - def get_col_spec(self): - if self.length is not None: - return "BIT(%s)" % self.length - else: - return "BIT" - - -class MSDateTime(sqltypes.DateTime): - """MySQL DATETIME type.""" - - def get_col_spec(self): - return "DATETIME" - - -class MSDate(sqltypes.Date): - """MySQL DATE type.""" - - def get_col_spec(self): - return "DATE" - - -class MSTime(sqltypes.Time): - """MySQL TIME type.""" - - def get_col_spec(self): - return "TIME" - - def result_processor(self, dialect): - def process(value): - # convert from a timedelta value - if value is not None: - return datetime.time(value.seconds/60/60, value.seconds/60%60, value.seconds - (value.seconds/60*60)) - else: - return None - return process - -class MSTimeStamp(sqltypes.TIMESTAMP): - """MySQL TIMESTAMP type. - - To signal the orm to automatically re-select modified rows to retrieve the - updated timestamp, add a ``server_default`` to your - :class:`~sqlalchemy.Column` specification:: - - from sqlalchemy.databases import mysql - Column('updated', mysql.MSTimeStamp, - server_default=sql.text('CURRENT_TIMESTAMP') - ) - - The full range of MySQL 4.1+ TIMESTAMP defaults can be specified in - the the default:: - - server_default=sql.text('CURRENT TIMESTAMP ON UPDATE CURRENT_TIMESTAMP') - - """ - - def get_col_spec(self): - return "TIMESTAMP" - - -class MSYear(sqltypes.TypeEngine): - """MySQL YEAR type, for single byte storage of years 1901-2155.""" - - def __init__(self, display_width=None): - self.display_width = display_width - - def get_col_spec(self): - if self.display_width is None: - return "YEAR" - else: - return "YEAR(%s)" % self.display_width - -class MSText(_StringType, sqltypes.Text): - """MySQL TEXT type, for text up to 2^16 characters.""" - - def __init__(self, length=None, **kwargs): - """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. - - """ - _StringType.__init__(self, **kwargs) - sqltypes.Text.__init__(self, length, - kwargs.get('convert_unicode', False), kwargs.get('assert_unicode', None)) - - def get_col_spec(self): - if self.length: - return self._extend("TEXT(%d)" % self.length) - else: - return self._extend("TEXT") - - -class MSTinyText(MSText): - """MySQL TINYTEXT type, for text up to 2^8 characters.""" - - 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(MSTinyText, self).__init__(**kwargs) - - def get_col_spec(self): - return self._extend("TINYTEXT") - - -class MSMediumText(MSText): - """MySQL MEDIUMTEXT type, for text up to 2^24 characters.""" - - 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(MSMediumText, self).__init__(**kwargs) - - def get_col_spec(self): - return self._extend("MEDIUMTEXT") - - -class MSLongText(MSText): - """MySQL LONGTEXT type, for text up to 2^32 characters.""" - - 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(MSLongText, self).__init__(**kwargs) - - def get_col_spec(self): - return self._extend("LONGTEXT") - - -class MSString(_StringType, sqltypes.String): - """MySQL VARCHAR type, for variable-length character data.""" - - 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. - - """ - _StringType.__init__(self, **kwargs) - sqltypes.String.__init__(self, length, - kwargs.get('convert_unicode', False), kwargs.get('assert_unicode', None)) - - def get_col_spec(self): - if self.length: - return self._extend("VARCHAR(%d)" % self.length) - else: - return self._extend("VARCHAR") - - -class MSChar(_StringType, sqltypes.CHAR): - """MySQL CHAR type, for fixed-length character data.""" - - def __init__(self, length, **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. - - """ - _StringType.__init__(self, **kwargs) - sqltypes.CHAR.__init__(self, length, - kwargs.get('convert_unicode', False)) - - def get_col_spec(self): - return self._extend("CHAR(%(length)s)" % {'length' : self.length}) - - -class MSNVarChar(_StringType, sqltypes.String): - """MySQL NVARCHAR type. - - For variable-length character data in the server's configured national - character set. - """ - - 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 - _StringType.__init__(self, **kwargs) - sqltypes.String.__init__(self, length, - kwargs.get('convert_unicode', False)) - - def get_col_spec(self): - # We'll actually generate the equiv. "NATIONAL VARCHAR" instead - # of "NVARCHAR". - return self._extend("VARCHAR(%(length)s)" % {'length': self.length}) - - -class MSNChar(_StringType, sqltypes.CHAR): - """MySQL NCHAR type. - - For fixed-length character data in the server's configured national - character set. - """ - - def __init__(self, length=None, **kwargs): - """Construct an NCHAR. Arguments are: - - :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 - _StringType.__init__(self, **kwargs) - sqltypes.CHAR.__init__(self, length, - kwargs.get('convert_unicode', False)) - def get_col_spec(self): - # We'll actually generate the equiv. "NATIONAL CHAR" instead of "NCHAR". - return self._extend("CHAR(%(length)s)" % {'length': self.length}) - - -class _BinaryType(sqltypes.Binary): - """Base for MySQL binary types.""" - - def get_col_spec(self): - if self.length: - return "BLOB(%d)" % self.length - else: - return "BLOB" - - def result_processor(self, dialect): - def process(value): - if value is None: - return None - else: - return util.buffer(value) - return process - -class MSVarBinary(_BinaryType): - """MySQL VARBINARY type, for variable length binary data.""" - - def __init__(self, length=None, **kw): - """Construct a VARBINARY. Arguments are: - - :param length: Maximum data length, in characters. - - """ - super(MSVarBinary, self).__init__(length, **kw) - - def get_col_spec(self): - if self.length: - return "VARBINARY(%d)" % self.length - else: - return "BLOB" - - -class MSBinary(_BinaryType): - """MySQL BINARY type, for fixed length binary data""" - - def __init__(self, length=None, **kw): - """Construct a BINARY. - - This is a fixed length type, and short values will be right-padded - with a server-version-specific pad value. - - :param length: Maximum data length, in bytes. If length is not - specified, this will generate a BLOB. This usage is deprecated. - - """ - super(MSBinary, self).__init__(length, **kw) - - def get_col_spec(self): - if self.length: - return "BINARY(%d)" % self.length - else: - return "BLOB" - - def result_processor(self, dialect): - def process(value): - if value is None: - return None - else: - return util.buffer(value) - return process - -class MSBlob(_BinaryType): - """MySQL BLOB type, for binary data up to 2^16 bytes""" - - def __init__(self, length=None, **kw): - """Construct a BLOB. Arguments are: - - :param length: Optional, if provided the server may optimize storage - by substituting the smallest TEXT type sufficient to store - ``length`` characters. - - """ - super(MSBlob, self).__init__(length, **kw) - - def get_col_spec(self): - if self.length: - return "BLOB(%d)" % self.length - else: - return "BLOB" - - def result_processor(self, dialect): - def process(value): - if value is None: - return None - else: - return util.buffer(value) - return process - - def __repr__(self): - return "%s()" % self.__class__.__name__ - - -class MSTinyBlob(MSBlob): - """MySQL TINYBLOB type, for binary data up to 2^8 bytes.""" - - def get_col_spec(self): - return "TINYBLOB" - - -class MSMediumBlob(MSBlob): - """MySQL MEDIUMBLOB type, for binary data up to 2^24 bytes.""" - - def get_col_spec(self): - return "MEDIUMBLOB" - - -class MSLongBlob(MSBlob): - """MySQL LONGBLOB type, for binary data up to 2^32 bytes.""" - - def get_col_spec(self): - return "LONGBLOB" - - -class MSEnum(MSString): - """MySQL ENUM type.""" - - def __init__(self, *enums, **kw): - """Construct an ENUM. - - Example: - - Column('myenum', MSEnum("foo", "bar", "baz")) - - Arguments are: - - :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. - - '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. - - """ - self.quoting = kw.pop('quoting', 'auto') - - if self.quoting == 'auto': - # What quoting character are we using? - q = None - for e in enums: - if len(e) == 0: - self.quoting = 'unquoted' - break - elif q is None: - q = e[0] - - if e[0] != q or e[-1] != q: - self.quoting = 'unquoted' - break - else: - self.quoting = 'quoted' - - if self.quoting == 'quoted': - util.warn_pending_deprecation( - 'Manually quoting ENUM value literals is deprecated. Supply ' - 'unquoted values and use the quoting= option in cases of ' - 'ambiguity.') - strip_enums = [] - for a in enums: - 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_enums.append(a) - self.enums = strip_enums - else: - self.enums = list(enums) - - self.strict = kw.pop('strict', False) - length = max([len(v) for v in self.enums] + [0]) - super(MSEnum, self).__init__(length, **kw) - - def bind_processor(self, dialect): - super_convert = super(MSEnum, 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 get_col_spec(self): - quoted_enums = [] - for e in self.enums: - quoted_enums.append("'%s'" % e.replace("'", "''")) - return self._extend("ENUM(%s)" % ",".join(quoted_enums)) - -class MSSet(MSString): - """MySQL SET type.""" - - def __init__(self, *values, **kw): - """Construct a SET. - - Example:: - - Column('myset', MSSet("'foo'", "'bar'", "'baz'")) - - Arguments are: - - :param values: The range of valid values for this SET. Values will be - used exactly as they appear when generating schemas. Strings must - be quoted, as in the example above. Single-quotes are suggested for - ANSI compatibility and are required for portability to servers with - ANSI_QUOTES enabled. - - :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. - - """ - self.__ddl_values = 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) - - self.values = strip_values - length = max([len(v) for v in strip_values] + [0]) - super(MSSet, self).__init__(length, **kw) - - def result_processor(self, dialect): - def process(value): - # The good news: - # No ',' quoting issues- commas aren't allowed in SET values - # The bad news: - # Plenty of driver inconsistencies here. - if isinstance(value, util.set_types): - # ..some versions convert '' to an empty set - if not value: - value.add('') - # ..some return sets.Set, even for pythons that have __builtin__.set - if not isinstance(value, set): - value = set(value) - return value - # ...and some versions return strings - if value is not None: - return set(value.split(',')) - else: - return value - return process - - def bind_processor(self, dialect): - super_convert = super(MSSet, self).bind_processor(dialect) - def process(value): - if value is None or isinstance(value, (int, long, basestring)): - pass - else: - if None in value: - value = set(value) - value.remove(None) - value.add('') - value = ','.join(value) - if super_convert: - return super_convert(value) - else: - return value - return process - - def get_col_spec(self): - return self._extend("SET(%s)" % ",".join(self.__ddl_values)) - - -class MSBoolean(sqltypes.Boolean): - """MySQL BOOLEAN type.""" - - def get_col_spec(self): - return "BOOL" - - def result_processor(self, dialect): - def process(value): - if value is None: - return None - return value and True or False - return process - - def bind_processor(self, dialect): - def process(value): - if value is True: - return 1 - elif value is False: - return 0 - elif value is None: - return None - else: - return value and True or False - return process - -colspecs = { - sqltypes.Integer: MSInteger, - sqltypes.Smallinteger: MSSmallInteger, - sqltypes.Numeric: MSNumeric, - sqltypes.Float: MSFloat, - sqltypes.DateTime: MSDateTime, - sqltypes.Date: MSDate, - sqltypes.Time: MSTime, - sqltypes.String: MSString, - sqltypes.Binary: MSBlob, - sqltypes.Boolean: MSBoolean, - sqltypes.Text: MSText, - sqltypes.CHAR: MSChar, - sqltypes.NCHAR: MSNChar, - sqltypes.TIMESTAMP: MSTimeStamp, - sqltypes.BLOB: MSBlob, - MSDouble: MSDouble, - MSReal: MSReal, - _BinaryType: _BinaryType, -} - -# Everything 3.23 through 5.1 excepting OpenGIS types. -ischema_names = { - 'bigint': MSBigInteger, - 'binary': MSBinary, - 'bit': MSBit, - 'blob': MSBlob, - 'boolean':MSBoolean, - 'char': MSChar, - 'date': MSDate, - 'datetime': MSDateTime, - 'decimal': MSDecimal, - 'double': MSDouble, - 'enum': MSEnum, - 'fixed': MSDecimal, - 'float': MSFloat, - 'int': MSInteger, - 'integer': MSInteger, - 'longblob': MSLongBlob, - 'longtext': MSLongText, - 'mediumblob': MSMediumBlob, - 'mediumint': MSMediumInteger, - 'mediumtext': MSMediumText, - 'nchar': MSNChar, - 'nvarchar': MSNVarChar, - 'numeric': MSNumeric, - 'set': MSSet, - 'smallint': MSSmallInteger, - 'text': MSText, - 'time': MSTime, - 'timestamp': MSTimeStamp, - 'tinyblob': MSTinyBlob, - 'tinyint': MSTinyInteger, - 'tinytext': MSTinyText, - 'varbinary': MSVarBinary, - 'varchar': MSString, - 'year': MSYear, -} - - -class MySQLExecutionContext(default.DefaultExecutionContext): - def post_exec(self): - if self.compiled.isinsert and not self.executemany: - if (not len(self._last_inserted_ids) or - self._last_inserted_ids[0] is None): - self._last_inserted_ids = ([self.cursor.lastrowid] + - self._last_inserted_ids[1:]) - elif (not self.isupdate and not self.should_autocommit and - self.statement and SET_RE.match(self.statement)): - # This misses if a user forces autocommit on text('SET NAMES'), - # which is probably a programming error anyhow. - self.connection.info.pop(('mysql', 'charset'), None) - - def should_autocommit_text(self, statement): - return AUTOCOMMIT_RE.match(statement) - - -class MySQLDialect(default.DefaultDialect): - """Details of the MySQL dialect. Not used directly in application code.""" - name = 'mysql' - supports_alter = True - supports_unicode_statements = False - # identifiers are 64, however aliases can be 255... - max_identifier_length = 255 - supports_sane_rowcount = True - default_paramstyle = 'format' - - def __init__(self, use_ansiquotes=None, **kwargs): - self.use_ansiquotes = use_ansiquotes - default.DefaultDialect.__init__(self, **kwargs) - - def dbapi(cls): - import MySQLdb as mysql - return mysql - dbapi = classmethod(dbapi) - - def create_connect_args(self, url): - opts = url.translate_connect_args(database='db', username='user', - password='passwd') - opts.update(url.query) - - util.coerce_kw_type(opts, 'compress', bool) - util.coerce_kw_type(opts, 'connect_timeout', int) - util.coerce_kw_type(opts, 'client_flag', int) - util.coerce_kw_type(opts, 'local_infile', int) - # Note: using either of the below will cause all strings to be returned - # as Unicode, both in raw SQL operations and with column types like - # String and MSString. - util.coerce_kw_type(opts, 'use_unicode', bool) - util.coerce_kw_type(opts, 'charset', str) - - # Rich values 'cursorclass' and 'conv' are not supported via - # query string. - - ssl = {} - for key in ['ssl_ca', 'ssl_key', 'ssl_cert', 'ssl_capath', 'ssl_cipher']: - if key in opts: - ssl[key[4:]] = opts[key] - util.coerce_kw_type(ssl, key[4:], str) - del opts[key] - if ssl: - opts['ssl'] = ssl - - # FOUND_ROWS must be set in CLIENT_FLAGS to enable - # supports_sane_rowcount. - client_flag = opts.get('client_flag', 0) - if self.dbapi is not None: - try: - import MySQLdb.constants.CLIENT as CLIENT_FLAGS - client_flag |= CLIENT_FLAGS.FOUND_ROWS - except: - pass - opts['client_flag'] = client_flag - return [[], opts] - - def type_descriptor(self, typeobj): - return sqltypes.adapt_type(typeobj, colspecs) - - def do_executemany(self, cursor, statement, parameters, context=None): - rowcount = cursor.executemany(statement, parameters) - if context is not None: - context._rowcount = rowcount - - def supports_unicode_statements(self): - return True - - def do_commit(self, connection): - """Execute a COMMIT.""" - - # COMMIT/ROLLBACK were introduced in 3.23.15. - # Yes, we have at least one user who has to talk to these old versions! - # - # Ignore commit/rollback if support isn't present, otherwise even basic - # operations via autocommit fail. - try: - connection.commit() - except: - if self._server_version_info(connection) < (3, 23, 15): - args = sys.exc_info()[1].args - if args and args[0] == 1064: - return - raise - - def do_rollback(self, connection): - """Execute a ROLLBACK.""" - - try: - connection.rollback() - except: - if self._server_version_info(connection) < (3, 23, 15): - args = sys.exc_info()[1].args - if args and args[0] == 1064: - return - raise - - def do_begin_twophase(self, connection, xid): - connection.execute("XA BEGIN %s", xid) - - def do_prepare_twophase(self, connection, xid): - connection.execute("XA END %s", xid) - connection.execute("XA PREPARE %s", xid) - - def do_rollback_twophase(self, connection, xid, is_prepared=True, - recover=False): - if not is_prepared: - connection.execute("XA END %s", xid) - connection.execute("XA ROLLBACK %s", xid) - - def do_commit_twophase(self, connection, xid, is_prepared=True, - recover=False): - if not is_prepared: - self.do_prepare_twophase(connection, xid) - connection.execute("XA COMMIT %s", xid) - - def do_recover_twophase(self, connection): - resultset = connection.execute("XA RECOVER") - return [row['data'][0:row['gtrid_length']] for row in resultset] - - def do_ping(self, connection): - connection.ping() - - def is_disconnect(self, e): - if isinstance(e, self.dbapi.OperationalError): - return e.args[0] in (2006, 2013, 2014, 2045, 2055) - elif isinstance(e, self.dbapi.InterfaceError): # if underlying connection is closed, this is the error you get - return "(0, '')" in str(e) - else: - return False - - def get_default_schema_name(self, connection): - return connection.execute('SELECT DATABASE()').scalar() - get_default_schema_name = engine_base.connection_memoize( - ('dialect', 'default_schema_name'))(get_default_schema_name) - - def table_names(self, connection, schema): - """Return a Unicode SHOW TABLES from a given schema.""" - - charset = self._detect_charset(connection) - self._autoset_identifier_style(connection) - rp = connection.execute("SHOW TABLES FROM %s" % - self.identifier_preparer.quote_identifier(schema)) - return [row[0] for row in _compat_fetchall(rp, charset=charset)] - - def has_table(self, connection, table_name, schema=None): - # SHOW TABLE STATUS LIKE and SHOW TABLES LIKE do not function properly - # on macosx (and maybe win?) with multibyte table names. - # - # TODO: if this is not a problem on win, make the strategy swappable - # based on platform. DESCRIBE is slower. - - # [ticket:726] - # full_name = self.identifier_preparer.format_table(table, - # use_schema=True) - - self._autoset_identifier_style(connection) - - full_name = '.'.join(self.identifier_preparer._quote_free_identifiers( - schema, table_name)) - - st = "DESCRIBE %s" % full_name - rs = None - try: - try: - rs = connection.execute(st) - have = rs.rowcount > 0 - rs.close() - return have - except exc.SQLError, e: - if e.orig.args[0] == 1146: - return False - raise - finally: - if rs: - rs.close() - - def server_version_info(self, connection): - """A tuple of the database server version. - - Formats the remote server version as a tuple of version values, - e.g. ``(5, 0, 44)``. If there are strings in the version number - they will be in the tuple too, so don't count on these all being - ``int`` values. - - This is a fast check that does not require a round trip. It is also - cached per-Connection. - """ - - return self._server_version_info(connection.connection.connection) - server_version_info = engine_base.connection_memoize( - ('mysql', 'server_version_info'))(server_version_info) - - def _server_version_info(self, dbapi_con): - """Convert a MySQL-python server_info string into a tuple.""" - - version = [] - r = re.compile('[.\-]') - for n in r.split(dbapi_con.get_server_info()): - try: - version.append(int(n)) - except ValueError: - version.append(n) - return tuple(version) - - def reflecttable(self, connection, table, include_columns): - """Load column definitions from the server.""" - - charset = self._detect_charset(connection) - self._autoset_identifier_style(connection) - - try: - reflector = self.reflector - except AttributeError: - preparer = self.identifier_preparer - if (self.server_version_info(connection) < (4, 1) and - self.use_ansiquotes): - # ANSI_QUOTES doesn't affect SHOW CREATE TABLE on < 4.1 - preparer = MySQLIdentifierPreparer(self) - - self.reflector = reflector = MySQLSchemaReflector(preparer) - - sql = self._show_create_table(connection, table, charset) - if sql.startswith('CREATE ALGORITHM'): - # Adapt views to something table-like. - columns = self._describe_table(connection, table, charset) - sql = reflector._describe_to_create(table, columns) - - self._adjust_casing(connection, table) - - return reflector.reflect(connection, table, sql, charset, - only=include_columns) - - def _adjust_casing(self, connection, table, charset=None): - """Adjust Table name to the server case sensitivity, if needed.""" - - casing = self._detect_casing(connection) - - # For winxx database hosts. TODO: is this really needed? - if casing == 1 and table.name != table.name.lower(): - table.name = table.name.lower() - lc_alias = schema._get_table_key(table.name, table.schema) - table.metadata.tables[lc_alias] = table - - - def _detect_charset(self, connection): - """Sniff out the character set in use for connection results.""" - - # Allow user override, won't sniff if force_charset is set. - if ('mysql', 'force_charset') in connection.info: - return connection.info[('mysql', 'force_charset')] - - # Note: MySQL-python 1.2.1c7 seems to ignore changes made - # on a connection via set_character_set() - if self.server_version_info(connection) < (4, 1, 0): - try: - return connection.connection.character_set_name() - except AttributeError: - # < 1.2.1 final MySQL-python drivers have no charset support. - # a query is needed. - pass - - # Prefer 'character_set_results' for the current connection over the - # value in the driver. SET NAMES or individual variable SETs will - # change the charset without updating the driver's view of the world. - # - # If it's decided that issuing that sort of SQL leaves you SOL, then - # this can prefer the driver value. - rs = connection.execute("SHOW VARIABLES LIKE 'character_set%%'") - opts = dict([(row[0], row[1]) for row in _compat_fetchall(rs)]) - - if 'character_set_results' in opts: - return opts['character_set_results'] - try: - return connection.connection.character_set_name() - except AttributeError: - # Still no charset on < 1.2.1 final... - if 'character_set' in opts: - return opts['character_set'] - else: - util.warn( - "Could not detect the connection character set with this " - "combination of MySQL server and MySQL-python. " - "MySQL-python >= 1.2.2 is recommended. Assuming latin1.") - return 'latin1' - _detect_charset = engine_base.connection_memoize( - ('mysql', 'charset'))(_detect_charset) - - - def _detect_casing(self, connection): - """Sniff out identifier case sensitivity. - - Cached per-connection. This value can not change without a server - restart. - - """ - # http://dev.mysql.com/doc/refman/5.0/en/name-case-sensitivity.html - - charset = self._detect_charset(connection) - row = _compat_fetchone(connection.execute( - "SHOW VARIABLES LIKE 'lower_case_table_names'"), - charset=charset) - if not row: - cs = 0 - else: - # 4.0.15 returns OFF or ON according to [ticket:489] - # 3.23 doesn't, 4.0.27 doesn't.. - if row[1] == 'OFF': - cs = 0 - elif row[1] == 'ON': - cs = 1 - else: - cs = int(row[1]) - row.close() - return cs - _detect_casing = engine_base.connection_memoize( - ('mysql', 'lower_case_table_names'))(_detect_casing) - - def _detect_collations(self, connection): - """Pull the active COLLATIONS list from the server. - - Cached per-connection. - """ - - collations = {} - if self.server_version_info(connection) < (4, 1, 0): - pass - else: - charset = self._detect_charset(connection) - rs = connection.execute('SHOW COLLATION') - for row in _compat_fetchall(rs, charset): - collations[row[0]] = row[1] - return collations - _detect_collations = engine_base.connection_memoize( - ('mysql', 'collations'))(_detect_collations) - - def use_ansiquotes(self, useansi): - self._use_ansiquotes = useansi - if useansi: - self.preparer = MySQLANSIIdentifierPreparer - else: - self.preparer = MySQLIdentifierPreparer - # icky - if hasattr(self, 'identifier_preparer'): - self.identifier_preparer = self.preparer(self) - if hasattr(self, 'reflector'): - del self.reflector - - use_ansiquotes = property(lambda s: s._use_ansiquotes, use_ansiquotes, - doc="True if ANSI_QUOTES is in effect.") - - def _autoset_identifier_style(self, connection, charset=None): - """Detect and adjust for the ANSI_QUOTES sql mode. - - If the dialect's use_ansiquotes is unset, query the server's sql mode - and reset the identifier style. - - Note that this currently *only* runs during reflection. Ideally this - would run the first time a connection pool connects to the database, - but the infrastructure for that is not yet in place. - """ - - if self.use_ansiquotes is not None: - return - - row = _compat_fetchone( - connection.execute("SHOW VARIABLES LIKE 'sql_mode'"), - charset=charset) - if not row: - mode = '' - else: - mode = row[1] or '' - # 4.0 - if mode.isdigit(): - mode_no = int(mode) - mode = (mode_no | 4 == mode_no) and 'ANSI_QUOTES' or '' - - self.use_ansiquotes = 'ANSI_QUOTES' in mode - - def _show_create_table(self, connection, table, charset=None, - full_name=None): - """Run SHOW CREATE TABLE for a ``Table``.""" - - if full_name is None: - full_name = self.identifier_preparer.format_table(table) - st = "SHOW CREATE TABLE %s" % full_name - - rp = None - try: - try: - rp = connection.execute(st) - except exc.SQLError, e: - if e.orig.args[0] == 1146: - raise exc.NoSuchTableError(full_name) - else: - raise - row = _compat_fetchone(rp, charset=charset) - if not row: - raise exc.NoSuchTableError(full_name) - return row[1].strip() - finally: - if rp: - rp.close() - - return sql - - def _describe_table(self, connection, table, charset=None, - full_name=None): - """Run DESCRIBE for a ``Table`` and return processed rows.""" - - if full_name is None: - full_name = self.identifier_preparer.format_table(table) - st = "DESCRIBE %s" % full_name - - rp, rows = None, None - try: - try: - rp = connection.execute(st) - except exc.SQLError, e: - if e.orig.args[0] == 1146: - raise exc.NoSuchTableError(full_name) - else: - raise - rows = _compat_fetchall(rp, charset=charset) - finally: - if rp: - rp.close() - return rows - -class _MySQLPythonRowProxy(object): - """Return consistent column values for all versions of MySQL-python. - - Smooth over data type issues (esp. with alpha driver versions) and - normalize strings as Unicode regardless of user-configured driver - encoding settings. - """ - - # Some MySQL-python versions can return some columns as - # sets.Set(['value']) (seriously) but thankfully that doesn't - # seem to come up in DDL queries. - - def __init__(self, rowproxy, charset): - self.rowproxy = rowproxy - self.charset = charset - def __getitem__(self, index): - item = self.rowproxy[index] - if isinstance(item, _array): - item = item.tostring() - if self.charset and isinstance(item, str): - return item.decode(self.charset) - else: - return item - def __getattr__(self, attr): - item = getattr(self.rowproxy, attr) - if isinstance(item, _array): - item = item.tostring() - if self.charset and isinstance(item, str): - return item.decode(self.charset) - else: - return item - - -class MySQLCompiler(compiler.DefaultCompiler): - operators = compiler.DefaultCompiler.operators.copy() - operators.update({ - sql_operators.concat_op: lambda x, y: "concat(%s, %s)" % (x, y), - sql_operators.mod: '%%', - sql_operators.match_op: lambda x, y: "MATCH (%s) AGAINST (%s IN BOOLEAN MODE)" % (x, y) - }) - functions = compiler.DefaultCompiler.functions.copy() - functions.update ({ - sql_functions.random: 'rand%(expr)s', - "utc_timestamp":"UTC_TIMESTAMP" - }) - - extract_map = compiler.DefaultCompiler.extract_map.copy() - extract_map.update ({ - 'milliseconds': 'millisecond', - }) - - def visit_typeclause(self, typeclause): - type_ = typeclause.type.dialect_impl(self.dialect) - if isinstance(type_, MSInteger): - if getattr(type_, 'unsigned', False): - return 'UNSIGNED INTEGER' - else: - return 'SIGNED INTEGER' - elif isinstance(type_, (MSDecimal, MSDateTime, MSDate, MSTime)): - return type_.get_col_spec() - elif isinstance(type_, MSText): - return 'CHAR' - elif (isinstance(type_, _StringType) and not - isinstance(type_, (MSEnum, MSSet))): - if getattr(type_, 'length'): - return 'CHAR(%s)' % type_.length - else: - return 'CHAR' - elif isinstance(type_, _BinaryType): - return 'BINARY' - elif isinstance(type_, MSNumeric): - return type_.get_col_spec().replace('NUMERIC', 'DECIMAL') - elif isinstance(type_, MSTimeStamp): - return 'DATETIME' - elif isinstance(type_, (MSDateTime, MSDate, MSTime)): - return type_.get_col_spec() - else: - return None - - def visit_cast(self, cast, **kwargs): - # No cast until 4, no decimals until 5. - type_ = self.process(cast.typeclause) - if type_ is None: - return self.process(cast.clause) - - return 'CAST(%s AS %s)' % (self.process(cast.clause), type_) - - - def post_process_text(self, text): - if '%%' in text: - util.warn("The SQLAlchemy MySQLDB dialect now automatically escapes '%' in text() expressions to '%%'.") - return text.replace('%', '%%') - - def get_select_precolumns(self, select): - if isinstance(select._distinct, basestring): - return select._distinct.upper() + " " - elif select._distinct: - return "DISTINCT " - else: - return "" - - def visit_join(self, join, asfrom=False, **kwargs): - # 'JOIN ... ON ...' for inner joins isn't available until 4.0. - # Apparently < 3.23.17 requires theta joins for inner joins - # (but not outer). Not generating these currently, but - # support can be added, preferably after dialects are - # refactored to be version-sensitive. - return ''.join( - (self.process(join.left, asfrom=True), - (join.isouter and " LEFT OUTER JOIN " or " INNER JOIN "), - self.process(join.right, asfrom=True), - " ON ", - self.process(join.onclause))) - - def for_update_clause(self, select): - if select.for_update == 'read': - return ' LOCK IN SHARE MODE' - else: - return super(MySQLCompiler, self).for_update_clause(select) - - def limit_clause(self, select): - # MySQL supports: - # LIMIT <limit> - # LIMIT <offset>, <limit> - # and in server versions > 3.3: - # LIMIT <limit> OFFSET <offset> - # The latter is more readable for offsets but we're stuck with the - # former until we can refine dialects by server revision. - - limit, offset = select._limit, select._offset - - if (limit, offset) == (None, None): - return '' - elif offset is not None: - # As suggested by the MySQL docs, need to apply an - # artificial limit if one wasn't provided - if limit is None: - limit = 18446744073709551615 - return ' \n LIMIT %s, %s' % (offset, limit) - else: - # No offset provided, so just use the limit - return ' \n LIMIT %s' % (limit,) - - def visit_update(self, update_stmt): - self.stack.append({'from': set([update_stmt.table])}) - - self.isupdate = True - colparams = self._get_colparams(update_stmt) - - text = "UPDATE " + self.preparer.format_table(update_stmt.table) + " SET " + ', '.join(["%s=%s" % (self.preparer.format_column(c[0]), c[1]) for c in colparams]) - - if update_stmt._whereclause: - text += " WHERE " + self.process(update_stmt._whereclause) - - limit = update_stmt.kwargs.get('mysql_limit', None) - if limit: - text += " LIMIT %s" % limit - - self.stack.pop(-1) - - return text - -# ug. "InnoDB needs indexes on foreign keys and referenced keys [...]. -# Starting with MySQL 4.1.2, these indexes are created automatically. -# In older versions, the indexes must be created explicitly or the -# creation of foreign key constraints fails." - -class MySQLSchemaGenerator(compiler.SchemaGenerator): - def get_column_specification(self, column, first_pk=False): - """Builds column DDL.""" - - colspec = [self.preparer.format_column(column), - column.type.dialect_impl(self.dialect).get_col_spec()] - - default = self.get_column_default_string(column) - if default is not None: - colspec.append('DEFAULT ' + default) - - if not column.nullable: - colspec.append('NOT NULL') - - if column.primary_key and column.autoincrement: - try: - first = [c for c in column.table.primary_key.columns - if (c.autoincrement and - isinstance(c.type, sqltypes.Integer) and - not c.foreign_keys)].pop(0) - if column is first: - colspec.append('AUTO_INCREMENT') - except IndexError: - pass - - return ' '.join(colspec) - - def post_create_table(self, table): - """Build table-level CREATE options like ENGINE and COLLATE.""" - - table_opts = [] - for k in table.kwargs: - if k.startswith('mysql_'): - opt = k[6:].upper() - joiner = '=' - if opt in ('TABLESPACE', 'DEFAULT CHARACTER SET', - 'CHARACTER SET', 'COLLATE'): - joiner = ' ' - - table_opts.append(joiner.join((opt, table.kwargs[k]))) - return ' '.join(table_opts) - - -class MySQLSchemaDropper(compiler.SchemaDropper): - def visit_index(self, index): - self.append("\nDROP INDEX %s ON %s" % - (self.preparer.quote(self._validate_identifier(index.name, False), index.quote), - self.preparer.format_table(index.table))) - self.execute() - - def drop_foreignkey(self, constraint): - self.append("ALTER TABLE %s DROP FOREIGN KEY %s" % - (self.preparer.format_table(constraint.table), - self.preparer.format_constraint(constraint))) - self.execute() - - -class MySQLSchemaReflector(object): - """Parses SHOW CREATE TABLE output.""" - - def __init__(self, identifier_preparer): - """Construct a MySQLSchemaReflector. - - identifier_preparer - An ANSIIdentifierPreparer type, used to determine the identifier - quoting style in effect. - """ - - self.preparer = identifier_preparer - self._prep_regexes() - - def reflect(self, connection, table, show_create, charset, only=None): - """Parse MySQL SHOW CREATE TABLE and fill in a ''Table''. - - show_create - Unicode output of SHOW CREATE TABLE - - table - A ''Table'', to be loaded with Columns, Indexes, etc. - table.name will be set if not already - - charset - FIXME, some constructed values (like column defaults) - currently can't be Unicode. ''charset'' will convert them - into the connection character set. - - only - An optional sequence of column names. If provided, only - these columns will be reflected, and any keys or constraints - that include columns outside this set will also be omitted. - That means that if ``only`` includes only one column in a - 2 part primary key, the entire primary key will be omitted. - """ - - keys, constraints = [], [] - - if only: - only = set(only) - - for line in re.split(r'\r?\n', show_create): - if line.startswith(' ' + self.preparer.initial_quote): - self._add_column(table, line, charset, only) - # a regular table options line - elif line.startswith(') '): - self._set_options(table, line) - # an ANSI-mode table options line - elif line == ')': - pass - elif line.startswith('CREATE '): - self._set_name(table, line) - # Not present in real reflection, but may be if loading from a file. - elif not line: - pass - else: - type_, spec = self.parse_constraints(line) - if type_ is None: - util.warn("Unknown schema content: %r" % line) - elif type_ == 'key': - keys.append(spec) - elif type_ == 'constraint': - constraints.append(spec) - else: - pass - - self._set_keys(table, keys, only) - self._set_constraints(table, constraints, connection, only) - - def _set_name(self, table, line): - """Override a Table name with the reflected name. - - table - A ``Table`` - - line - The first line of SHOW CREATE TABLE output. - """ - - # Don't override by default. - if table.name is None: - table.name = self.parse_name(line) - - def _add_column(self, table, line, charset, only=None): - spec = self.parse_column(line) - 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, notnull = \ - spec['name'], spec['coltype'], spec['arg'], spec['notnull'] - - if only and name not in only: - self.logger.info("Omitting reflected column %s.%s" % - (table.name, name)) - return - - # Convention says that TINYINT(1) columns == BOOLEAN - if type_ == 'tinyint' and args == '1': - type_ = 'boolean' - args = None - - try: - col_type = ischema_names[type_] - except KeyError: - 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 = {} - 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 type_ == 'enum': - type_kw['quoting'] = 'quoted' - - type_instance = col_type(*type_args, **type_kw) - - col_args, col_kw = [], {} - - # NOT NULL - if spec.get('notnull', False): - col_kw['nullable'] = False - - # AUTO_INCREMENT - if spec.get('autoincr', False): - col_kw['autoincrement'] = True - elif issubclass(col_type, sqltypes.Integer): - col_kw['autoincrement'] = False - - # DEFAULT - default = spec.get('default', None) - if default is not None and default != 'NULL': - # Defaults should be in the native charset for the moment - default = default.encode(charset) - if type_ == 'timestamp': - # can't be NULL for TIMESTAMPs - if (default[0], default[-1]) != ("'", "'"): - default = sql.text(default) - else: - default = default[1:-1] - col_args.append(schema.DefaultClause(default)) - - table.append_column(schema.Column(name, type_instance, - *col_args, **col_kw)) - - def _set_keys(self, table, keys, only): - """Add ``Index`` and ``PrimaryKeyConstraint`` items to a ``Table``. - - Most of the information gets dropped here- more is reflected than - the schema objects can currently represent. - - table - A ``Table`` - - keys - A sequence of key specifications produced by `constraints` - - only - Optional `set` of column names. If provided, keys covering - columns not in this set will be omitted. - """ - - for spec in keys: - flavor = spec['type'] - col_names = [s[0] for s in spec['columns']] - - if only and not set(col_names).issubset(only): - if flavor is None: - flavor = 'index' - self.logger.info( - "Omitting %s KEY for (%s), key covers ommitted columns." % - (flavor, ', '.join(col_names))) - continue - - constraint = False - if flavor == 'PRIMARY': - key = schema.PrimaryKeyConstraint() - constraint = True - elif flavor == 'UNIQUE': - key = schema.Index(spec['name'], unique=True) - elif flavor in (None, 'FULLTEXT', 'SPATIAL'): - key = schema.Index(spec['name']) - else: - self.logger.info( - "Converting unknown KEY type %s to a plain KEY" % flavor) - key = schema.Index(spec['name']) - - for col in [table.c[name] for name in col_names]: - key.append_column(col) - - if constraint: - table.append_constraint(key) - - def _set_constraints(self, table, constraints, connection, only): - """Apply constraints to a ``Table``.""" - - default_schema = None - - for spec in constraints: - # only FOREIGN KEYs - ref_name = spec['table'][-1] - ref_schema = len(spec['table']) > 1 and spec['table'][-2] or table.schema - - if not ref_schema: - if default_schema is None: - default_schema = connection.dialect.get_default_schema_name( - connection) - if table.schema == default_schema: - ref_schema = table.schema - - loc_names = spec['local'] - if only and not set(loc_names).issubset(only): - self.logger.info( - "Omitting FOREIGN KEY for (%s), key covers ommitted " - "columns." % (', '.join(loc_names))) - continue - - ref_key = schema._get_table_key(ref_name, ref_schema) - if ref_key in table.metadata.tables: - ref_table = table.metadata.tables[ref_key] - else: - ref_table = schema.Table( - ref_name, table.metadata, schema=ref_schema, - autoload=True, autoload_with=connection) - - ref_names = spec['foreign'] - - if ref_schema: - refspec = [".".join([ref_schema, ref_name, column]) for column in ref_names] - else: - refspec = [".".join([ref_name, column]) for column in ref_names] - - con_kw = {} - for opt in ('name', 'onupdate', 'ondelete'): - if spec.get(opt, False): - con_kw[opt] = spec[opt] - - key = schema.ForeignKeyConstraint(loc_names, refspec, link_to_name=True, **con_kw) - table.append_constraint(key) - - def _set_options(self, table, line): - """Apply safe reflected table options to a ``Table``. - - table - A ``Table`` - - line - The final line of SHOW CREATE TABLE output. - """ - - options = self.parse_table_options(line) - for nope in ('auto_increment', 'data_directory', 'index_directory'): - options.pop(nope, None) - - for opt, val in options.items(): - table.kwargs['mysql_%s' % opt] = val - - def _prep_regexes(self): - """Pre-compile regular expressions.""" - - self._re_columns = [] - self._pr_options = [] - self._re_options_util = {} - - _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|CASCASDE|SET NULL|NOACTION' - self._re_constraint = _re_compile( - r' ' - r'CONSTRAINT +' - r'%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +' - r'FOREIGN KEY +' - r'\((?P<local>[^\)]+?)\) REFERENCES +' - r'(?P<table>%(iq)s[^%(fq)s]+%(fq)s(?:\.%(iq)s[^%(fq)s]+%(fq)s)?) +' - r'\((?P<foreign>[^\)]+?)\)' - r'(?: +(?P<match>MATCH \w+))?' - r'(?: +ON DELETE (?P<ondelete>%(on)s))?' - r'(?: +ON UPDATE (?P<onupdate>%(on)s))?' - % kw - ) - - # PARTITION - # - # punt! - self._re_partition = _re_compile( - r' ' - r'(?:SUB)?PARTITION') - - # Table-level options (COLLATE, ENGINE, etc.) - for option in ('ENGINE', 'TYPE', 'AUTO_INCREMENT', - 'AVG_ROW_LENGTH', 'CHARACTER SET', - 'DEFAULT CHARSET', 'CHECKSUM', - 'COLLATE', 'DELAY_KEY_WRITE', 'INSERT_METHOD', - 'MAX_ROWS', 'MIN_ROWS', 'PACK_KEYS', 'ROW_FORMAT', - 'KEY_BLOCK_SIZE'): - self._add_option_word(option) - - for option in (('COMMENT', 'DATA_DIRECTORY', 'INDEX_DIRECTORY', - 'PASSWORD', 'CONNECTION')): - self._add_option_string(option) - - self._add_option_regex('UNION', r'\([^\)]+\)') - self._add_option_regex('TABLESPACE', r'.*? STORAGE DISK') - self._add_option_regex('RAID_TYPE', - r'\w+\s+RAID_CHUNKS\s*\=\s*\w+RAID_CHUNKSIZE\s*=\s*\w+') - self._re_options_util['='] = _re_compile(r'\s*=\s*$') - - def _add_option_string(self, directive): - regex = (r'(?P<directive>%s\s*(?:=\s*)?)' - r'(?:\x27.(?P<val>.*?)\x27(?!\x27)\x27)' % - re.escape(directive)) - self._pr_options.append( - _pr_compile(regex, lambda v: v.replace("''", "'"))) - - def _add_option_word(self, directive): - regex = (r'(?P<directive>%s\s*(?:=\s*)?)' - r'(?P<val>\w+)' % re.escape(directive)) - self._pr_options.append(_pr_compile(regex)) - - def _add_option_regex(self, directive, regex): - regex = (r'(?P<directive>%s\s*(?:=\s*)?)' - r'(?P<val>%s)' % (re.escape(directive), regex)) - self._pr_options.append(_pr_compile(regex)) - - - def parse_name(self, line): - """Extract the table name. - - line - The first line of SHOW CREATE TABLE - """ - - regex, cleanup = self._pr_name - m = regex.match(line) - if not m: - return None - return cleanup(m.group('name')) - - def parse_column(self, line): - """Extract column details. - - Falls back to a 'minimal support' variant if full parse fails. - - line - Any column-bearing line from SHOW CREATE TABLE - """ - - m = self._re_column.match(line) - if m: - spec = m.groupdict() - spec['full'] = True - return spec - m = self._re_column_loose.match(line) - if m: - spec = m.groupdict() - spec['full'] = False - return spec - return None - - def parse_constraints(self, line): - """Parse a KEY or CONSTRAINT line. - - line - A line of SHOW CREATE TABLE output - """ - - # KEY - m = self._re_key.match(line) - if m: - spec = m.groupdict() - # convert columns into name, length pairs - spec['columns'] = self._parse_keyexprs(spec['columns']) - return 'key', spec - - # CONSTRAINT - m = self._re_constraint.match(line) - if m: - spec = m.groupdict() - spec['table'] = \ - self.preparer.unformat_identifiers(spec['table']) - spec['local'] = [c[0] - for c in self._parse_keyexprs(spec['local'])] - spec['foreign'] = [c[0] - for c in self._parse_keyexprs(spec['foreign'])] - return 'constraint', spec - - # PARTITION and SUBPARTITION - m = self._re_partition.match(line) - if m: - # Punt! - return 'partition', line - - # No match. - return (None, line) - - def parse_table_options(self, line): - """Build a dictionary of all reflected table-level options. - - line - The final line of SHOW CREATE TABLE output. - """ - - options = {} - - if not line or line == ')': - return options - - r_eq_trim = self._re_options_util['='] - - for regex, cleanup in self._pr_options: - m = regex.search(line) - if not m: - continue - directive, value = m.group('directive'), m.group('val') - directive = r_eq_trim.sub('', directive).lower() - if cleanup: - value = cleanup(value) - options[directive] = value - - return options - - def _describe_to_create(self, table, 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. - - `columns` is 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) - -log.class_logger(MySQLSchemaReflector) - - -class _MySQLIdentifierPreparer(compiler.IdentifierPreparer): - """MySQL-specific schema identifier configuration.""" - - reserved_words = RESERVED_WORDS - - def __init__(self, dialect, **kw): - super(_MySQLIdentifierPreparer, self).__init__(dialect, **kw) - - def _quote_free_identifiers(self, *ids): - """Unilaterally identifier-quote any number of strings.""" - - return tuple([self.quote_identifier(i) for i in ids if i is not None]) - - -class MySQLIdentifierPreparer(_MySQLIdentifierPreparer): - """Traditional MySQL-specific schema identifier configuration.""" - - def __init__(self, dialect): - super(MySQLIdentifierPreparer, self).__init__(dialect, initial_quote="`") - - def _escape_identifier(self, value): - return value.replace('`', '``') - - def _unescape_identifier(self, value): - return value.replace('``', '`') - - -class MySQLANSIIdentifierPreparer(_MySQLIdentifierPreparer): - """ANSI_QUOTES MySQL schema identifier configuration.""" - - pass - - -def _compat_fetchall(rp, charset=None): - """Proxy result rows to smooth over MySQL-Python driver inconsistencies.""" - - return [_MySQLPythonRowProxy(row, charset) for row in rp.fetchall()] - -def _compat_fetchone(rp, charset=None): - """Proxy a result row to smooth over MySQL-Python driver inconsistencies.""" - - return _MySQLPythonRowProxy(rp.fetchone(), charset) - -def _pr_compile(regex, cleanup=None): - """Prepare a 2-tuple of compiled regex and callable.""" - - return (_re_compile(regex), cleanup) - -def _re_compile(regex): - """Compile a string to regex, I and UNICODE.""" - - return re.compile(regex, re.I | re.UNICODE) - -dialect = MySQLDialect -dialect.statement_compiler = MySQLCompiler -dialect.schemagenerator = MySQLSchemaGenerator -dialect.schemadropper = MySQLSchemaDropper -dialect.execution_ctx_cls = MySQLExecutionContext |