--- /dev/null
+.. 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
* ``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
.. 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`
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")
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:
"""
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
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.
#! coding: utf-8
-from sqlalchemy import event
from .. import assert_raises
from .. import config
from .. import engines
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
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"