]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- new dialect for Drizzle [ticket:2003]
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 26 Jan 2011 15:44:00 +0000 (10:44 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 26 Jan 2011 15:44:00 +0000 (10:44 -0500)
- move mysqldb to a connector, can be shared among mysql/drizzle

17 files changed:
CHANGES
doc/build/core/engines.rst
doc/build/dialects/drizzle.rst [new file with mode: 0644]
doc/build/dialects/index.rst
lib/sqlalchemy/connectors/mysqldb.py [new file with mode: 0644]
lib/sqlalchemy/databases/__init__.py
lib/sqlalchemy/dialects/__init__.py
lib/sqlalchemy/dialects/drizzle/__init__.py [new file with mode: 0644]
lib/sqlalchemy/dialects/drizzle/base.py [new file with mode: 0644]
lib/sqlalchemy/dialects/drizzle/mysqldb.py [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/mysql/mysqldb.py
lib/sqlalchemy/engine/default.py
test/engine/test_reflection.py
test/lib/engines.py
test/lib/requires.py
test/sql/test_types.py

diff --git a/CHANGES b/CHANGES
index 0593ba3743f34db8957f650a6afd2c26cb7a6b05..1c9241478621a97964b0d523b16f21affbc32e12 100644 (file)
--- 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
index 742b19e624eec4ff55c369b7e87c20bacfa10610..082b50a2193f38c2b91114671d770b73026ef8a8 100644 (file)
@@ -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 (file)
index 0000000..0857ca8
--- /dev/null
@@ -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
index 8ad9330bcc9e355c04b3fdc1dcfcbb635f281bb9..7d745d4cb0dd413e3a13dcb7a68cc6e43000e52d 100644 (file)
@@ -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 (file)
index 0000000..7696cdb
--- /dev/null
@@ -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'
+
index 0d53149f0f60c212a07a471b6a56913d4f9bd5c0..dddc8f68a6f109cfece0c8a283f9d3ff4926ead5 100644 (file)
@@ -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',
index 33eeefcec2a4f1e9f0afcb0a5d3d021fd20881f2..48e578a0211258944aa8ce67b9ebdea3028501ff 100644 (file)
@@ -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 (file)
index 0000000..bbd716f
--- /dev/null
@@ -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 (file)
index 0000000..5c268fb
--- /dev/null
@@ -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 <mordred@inaugust.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 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 (file)
index 0000000..dcf0260
--- /dev/null
@@ -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://<user>:<password>@<host>[:<port>]/<dbname>
+
+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
index 8b838047131f95fdd40af98bbe8d7869d811135d..b495cc36e71f18d8fab8adc47a453559be15d68f 100644 (file)
@@ -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.
index e9e1cdbba47a9f147c90501ed2004c6938794130..003502b13c3600aa49a3d8a9415b5236088478e7 100644 (file)
@@ -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
index 3c41a91de5722d4624c0a1bef758aa4149819ad2..aa75a28532f8a67b4cb3815d5971638558704ac2 100644 (file)
@@ -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
index 8e7049b8f90bbe8c4083797484e8a4aef56e78f2..e23633802bd5e2a63b171be206323ebd7d21cd7d 100644 (file)
@@ -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')
index 1c97e23cfca8b44b595d5dcfa014c5a44d4789b1..e6ea246e60769eca62767c75a84ffb077f30eb9f 100644 (file)
@@ -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),
index 03bda74d29876123743a48f13d23ebc81d402e3e..29c7d1ee4f98c97cb6ab2db53170ca2880f4ddb0 100644 (file)
@@ -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
index e65f684c3a60220493193eae0bfc8deac1502127..e41eab140778ce36331073c4dc6984d02ff00390 100644 (file)
@@ -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" % \