From 0cc286b5cabc147ced35b63587632ccbcbb17fd3 Mon Sep 17 00:00:00 2001 From: AVRC26 Date: Tue, 7 Jul 2026 15:01:23 -0400 Subject: [PATCH] update oracledb async dialect for oracledb __aenter__() Updated the oracledb async dialect where the async cursor adapter invoked ``__enter__()`` rather than ``__aenter__()`` on the underlying cursor. While these are equivalent in oracledb itself, the correct async form is now used for correctness. As ``AsyncCursor.__aenter__()`` was added in oracledb 2.0.1, the minimum supported oracledb version is now 2.0.1, declared via the ``oracle-oracledb`` extra. Pull request courtesy AVRC26. Fixes: #13420 Closes: #13421 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13421 Pull-request-sha: 20582e2eaf1cdf9cd8af94ecf216f5d5d9fb3d62 Change-Id: I04f0e6cf3f753a1bfc664c4f10c290093526b4cb --- doc/build/changelog/unreleased_21/13420.rst | 10 +++++ lib/sqlalchemy/dialects/oracle/oracledb.py | 8 +--- pyproject.toml | 4 +- test/dialect/oracle/test_dialect.py | 42 +++++++++++++++++++++ 4 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 doc/build/changelog/unreleased_21/13420.rst diff --git a/doc/build/changelog/unreleased_21/13420.rst b/doc/build/changelog/unreleased_21/13420.rst new file mode 100644 index 0000000000..01b75d6bb8 --- /dev/null +++ b/doc/build/changelog/unreleased_21/13420.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, oracle + :tickets: 13420 + + Updated the oracledb async dialect where the async cursor adapter invoked + ``__enter__()`` rather than ``__aenter__()`` on the underlying cursor. + While these are equivalent in oracledb itself, the correct async form is + now used for correctness. As ``AsyncCursor.__aenter__()`` was added in + oracledb 2.0.1, the minimum supported oracledb version is now 2.0.1, + declared via the ``oracle-oracledb`` extra. Pull request courtesy AVRC26. diff --git a/lib/sqlalchemy/dialects/oracle/oracledb.py b/lib/sqlalchemy/dialects/oracle/oracledb.py index e76ca13c69..cf15976906 100644 --- a/lib/sqlalchemy/dialects/oracle/oracledb.py +++ b/lib/sqlalchemy/dialects/oracle/oracledb.py @@ -747,12 +747,6 @@ class AsyncAdapt_oracledb_cursor(AsyncAdapt_dbapi_cursor): def setinputsizes(self, *args: Any, **kwargs: Any) -> Any: return self._cursor.setinputsizes(*args, **kwargs) - def _aenter_cursor(self, cursor: AsyncCursor) -> AsyncCursor: - try: - return cursor.__enter__() - except Exception as error: - self._adapt_connection._handle_exception(error) - async def _execute_async(self, operation, parameters): # override to not use mutex, oracledb already has a mutex @@ -893,7 +887,7 @@ class OracleDialectAsync_oracledb(OracleDialect_oracledb): supports_statement_cache = True execution_ctx_cls = OracleExecutionContextAsync_oracledb - _min_version = (2,) + _min_version = (2, 0, 1) # thick_mode mode is not supported by asyncio, oracledb will raise @classmethod diff --git a/pyproject.toml b/pyproject.toml index 08cfe57dfb..1c5837b711 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,9 +55,9 @@ mssql-python = ["mssql-python>=1.9.0"] mysql = ["mysqlclient>=1.4.0"] mysql-connector = ["mysql-connector-python"] mariadb-connector = ["mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10"] -oracle = ["oracledb>=1.0.1"] +oracle = ["oracledb>=2.0.1"] oracle-cxoracle = ["cx_oracle>=8"] -oracle-oracledb = ["oracledb>=1.0.1"] +oracle-oracledb = ["oracledb>=2.0.1"] postgresql = ["psycopg>=3.0.7,!=3.1.15"] postgresql-pg8000 = ["pg8000>=1.29.3"] postgresql-asyncpg = [ diff --git a/test/dialect/oracle/test_dialect.py b/test/dialect/oracle/test_dialect.py index 1cc41e453f..6ea6fe0c3c 100644 --- a/test/dialect/oracle/test_dialect.py +++ b/test/dialect/oracle/test_dialect.py @@ -29,6 +29,7 @@ from sqlalchemy.testing import assert_raises from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import AssertsCompiledSQL from sqlalchemy.testing import AssertsExecutionResults +from sqlalchemy.testing import async_test from sqlalchemy.testing import config from sqlalchemy.testing import engines from sqlalchemy.testing import eq_ @@ -41,6 +42,7 @@ from sqlalchemy.testing.schema import Column from sqlalchemy.testing.schema import pep435_enum from sqlalchemy.testing.schema import Table from sqlalchemy.testing.suite import test_select +from sqlalchemy.util import greenlet_spawn class CxOracleDialectTest(fixtures.TestBase): @@ -94,6 +96,18 @@ class OracleDbDialectTest(fixtures.TestBase): dialect = oracledb.OracleDialect_oracledb(dbapi=Mock(version="7.1.0")) eq_(dialect.oracledb_ver, (7, 1, 0)) + def test_async_minimum_version(self): + with expect_raises_message( + exc.InvalidRequestError, + r"oracledb version \(2, 0, 1\) and above are supported", + ): + oracledb.OracleDialectAsync_oracledb(dbapi=Mock(version="2.0.0")) + + dialect = oracledb.OracleDialectAsync_oracledb( + dbapi=Mock(version="2.0.1") + ) + eq_(dialect.oracledb_ver, (2, 0, 1)) + def test_get_dialect(self): u = url.URL.create("oracle://") d = oracledb.OracleDialect_oracledb.get_dialect_cls(u) @@ -109,6 +123,34 @@ class OracleDbDialectTest(fixtures.TestBase): e = create_engine("oracle+oracledb_async://") is_true(isinstance(e.dialect, oracledb.OracleDialectAsync_oracledb)) + @async_test + async def test_async_cursor_enters_asynchronously(self): + """test #13420""" + + adapt_connection = Mock() + + underlying_cursor = Mock() + + async def aenter(*arg, **kw): + return underlying_cursor + + underlying_cursor.__aenter__ = Mock(side_effect=aenter) + underlying_cursor.__enter__ = Mock( + side_effect=AssertionError("sync __enter__ should not be called") + ) + + adapt_connection._connection.cursor = Mock( + return_value=underlying_cursor + ) + + cursor = await greenlet_spawn( + oracledb.AsyncAdapt_oracledb_cursor, adapt_connection + ) + + is_(cursor._cursor, underlying_cursor) + underlying_cursor.__aenter__.assert_called_once() + underlying_cursor.__enter__.assert_not_called() + class OracledbMode(fixtures.TestBase): __backend__ = True -- 2.47.3