]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- new oursql dialect added. [ticket:1613]
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 10 Nov 2009 22:39:42 +0000 (22:39 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 10 Nov 2009 22:39:42 +0000 (22:39 +0000)
CHANGES
lib/sqlalchemy/dialects/mysql/__init__.py
lib/sqlalchemy/dialects/mysql/oursql.py [new file with mode: 0644]
test/dialect/test_mysql.py
test/engine/test_execute.py
test/orm/test_generative.py
test/orm/test_query.py
test/orm/test_relationships.py
test/sql/test_defaults.py
test/sql/test_types.py

diff --git a/CHANGES b/CHANGES
index 698c4760e276a18e860e42dc3f01202ef04b8957..41fcbb8a35bcae6855a068cfcafd922f4e2624b2 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -434,6 +434,9 @@ CHANGES
       object is passed in.
 
 - postgresql
+    - New dialects: pg8000, zxjdbc, and pypostgresql
+      on py3k.
+      
     - The "postgres" dialect is now named "postgresql" !
       Connection strings look like:
 
@@ -497,6 +500,10 @@ CHANGES
       used for such statements.)
       
 - mysql
+    - New dialects: oursql, a new native dialect, 
+      MySQL Connector/Python, a native Python port of MySQLdb,
+      and of course zxjdbc on Jython.
+      
     - all the _detect_XXX() functions now run once underneath
       dialect.initialize()
 
index e2a6fdc71dd5ce5913869a74150506f0515952c3..1685295162dd813b47b2d2d7adf793b00a72fe4d 100644 (file)
@@ -1,4 +1,4 @@
-from sqlalchemy.dialects.mysql import base, mysqldb, pyodbc, zxjdbc, myconnpy
+from sqlalchemy.dialects.mysql import base, mysqldb, oursql, pyodbc, zxjdbc, myconnpy
 
 # default dialect
 base.dialect = mysqldb.dialect
diff --git a/lib/sqlalchemy/dialects/mysql/oursql.py b/lib/sqlalchemy/dialects/mysql/oursql.py
new file mode 100644 (file)
index 0000000..4836e76
--- /dev/null
@@ -0,0 +1,217 @@
+"""Support for the MySQL database via the oursql adapter.
+
+Character Sets
+--------------
+
+oursql defaults to using ``utf8`` as the connection charset, but other 
+encodings may be used instead. Like the MySQL-Python driver, unicode support 
+can be completely disabled::
+
+  # oursql sets the connection charset to utf8 automatically; all strings come 
+  # back as utf8 str
+  create_engine('mysql+oursql:///mydb?use_unicode=0')
+
+To not automatically use ``utf8`` and instead use whatever the connection 
+defaults to, there is a separate parameter::
+
+  # use the default connection charset; all strings come back as unicode
+  create_engine('mysql+oursql:///mydb?default_charset=1')
+  
+  # use latin1 as the connection charset; all strings come back as unicode
+  create_engine('mysql+oursql:///mydb?charset=latin1')
+"""
+
+import decimal
+import re
+
+from sqlalchemy.dialects.mysql.base import (BIT, MySQLDialect, MySQLExecutionContext,
+                                            MySQLCompiler, MySQLIdentifierPreparer, NUMERIC, _NumericType)
+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
+
+
+class _PlainQuery(unicode): 
+    pass
+
+
+class _oursqlNumeric(NUMERIC):
+    def result_processor(self, dialect):
+        if self.asdecimal:
+            return
+        def process(value):
+            if isinstance(value, decimal.Decimal):
+                return float(value)
+            else:
+                return value
+        return process
+
+
+class _oursqlBIT(BIT):
+    def result_processor(self, dialect):
+        """oursql already converts mysql bits, so."""
+        def process(value):
+            return value
+        return process
+
+
+class MySQL_oursql(MySQLDialect):
+    driver = 'oursql'
+    supports_unicode_statements = True
+    supports_unicode_binds = True
+    supports_sane_rowcount = True
+    supports_sane_multi_rowcount = True
+    
+    colspecs = util.update_copy(
+        MySQLDialect.colspecs,
+        {
+            sqltypes.Time: sqltypes.Time,
+            sqltypes.Numeric: _oursqlNumeric,
+            BIT: _oursqlBIT,
+        }
+    )
+    
+    @classmethod
+    def dbapi(cls):
+        return __import__('oursql')
+
+    def do_execute(self, cursor, statement, parameters, context=None):
+        """Provide an implementation of *cursor.execute(statement, parameters)*."""
+        if isinstance(statement, _PlainQuery):
+            cursor.execute(statement, plain_query=True)
+        else:
+            cursor.execute(statement, parameters)
+
+    def do_begin(self, connection):
+        connection.cursor().execute('BEGIN', plain_query=True)
+
+    def _xa_query(self, connection, query, xid):
+        connection.execute(_PlainQuery(query % connection.connection._escape_string(xid)))
+
+    # Because mysql is bad, these methods have to be reimplemented to use _PlainQuery. Basically, some queries
+    # refuse to return any data if they're run through the parameterized query API, or refuse to be parameterized
+    # in the first place.
+    def do_begin_twophase(self, connection, xid):
+        self._xa_query(connection, 'XA BEGIN "%s"', xid)
+
+    def do_prepare_twophase(self, connection, xid):
+        self._xa_query(connection, 'XA END "%s"', xid)
+        self._xa_query(connection, 'XA PREPARE "%s"', xid)
+
+    def do_rollback_twophase(self, connection, xid, is_prepared=True,
+                             recover=False):
+        if not is_prepared:
+            self._xa_query(connection, 'XA END "%s"', xid)
+        self._xa_query(connection, 'XA ROLLBACK "%s"', xid)
+
+    def do_commit_twophase(self, connection, xid, is_prepared=True,
+                           recover=False):
+        if not is_prepared:
+            self.do_prepare_twophase(connection, xid)
+        self._xa_query(connection, 'XA COMMIT "%s"', xid)
+
+    def has_table(self, connection, table_name, schema=None):
+        full_name = '.'.join(self.identifier_preparer._quote_free_identifiers(
+            schema, table_name))
+
+        st = "DESCRIBE %s" % full_name
+        rs = None
+        try:
+            try:
+                rs = connection.execute(_PlainQuery(st))
+                have = rs.rowcount > 0
+                rs.close()
+                return have
+            except exc.SQLError, e:
+                if self._extract_error_code(e) == 1146:
+                    return False
+                raise
+        finally:
+            if rs:
+                rs.close()
+
+    def _show_create_table(self, connection, table, charset=None,
+                           full_name=None):
+        """Run SHOW CREATE TABLE for a ``Table``."""
+
+        if full_name is None:
+            full_name = self.identifier_preparer.format_table(table)
+        st = "SHOW CREATE TABLE %s" % full_name
+
+        rp = None
+        try:
+            try:
+                rp = connection.execute(_PlainQuery(st))
+            except exc.SQLError, e:
+                if self._extract_error_code(e) == 1146:
+                    raise exc.NoSuchTableError(full_name)
+                else:
+                    raise
+            row = rp.fetchone()
+            if not row:
+                raise exc.NoSuchTableError(full_name)
+            return row[1].strip()
+        finally:
+            if rp:
+                rp.close()
+
+    def is_disconnect(self, e):
+        if isinstance(e, self.dbapi.ProgrammingError):  # if underlying connection is closed, this is the error you get
+            return e.errno is None and e[1].endswith('closed')
+        else:
+            return e.errno in (2006, 2013, 2014, 2045, 2055)
+
+    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, 'port', int)
+        util.coerce_kw_type(opts, 'compress', bool)
+        util.coerce_kw_type(opts, 'autoping', bool)
+
+        util.coerce_kw_type(opts, 'default_charset', bool)
+        if opts.pop('default_charset', False):
+            opts['charset'] = None
+        else:
+            util.coerce_kw_type(opts, 'charset', str)
+        util.coerce_kw_type(opts, 'use_unicode', bool)
+
+        # FOUND_ROWS must be set in CLIENT_FLAGS to enable
+        # supports_sane_rowcount.
+        opts['found_rows'] = True
+        # And sqlalchemy assumes that you get an exception when mysql reports a warning.
+        opts['raise_on_warnings'] = True
+        return [[], opts]
+    
+    def _get_server_version_info(self, connection):
+        dbapi_con = connection.connection
+        version = []
+        r = re.compile('[.\-]')
+        for n in r.split(dbapi_con.server_info):
+            try:
+                version.append(int(n))
+            except ValueError:
+                version.append(n)
+        return tuple(version)
+
+    def _extract_error_code(self, exception):
+        try:
+            return exception.orig.errno
+        except AttributeError:
+            return None
+
+    def _detect_charset(self, connection):
+        """Sniff out the character set in use for connection results."""
+        return connection.connection.charset
+    
+    def _compat_fetchall(self, rp, charset=None):
+        """oursql isn't super-broken like MySQLdb, yaaay."""
+        return rp.fetchall()
+
+    def _compat_fetchone(self, rp, charset=None):
+        """oursql isn't super-broken like MySQLdb, yaaay."""
+        return rp.fetchone()
+
+
+dialect = MySQL_oursql
index 49dde1520f03fab8120b5fa9eea1e84392bf9b66..b65ab6312df06c24bfadff84a4a6722b481495ae 100644 (file)
@@ -595,7 +595,7 @@ class TypesTest(TestBase, AssertsExecutionResults, AssertsCompiledSQL):
         # This is known to fail with MySQLDB 1.2.2 beta versions
         # which return these as sets.Set(['a']), sets.Set(['b'])
         # (even on Pythons with __builtin__.set)
-        if (not testing.against('+zxjdbc') and
+        if (testing.against('mysql+mysqldb') and
             testing.db.dialect.dbapi.version_info < (1, 2, 2, 'beta', 3) and
             testing.db.dialect.dbapi.version_info >= (1, 2, 2)):
             # these mysqldb seem to always uses 'sets', even on later pythons
index 7ec4124a9899997da13ada4b7485a5668d290b38..4a1342bd5c5401934aa52a80e47160efc9fc6c6c 100644 (file)
@@ -28,7 +28,7 @@ class ExecuteTest(TestBase):
     def teardown_class(cls):
         metadata.drop_all()
 
-    @testing.fails_on_everything_except('firebird', 'maxdb', 'sqlite', 'mysql+pyodbc', '+zxjdbc')
+    @testing.fails_on_everything_except('firebird', 'maxdb', 'sqlite', 'mysql+pyodbc', '+zxjdbc', 'mysql+oursql')
     def test_raw_qmark(self):
         for conn in (testing.db, testing.db.connect()):
             conn.execute("insert into users (user_id, user_name) values (?, ?)", (1,"jack"))
index 4274b468614533367c95978e5455bf0779ec7926..f30d844320fd95bc82d72f02b2d63b2184fe2a7e 100644 (file)
@@ -80,7 +80,7 @@ class GenerativeQueryTest(_base.MappedTest):
         
     @testing.resolve_artifact_names
     def test_aggregate_1(self):
-        if (testing.against('mysql') and not testing.against('+zxjdbc') and
+        if (testing.against('mysql+mysqldb') and
             testing.db.dialect.dbapi.version_info[:4] == (1, 2, 1, 'gamma')):
             return
 
index 83550b060bf82f4c2b05e5c17fb993961b849bdf..be763f009608dd5e28960e4dbec3693ddaae0534 100644 (file)
@@ -213,7 +213,7 @@ class GetTest(QueryTest):
         assert u.addresses[0].email_address == 'jack@bean.com'
         assert u.orders[1].items[2].description == 'item 5'
 
-    @testing.fails_on_everything_except('sqlite', '+pyodbc', '+zxjdbc')
+    @testing.fails_on_everything_except('sqlite', '+pyodbc', '+zxjdbc', 'mysql+oursql')
     def test_query_str(self):
         s = create_session()
         q = s.query(User).filter(User.id==1)
index e8a7f76b12ca104e2e1172de6d374f95b110af1e..aa1565794f55cbc1f963e882435515779ec3afa2 100644 (file)
@@ -603,7 +603,7 @@ class RelationTest5(_base.MappedTest):
                    lineItems=relation(LineItem,
                        lazy=True,
                        cascade='all, delete-orphan',
-                       order_by=sa.asc(items.c.type),
+                       order_by=sa.asc(items.c.id),
                        primaryjoin=sa.and_(
                          container_select.c.policyNum==items.c.policyNum,
                          container_select.c.policyEffDate==items.c.policyEffDate,
@@ -630,7 +630,7 @@ class RelationTest5(_base.MappedTest):
         assert con.policyNum == newcon.policyNum
         assert len(newcon.lineItems) == 10
         for old, new in zip(con.lineItems, newcon.lineItems):
-            assert old.id == new.id
+            eq_(old.id, new.id)
 
 class RelationTest6(_base.MappedTest):
     """test a relation with a non-column entity in the primary join, 
index 092c7640e3c2a9c5b993095bf3ef76cc24e3d6ff..f49e4d0d3e4721e4770f77f51b19301a7677924b 100644 (file)
@@ -285,7 +285,7 @@ class DefaultTest(testing.TestBase):
     @testing.fails_on('firebird', 'Data type unknown')
     def test_insertmany(self):
         # MySQL-Python 1.2.2 breaks functions in execute_many :(
-        if (testing.against('mysql') and not testing.against('+zxjdbc') and
+        if (testing.against('mysql+mysqldb') and
             testing.db.dialect.dbapi.version_info[:3] == (1, 2, 2)):
             return
 
@@ -319,7 +319,7 @@ class DefaultTest(testing.TestBase):
     @testing.fails_on('firebird', 'Data type unknown')
     def test_updatemany(self):
         # MySQL-Python 1.2.2 breaks functions in execute_many :(
-        if (testing.against('mysql') and not testing.against('+zxjdbc') and
+        if (testing.against('mysql+mysqldb') and
             testing.db.dialect.dbapi.version_info[:3] == (1, 2, 2)):
             return
 
index 15815a420c72422bb76d62d8d9f3eda3398e56ba..c0b86c1e4307aa3dfd7ff8b377b8aafb23f7854c 100644 (file)
@@ -263,8 +263,9 @@ class UnicodeTest(TestBase, AssertsExecutionResults):
             (
                 ('postgresql','psycopg2'),
                 ('postgresql','pg8000'),
-                ('postgresql','zxjdbc'),
-                ('mysql','zxjdbc'),  
+                ('postgresql','zxjdbc'),  
+                ('mysql','oursql'),
+                ('mysql','zxjdbc'),
                 ('sqlite','pysqlite'),
             )), \
             "name: %s driver %s returns_unicode_strings=%s" % \