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
========================= =========================== =========== =========== =========== ================= ============
**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**
.. _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`.
--- /dev/null
+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
:maxdepth: 1
:glob:
+ drizzle
firebird
informix
maxdb
--- /dev/null
+"""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'
+
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
__all__ = (
'access',
+ 'drizzle',
'firebird',
'informix',
'maxdb',
__all__ = (
# 'access',
+ 'drizzle',
'firebird',
# 'informix',
# 'maxdb',
--- /dev/null
+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'
+)
--- /dev/null
+# -*- 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)
+
--- /dev/null
+"""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
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,
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
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
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
"""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
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:
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:
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:
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")
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")
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:
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".
self.dialect = dialect
self.preparer = preparer
self._prep_regexes()
-
+
def parse(self, show_create, charset):
state = ReflectedState()
state.charset = charset
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.
"""
-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
# not cx_oracle.
execute_sequence_format = tuple
+ supports_views = True
supports_sequences = False
sequences_optional = False
preexecute_autoincrement_sequences = False
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)
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)
u'users', u'email_addresses'])
)
finally:
- dropViews(metadata.bind)
+ _drop_views(metadata.bind)
class CreateDropTest(TestBase):
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:
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:
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':
answer = ['email_addresses', 'users']
eq_(table_names, answer)
finally:
- dropViews(meta.bind, schema)
+ _drop_views(meta.bind, schema)
addresses.drop()
users.drop()
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')
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)
col.type, cols[i]['name'], ctype))
finally:
if table_type == 'view':
- dropViews(meta.bind, schema)
+ _drop_views(meta.bind, schema)
addresses.drop()
users.drop()
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')
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:
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')
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),
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'),
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 '
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
(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" % \