From: Mike Bayer Date: Wed, 26 Jan 2011 15:44:00 +0000 (-0500) Subject: - new dialect for Drizzle [ticket:2003] X-Git-Tag: rel_0_7b1~51 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9122268ed03d9fb59fafb62ee5a435775f1020d5;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - new dialect for Drizzle [ticket:2003] - move mysqldb to a connector, can be shared among mysql/drizzle --- diff --git a/CHANGES b/CHANGES index 0593ba3743..1c92414786 100644 --- a/CHANGES +++ b/CHANGES @@ -166,6 +166,10 @@ CHANGES VARCHAR type which is similarly unbounded when no length specified. +- drizzle + - New dialect for Drizzle, a MySQL variant. Uses MySQL-python + for the DBAPI. [ticket:2003] + 0.6.6 ===== - orm diff --git a/doc/build/core/engines.rst b/doc/build/core/engines.rst index 742b19e624..082b50a219 100644 --- a/doc/build/core/engines.rst +++ b/doc/build/core/engines.rst @@ -61,6 +61,8 @@ Driver Connect string Py2K Py3K ========================= =========================== =========== =========== =========== ================= ============ **DB2/Informix IDS** ibm-db_ thirdparty thirdparty thirdparty thirdparty thirdparty thirdparty +**Drizzle** +drizzle_ ``drizzle+mysqldb``\* yes development no yes yes **Firebird** kinterbasdb_ ``firebird+kinterbasdb``\* yes development no yes yes **Informix** @@ -121,6 +123,7 @@ python-sybase_ ``sybase+pysybase`` yes [1]_ development .. _informixdb: http://informixdb.sourceforge.net/ .. _sapdb: http://www.sapdb.org/sapdbapi.html .. _python-sybase: http://python-sybase.sourceforge.net/ +.. _drizzle: http://drizzle.org/ Further detail on dialects is available at :ref:`dialect_toplevel`. diff --git a/doc/build/dialects/drizzle.rst b/doc/build/dialects/drizzle.rst new file mode 100644 index 0000000000..0857ca8598 --- /dev/null +++ b/doc/build/dialects/drizzle.rst @@ -0,0 +1,72 @@ +Drizzle +======= + +.. automodule:: sqlalchemy.dialects.drizzle.base + +Drizzle Data Types +------------------ + +As with all SQLAlchemy dialects, all UPPERCASE types that are known to be +valid with Drizzle are importable from the top level dialect:: + + from sqlalchemy.dialects.drizzle import \ + BIGINT, BINARY, BLOB, BOOLEAN, CHAR, DATE, DATETIME, + DECIMAL, DOUBLE, ENUM, FLOAT, INT, INTEGER, + NUMERIC, TEXT, TIME, TIMESTAMP, VARBINARY, VARCHAR + +Types which are specific to Drizzle, or have Drizzle-specific +construction arguments, are as follows: + +.. autoclass:: BIGINT + :members: __init__ + :show-inheritance: + +.. autoclass:: CHAR + :members: __init__ + :show-inheritance: + +.. autoclass:: DECIMAL + :members: __init__ + :show-inheritance: + +.. autoclass:: DOUBLE + :members: __init__ + :show-inheritance: + +.. autoclass:: ENUM + :members: __init__ + :show-inheritance: + +.. autoclass:: FLOAT + :members: __init__ + :show-inheritance: + +.. autoclass:: INTEGER + :members: __init__ + :show-inheritance: + +.. autoclass:: NUMERIC + :members: __init__ + :show-inheritance: + +.. autoclass:: REAL + :members: __init__ + :show-inheritance: + +.. autoclass:: TEXT + :members: __init__ + :show-inheritance: + +.. autoclass:: TIMESTAMP + :members: __init__ + :show-inheritance: + +.. autoclass:: VARCHAR + :members: __init__ + :show-inheritance: + + +MySQL-Python Notes +-------------------- + +.. automodule:: sqlalchemy.dialects.drizzle.mysqldb diff --git a/doc/build/dialects/index.rst b/doc/build/dialects/index.rst index 8ad9330bcc..7d745d4cb0 100644 --- a/doc/build/dialects/index.rst +++ b/doc/build/dialects/index.rst @@ -16,6 +16,7 @@ should be consulted to check for current support level. :maxdepth: 1 :glob: + drizzle firebird informix maxdb diff --git a/lib/sqlalchemy/connectors/mysqldb.py b/lib/sqlalchemy/connectors/mysqldb.py new file mode 100644 index 0000000000..7696cdb797 --- /dev/null +++ b/lib/sqlalchemy/connectors/mysqldb.py @@ -0,0 +1,147 @@ +"""Define behaviors common to MySQLdb dialects. + +Currently includes MySQL and Drizzle. + +""" + +from sqlalchemy.connectors import Connector +from sqlalchemy.engine import base as engine_base, default +from sqlalchemy.sql import operators as sql_operators +from sqlalchemy import exc, log, schema, sql, types as sqltypes, util +from sqlalchemy import processors +import re + +# the subclassing of Connector by all classes +# here is not strictly necessary + +class MySQLDBExecutionContext(Connector): + + @property + def rowcount(self): + if hasattr(self, '_rowcount'): + return self._rowcount + else: + return self.cursor.rowcount + +class MySQLDBCompiler(Connector): + def visit_mod(self, binary, **kw): + return self.process(binary.left) + " %% " + self.process(binary.right) + + def post_process_text(self, text): + return text.replace('%', '%%') + +class MySQLDBIdentifierPreparer(Connector): + + def _escape_identifier(self, value): + value = value.replace(self.escape_quote, self.escape_to_quote) + return value.replace("%", "%%") + +class MySQLDBConnector(Connector): + driver = 'mysqldb' + supports_unicode_statements = False + supports_sane_rowcount = True + supports_sane_multi_rowcount = True + + supports_native_decimal = True + + default_paramstyle = 'format' + + @classmethod + def dbapi(cls): + return __import__('MySQLdb') + + def do_executemany(self, cursor, statement, parameters, context=None): + rowcount = cursor.executemany(statement, parameters) + if context is not None: + context._rowcount = rowcount + + 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: + from MySQLdb.constants import CLIENT as CLIENT_FLAGS + client_flag |= CLIENT_FLAGS.FOUND_ROWS + except: + pass + opts['client_flag'] = client_flag + return [[], opts] + + def _get_server_version_info(self, connection): + dbapi_con = connection.connection + 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 _extract_error_code(self, exception): + return exception.args[0] + + def _detect_charset(self, connection): + """Sniff out the character set in use for connection results.""" + + # Note: MySQL-python 1.2.1c7 seems to ignore changes made + # on a connection via set_character_set() + if self.server_version_info < (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 self._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' + diff --git a/lib/sqlalchemy/databases/__init__.py b/lib/sqlalchemy/databases/__init__.py index 0d53149f0f..dddc8f68a6 100644 --- a/lib/sqlalchemy/databases/__init__.py +++ b/lib/sqlalchemy/databases/__init__.py @@ -12,6 +12,7 @@ from sqlalchemy.dialects.sqlite import base as sqlite from sqlalchemy.dialects.postgresql import base as postgresql postgres = postgresql from sqlalchemy.dialects.mysql import base as mysql +from sqlalchemy.dialects.drizzle import base as drizzle from sqlalchemy.dialects.oracle import base as oracle from sqlalchemy.dialects.firebird import base as firebird from sqlalchemy.dialects.maxdb import base as maxdb @@ -23,6 +24,7 @@ from sqlalchemy.dialects.sybase import base as sybase __all__ = ( 'access', + 'drizzle', 'firebird', 'informix', 'maxdb', diff --git a/lib/sqlalchemy/dialects/__init__.py b/lib/sqlalchemy/dialects/__init__.py index 33eeefcec2..48e578a021 100644 --- a/lib/sqlalchemy/dialects/__init__.py +++ b/lib/sqlalchemy/dialects/__init__.py @@ -6,6 +6,7 @@ __all__ = ( # 'access', + 'drizzle', 'firebird', # 'informix', # 'maxdb', diff --git a/lib/sqlalchemy/dialects/drizzle/__init__.py b/lib/sqlalchemy/dialects/drizzle/__init__.py new file mode 100644 index 0000000000..bbd716f594 --- /dev/null +++ b/lib/sqlalchemy/dialects/drizzle/__init__.py @@ -0,0 +1,18 @@ +from sqlalchemy.dialects.drizzle import base, mysqldb + +# default dialect +base.dialect = mysqldb.dialect + +from sqlalchemy.dialects.drizzle.base import \ + BIGINT, BINARY, BLOB, BOOLEAN, CHAR, DATE, DATETIME, \ + DECIMAL, DOUBLE, ENUM, \ + FLOAT, INTEGER, \ + NUMERIC, REAL, TEXT, TIME, TIMESTAMP, \ + VARBINARY, VARCHAR, dialect + +__all__ = ( +'BIGINT', 'BINARY', 'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'DOUBLE', +'ENUM', 'FLOAT', 'INTEGER', +'NUMERIC', 'SET', 'REAL', 'TEXT', 'TIME', 'TIMESTAMP', +'VARBINARY', 'VARCHAR', 'dialect' +) diff --git a/lib/sqlalchemy/dialects/drizzle/base.py b/lib/sqlalchemy/dialects/drizzle/base.py new file mode 100644 index 0000000000..5c268fb603 --- /dev/null +++ b/lib/sqlalchemy/dialects/drizzle/base.py @@ -0,0 +1,584 @@ +# -*- fill-column: 78 -*- +# drizzle/base.py +# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Michael Bayer mike_mp@zzzcomputing.com +# and Jason Kirtland. +# Copyright (C) 2010 Monty Taylor +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Support for the Drizzle database. + +Supported Versions and Features +------------------------------- + +SQLAlchemy supports the Drizzle database starting with 2010.08. +with capabilities increasing with more modern servers. + +Most available DBAPI drivers are supported; see below. + +===================================== =============== +Feature Minimum Version +===================================== =============== +sqlalchemy.orm 2010.08 +Table Reflection 2010.08 +DDL Generation 2010.08 +utf8/Full Unicode Connections 2010.08 +Transactions 2010.08 +Two-Phase Transactions 2010.08 +Nested Transactions 2010.08 +===================================== =============== + +See the official Drizzle documentation for detailed information about features +supported in any given server release. + +Connecting +---------- + +See the API documentation on individual drivers for details on connecting. + +Connection Timeouts +------------------- + +Drizzle features an automatic connection close behavior, for connections that +have been idle for eight hours or more. To circumvent having this issue, use +the ``pool_recycle`` option which controls the maximum age of any connection:: + + engine = create_engine('drizzle+mysqldb://...', pool_recycle=3600) + +Storage Engines +--------------- + +Drizzle defaults to the ``InnoDB`` storage engine, which is transactional. + +Storage engines can be elected when creating tables in SQLAlchemy by supplying +a ``drizzle_engine='whatever'`` to the ``Table`` constructor. Any Drizzle table +creation option can be specified in this syntax:: + + Table('mytable', metadata, + Column('data', String(32)), + drizzle_engine='InnoDB', + ) + +Keys +---- + +Not all Drizzle storage engines support foreign keys. For ``BlitzDB`` 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) + ) + +Drizzle SQL Extensions +---------------------- + +Many of the Drizzle 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 Drizzle statement can be executed as a string as well. + +Some limited direct support for Drizzle extensions to SQL is currently +available. + +* SELECT pragma:: + + select(..., prefixes=['HIGH_PRIORITY', 'SQL_SMALL_RESULT']) + +* UPDATE with LIMIT:: + + update(..., drizzle_limit=10) + +""" + +import datetime, inspect, re, sys + +from sqlalchemy import schema as sa_schema +from sqlalchemy import exc, log, sql, util +from sqlalchemy.sql import operators as sql_operators +from sqlalchemy.sql import functions as sql_functions +from sqlalchemy.sql import compiler +from array import array as _array + +from sqlalchemy.engine import reflection +from sqlalchemy.engine import base as engine_base, default +from sqlalchemy import types as sqltypes +from sqlalchemy.dialects.mysql import base as mysql_dialect + +from sqlalchemy.types import DATE, DATETIME, BOOLEAN, TIME, \ + BLOB, BINARY, VARBINARY + +class _NumericType(object): + """Base for Drizzle numeric types.""" + + def __init__(self, **kw): + super(_NumericType, self).__init__(**kw) + +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 + +class _StringType(mysql_dialect._StringType): + """Base for Drizzle string types.""" + + def __init__(self, collation=None, + binary=False, + **kw): + kw['national'] = False + super(_StringType, self).__init__(collation=collation, + binary=binary, + **kw) + + +class NUMERIC(_NumericType, sqltypes.NUMERIC): + """Drizzle 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. + + """ + super(NUMERIC, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal, **kw) + + +class DECIMAL(_NumericType, sqltypes.DECIMAL): + """Drizzle 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. + + """ + super(DECIMAL, self).__init__(precision=precision, scale=scale, + asdecimal=asdecimal, **kw) + + +class DOUBLE(_FloatType): + """Drizzle DOUBLE type.""" + + __visit_name__ = 'DOUBLE' + + 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. + + """ + super(DOUBLE, self).__init__(precision=precision, scale=scale, + asdecimal=asdecimal, **kw) + +class REAL(_FloatType): + """Drizzle REAL type.""" + + __visit_name__ = 'REAL' + + 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. + + """ + super(REAL, self).__init__(precision=precision, scale=scale, + asdecimal=asdecimal, **kw) + +class FLOAT(_FloatType, sqltypes.FLOAT): + """Drizzle 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. + + """ + super(FLOAT, self).__init__(precision=precision, scale=scale, + asdecimal=asdecimal, **kw) + + def bind_processor(self, dialect): + return None + +class INTEGER(sqltypes.INTEGER): + """Drizzle INTEGER type.""" + + __visit_name__ = 'INTEGER' + + def __init__(self, **kw): + """Construct an INTEGER. + + """ + super(INTEGER, self).__init__(**kw) + +class BIGINT(sqltypes.BIGINT): + """Drizzle BIGINTEGER type.""" + + __visit_name__ = 'BIGINT' + + def __init__(self, **kw): + """Construct a BIGINTEGER. + + """ + super(BIGINT, self).__init__(**kw) + + +class _DrizzleTime(mysql_dialect._MSTime): + """Drizzle TIME type.""" + +class TIMESTAMP(sqltypes.TIMESTAMP): + """Drizzle TIMESTAMP type.""" + __visit_name__ = 'TIMESTAMP' + +class TEXT(_StringType, sqltypes.TEXT): + """Drizzle 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 collation: Optional, a column-level collation for this string + value. Takes precedence to 'binary' short-hand. + + :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 VARCHAR(_StringType, sqltypes.VARCHAR): + """Drizzle VARCHAR type, for variable-length character data.""" + + __visit_name__ = 'VARCHAR' + + def __init__(self, length=None, **kwargs): + """Construct a VARCHAR. + + :param collation: Optional, a column-level collation for this string + value. Takes precedence to 'binary' short-hand. + + :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): + """Drizzle 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) + +class ENUM(mysql_dialect.ENUM): + """Drizzle ENUM type.""" + + def __init__(self, *enums, **kw): + """Construct an ENUM. + + Example: + + 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 Drizzle will not raise a fatal error if you attempt to store + an out of range value- an alternate value will be stored instead. + (See Drizzle ENUM documentation.) + + :param collation: Optional, a column-level collation for this string + value. Takes precedence to 'binary' short-hand. + + :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. + + """ + super(ENUM, self).__init__(*enums, **kw) + +class _DrizzleBoolean(sqltypes.Boolean): + def get_dbapi_type(self, dbapi): + return dbapi.NUMERIC + +colspecs = { + sqltypes.Numeric: NUMERIC, + sqltypes.Float: FLOAT, + sqltypes.Time: _DrizzleTime, + sqltypes.Enum: ENUM, + sqltypes.Boolean: _DrizzleBoolean, +} + +# All the types we have in Drizzle +ischema_names = { + 'BIGINT': BIGINT, + 'BINARY': BINARY, + 'BLOB': BLOB, + 'BOOLEAN': BOOLEAN, + 'CHAR': CHAR, + 'DATE': DATE, + 'DATETIME': DATETIME, + 'DECIMAL': DECIMAL, + 'DOUBLE': DOUBLE, + 'ENUM': ENUM, + 'FLOAT': FLOAT, + 'INT': INTEGER, + 'INTEGER': INTEGER, + 'NUMERIC': NUMERIC, + 'TEXT': TEXT, + 'TIME': TIME, + 'TIMESTAMP': TIMESTAMP, + 'VARBINARY': VARBINARY, + 'VARCHAR': VARCHAR, +} + +class DrizzleCompiler(mysql_dialect.MySQLCompiler): + + def visit_typeclause(self, typeclause): + type_ = typeclause.type.dialect_impl(self.dialect) + if isinstance(type_, sqltypes.Integer): + return 'INTEGER' + else: + return super(DrizzleCompiler, self).visit_typeclause(typeclause) + + def visit_cast(self, cast, **kwargs): + type_ = self.process(cast.typeclause) + if type_ is None: + return self.process(cast.clause) + + return 'CAST(%s AS %s)' % (self.process(cast.clause), type_) + + +class DrizzleDDLCompiler(mysql_dialect.MySQLDDLCompiler): + pass + +class DrizzleTypeCompiler(mysql_dialect.MySQLTypeCompiler): + def _extend_numeric(self, type_, spec): + return spec + + def _extend_string(self, type_, defaults, spec): + """Extend a string-type declaration with standard SQL + COLLATE annotations and Drizzle specific extensions. + + """ + + def attr(name): + return getattr(type_, name, defaults.get(name)) + + if attr('collation'): + collation = 'COLLATE %s' % type_.collation + elif attr('binary'): + collation = 'BINARY' + else: + collation = None + + return ' '.join([c for c in (spec, collation) + if c is not None]) + + def visit_NCHAR(self, type): + raise NotImplementedError("Drizzle does not support NCHAR") + + def visit_NVARCHAR(self, type): + raise NotImplementedError("Drizzle does not support NVARCHAR") + + def visit_FLOAT(self, type_): + if type_.scale is not None and type_.precision is not None: + return "FLOAT(%s, %s)" % (type_.precision, type_.scale) + else: + return "FLOAT" + + def visit_BOOLEAN(self, type_): + return "BOOLEAN" + + def visit_BLOB(self, type_): + return "BLOB" + + +class DrizzleExecutionContext(mysql_dialect.MySQLExecutionContext): + pass + +class DrizzleIdentifierPreparer(mysql_dialect.MySQLIdentifierPreparer): + pass + +class DrizzleDialect(mysql_dialect.MySQLDialect): + """Details of the Drizzle dialect. Not used directly in application code.""" + + name = 'drizzle' + + _supports_cast = True + supports_sequences = False + supports_native_boolean = True + supports_views = False + + + default_paramstyle = 'format' + colspecs = colspecs + + statement_compiler = DrizzleCompiler + ddl_compiler = DrizzleDDLCompiler + type_compiler = DrizzleTypeCompiler + ischema_names = ischema_names + preparer = DrizzleIdentifierPreparer + + def on_connect(self): + """Force autocommit - Drizzle Bug#707842 doesn't set this + properly""" + def connect(conn): + conn.autocommit(False) + return connect + + def do_commit(self, connection): + """Execute a COMMIT.""" + + connection.commit() + + def do_rollback(self, connection): + """Execute a ROLLBACK.""" + + connection.rollback() + + @reflection.cache + def get_table_names(self, connection, schema=None, **kw): + """Return a Unicode SHOW TABLES from a given schema.""" + if schema is not None: + current_schema = schema + else: + current_schema = self.default_schema_name + + charset = 'utf8' + rp = connection.execute("SHOW TABLES FROM %s" % + self.identifier_preparer.quote_identifier(current_schema)) + return [row[0] for row in self._compat_fetchall(rp, charset=charset)] + + @reflection.cache + def get_view_names(self, connection, schema=None, **kw): + raise NotImplementedError + + def _detect_casing(self, connection): + """Sniff out identifier case sensitivity. + + Cached per-connection. This value can not change without a server + restart. + + """ + return 0 + + def _detect_collations(self, connection): + """Pull the active COLLATIONS list from the server. + + Cached per-connection. + """ + + collations = {} + charset = self._connection_charset + rs = connection.execute('SELECT CHARACTER_SET_NAME, COLLATION_NAME from data_dictionary.COLLATIONS') + for row in self._compat_fetchall(rs, charset): + collations[row[0]] = row[1] + return collations + + def _detect_ansiquotes(self, connection): + """Detect and adjust for the ANSI_QUOTES sql mode.""" + + self._server_ansiquotes = False + + self._backslash_escapes = False + +log.class_logger(DrizzleDialect) + diff --git a/lib/sqlalchemy/dialects/drizzle/mysqldb.py b/lib/sqlalchemy/dialects/drizzle/mysqldb.py new file mode 100644 index 0000000000..dcf02609e5 --- /dev/null +++ b/lib/sqlalchemy/dialects/drizzle/mysqldb.py @@ -0,0 +1,68 @@ +"""Support for the Drizzle database via the Drizzle-python adapter. + +Drizzle-Python is available at: + + http://sourceforge.net/projects/mysql-python + +At least version 1.2.1 or 1.2.2 should be used. + +Connecting +----------- + +Connect string format:: + + drizzle+mysqldb://:@[:]/ + +Character Sets +-------------- + +Drizzle is all utf8 all the time. + +Known Issues +------------- + +Drizzle-python at least as of version 1.2.2 has a serious memory leak related +to unicode conversion, a feature which is disabled via ``use_unicode=0``. +The recommended connection form with SQLAlchemy is:: + + engine = create_engine('mysql://scott:tiger@localhost/test?charset=utf8&use_unicode=0', pool_recycle=3600) + + +""" + +from sqlalchemy.dialects.drizzle.base import (DrizzleDialect, + DrizzleExecutionContext, + DrizzleCompiler, DrizzleIdentifierPreparer) +from sqlalchemy.connectors.mysqldb import ( + MySQLDBExecutionContext, + MySQLDBCompiler, + MySQLDBIdentifierPreparer, + MySQLDBConnector + ) + +class DrizzleExecutionContext_mysqldb( + MySQLDBExecutionContext, + DrizzleExecutionContext): + pass + + +class DrizzleCompiler_mysqldb(MySQLDBCompiler, DrizzleCompiler): + pass + + +class DrizzleIdentifierPreparer_mysqldb( + MySQLDBIdentifierPreparer, + DrizzleIdentifierPreparer): + pass + +class DrizzleDialect_mysqldb(MySQLDBConnector, DrizzleDialect): + execution_ctx_cls = DrizzleExecutionContext_mysqldb + statement_compiler = DrizzleCompiler_mysqldb + preparer = DrizzleIdentifierPreparer_mysqldb + + def _detect_charset(self, connection): + """Sniff out the character set in use for connection results.""" + return 'utf8' + + +dialect = DrizzleDialect_mysqldb diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 8b83804713..b495cc36e7 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -81,7 +81,7 @@ foreign keys. For these tables, you may supply a autoload=True ) -When creating tables, SQLAlchemy will automatically set ``AUTO_INCREMENT``` on +When creating tables, SQLAlchemy will automatically set ``AUTO_INCREMENT`` on an integer primary key column:: >>> t = Table('mytable', metadata, @@ -152,14 +152,6 @@ available. 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, inspect, re, sys @@ -1161,7 +1153,7 @@ class MySQLCompiler(compiler.SQLCompiler): return 'CHAR' elif isinstance(type_, sqltypes._Binary): return 'BINARY' - elif isinstance(type_, NUMERIC): + elif isinstance(type_, sqltypes.NUMERIC): return self.dialect.type_compiler.process(type_).replace('NUMERIC', 'DECIMAL') else: return None @@ -1257,7 +1249,7 @@ class MySQLCompiler(compiler.SQLCompiler): if update_stmt._whereclause is not None: text += " WHERE " + self.process(update_stmt._whereclause) - limit = update_stmt.kwargs.get('mysql_limit', None) + limit = update_stmt.kwargs.get('%s_limit' % self.dialect.name, None) if limit: text += " LIMIT %s" % limit @@ -1275,8 +1267,9 @@ class MySQLDDLCompiler(compiler.DDLCompiler): """Get table constraints.""" constraint_string = super(MySQLDDLCompiler, self).create_table_constraints(table) - is_innodb = table.kwargs.has_key('mysql_engine') and \ - table.kwargs['mysql_engine'].lower() == 'innodb' + engine_key = '%s_engine' % self.dialect.name + is_innodb = table.kwargs.has_key(engine_key) and \ + table.kwargs[engine_key].lower() == 'innodb' auto_inc_column = table._autoincrement_column @@ -1319,8 +1312,8 @@ class MySQLDDLCompiler(compiler.DDLCompiler): table_opts = [] for k in table.kwargs: - if k.startswith('mysql_'): - opt = k[6:].upper() + if k.startswith('%s_' % self.dialect.name): + opt = k[len(self.dialect.name)+1:].upper() arg = table.kwargs[k] if opt in _options_of_type_string: @@ -1417,17 +1410,25 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): if type_.precision is None: return self._extend_numeric(type_, "NUMERIC") elif type_.scale is None: - return self._extend_numeric(type_, "NUMERIC(%(precision)s)" % {'precision': type_.precision}) + return self._extend_numeric(type_, + "NUMERIC(%(precision)s)" % + {'precision': type_.precision}) else: - return self._extend_numeric(type_, "NUMERIC(%(precision)s, %(scale)s)" % {'precision': type_.precision, 'scale' : type_.scale}) + return self._extend_numeric(type_, + "NUMERIC(%(precision)s, %(scale)s)" % + {'precision': type_.precision, 'scale' : type_.scale}) def visit_DECIMAL(self, type_): if type_.precision is None: return self._extend_numeric(type_, "DECIMAL") elif type_.scale is None: - return self._extend_numeric(type_, "DECIMAL(%(precision)s)" % {'precision': type_.precision}) + return self._extend_numeric(type_, + "DECIMAL(%(precision)s)" % + {'precision': type_.precision}) else: - return self._extend_numeric(type_, "DECIMAL(%(precision)s, %(scale)s)" % {'precision': type_.precision, 'scale' : type_.scale}) + return self._extend_numeric(type_, + "DECIMAL(%(precision)s, %(scale)s)" % + {'precision': type_.precision, 'scale' : type_.scale}) def visit_DOUBLE(self, type_): if type_.precision is not None and type_.scale is not None: @@ -1446,8 +1447,11 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): return self._extend_numeric(type_, 'REAL') def visit_FLOAT(self, type_): - if self._mysql_type(type_) and type_.scale is not None and type_.precision is not None: - return self._extend_numeric(type_, "FLOAT(%s, %s)" % (type_.precision, type_.scale)) + if self._mysql_type(type_) and \ + type_.scale is not None and \ + type_.precision is not None: + return self._extend_numeric(type_, + "FLOAT(%s, %s)" % (type_.precision, type_.scale)) elif type_.precision is not None: return self._extend_numeric(type_, "FLOAT(%s)" % (type_.precision,)) else: @@ -1455,19 +1459,25 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): def visit_INTEGER(self, type_): if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, "INTEGER(%(display_width)s)" % {'display_width': type_.display_width}) + return self._extend_numeric(type_, + "INTEGER(%(display_width)s)" % + {'display_width': type_.display_width}) else: return self._extend_numeric(type_, "INTEGER") def visit_BIGINT(self, type_): if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, "BIGINT(%(display_width)s)" % {'display_width': type_.display_width}) + return self._extend_numeric(type_, + "BIGINT(%(display_width)s)" % + {'display_width': type_.display_width}) else: return self._extend_numeric(type_, "BIGINT") def visit_MEDIUMINT(self, type_): if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, "MEDIUMINT(%(display_width)s)" % {'display_width': type_.display_width}) + return self._extend_numeric(type_, + "MEDIUMINT(%(display_width)s)" % + {'display_width': type_.display_width}) else: return self._extend_numeric(type_, "MEDIUMINT") @@ -1479,7 +1489,10 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): def visit_SMALLINT(self, type_): if self._mysql_type(type_) and type_.display_width is not None: - return self._extend_numeric(type_, "SMALLINT(%(display_width)s)" % {'display_width': type_.display_width}) + return self._extend_numeric(type_, + "SMALLINT(%(display_width)s)" % + {'display_width': type_.display_width} + ) else: return self._extend_numeric(type_, "SMALLINT") @@ -1526,7 +1539,9 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): if type_.length: return self._extend_string(type_, {}, "VARCHAR(%d)" % type_.length) else: - raise exc.InvalidRequestError("VARCHAR requires a length when rendered on MySQL") + raise exc.InvalidRequestError( + "VARCHAR requires a length on dialect %s" % + self.dialect.name) def visit_CHAR(self, type_): if type_.length: @@ -1540,7 +1555,9 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler): if type_.length: return self._extend_string(type_, {'national':True}, "VARCHAR(%(length)s)" % {'length': type_.length}) else: - raise exc.InvalidRequestError("NVARCHAR requires a length when rendered on MySQL") + raise exc.InvalidRequestError( + "NVARCHAR requires a length on dialect %s" % + self.dialect.name) def visit_NCHAR(self, type_): # We'll actually generate the equiv. "NATIONAL CHAR" instead of "NCHAR". @@ -2090,7 +2107,7 @@ class MySQLTableDefinitionParser(object): self.dialect = dialect self.preparer = preparer self._prep_regexes() - + def parse(self, show_create, charset): state = ReflectedState() state.charset = charset @@ -2194,7 +2211,7 @@ class MySQLTableDefinitionParser(object): options.pop(nope, None) for opt, val in options.items(): - state.table_options['mysql_%s' % opt] = val + state.table_options['%s_%s' % (self.dialect.name, opt)] = val def _parse_column(self, line, state): """Extract column details. diff --git a/lib/sqlalchemy/dialects/mysql/mysqldb.py b/lib/sqlalchemy/dialects/mysql/mysqldb.py index e9e1cdbba4..003502b13c 100644 --- a/lib/sqlalchemy/dialects/mysql/mysqldb.py +++ b/lib/sqlalchemy/dialects/mysql/mysqldb.py @@ -50,156 +50,29 @@ The recommended connection form with SQLAlchemy is:: """ -import re - from sqlalchemy.dialects.mysql.base import (MySQLDialect, MySQLExecutionContext, MySQLCompiler, MySQLIdentifierPreparer) -from sqlalchemy.engine import base as engine_base, default -from sqlalchemy.sql import operators as sql_operators -from sqlalchemy import exc, log, schema, sql, types as sqltypes, util -from sqlalchemy import processors - -class MySQLExecutionContext_mysqldb(MySQLExecutionContext): - - @property - def rowcount(self): - if hasattr(self, '_rowcount'): - return self._rowcount - else: - return self.cursor.rowcount - +from sqlalchemy.connectors.mysqldb import ( + MySQLDBExecutionContext, + MySQLDBCompiler, + MySQLDBIdentifierPreparer, + MySQLDBConnector + ) -class MySQLCompiler_mysqldb(MySQLCompiler): - def visit_mod(self, binary, **kw): - return self.process(binary.left) + " %% " + self.process(binary.right) +class MySQLExecutionContext_mysqldb(MySQLDBExecutionContext, MySQLExecutionContext): + pass - def post_process_text(self, text): - return text.replace('%', '%%') +class MySQLCompiler_mysqldb(MySQLDBCompiler, MySQLCompiler): + pass -class MySQLIdentifierPreparer_mysqldb(MySQLIdentifierPreparer): - def _escape_identifier(self, value): - value = value.replace(self.escape_quote, self.escape_to_quote) - return value.replace("%", "%%") +class MySQLIdentifierPreparer_mysqldb(MySQLDBIdentifierPreparer, MySQLIdentifierPreparer): + pass -class MySQLDialect_mysqldb(MySQLDialect): - driver = 'mysqldb' - supports_unicode_statements = False - supports_sane_rowcount = True - supports_sane_multi_rowcount = True - - supports_native_decimal = True - - default_paramstyle = 'format' +class MySQLDialect_mysqldb(MySQLDBConnector, MySQLDialect): execution_ctx_cls = MySQLExecutionContext_mysqldb statement_compiler = MySQLCompiler_mysqldb preparer = MySQLIdentifierPreparer_mysqldb - colspecs = util.update_copy( - MySQLDialect.colspecs, - { - } - ) - - @classmethod - def dbapi(cls): - return __import__('MySQLdb') - - def do_executemany(self, cursor, statement, parameters, context=None): - rowcount = cursor.executemany(statement, parameters) - if context is not None: - context._rowcount = rowcount - - 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: - from MySQLdb.constants import CLIENT as CLIENT_FLAGS - client_flag |= CLIENT_FLAGS.FOUND_ROWS - except: - pass - opts['client_flag'] = client_flag - return [[], opts] - - def _get_server_version_info(self, connection): - dbapi_con = connection.connection - 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 _extract_error_code(self, exception): - return exception.args[0] - - def _detect_charset(self, connection): - """Sniff out the character set in use for connection results.""" - - # Note: MySQL-python 1.2.1c7 seems to ignore changes made - # on a connection via set_character_set() - if self.server_version_info < (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 self._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' - - dialect = MySQLDialect_mysqldb diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 3c41a91de5..aa75a28532 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -37,6 +37,7 @@ class DefaultDialect(base.Dialect): # not cx_oracle. execute_sequence_format = tuple + supports_views = True supports_sequences = False sequences_optional = False preexecute_autoincrement_sequences = False diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index 8e7049b8f9..e23633802b 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -761,12 +761,13 @@ class ReflectionTest(TestBase, ComparesTables): finally: m1.drop_all() + @testing.requires.views @testing.provide_metadata def test_views(self): users, addresses = createTables(metadata, None) try: metadata.create_all() - createViews(metadata.bind, None) + _create_views(metadata.bind, None) m2 = MetaData(testing.db) users_v = Table("users_v", m2, autoload=True) addresses_v = Table("email_addresses_v", m2, autoload=True) @@ -779,14 +780,15 @@ class ReflectionTest(TestBase, ComparesTables): eq_(c1.name, c2.name) self.assert_types_base(c1, c2) finally: - dropViews(metadata.bind) + _drop_views(metadata.bind) + @testing.requires.views @testing.provide_metadata def test_reflect_all_with_views(self): users, addresses = createTables(metadata, None) try: metadata.create_all() - createViews(metadata.bind, None) + _create_views(metadata.bind, None) m2 = MetaData(testing.db) m2.reflect(views=False) @@ -803,7 +805,7 @@ class ReflectionTest(TestBase, ComparesTables): u'users', u'email_addresses']) ) finally: - dropViews(metadata.bind) + _drop_views(metadata.bind) class CreateDropTest(TestBase): @@ -1108,7 +1110,8 @@ def createIndexes(con, schema=None): query = "CREATE INDEX users_t_idx ON %s (test1, test2)" % fullname con.execute(sa.sql.text(query)) -def createViews(con, schema=None): +@testing.requires.views +def _create_views(con, schema=None): for table_name in ('users', 'email_addresses'): fullname = table_name if schema: @@ -1118,7 +1121,8 @@ def createViews(con, schema=None): fullname) con.execute(sa.sql.text(query)) -def dropViews(con, schema=None): +@testing.requires.views +def _drop_views(con, schema=None): for table_name in ('email_addresses', 'users'): fullname = table_name if schema: @@ -1176,7 +1180,7 @@ class ComponentReflectionTest(TestBase): meta = MetaData(testing.db) (users, addresses) = createTables(meta, schema) meta.create_all() - createViews(meta.bind, schema) + _create_views(meta.bind, schema) try: insp = Inspector(meta.bind) if table_type == 'view': @@ -1193,7 +1197,7 @@ class ComponentReflectionTest(TestBase): answer = ['email_addresses', 'users'] eq_(table_names, answer) finally: - dropViews(meta.bind, schema) + _drop_views(meta.bind, schema) addresses.drop() users.drop() @@ -1204,6 +1208,7 @@ class ComponentReflectionTest(TestBase): def test_get_table_names_with_schema(self): self._test_get_table_names('test_schema') + @testing.requires.views def test_get_view_names(self): self._test_get_table_names(table_type='view') @@ -1217,7 +1222,7 @@ class ComponentReflectionTest(TestBase): table_names = ['users', 'email_addresses'] meta.create_all() if table_type == 'view': - createViews(meta.bind, schema) + _create_views(meta.bind, schema) table_names = ['users_v', 'email_addresses_v'] try: insp = Inspector(meta.bind) @@ -1258,7 +1263,7 @@ class ComponentReflectionTest(TestBase): col.type, cols[i]['name'], ctype)) finally: if table_type == 'view': - dropViews(meta.bind, schema) + _drop_views(meta.bind, schema) addresses.drop() users.drop() @@ -1269,9 +1274,11 @@ class ComponentReflectionTest(TestBase): def test_get_columns_with_schema(self): self._test_get_columns(schema='test_schema') + @testing.requires.views def test_get_view_columns(self): self._test_get_columns(table_type='view') + @testing.requires.views @testing.requires.schemas def test_get_view_columns_with_schema(self): self._test_get_columns(schema='test_schema', table_type='view') @@ -1379,7 +1386,7 @@ class ComponentReflectionTest(TestBase): meta = MetaData(testing.db) (users, addresses) = createTables(meta, schema) meta.create_all() - createViews(meta.bind, schema) + _create_views(meta.bind, schema) view_name1 = 'users_v' view_name2 = 'email_addresses_v' try: @@ -1389,13 +1396,15 @@ class ComponentReflectionTest(TestBase): v2 = insp.get_view_definition(view_name2, schema=schema) self.assert_(v2) finally: - dropViews(meta.bind, schema) + _drop_views(meta.bind, schema) addresses.drop() users.drop() + @testing.requires.views def test_get_view_definition(self): self._test_get_view_definition() + @testing.requires.views @testing.requires.schemas def test_get_view_definition_with_schema(self): self._test_get_view_definition(schema='test_schema') diff --git a/test/lib/engines.py b/test/lib/engines.py index 1c97e23cfc..e6ea246e60 100644 --- a/test/lib/engines.py +++ b/test/lib/engines.py @@ -147,7 +147,7 @@ def utf8_engine(url=None, options=None): from sqlalchemy.engine import url as engine_url - if config.db.driver == 'mysqldb': + if config.db.driver == 'mysqldb' and config.db.dialect.name != 'drizzle': dbapi_ver = config.db.dialect.dbapi.version_info if (dbapi_ver < (1, 2, 1) or dbapi_ver in ((1, 2, 1, 'gamma', 1), (1, 2, 1, 'gamma', 2), diff --git a/test/lib/requires.py b/test/lib/requires.py index 03bda74d29..29c7d1ee4f 100644 --- a/test/lib/requires.py +++ b/test/lib/requires.py @@ -155,6 +155,7 @@ def sequences(fn): return _chain_decorators_on( fn, no_support('access', 'no SEQUENCE support'), + no_support('drizzle', 'no SEQUENCE support'), no_support('mssql', 'no SEQUENCE support'), no_support('mysql', 'no SEQUENCE support'), no_support('sqlite', 'no SEQUENCE support'), @@ -228,6 +229,7 @@ def two_phase_transactions(fn): no_support('maxdb', 'not supported by database'), no_support('mssql', 'FIXME: guessing, needs confirmation'), no_support('oracle', 'no SA implementation'), + no_support('drizzle', 'not supported by database'), no_support('sqlite', 'not supported by database'), no_support('sybase', 'FIXME: guessing, needs confirmation'), no_support('postgresql+zxjdbc', 'FIXME: JDBC driver confuses the transaction state, may ' @@ -235,6 +237,13 @@ def two_phase_transactions(fn): exclude('mysql', '<', (5, 0, 3), 'not supported by database'), ) +def views(fn): + """Target database must support VIEWs.""" + return _chain_decorators_on( + fn, + no_support('drizzle', 'no VIEW support'), + ) + def unicode_connections(fn): """Target driver must support some encoding of Unicode across the wire.""" # TODO: expand to exclude MySQLdb versions w/ broken unicode diff --git a/test/sql/test_types.py b/test/sql/test_types.py index e65f684c3a..e41eab1407 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -70,13 +70,16 @@ class AdaptTest(TestBase): (CHAR, "CHAR"), (NCHAR, ("NCHAR", "NATIONAL CHAR")), (BLOB, ("BLOB", "BLOB SUB_TYPE 0")), - (BOOLEAN, ("BOOLEAN", "BOOL")) + (BOOLEAN, ("BOOLEAN", "BOOL", "INTEGER")) ): if isinstance(expected, str): expected = (expected, ) - compiled = types.to_instance(type_).\ + try: + compiled = types.to_instance(type_).\ compile(dialect=dialect) + except NotImplementedError: + continue assert compiled in expected, \ "%r matches none of %r for dialect %s" % \