]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Repaired the usage of ``.prepare()`` in conjunction with
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 7 Dec 2012 00:10:06 +0000 (19:10 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 7 Dec 2012 00:10:06 +0000 (19:10 -0500)
cx_Oracle so that a return value of ``False`` will result
in no call to ``connection.commit()``, hence avoiding
"no transaction" errors.   Two-phase transactions have
now been shown to work in a rudimental fashion with
SQLAlchemy and cx_oracle, however are subject to caveats
observed with the driver; check the documentation
for details.  Also in 0.7.10.
[ticket:2611]

doc/build/changelog/changelog_07.rst
doc/build/changelog/changelog_08.rst
lib/sqlalchemy/dialects/oracle/cx_oracle.py
test/dialect/test_oracle.py

index 314a6b9a0afee82843c5f4be200c34798ab8486a..1bb483280f7964107cdac9ecd9523a1c7b07be32 100644 (file)
@@ -8,6 +8,19 @@
     :version: 0.7.10
     :released:
 
+    .. change::
+        :tags: oracle, bug
+        :tickets: 2611
+
+      Repaired the usage of ``.prepare()`` in conjunction with
+      cx_Oracle so that a return value of ``False`` will result
+      in no call to ``connection.commit()``, hence avoiding
+      "no transaction" errors.   Two-phase transactions have
+      now been shown to work in a rudimental fashion with
+      SQLAlchemy and cx_oracle, however are subject to caveats
+      observed with the driver; check the documentation
+      for details.
+
     .. change::
         :tags: orm, bug
         :tickets: 2624
index 6bc6bd4ce13c00a92ed2b65f0905a2e6e5556bad..dad8df6ce002e20a100620caa96e428ddc345aab 100644 (file)
@@ -6,6 +6,19 @@
 .. changelog::
     :version: 0.8.0b2
 
+    .. change::
+        :tags: oracle, bug
+        :tickets: 2611
+
+      Repaired the usage of ``.prepare()`` in conjunction with
+      cx_Oracle so that a return value of ``False`` will result
+      in no call to ``connection.commit()``, hence avoiding
+      "no transaction" errors.   Two-phase transactions have
+      now been shown to work in a rudimental fashion with
+      SQLAlchemy and cx_oracle, however are subject to caveats
+      observed with the driver; check the documentation
+      for details.  Also in 0.7.10.
+
     .. change::
         :tags: sql, bug
         :tickets: 2618
index bee7308005ea28044d35e31dc0bed1e0b5d8adfd..8b60d9af816676c566e528057eb8ccdc01601879 100644 (file)
@@ -80,8 +80,40 @@ To disable this processing, pass ``auto_convert_lobs=False`` to :func:`create_en
 Two Phase Transaction Support
 -----------------------------
 
-Two Phase transactions are implemented using XA transactions.  Success has been reported
-with this feature but it should be regarded as experimental.
+Two Phase transactions are implemented using XA transactions, and are known
+to work in a rudimental fashion with recent versions of cx_Oracle
+as of SQLAlchemy 0.8.0b2, 0.7.10.   However, the mechanism is not yet
+considered to be robust and should still be regarded as experimental.
+
+In particular, the cx_Oracle DBAPI as recently as 5.1.2 has a bug regarding
+two phase which prevents
+a particular DBAPI connection from being consistently usable in both
+prepared transactions as well as traditional DBAPI usage patterns; therefore
+once a particular connection is used via :meth:`.Connection.begin_prepared`,
+all subsequent usages of the underlying DBAPI connection must be within
+the context of prepared transactions.
+
+The default behavior of :class:`.Engine` is to maintain a pool of DBAPI
+connections.  Therefore, due to the above glitch, a DBAPI connection that has
+been used in a two-phase operation, and is then returned to the pool, will
+not be usable in a non-two-phase context.   To avoid this situation,
+the application can make one of several choices:
+
+* Disable connection pooling using :class:`.NullPool`
+
+* Ensure that the particular :class:`.Engine` in use is only used
+  for two-phase operations.   A :class:`.Engine` bound to an ORM
+  :class:`.Session` which includes ``twophase=True`` will consistently
+  use the two-phase transaction style.
+
+* For ad-hoc two-phase operations without disabling pooling, the DBAPI
+  connection in use can be evicted from the connection pool using the
+  :class:`.Connection.detach` method.
+
+.. versionchanged:: 0.8.0b2,0.7.10
+    Support for cx_oracle prepared transactions has been implemented
+    and tested.
+
 
 Precision Numerics
 ------------------
@@ -150,8 +182,9 @@ a period "." as the decimal character.
 
 """
 
-from .base import OracleCompiler, OracleDialect, \
-                                        RESERVED_WORDS, OracleExecutionContext
+from __future__ import absolute_import
+
+from .base import OracleCompiler, OracleDialect, OracleExecutionContext
 from . import base as oracle
 from ...engine import result as _result
 from sqlalchemy import types as sqltypes, util, exc, processors
@@ -779,15 +812,23 @@ class OracleDialect_cx_oracle(OracleDialect):
         connection.connection.begin(*xid)
 
     def do_prepare_twophase(self, connection, xid):
-        connection.connection.prepare()
+        result = connection.connection.prepare()
+        connection.info['cx_oracle_prepared'] = result
 
-    def do_rollback_twophase(self, connection, xid, is_prepared=True, recover=False):
+    def do_rollback_twophase(self, connection, xid, is_prepared=True,
+                                                recover=False):
         self.do_rollback(connection.connection)
 
-    def do_commit_twophase(self, connection, xid, is_prepared=True, recover=False):
-        self.do_commit(connection.connection)
+    def do_commit_twophase(self, connection, xid, is_prepared=True,
+                                                recover=False):
+        if not is_prepared:
+            self.do_commit(connection.connection)
+        else:
+            oci_prepared = connection.info['cx_oracle_prepared']
+            if oci_prepared:
+                self.do_commit(connection.connection)
 
     def do_recover_twophase(self, connection):
-        pass
+        connection.info.pop('cx_oracle_prepared', None)
 
 dialect = OracleDialect_cx_oracle
index 3e7ebf012155e3d765ecab5d1733399a6e3ca640..7604bf9287363c1f9ee78894d4180a021f02528a 100644 (file)
@@ -781,6 +781,76 @@ class ConstraintTest(fixtures.TestBase):
                     onupdate='CASCADE'))
         assert_raises(exc.SAWarning, bat.create)
 
+
+class TwoPhaseTest(fixtures.TablesTest):
+    """test cx_oracle two phase, which remains in a semi-broken state
+    so requires a carefully written test."""
+
+    __only_on__ = 'oracle+cx_oracle'
+
+    @classmethod
+    def define_tables(cls, metadata):
+        Table('datatable', metadata,
+                Column('id', Integer, primary_key=True),
+                Column('data', String(50))
+            )
+
+    def _connection(self):
+        conn = testing.db.connect()
+        conn.detach()
+        return conn
+
+    def _assert_data(self, rows):
+        eq_(
+            testing.db.scalar("select count(*) from datatable"),
+            rows
+        )
+    def test_twophase_prepare_false(self):
+        conn = self._connection()
+        for i in xrange(2):
+            trans = conn.begin_twophase()
+            conn.execute("select 1 from dual")
+            trans.prepare()
+            trans.commit()
+        conn.close()
+        self._assert_data(0)
+
+    def test_twophase_prepare_true(self):
+        conn = self._connection()
+        for i in xrange(2):
+            trans = conn.begin_twophase()
+            conn.execute("insert into datatable (id, data) "
+                    "values (%s, 'somedata')" % i)
+            trans.prepare()
+            trans.commit()
+        conn.close()
+        self._assert_data(2)
+
+    def test_twophase_rollback(self):
+        conn = self._connection()
+        trans = conn.begin_twophase()
+        conn.execute("insert into datatable (id, data) "
+                "values (%s, 'somedata')" % 1)
+        trans.rollback()
+
+        trans = conn.begin_twophase()
+        conn.execute("insert into datatable (id, data) "
+                "values (%s, 'somedata')" % 1)
+        trans.prepare()
+        trans.commit()
+
+        conn.close()
+        self._assert_data(1)
+
+    def test_not_prepared(self):
+        conn = self._connection()
+        trans = conn.begin_twophase()
+        conn.execute("insert into datatable (id, data) "
+                "values (%s, 'somedata')" % 1)
+        trans.commit()
+        conn.close()
+        self._assert_data(1)
+
 class DialectTypesTest(fixtures.TestBase, AssertsCompiledSQL):
     __dialect__ = oracle.OracleDialect()