]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- threadlocal engine wasn't properly closing the connection
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 27 Feb 2010 00:44:26 +0000 (00:44 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 27 Feb 2010 00:44:26 +0000 (00:44 +0000)
upon close() - fixed that.
- Transaction object doesn't rollback or commit if it isn't
"active", allows more accurate nesting of begin/rollback/commit.
- Added basic support for mxODBC [ticket:1710].
- Python unicode objects as binds result in the Unicode type,
not string, thus eliminating a certain class of unicode errors
on drivers that don't support unicode binds.

CHANGES
lib/sqlalchemy/connectors/mxodbc.py
lib/sqlalchemy/connectors/pyodbc.py
lib/sqlalchemy/dialects/mssql/__init__.py
lib/sqlalchemy/dialects/mssql/mxodbc.py [new file with mode: 0644]
lib/sqlalchemy/engine/base.py
lib/sqlalchemy/engine/threadlocal.py
lib/sqlalchemy/types.py
test/engine/test_transaction.py
test/sql/test_query.py

diff --git a/CHANGES b/CHANGES
index b335972de433c7bc65969eea3bd32b4aff7d5542..df49116bc045ce8fad0381e7267113083ee4db29 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -194,6 +194,16 @@ CHANGES
 
   - Bind parameters are sent as a tuple instead of a list. Some
     backend drivers will not accept bind parameters as a list.
+
+  - threadlocal engine wasn't properly closing the connection
+    upon close() - fixed that.
+
+  - Transaction object doesn't rollback or commit if it isn't 
+    "active", allows more accurate nesting of begin/rollback/commit.
+
+  - Python unicode objects as binds result in the Unicode type, 
+    not string, thus eliminating a certain class of unicode errors
+    on drivers that don't support unicode binds.
     
 - metadata
   - Added the ability to strip schema information when using
@@ -245,6 +255,8 @@ CHANGES
 - mssql
   - Re-established initial support for pymssql.
 
+  - Added basic support for mxODBC [ticket:1710].
+  
   - Removed the text_as_varchar option.
 
 - oracle
index a0f3f02161ca47b56add1212d70067f33d78f32e..c5d055b5aa0f0856dddd73d74e51a850a87cefad 100644 (file)
@@ -1,3 +1,5 @@
+
+import sys
 from sqlalchemy.connectors import Connector
 
 class MxODBCConnector(Connector):
@@ -8,17 +10,53 @@ class MxODBCConnector(Connector):
     supports_unicode_binds = False
 
     @classmethod
-    def import_dbapi(cls):
-        import mxODBC as module
+    def dbapi(cls):
+        if 'win32' in sys.platform:
+            from mx.ODBC import Windows as module
+        elif 'linux' in sys.platform:
+            from mx.ODBC import unixODBC as module
+        elif 'darwin' in sys.platform:
+            from mx.ODBC import iODBC as module
+        else:
+            raise ImportError, "Unrecognized platform for mxODBC import"
         return module
 
+    def visit_pool(self, pool):
+        def connect(conn, rec):
+            conn.stringformat = self.dbapi.MIXED_STRINGFORMAT
+            conn.datetimeformat = self.dbapi.PYDATETIME_DATETIMEFORMAT
+            #conn.bindmethod = self.dbapi.BIND_USING_PYTHONTYPE
+            #conn.bindmethod = self.dbapi.BIND_USING_SQLTYPE
+        
+        pool.add_listener({'connect':connect})
+
     def create_connect_args(self, url):
-        '''Return a tuple of *args,**kwargs'''
-        # FIXME: handle mx.odbc.Windows proprietary args
+        """ Return a tuple of *args,**kwargs for creating a connection.
+        
+        The mxODBC 3.x connection constructor looks like this:
+        
+            connect(dsn, user='', password='',
+                    clear_auto_commit=1, errorhandler=None)
+        
+        This method translates the values in the provided uri
+        into args and kwargs needed to instantiate an mxODBC Connection.
+        
+        The arg 'errorhandler' is not used by SQLAlchemy and will
+        not be populated.
+        """
         opts = url.translate_connect_args(username='user')
         opts.update(url.query)
-        argsDict = {}
-        argsDict['user'] = opts['user']
-        argsDict['password'] = opts['password']
-        connArgs = [[opts['dsn']], argsDict]
-        return connArgs
+        args = opts['host'],
+        kwargs = {'user':opts['user'],
+                  'password': opts['password']} 
+        return args, kwargs
+
+    def is_disconnect(self, e):
+        if isinstance(e, self.dbapi.ProgrammingError):
+            return "connection already closed" in str(e)
+        elif isinstance(e, self.dbapi.Error):
+            return '[08S01]' in str(e)
+        else:
+            return False
+
+
index 30a7f98d02a644ca8e36b39eb82df7add385fc7f..46b0556d563d50999d5041af8216e34b070530cb 100644 (file)
@@ -68,7 +68,8 @@ class PyODBCConnector(Connector):
         
     def is_disconnect(self, e):
         if isinstance(e, self.dbapi.ProgrammingError):
-            return "The cursor's connection has been closed." in str(e) or 'Attempt to use a closed connection.' in str(e)
+            return "The cursor's connection has been closed." in str(e) or \
+                            'Attempt to use a closed connection.' in str(e)
         elif isinstance(e, self.dbapi.Error):
             return '[08S01]' in str(e)
         else:
index 23ce3aec85fcc4a675bb7958fbee2f04c1180686..65ae3e39bd4b159077745c3e1ab748d11c4f81c2 100644 (file)
@@ -1,4 +1,4 @@
-from sqlalchemy.dialects.mssql import base, pyodbc, adodbapi, pymssql, zxjdbc
+from sqlalchemy.dialects.mssql import base, pyodbc, adodbapi, pymssql, zxjdbc, mxodbc
 
 base.dialect = pyodbc.dialect
 
diff --git a/lib/sqlalchemy/dialects/mssql/mxodbc.py b/lib/sqlalchemy/dialects/mssql/mxodbc.py
new file mode 100644 (file)
index 0000000..0047a55
--- /dev/null
@@ -0,0 +1,56 @@
+from sqlalchemy.dialects.mssql.base import MSExecutionContext, MSDialect
+from sqlalchemy.connectors.mxodbc import MxODBCConnector
+from sqlalchemy import types as sqltypes
+import re
+import sys
+
+from sqlalchemy.dialects.mssql.pyodbc import MSExecutionContext_pyodbc
+
+# The pyodbc execution context seems to work for mxODBC; reuse it here
+MSExecutionContext_mxodbc = MSExecutionContext_pyodbc
+
+
+class MSDialect_mxodbc(MxODBCConnector, MSDialect):
+    supports_sane_rowcount = True
+    supports_sane_multi_rowcount = False
+
+    execution_ctx_cls = MSExecutionContext_mxodbc
+
+
+    def __init__(self, description_encoding='latin-1', **params):
+        super(MSDialect_mxodbc, self).__init__(**params)
+        self.description_encoding = description_encoding
+        
+    def initialize(self, connection):
+        super(MSDialect_mxodbc, self).initialize(connection)
+        dbapi_con = connection.connection
+        
+dialect = MSDialect_mxodbc
+from sqlalchemy.dialects.mssql.base import MSExecutionContext, MSDialect
+from sqlalchemy.connectors.mxodbc import MxODBCConnector
+from sqlalchemy import types as sqltypes
+import re
+import sys
+
+from sqlalchemy.dialects.mssql.pyodbc import MSExecutionContext_pyodbc
+
+# The pyodbc execution context seems to work for mxODBC; reuse it here
+MSExecutionContext_mxodbc = MSExecutionContext_pyodbc
+
+
+class MSDialect_mxodbc(MxODBCConnector, MSDialect):
+    supports_sane_rowcount = True
+    supports_sane_multi_rowcount = False
+
+    execution_ctx_cls = MSExecutionContext_mxodbc
+
+
+    def __init__(self, description_encoding='latin-1', **params):
+        super(MSDialect_mxodbc, self).__init__(**params)
+        self.description_encoding = description_encoding
+        
+    def initialize(self, connection):
+        super(MSDialect_mxodbc, self).initialize(connection)
+        dbapi_con = connection.connection
+        
+dialect = MSDialect_mxodbc
index ff475ee3d2e08a137d2edda4acb9fa59fed78ecd..578bd58d70517910c029b98de838e537557d560b 100644 (file)
@@ -1287,8 +1287,8 @@ class Transaction(object):
     def rollback(self):
         if not self._parent.is_active:
             return
-        self.is_active = False
         self._do_rollback()
+        self.is_active = False
 
     def _do_rollback(self):
         self._parent.rollback()
@@ -1318,10 +1318,12 @@ class RootTransaction(Transaction):
         self.connection._begin_impl()
 
     def _do_rollback(self):
-        self.connection._rollback_impl()
+        if self.is_active:
+            self.connection._rollback_impl()
 
     def _do_commit(self):
-        self.connection._commit_impl()
+        if self.is_active:
+            self.connection._commit_impl()
 
 
 class NestedTransaction(Transaction):
@@ -1330,10 +1332,12 @@ class NestedTransaction(Transaction):
         self._savepoint = self.connection._savepoint_impl()
 
     def _do_rollback(self):
-        self.connection._rollback_to_savepoint_impl(self._savepoint, self._parent)
+        if self.is_active:
+            self.connection._rollback_to_savepoint_impl(self._savepoint, self._parent)
 
     def _do_commit(self):
-        self.connection._release_savepoint_impl(self._savepoint, self._parent)
+        if self.is_active:
+            self.connection._release_savepoint_impl(self._savepoint, self._parent)
 
 
 class TwoPhaseTransaction(Transaction):
index a9892ae7e09cbfda7cc8f97fbacd19e436db5323..001caee2a74ea56f7dc6e95fd0d40d670d47c455 100644 (file)
@@ -94,6 +94,8 @@ class TLEngine(base.Engine):
     def close(self):
         if not self.closed:
             self.contextual_connect().close()
+            connection = self._connections.conn()
+            connection._force_close()
             del self._connections.conn
             self._connections.trans = []
         
index d7b8f9289106ce865e56d6f364fee9a23f08decd..7f1e38003e258fc651a38b812a09ecdec483eb0a 100644 (file)
@@ -1607,8 +1607,10 @@ BOOLEANTYPE = Boolean()
 # type which usually resolves to TEXT/CLOB
 type_map = {
     str: String(),
+    # Py3K
+    #bytes : LargeBinary(),
     # Py2K
-    unicode : String(),
+    unicode : Unicode(),
     # end Py2K
     int : Integer(),
     float : Numeric(),
index de2cc46d33cf6c77c7fb5868e7496eadc87325dc..84ffaf455de8b3ed6d9ed1339f175deddb8922b8 100644 (file)
@@ -830,9 +830,6 @@ class TLTransactionTest(TestBase):
         tlengine.execute(users.insert(), user_id=3, user_name='user3')
         tlengine.rollback()
         
-        # TODO: removing this line, the test still tends to pass in most
-        # cases, except sporadically on PG.  this should be nailed down
-        # in TLEngine - removing this line should be guaranteed fail.
         tlengine.rollback()
         
         tlengine.execute(users.insert(), user_id=4, user_name='user4')
index 5433cb92fc23567ec0bb93ee212866314b90093c..d7bca1af49a1885b0a1539ad94575627c4d18bb6 100644 (file)
@@ -536,7 +536,18 @@ class QueryTest(TestBase):
                         use_labels=labels,
                         order_by=[users.c.user_id.desc()]),
                  [(3,), (2,), (1,)])
+    
+    @testing.fails_on("+pyodbc", "pyodbc row doesn't seem to accept slices")
+    def test_column_slices(self):
+        users.insert().execute(user_id=1, user_name='john')
+        users.insert().execute(user_id=2, user_name='jack')
+        addresses.insert().execute(address_id=1, user_id=2, address='foo@bar.com')
 
+        r = text("select * from query_addresses", bind=testing.db).execute().first()
+        self.assert_(r[0:1] == (1,))
+        self.assert_(r[1:] == (2, 'foo@bar.com'))
+        self.assert_(r[:-1] == (1, 2))
+        
     def test_column_accessor(self):
         users.insert().execute(user_id=1, user_name='john')
         users.insert().execute(user_id=2, user_name='jack')
@@ -550,15 +561,11 @@ class QueryTest(TestBase):
         self.assert_(r.user_id == r['user_id'] == r[users.c.user_id] == 2)
         self.assert_(r.user_name == r['user_name'] == r[users.c.user_name] == 'jack')
         
-        # test slices
-        r = text("select * from query_addresses", bind=testing.db).execute().first()
-        self.assert_(r[0:1] == (1,))
-        self.assert_(r[1:] == (2, 'foo@bar.com'))
-        self.assert_(r[:-1] == (1, 2))
-        
-        # test a little sqlite weirdness - with the UNION, cols come back as "query_users.user_id" in cursor.description
+        # test a little sqlite weirdness - with the UNION, 
+        # cols come back as "query_users.user_id" in cursor.description
         r = text("select query_users.user_id, query_users.user_name from query_users "
-            "UNION select query_users.user_id, query_users.user_name from query_users", bind=testing.db).execute().first()
+            "UNION select query_users.user_id, query_users.user_name from query_users",
+            bind=testing.db).execute().first()
         self.assert_(r['user_id']) == 1
         self.assert_(r['user_name']) == "john"