]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Gracefully degrade on v$transaction not readable
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 18 Dec 2020 14:28:06 +0000 (09:28 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 18 Dec 2020 15:48:30 +0000 (10:48 -0500)
Fixed regression which occured due to [ticket:5755] which implemented
isolation level support for Oracle.   It has been reported that many Oracle
accounts don't actually have permission to query the ``v$transaction``
view so this feature has been altered to gracefully fallback when it fails
upon database connect, where the dialect will assume "READ COMMITTED" is
the default isolation level as was the case prior to SQLAlchemy 1.3.21.
However, explicit use of the :meth:`_engine.Connection.get_isolation_level`
method must now necessarily raise an exception, as Oracle databases with
this restriction explicitly disallow the user from reading the current
isolation level.

Fixes: #5784
Change-Id: Iefc82928744f3c944c18ae8000eb3c9e52e523bc

doc/build/changelog/unreleased_13/5784.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/oracle/base.py
lib/sqlalchemy/engine/default.py
lib/sqlalchemy/engine/interfaces.py
lib/sqlalchemy/testing/suite/test_dialect.py
test/dialect/oracle/test_dialect.py

diff --git a/doc/build/changelog/unreleased_13/5784.rst b/doc/build/changelog/unreleased_13/5784.rst
new file mode 100644 (file)
index 0000000..87ee080
--- /dev/null
@@ -0,0 +1,15 @@
+.. change::
+    :tags: bug, oracle
+    :tickets: 5784
+    :versions: 1.4.0b2
+
+    Fixed regression which occured due to :ticket:`5755` which implemented
+    isolation level support for Oracle.   It has been reported that many Oracle
+    accounts don't actually have permission to query the ``v$transaction``
+    view so this feature has been altered to gracefully fallback when it fails
+    upon database connect, where the dialect will assume "READ COMMITTED" is
+    the default isolation level as was the case prior to SQLAlchemy 1.3.21.
+    However, explicit use of the :meth:`_engine.Connection.get_isolation_level`
+    method must now necessarily raise an exception, as Oracle databases with
+    this restriction explicitly disallow the user from reading the current
+    isolation level.
\ No newline at end of file
index 3ead97faf66a789d1db9496829753ad1f0100a60..852bafe5beefd81c063e0d9a81a58a0c859cbee2 100644 (file)
@@ -106,10 +106,28 @@ Valid values for ``isolation_level`` include:
 * ``AUTOCOMMIT``
 * ``SERIALIZABLE``
 
-.. note:: The implementation :meth:`_engine.Connection.get_isolation_level`
-   implemented by the Oracle dialect necessarily forces the start of
-   a transaction using the Oracle LOCAL_TRANSACTION_ID function; otherwise
-   no level is normally readable.
+.. note:: The implementation for the
+   :meth:`_engine.Connection.get_isolation_level` method as implemented by the
+   Oracle dialect necessarily forces the start of a transaction using the
+   Oracle LOCAL_TRANSACTION_ID function; otherwise no level is normally
+   readable.
+
+   Additionally, the :meth:`_engine.Connection.get_isolation_level` method will
+   raise an exception if the ``v$transaction`` view is not available due to
+   permissions or other reasons, which is a common occurrence in Oracle
+   installations.
+
+   The cx_Oracle dialect attempts to call the
+   :meth:`_engine.Connection.get_isolation_level` method when the dialect makes
+   its first connection to the database in order to acquire the
+   "default"isolation level.  This default level is necessary so that the level
+   can be reset on a connection after it has been temporarily modified using
+   :meth:`_engine.Connection.execution_options` method.   In the common event
+   that the :meth:`_engine.Connection.get_isolation_level` method raises an
+   exception due to ``v$transaction`` not being readable as well as any other
+   database-related failure, the level is assumed to be "READ COMMITTED".  No
+   warning is emitted for this initial first-connect condition as it is
+   expected to be a common restriction on Oracle databases.
 
 .. versionadded:: 1.3.16 added support for AUTOCOMMIT to the cx_oracle dialect
    as well as the notion of a default isolation level
@@ -117,6 +135,11 @@ Valid values for ``isolation_level`` include:
 .. versionadded:: 1.3.21 Added support for SERIALIZABLE as well as live
    reading of the isolation level.
 
+.. versionchanged:: 1.3.22 In the event that the default isolation
+   level cannot be read due to permissions on the v$transaction view as
+   is common in Oracle installations, the default isolation level is hardcoded
+   to "READ COMMITTED" which was the behavior prior to 1.3.21.
+
 .. seealso::
 
     :ref:`dbapi_autocommit`
@@ -1561,6 +1584,14 @@ class OracleDialect(default.DefaultDialect):
     def get_isolation_level(self, connection):
         raise NotImplementedError("implemented by cx_Oracle dialect")
 
+    def get_default_isolation_level(self, dbapi_conn):
+        try:
+            return self.get_isolation_level(dbapi_conn)
+        except NotImplementedError:
+            raise
+        except:
+            return "READ COMMITTED"
+
     def set_isolation_level(self, connection, level):
         raise NotImplementedError("implemented by cx_Oracle dialect")
 
index 59a4b47dc07e521ed593bc9abc467046c183b654..a754ebe586ea9adfde38cd580b2fe27ba01c81bf 100644 (file)
@@ -367,7 +367,7 @@ class DefaultDialect(interfaces.Dialect):
             self.default_schema_name = None
 
         try:
-            self.default_isolation_level = self.get_isolation_level(
+            self.default_isolation_level = self.get_default_isolation_level(
                 connection.connection
             )
         except NotImplementedError:
@@ -419,6 +419,22 @@ class DefaultDialect(interfaces.Dialect):
         """
         return None
 
+    def get_default_isolation_level(self, dbapi_conn):
+        """Given a DBAPI connection, return its isolation level, or
+        a default isolation level if one cannot be retrieved.
+
+        May be overridden by subclasses in order to provide a
+        "fallback" isolation level for databases that cannot reliably
+        retrieve the actual isolation level.
+
+        By default, calls the :meth:`_engine.Interfaces.get_isolation_level`
+        method, propagating any exceptions raised.
+
+        .. versionadded:: 1.3.22
+
+        """
+        return self.get_isolation_level(dbapi_conn)
+
     def _check_unicode_returns(self, connection, additional_tests=None):
         # this now runs in py2k only and will be removed in 2.0; disabled for
         # Python 3 in all cases under #5315
index fddbc501a165025861cdb834eaff1cf4e2069a2e..b6f7bc49af49ee9ef717becdf7c748b8a5ead4b7 100644 (file)
@@ -876,6 +876,26 @@ class Dialect(object):
 
         raise NotImplementedError()
 
+    def get_default_isolation_level(self, dbapi_conn):
+        """Given a DBAPI connection, return its isolation level, or
+        a default isolation level if one cannot be retrieved.
+
+        This method may only raise NotImplementedError and
+        **must not raise any other exception**, as it is used implicitly upon
+        first connect.
+
+        The method **must return a value** for a dialect that supports
+        isolation level settings, as this level is what will be reverted
+        towards when a per-connection isolation level change is made.
+
+        The method defaults to using the :meth:`.Dialect.get_isolation_level`
+        method unless overridden by a dialect.
+
+        .. versionadded:: 1.3.22
+
+        """
+        raise NotImplementedError()
+
     @classmethod
     def get_dialect_cls(cls, url):
         """Given a URL, return the :class:`.Dialect` that will be used.
index 6a5f2d91b129c4669fe1f7b38ed5401bc6329221..8709aca9d35ca5eea5dc045bdcc39592ee39dadd 100644 (file)
@@ -1,6 +1,5 @@
 #! coding: utf-8
 
-from sqlalchemy import event
 from .. import assert_raises
 from .. import config
 from .. import engines
@@ -12,6 +11,7 @@ from ..config import requirements
 from ..provision import set_default_schema_on_connection
 from ..schema import Column
 from ..schema import Table
+from ... import event
 from ... import exc
 from ... import Integer
 from ... import literal_column
index 00047296e8154b4349f068755e3e207cc6048462..edee760cf37617824b8c25fe422299d94fa131d6 100644 (file)
@@ -67,6 +67,73 @@ class DialectTest(fixtures.TestBase):
             cx_oracle.OracleDialect_cx_oracle(dbapi=Mock())
 
 
+class DialectWBackendTest(fixtures.TestBase):
+    __backend__ = True
+    __only_on__ = "oracle"
+
+    def test_hypothetical_not_implemented_isolation_level(self):
+        engine = engines.testing_engine()
+
+        def get_isolation_level(connection):
+            raise NotImplementedError
+
+        with mock.patch.object(
+            engine.dialect, "get_isolation_level", get_isolation_level
+        ):
+            conn = engine.connect()
+
+            # for NotImplementedError we get back None.  But the
+            # cx_Oracle dialect does not raise this.
+            eq_(conn.dialect.default_isolation_level, None)
+
+            dbapi_conn = conn.connection.connection
+
+            eq_(
+                testing.db.dialect.get_isolation_level(dbapi_conn),
+                "READ COMMITTED",
+            )
+
+    def test_graceful_failure_isolation_level_not_available(self):
+        engine = engines.testing_engine()
+
+        def get_isolation_level(connection):
+            raise exc.DBAPIError(
+                "get isolation level",
+                {},
+                engine.dialect.dbapi.Error("isolation level failed"),
+            )
+
+        with mock.patch.object(
+            engine.dialect, "get_isolation_level", get_isolation_level
+        ):
+            conn = engine.connect()
+            eq_(conn.dialect.default_isolation_level, "READ COMMITTED")
+
+            # test that we can use isolation level setting and that it
+            # reverts for "real" back to READ COMMITTED even though we
+            # can't read it
+            dbapi_conn = conn.connection.connection
+
+            conn = conn.execution_options(isolation_level="SERIALIZABLE")
+            eq_(
+                testing.db.dialect.get_isolation_level(dbapi_conn),
+                "SERIALIZABLE",
+            )
+
+            conn.close()
+            eq_(
+                testing.db.dialect.get_isolation_level(dbapi_conn),
+                "READ COMMITTED",
+            )
+
+            with engine.connect() as conn:
+                assert_raises_message(
+                    exc.DBAPIError,
+                    r".*isolation level failed.*",
+                    conn.get_isolation_level,
+                )
+
+
 class DefaultSchemaNameTest(fixtures.TestBase):
     __backend__ = True
     __only_on__ = "oracle"