From 2703e74142645591430f6b1d162b6a5dc9ccf3f4 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:30:42 -0700 Subject: [PATCH 01/17] Bump version in preparation for new changes. --- doc/src/release_notes.rst | 13 +++++++++++++ src/oracledb/version.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 86646806..93237767 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -13,6 +13,19 @@ Release changes are listed as affecting Thin Mode (the default runtime behavior of python-oracledb), as affecting the optional :ref:`Thick Mode `, or as being 'Common' for changes that impact both modes. +oracledb `3.4.1 `__ (TBD) +-------------------------------------------------------------------------------------------- + +Thin Mode Changes ++++++++++++++++++ + +Thick Mode Changes +++++++++++++++++++ + +Common Changes +++++++++++++++ + + oracledb `3.4.0 `__ (October 2025) ----------------------------------------------------------------------------------------------------- diff --git a/src/oracledb/version.py b/src/oracledb/version.py index 44b21913..56f8d3e7 100644 --- a/src/oracledb/version.py +++ b/src/oracledb/version.py @@ -30,4 +30,4 @@ # file doc/src/conf.py both reference this file directly. # ----------------------------------------------------------------------------- -__version__ = "3.4.0" +__version__ = "3.4.1" From 8c0581ca12b6ed80f241bb3e2139e6165dbc6404 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:31:19 -0700 Subject: [PATCH 02/17] Update ODPI-C. --- src/oracledb/impl/thick/odpi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oracledb/impl/thick/odpi b/src/oracledb/impl/thick/odpi index a6ac6133..a98ef338 160000 --- a/src/oracledb/impl/thick/odpi +++ b/src/oracledb/impl/thick/odpi @@ -1 +1 @@ -Subproject commit a6ac6133646856ea1563bd09aa57a0aeac9b40c8 +Subproject commit a98ef3384310081155e753063ef19c3e9abd9c23 From 5cd753ee3afd14a505d0385ef75dd7cadafaff18 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:31:31 -0700 Subject: [PATCH 03/17] Test suite updates. --- tests/conftest.py | 3 ++- tests/test_2400_pool.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 40014370..f12fe856 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -385,9 +385,10 @@ def get_admin_connection(self, use_async=False): """ Returns an administrative connection to the database. """ - self._initialize() if not self.admin_user or not self.admin_password: pytest.skip("missing administrative credentials") + if self.use_thick_mode and oracledb.is_thin_mode(): + oracledb.init_oracle_client(lib_dir=self.oracle_client_path) params = self.get_connect_params() if self.admin_user.upper() == "SYS": params = params.copy() diff --git a/tests/test_2400_pool.py b/tests/test_2400_pool.py index 4c930d9e..f81adfee 100644 --- a/tests/test_2400_pool.py +++ b/tests/test_2400_pool.py @@ -434,7 +434,6 @@ def test_2411(skip_unless_thick_mode, test_env): min=2, max=8, increment=3, - getmode=oracledb.POOL_GETMODE_NOWAIT, session_callback=callback, ) tags = [ @@ -1066,6 +1065,6 @@ def test_2457(skip_if_drcp, test_env): def test_2458(test_env): "2458 - connection to database with bad password" - pool = test_env.get_pool(password=test_env.main_password + "X") with test_env.assert_raises_full_code("ORA-01017"): + pool = test_env.get_pool(password=test_env.main_password + "X") pool.acquire() From 2f90c3f53683fb6cd7d805d851761a34d674824b Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:31:46 -0700 Subject: [PATCH 04/17] Fixed bug that caused "ORA-03137: malformed TTC packet from client rejected" exception to be raised when attempting to call parse() on a scrollable cursor. --- doc/src/release_notes.rst | 4 ++++ src/oracledb/impl/thin/messages/execute.pyx | 2 +- tests/test_4200_cursor_scrollable.py | 23 +++++++++++++++++++++ tests/test_8600_cursor_scrollable_async.py | 23 +++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 93237767..b5808be2 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -25,6 +25,10 @@ Thick Mode Changes Common Changes ++++++++++++++ +#) Fixed bug that caused ``ORA-03137: malformed TTC packet from client + rejected`` exception to be raised when attempting to call + :meth:`Cursor.parse()` on a scrollable cursor. + oracledb `3.4.0 `__ (October 2025) ----------------------------------------------------------------------------------------------------- diff --git a/src/oracledb/impl/thin/messages/execute.pyx b/src/oracledb/impl/thin/messages/execute.pyx index cda1183a..9f8468ed 100644 --- a/src/oracledb/impl/thin/messages/execute.pyx +++ b/src/oracledb/impl/thin/messages/execute.pyx @@ -81,7 +81,7 @@ cdef class ExecuteMessage(MessageWithData): exec_flags |= TNS_EXEC_FLAGS_IMPLICIT_RESULTSET if not self.scroll_operation: options |= TNS_EXEC_OPTION_EXECUTE - if cursor_impl.scrollable: + if cursor_impl.scrollable and not self.parse_only: exec_flags |= TNS_EXEC_FLAGS_SCROLLABLE if stmt._cursor_id == 0 or stmt._is_ddl: options |= TNS_EXEC_OPTION_PARSE diff --git a/tests/test_4200_cursor_scrollable.py b/tests/test_4200_cursor_scrollable.py index 7a341731..2f39d87a 100644 --- a/tests/test_4200_cursor_scrollable.py +++ b/tests/test_4200_cursor_scrollable.py @@ -223,3 +223,26 @@ def test_4214(conn): (value,) = cursor.fetchone() assert value == 6.25 assert cursor.rowcount == 5 + + +def test_4215(conn): + "4215 - test parse() on a scrollable cursor" + cursor = conn.cursor(scrollable=True) + statement = """ + select 1 from dual + union all + select 2 from dual + union all + select 3 from dual + union all + select 4 from dual + union all + select 5 from dual + """ + cursor.parse(statement) + cursor.execute(statement) + (fetched_value,) = cursor.fetchone() + assert fetched_value == 1 + cursor.scroll(mode="last") + (fetched_value,) = cursor.fetchone() + assert fetched_value == 5 diff --git a/tests/test_8600_cursor_scrollable_async.py b/tests/test_8600_cursor_scrollable_async.py index 636d72c9..46a1c4e2 100644 --- a/tests/test_8600_cursor_scrollable_async.py +++ b/tests/test_8600_cursor_scrollable_async.py @@ -226,3 +226,26 @@ async def test_8614(async_conn): (value,) = await cursor.fetchone() assert value == 6.25 assert cursor.rowcount == 5 + + +async def test_8615(async_conn): + "8615 - test parse() on a scrollable cursor" + cursor = async_conn.cursor(scrollable=True) + statement = """ + select 1 from dual + union all + select 2 from dual + union all + select 3 from dual + union all + select 4 from dual + union all + select 5 from dual + """ + await cursor.parse(statement) + await cursor.execute(statement) + (fetched_value,) = await cursor.fetchone() + assert fetched_value == 1 + await cursor.scroll(mode="last") + (fetched_value,) = await cursor.fetchone() + assert fetched_value == 5 From 05b15612bfe17409653a2c96f1cae620909ba1ce Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:32:15 -0700 Subject: [PATCH 05/17] Fixed segfault on some platforms when trying to execute queries returning vector columns. --- doc/src/release_notes.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index b5808be2..242e4600 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -22,6 +22,10 @@ Thin Mode Changes Thick Mode Changes ++++++++++++++++++ +#) Fixed segfault on some platforms when trying to execute queries returning + vector columns + (`ODPI-C `__ dependency update). + Common Changes ++++++++++++++ From fd3ca3305997d4f97c34b6e91dac99562a20cbf6 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:32:39 -0700 Subject: [PATCH 06/17] Fixed bug when using bind variables with scrollable cursors. --- doc/src/release_notes.rst | 2 ++ src/oracledb/impl/thin/constants.pxi | 1 + src/oracledb/impl/thin/cursor.pyx | 12 ++++++------ src/oracledb/impl/thin/messages/execute.pyx | 3 ++- tests/test_4200_cursor_scrollable.py | 19 +++++++++++++++++++ tests/test_8600_cursor_scrollable_async.py | 19 +++++++++++++++++++ 6 files changed, 49 insertions(+), 7 deletions(-) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 242e4600..f5b9feac 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -19,6 +19,8 @@ oracledb `3.4.1 desired_row return message @@ -259,8 +259,8 @@ cdef class ThinCursorImpl(BaseThinCursorImpl): cdef: Protocol protocol = self._conn_impl._protocol MessageWithData message - if self._statement._sql is None: - message = self._create_message(ExecuteMessage, cursor) + if self._statement._sql is None or self.scrollable: + message = self._create_execute_message(cursor) else: message = self._create_message(FetchMessage, cursor) protocol._process_single_message(message) @@ -357,8 +357,8 @@ cdef class AsyncThinCursorImpl(BaseThinCursorImpl): Internal method used for fetching rows from the database. """ cdef MessageWithData message - if self._statement._sql is None: - message = self._create_message(ExecuteMessage, cursor) + if self._statement._sql is None or self.scrollable: + message = self._create_execute_message(cursor) else: message = self._create_message(FetchMessage, cursor) await self._conn_impl._protocol._process_single_message(message) diff --git a/src/oracledb/impl/thin/messages/execute.pyx b/src/oracledb/impl/thin/messages/execute.pyx index 9f8468ed..c5381c21 100644 --- a/src/oracledb/impl/thin/messages/execute.pyx +++ b/src/oracledb/impl/thin/messages/execute.pyx @@ -83,6 +83,7 @@ cdef class ExecuteMessage(MessageWithData): options |= TNS_EXEC_OPTION_EXECUTE if cursor_impl.scrollable and not self.parse_only: exec_flags |= TNS_EXEC_FLAGS_SCROLLABLE + exec_flags |= TNS_EXEC_FLAGS_NO_CANCEL_ON_EOF if stmt._cursor_id == 0 or stmt._is_ddl: options |= TNS_EXEC_OPTION_PARSE if stmt._is_query: @@ -100,7 +101,7 @@ cdef class ExecuteMessage(MessageWithData): options |= TNS_EXEC_OPTION_NOT_PLSQL elif stmt._is_plsql and num_params > 0: options |= TNS_EXEC_OPTION_PLSQL_BIND - if num_params > 0: + if num_params > 0 and not self.scroll_operation: options |= TNS_EXEC_OPTION_BIND if self.batcherrors: options |= TNS_EXEC_OPTION_BATCH_ERRORS diff --git a/tests/test_4200_cursor_scrollable.py b/tests/test_4200_cursor_scrollable.py index 2f39d87a..7f9a1e21 100644 --- a/tests/test_4200_cursor_scrollable.py +++ b/tests/test_4200_cursor_scrollable.py @@ -246,3 +246,22 @@ def test_4215(conn): cursor.scroll(mode="last") (fetched_value,) = cursor.fetchone() assert fetched_value == 5 + + +def test_4216(conn): + "4216 - test scroll operation with bind values" + cursor = conn.cursor(scrollable=True) + base_value = 4215 + cursor.execute( + """ + select :base_value + 1 from dual + union all + select :base_value + 2 from dual + union all + select :base_value + 3 from dual + """, + dict(base_value=base_value), + ) + cursor.scroll(mode="last") + (fetched_value,) = cursor.fetchone() + assert fetched_value == base_value + 3 diff --git a/tests/test_8600_cursor_scrollable_async.py b/tests/test_8600_cursor_scrollable_async.py index 46a1c4e2..de3dc219 100644 --- a/tests/test_8600_cursor_scrollable_async.py +++ b/tests/test_8600_cursor_scrollable_async.py @@ -249,3 +249,22 @@ async def test_8615(async_conn): await cursor.scroll(mode="last") (fetched_value,) = await cursor.fetchone() assert fetched_value == 5 + + +async def test_8616(async_conn): + "8616 - test scroll operation with bind values" + cursor = async_conn.cursor(scrollable=True) + base_value = 4215 + await cursor.execute( + """ + select :base_value + 1 from dual + union all + select :base_value + 2 from dual + union all + select :base_value + 3 from dual + """, + dict(base_value=base_value), + ) + await cursor.scroll(mode="last") + (fetched_value,) = await cursor.fetchone() + assert fetched_value == base_value + 3 From 8af6668990c914bbe5585e40db1a9173faaa3147 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:34:52 -0700 Subject: [PATCH 07/17] Doc updates. --- doc/src/user_guide/aq.rst | 4 ++++ src/oracledb/__init__.py | 2 +- src/oracledb/aq.py | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/doc/src/user_guide/aq.rst b/doc/src/user_guide/aq.rst index 6db66b5a..1bd838e6 100644 --- a/doc/src/user_guide/aq.rst +++ b/doc/src/user_guide/aq.rst @@ -43,9 +43,13 @@ types which are detailed below. - Supported for single and array message enqueuing and dequeuing when using Oracle Client 19c (or later) and connected to Oracle Database 19c (or later). * - JSON - Supported when using Oracle Database 21c (or later). In python-oracle Thick mode, Oracle Client libraries 21c (or later) are also needed. + + Buffered messaging using the :data:`~oracledb.MSG_BUFFERED` delivery mode is not supported for JSON payloads. - Supported for single message enqueuing and dequeuing when using Oracle Client libraries 21c (or later) and Oracle Database 21c (or later). Array enqueuing and dequeuing is not supported for JSON payloads. + + Buffered messaging using the :data:`~oracledb.MSG_BUFFERED` delivery mode is not supported for JSON payloads. * - JMS - Supported - Supported for single and array message enqueuing and dequeuing when using Oracle Client 19 (or later) and Oracle Database version 23 (or later). diff --git a/src/oracledb/__init__.py b/src/oracledb/__init__.py index a05263f8..7e11e7f0 100644 --- a/src/oracledb/__init__.py +++ b/src/oracledb/__init__.py @@ -734,7 +734,7 @@ needs to be created prior to enqueuing buffered messages. This mode is not supported for bulk array operations in python-oracledb Thick -mode. +mode, and for JSON payloads. """ MSG_PERSISTENT: int = constants.MSG_PERSISTENT diff --git a/src/oracledb/aq.py b/src/oracledb/aq.py index cbd62be4..ebca3ec8 100644 --- a/src/oracledb/aq.py +++ b/src/oracledb/aq.py @@ -326,6 +326,9 @@ def deliverymode(self) -> int: :data:`~oracledb.MSG_PERSISTENT` (default), :data:`~oracledb.MSG_BUFFERED`, or :data:`~oracledb.MSG_PERSISTENT_OR_BUFFERED`. + + Note that :data:`~oracledb.MSG_BUFFERED` is not supported for JSON + payloads. """ raise AttributeError("deliverymode can only be written") @@ -435,6 +438,9 @@ def deliverymode(self) -> int: enqueued. It should be one of the values :data:`~oracledb.MSG_PERSISTENT` (default) or :data:`~oracledb.MSG_BUFFERED`. + + Note that :data:`~oracledb.MSG_BUFFERED` is not supported for JSON + payloads. """ raise AttributeError("deliverymode can only be written") From 3df1328a2a9553752408f8f7b079704b0d47c6b2 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:37:07 -0700 Subject: [PATCH 08/17] Error "DPY-2068: scroll operation is not supported on a non-scrollable cursor" is now raised when using the Cursor.scroll() method on a non-scrollable cursor. --- doc/src/release_notes.rst | 3 +++ src/oracledb/cursor.py | 4 ++++ src/oracledb/errors.py | 4 ++++ tests/test_4200_cursor_scrollable.py | 8 ++++++++ tests/test_8600_cursor_scrollable_async.py | 8 ++++++++ 5 files changed, 27 insertions(+) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index f5b9feac..76c81928 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -34,6 +34,9 @@ Common Changes #) Fixed bug that caused ``ORA-03137: malformed TTC packet from client rejected`` exception to be raised when attempting to call :meth:`Cursor.parse()` on a scrollable cursor. +#) Error ``DPY-2068: scroll operation is not supported on a non-scrollable + cursor`` is now raised when using :meth:`Cursor.scroll()` method on a + non-scrollable cursor. oracledb `3.4.0 `__ (October 2025) diff --git a/src/oracledb/cursor.py b/src/oracledb/cursor.py index 42c7ae8c..7d1a2ed3 100644 --- a/src/oracledb/cursor.py +++ b/src/oracledb/cursor.py @@ -1034,6 +1034,8 @@ def scroll(self, value: int = 0, mode: str = "relative") -> None: scroll operation would position the cursor outside of the result set. """ self._verify_open() + if not self._impl.scrollable: + errors._raise_err(errors.ERR_SCROLL_NOT_SUPPORTED) self._impl.scroll(self, value, mode) @@ -1366,4 +1368,6 @@ async def scroll(self, value: int = 0, mode: str = "relative") -> None: scroll operation would position the cursor outside of the result set. """ self._verify_open() + if not self._impl.scrollable: + errors._raise_err(errors.ERR_SCROLL_NOT_SUPPORTED) await self._impl.scroll(self, value, mode) diff --git a/src/oracledb/errors.py b/src/oracledb/errors.py index a0ac34f9..7b32ed43 100644 --- a/src/oracledb/errors.py +++ b/src/oracledb/errors.py @@ -291,6 +291,7 @@ def _raise_not_supported(feature: str) -> None: ERR_ARROW_SPARSE_VECTOR_NOT_ALLOWED = 2065 ERR_EMPTY_STATEMENT = 2066 ERR_WRONG_DIRECT_PATH_DATA_TYPE = 2067 +ERR_SCROLL_NOT_SUPPORTED = 2068 # error numbers that result in NotSupportedError ERR_TIME_NOT_SUPPORTED = 3000 @@ -894,6 +895,9 @@ def _raise_not_supported(feature: str) -> None: ERR_PYTHON_VALUE_NOT_SUPPORTED: ( 'Python value of type "{type_name}" is not supported' ), + ERR_SCROLL_NOT_SUPPORTED: ( + "scroll operation is not supported on a non-scrollable cursor" + ), ERR_SCROLL_OUT_OF_RESULT_SET: ( "scroll operation would go out of the result set" ), diff --git a/tests/test_4200_cursor_scrollable.py b/tests/test_4200_cursor_scrollable.py index 7f9a1e21..dc34c50c 100644 --- a/tests/test_4200_cursor_scrollable.py +++ b/tests/test_4200_cursor_scrollable.py @@ -265,3 +265,11 @@ def test_4216(conn): cursor.scroll(mode="last") (fetched_value,) = cursor.fetchone() assert fetched_value == base_value + 3 + + +def test_4217(conn, test_env): + "4217 - test calling scroll() on a non-scrollable cursor" + cursor = conn.cursor() + cursor.execute("select NumberCol from TestNumbers order by IntCol") + with test_env.assert_raises_full_code("DPY-2068"): + cursor.scroll(mode="first") diff --git a/tests/test_8600_cursor_scrollable_async.py b/tests/test_8600_cursor_scrollable_async.py index de3dc219..5bd47140 100644 --- a/tests/test_8600_cursor_scrollable_async.py +++ b/tests/test_8600_cursor_scrollable_async.py @@ -268,3 +268,11 @@ async def test_8616(async_conn): await cursor.scroll(mode="last") (fetched_value,) = await cursor.fetchone() assert fetched_value == base_value + 3 + + +async def test_8617(async_conn, test_env): + "8717 - test calling scroll() on a non-scrollable cursor" + cursor = async_conn.cursor() + await cursor.execute("select NumberCol from TestNumbers order by IntCol") + with test_env.assert_raises_full_code("DPY-2068"): + await cursor.scroll(mode="first") From c3c25730709a51bf264b819d310f2c025d602af5 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:42:33 -0700 Subject: [PATCH 09/17] Fixed bug when setting "SOURCE_ROUTE" on the "DESCRIPTION" section of a full connect descriptor instead of the "ADDRESS_LIST" section. --- doc/src/release_notes.rst | 2 ++ src/oracledb/impl/base/parsers.pyx | 1 + tests/test_4500_connect_params.py | 3 +-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 76c81928..5db2207e 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -20,6 +20,8 @@ Thin Mode Changes +++++++++++++++++ #) Fixed bug when using bind variables with scrollable cursors. +#) Fixed bug when setting ``SOURCE_ROUTE`` on the ``DESCRIPTION`` section of a + full connect descriptor instead of the ``ADDRESS_LIST`` section. Thick Mode Changes ++++++++++++++++++ diff --git a/src/oracledb/impl/base/parsers.pyx b/src/oracledb/impl/base/parsers.pyx index 19fa04eb..a894595e 100644 --- a/src/oracledb/impl/base/parsers.pyx +++ b/src/oracledb/impl/base/parsers.pyx @@ -362,6 +362,7 @@ cdef class ConnectStringParser(BaseParser): description.set_from_security_args(sub_args) address_lists = desc_args.get("address_list", desc_args) if not isinstance(address_lists, list): + description.source_route = False address_lists = [address_lists] for list_args in address_lists: address_list = AddressList() diff --git a/tests/test_4500_connect_params.py b/tests/test_4500_connect_params.py index bc745135..69b85ff7 100644 --- a/tests/test_4500_connect_params.py +++ b/tests/test_4500_connect_params.py @@ -588,8 +588,7 @@ def test_4531(): params.parse_connect_string(connect_string) source_route_clause = "(SOURCE_ROUTE=ON)" if has_section else "" connect_string = ( - f"(DESCRIPTION={source_route_clause}" - f"(ADDRESS_LIST={source_route_clause}" + f"(DESCRIPTION=(ADDRESS_LIST={source_route_clause}" "(ADDRESS=(PROTOCOL=tcp)(HOST=host1)(PORT=1521))" "(ADDRESS=(PROTOCOL=tcp)(HOST=host2)(PORT=1522)))" "(CONNECT_DATA=(SERVICE_NAME=my_service_35)))" From 55e94919a70b598240badffb54ed4cc67fe46df1 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:44:28 -0700 Subject: [PATCH 10/17] Raise helpful exception for requested_schema programming errors. --- doc/src/release_notes.rst | 7 ++++++ src/oracledb/errors.py | 5 ++++ src/oracledb/impl/base/cursor.pyx | 7 ++++++ tests/test_9300_dataframe_requested_schema.py | 25 +++++++++++++++++++ ...t_9400_dataframe_requested_schema_async.py | 23 +++++++++++++++++ 5 files changed, 67 insertions(+) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 5db2207e..cd3216f6 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -36,6 +36,13 @@ Common Changes #) Fixed bug that caused ``ORA-03137: malformed TTC packet from client rejected`` exception to be raised when attempting to call :meth:`Cursor.parse()` on a scrollable cursor. +#) Error ``DPY-2069: requested schema has {num_schema_columns} columns defined + but {num_fetched_columns} columns are being fetched`` is now raised when + the number of elements in the ``requested_schema`` parameter to + :meth:`Connection.fetch_df_all()` and :meth:`Connection.fetch_df_batches()` + doesn't match the number of columns being fetched. Previously this scenario + would throw unhelpful execptions or cause a segfault under certain + circumstances. #) Error ``DPY-2068: scroll operation is not supported on a non-scrollable cursor`` is now raised when using :meth:`Cursor.scroll()` method on a non-scrollable cursor. diff --git a/src/oracledb/errors.py b/src/oracledb/errors.py index 7b32ed43..44d406f7 100644 --- a/src/oracledb/errors.py +++ b/src/oracledb/errors.py @@ -292,6 +292,7 @@ def _raise_not_supported(feature: str) -> None: ERR_EMPTY_STATEMENT = 2066 ERR_WRONG_DIRECT_PATH_DATA_TYPE = 2067 ERR_SCROLL_NOT_SUPPORTED = 2068 +ERR_WRONG_REQUESTED_SCHEMA_LENGTH = 2069 # error numbers that result in NotSupportedError ERR_TIME_NOT_SUPPORTED = 3000 @@ -1007,6 +1008,10 @@ def _raise_not_supported(feature: str) -> None: 'found object of type "{actual_schema}.{actual_name}" when ' 'expecting object of type "{expected_schema}.{expected_name}"' ), + ERR_WRONG_REQUESTED_SCHEMA_LENGTH: ( + "requested schema has {num_schema_columns} columns defined but " + "{num_fetched_columns} are being fetched" + ), ERR_WRONG_SCROLL_MODE: ( "scroll mode must be relative, absolute, first or last" ), diff --git a/src/oracledb/impl/base/cursor.pyx b/src/oracledb/impl/base/cursor.pyx index b78e7d74..54978d2b 100644 --- a/src/oracledb/impl/base/cursor.pyx +++ b/src/oracledb/impl/base/cursor.pyx @@ -326,6 +326,13 @@ cdef class BaseCursorImpl: Initializes the fetch variable lists in preparation for creating the fetch variables used in fetching rows from the database. """ + cdef ssize_t num_schema_columns + if self.schema_impl is not None: + num_schema_columns = len(self.schema_impl.child_schemas) + if num_schema_columns != num_columns: + errors._raise_err(errors.ERR_WRONG_REQUESTED_SCHEMA_LENGTH, + num_schema_columns=num_schema_columns, + num_fetched_columns=num_columns) self.fetch_metadata = [None] * num_columns self.fetch_vars = [None] * num_columns self.fetch_var_impls = [None] * num_columns diff --git a/tests/test_9300_dataframe_requested_schema.py b/tests/test_9300_dataframe_requested_schema.py index c851ea11..d457ded3 100644 --- a/tests/test_9300_dataframe_requested_schema.py +++ b/tests/test_9300_dataframe_requested_schema.py @@ -755,3 +755,28 @@ def test_9327(value, conn, test_env): conn.fetch_df_all( "select :1 from dual", [value], requested_schema=requested_schema ) + + +@pytest.mark.parametrize("num_elements", [1, 3]) +def test_9328(num_elements, conn, test_env): + "9328 - fetch_df_all() with wrong requested_schema size" + elements = [(f"COL_{i}", pyarrow.string()) for i in range(num_elements)] + requested_schema = pyarrow.schema(elements) + with test_env.assert_raises_full_code("DPY-2069"): + conn.fetch_df_all( + "select user, user from dual", requested_schema=requested_schema + ) + + +@pytest.mark.parametrize("num_elements", [1, 3]) +def test_9329(num_elements, conn, test_env): + "9329 - fetch_df_batches() with wrong requested_schema size" + elements = [(f"COL_{i}", pyarrow.string()) for i in range(num_elements)] + requested_schema = pyarrow.schema(elements) + with test_env.assert_raises_full_code("DPY-2069"): + list( + conn.fetch_df_batches( + "select user, user from dual", + requested_schema=requested_schema, + ) + ) diff --git a/tests/test_9400_dataframe_requested_schema_async.py b/tests/test_9400_dataframe_requested_schema_async.py index ea52254c..1308b15d 100644 --- a/tests/test_9400_dataframe_requested_schema_async.py +++ b/tests/test_9400_dataframe_requested_schema_async.py @@ -778,3 +778,26 @@ async def test_9427(value, async_conn, test_env): await async_conn.fetch_df_all( "select :1 from dual", [value], requested_schema=requested_schema ) + + +@pytest.mark.parametrize("num_elements", [1, 3]) +async def test_9428(num_elements, async_conn, test_env): + "9428 - fetch_df_all() with wrong requested_schema size" + elements = [(f"COL_{i}", pyarrow.string()) for i in range(num_elements)] + requested_schema = pyarrow.schema(elements) + with test_env.assert_raises_full_code("DPY-2069"): + await async_conn.fetch_df_all( + "select user, user from dual", requested_schema=requested_schema + ) + + +@pytest.mark.parametrize("num_elements", [1, 3]) +async def test_9429(num_elements, async_conn, test_env): + "9429 - fetch_df_batches() with wrong requested_schema size" + elements = [(f"COL_{i}", pyarrow.string()) for i in range(num_elements)] + requested_schema = pyarrow.schema(elements) + with test_env.assert_raises_full_code("DPY-2069"): + async for df in async_conn.fetch_df_batches( + "select user, user from dual", requested_schema=requested_schema + ): + pass From 75417f1628cd09a46d20446401b61425c66d82e1 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:45:05 -0700 Subject: [PATCH 11/17] Ensure that PL/SQL errors found while building the schema are reported. --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index f12fe856..b5e784ca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -665,6 +665,7 @@ def run_sql_script(self, conn, script_name, **kwargs): prev_name = name prev_obj_type = obj_type print(" %s/%s %s" % (line_num, position, text)) + assert prev_name is None def skip_unless_client_version(self, major_version, minor_version=0): """ From 20e37fe66b66c93edc2bcdd9befc3b1f02a46643 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:45:27 -0700 Subject: [PATCH 12/17] Fixed bug when adding a call to a PL/SQL function which returns LOBs to a pipeline. --- doc/src/release_notes.rst | 2 + src/oracledb/impl/thin/connection.pyx | 77 ++++++--------------------- tests/sql/create_schema.sql | 13 +++++ tests/test_7600_pipelining_async.py | 17 ++++++ 4 files changed, 48 insertions(+), 61 deletions(-) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index cd3216f6..1eef507f 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -19,6 +19,8 @@ oracledb `3.4.1 `. #) Fixed bug when using bind variables with scrollable cursors. #) Fixed bug when setting ``SOURCE_ROUTE`` on the ``DESCRIPTION`` section of a full connect descriptor instead of the ``ADDRESS_LIST`` section. diff --git a/src/oracledb/impl/thin/connection.pyx b/src/oracledb/impl/thin/connection.pyx index 6a820a52..c83bd620 100644 --- a/src/oracledb/impl/thin/connection.pyx +++ b/src/oracledb/impl/thin/connection.pyx @@ -721,6 +721,7 @@ cdef class AsyncThinConnImpl(BaseThinConnImpl): PipelineOpImpl op_impl = result_impl.operation uint8_t op_type = op_impl.op_type AsyncThinCursorImpl cursor_impl + BindVar bind_var # all operations other than commit make use of a cursor if op_type == PIPELINE_OP_TYPE_COMMIT: @@ -732,23 +733,27 @@ cdef class AsyncThinConnImpl(BaseThinConnImpl): # resend the message if that is required (for operations that fetch # LOBS, for example) - cursor_impl = message_with_data.cursor_impl + cursor_impl = message_with_data.cursor_impl if message.resend: await protocol._process_message(message) - await message.postprocess_async() - if op_type in ( - PIPELINE_OP_TYPE_FETCH_ONE, - PIPELINE_OP_TYPE_FETCH_MANY, - PIPELINE_OP_TYPE_FETCH_ALL, - ): - result_impl.rows = [] - while cursor_impl._buffer_rowcount > 0: - result_impl.rows.append(cursor_impl._create_row()) + await message.postprocess_async() + if op_impl.op_type == PIPELINE_OP_TYPE_CALL_FUNC: + bind_var = cursor_impl.bind_vars[0] + result_impl.return_value = bind_var.var_impl.get_value(0) + elif op_type in ( + PIPELINE_OP_TYPE_FETCH_ONE, + PIPELINE_OP_TYPE_FETCH_MANY, + PIPELINE_OP_TYPE_FETCH_ALL, + ): + result_impl.rows = [] + while cursor_impl._buffer_rowcount > 0: + result_impl.rows.append(cursor_impl._create_row()) result_impl.fetch_metadata = cursor_impl.fetch_metadata # for fetchall(), perform as many round trips as are required to # complete the fetch - if op_type == PIPELINE_OP_TYPE_FETCH_ALL: + if op_type == PIPELINE_OP_TYPE_FETCH_ALL \ + and cursor_impl._more_rows_to_fetch: fetch_message = cursor_impl._create_message( FetchMessage, message_with_data.cursor ) @@ -980,55 +985,6 @@ cdef class AsyncThinConnImpl(BaseThinConnImpl): messages.append(message) return messages - cdef int _populate_pipeline_op_result(self, Message message) except -1: - """ - Populates the pipeline operation result object. - """ - cdef: - MessageWithData message_with_data - AsyncThinCursorImpl cursor_impl - PipelineOpResultImpl result_impl - PipelineOpImpl op_impl - BindVar bind_var - result_impl = message.pipeline_result_impl - op_impl = result_impl.operation - if op_impl.op_type == PIPELINE_OP_TYPE_COMMIT: - return 0 - message_with_data = message - cursor_impl = message_with_data.cursor_impl - if op_impl.op_type == PIPELINE_OP_TYPE_CALL_FUNC: - bind_var = cursor_impl.bind_vars[0] - result_impl.return_value = bind_var.var_impl.get_value(0) - elif op_impl.op_type in ( - PIPELINE_OP_TYPE_FETCH_ONE, - PIPELINE_OP_TYPE_FETCH_MANY, - PIPELINE_OP_TYPE_FETCH_ALL, - ): - result_impl.rows = [] - while cursor_impl._buffer_rowcount > 0: - result_impl.rows.append(cursor_impl._create_row()) - - cdef int _populate_pipeline_op_results( - self, list messages, bint continue_on_error - ) except -1: - """ - Populates the pipeline operation result objects associated with the - messages that were processed on the database. - """ - cdef: - PipelineOpResultImpl result_impl - Message message - for message in messages: - result_impl = message.pipeline_result_impl - if result_impl.error is not None or message.resend: - continue - try: - self._populate_pipeline_op_result(message) - except Exception as e: - if not continue_on_error: - raise - result_impl._capture_err(e) - async def _run_pipeline_op_without_pipelining( self, object conn, PipelineOpResultImpl result_impl ): @@ -1262,7 +1218,6 @@ cdef class AsyncThinConnImpl(BaseThinConnImpl): self.pipeline_mode = TNS_PIPELINE_MODE_ABORT_ON_ERROR self._send_messages_for_pipeline(messages, continue_on_error) await protocol.end_pipeline(self, messages, continue_on_error) - self._populate_pipeline_op_results(messages, continue_on_error) await self._complete_pipeline_ops(messages, continue_on_error) async def run_pipeline_without_pipelining( diff --git a/tests/sql/create_schema.sql b/tests/sql/create_schema.sql index d7d5d5c3..9f35eb5b 100644 --- a/tests/sql/create_schema.sql +++ b/tests/sql/create_schema.sql @@ -1559,6 +1559,11 @@ create or replace package &main_user..pkg_TestLOBs as a_ReplaceValue varchar2 ); + function GetLOB ( + a_Num number, + a_Format varchar2 + ) return clob; + end; / @@ -1591,5 +1596,13 @@ create or replace package body &main_user..pkg_TestLOBs as end if; end; + function GetLOB ( + a_Num number, + a_Format varchar2 + ) return clob is + begin + return to_clob(replace(a_Format, '{}', to_char(a_Num))); + end; + end; / diff --git a/tests/test_7600_pipelining_async.py b/tests/test_7600_pipelining_async.py index ad84328c..d00489be 100644 --- a/tests/test_7600_pipelining_async.py +++ b/tests/test_7600_pipelining_async.py @@ -1009,3 +1009,20 @@ async def test_7648(async_conn, test_env): res = await async_conn.run_pipeline(pipeline) assert [res[-1].rows] == [[(clob_1_value,), (clob_2_value,)]] assert [res[-2].rows] == [[(clob_1_value,), (clob_2_value,)]] + + +async def test_7649(async_conn, test_env): + "7649 - test PL/SQL returning LOB data from a function" + clob_format = "Sample data for test 7649 - {}" + num_values = [5, 38, 1549] + pipeline = oracledb.create_pipeline() + for num in num_values: + pipeline.add_callfunc( + "pkg_TestLOBs.GetLOB", + return_type=oracledb.DB_TYPE_CLOB, + parameters=[num, clob_format], + ) + res = await async_conn.run_pipeline(pipeline) + for result, num in zip(res, num_values): + expected_value = clob_format.replace("{}", str(num)) + assert await result.return_value.read() == expected_value From 7451c305ce5b5c5407e5c4e8c4b143d19213ca94 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:45:47 -0700 Subject: [PATCH 13/17] Fix template. --- utils/templates/connect_params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/templates/connect_params.py b/utils/templates/connect_params.py index ceab8c5e..35f7aeae 100644 --- a/utils/templates/connect_params.py +++ b/utils/templates/connect_params.py @@ -37,16 +37,16 @@ import oracledb +from .base import BaseMetaClass from . import base_impl, utils -class ConnectParams: +class ConnectParams(metaclass=BaseMetaClass): """ Contains all parameters used for establishing a connection to the database. """ - __module__ = oracledb.__name__ __slots__ = ["_impl"] _impl_class = base_impl.ConnectParamsImpl From 30d956eaa293ae4b9a24a77a403a6e0b1621db9f Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:45:59 -0700 Subject: [PATCH 14/17] Remove dead code. --- src/oracledb/impl/thin/connection.pyx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/oracledb/impl/thin/connection.pyx b/src/oracledb/impl/thin/connection.pyx index c83bd620..442c98b7 100644 --- a/src/oracledb/impl/thin/connection.pyx +++ b/src/oracledb/impl/thin/connection.pyx @@ -340,16 +340,6 @@ cdef class BaseThinConnImpl(BaseConnImpl): get_dbobject_type_cache(self._dbobject_type_cache_num) return cache.get_type(conn, name) - def ping(self): - cdef Message message - message = self._create_message(PingMessage) - self._protocol._process_single_message(message) - - def rollback(self): - cdef Message message - message = self._create_message(RollbackMessage) - self._protocol._process_single_message(message) - def set_action(self, str value): self._action = value self._action_modified = True From 306e41b1aaf6f04fef4538cda5fc1999b47a1d84 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 10:46:14 -0700 Subject: [PATCH 15/17] Fixed bug when fetching a timestamp with nanosecond precision into a data frame (#538). --- doc/src/release_notes.rst | 3 ++ src/oracledb/impl/base/converters.pyx | 13 ++++++-- tests/test_9300_dataframe_requested_schema.py | 31 ++++++++++++------- ...t_9400_dataframe_requested_schema_async.py | 31 ++++++++++++------- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 1eef507f..ebbe0794 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -19,6 +19,9 @@ oracledb `3.4.1 `__). #) Fixed bug when adding a call to a PL/SQL function which returns LOBs to a :ref:`pipeline `. #) Fixed bug when using bind variables with scrollable cursors. diff --git a/src/oracledb/impl/base/converters.pyx b/src/oracledb/impl/base/converters.pyx index 27ed64dd..fa4d0adb 100644 --- a/src/oracledb/impl/base/converters.pyx +++ b/src/oracledb/impl/base/converters.pyx @@ -186,10 +186,19 @@ cdef int convert_date_to_arrow_timestamp(ArrowArrayImpl array_impl, cdef: cydatetime.timedelta td cydatetime.datetime dt - int64_t ts + int64_t ts, us dt = convert_date_to_python(buffer) td = dt - EPOCH_DATE - ts = int(cydatetime.total_seconds(td) * array_impl.schema_impl.time_factor) + ts = ( cydatetime.timedelta_days(td)) * (24 * 60 * 60) + \ + cydatetime.timedelta_seconds(td) + ts *= array_impl.schema_impl.time_factor + us = cydatetime.timedelta_microseconds(td) + if array_impl.schema_impl.time_factor == 1_000: + ts += us // 1_000 + elif array_impl.schema_impl.time_factor == 1_000_000: + ts += us + elif array_impl.schema_impl.time_factor != 1: + ts += us * 1_000 array_impl.append_int(ts) diff --git a/tests/test_9300_dataframe_requested_schema.py b/tests/test_9300_dataframe_requested_schema.py index d457ded3..d93627b3 100644 --- a/tests/test_9300_dataframe_requested_schema.py +++ b/tests/test_9300_dataframe_requested_schema.py @@ -28,6 +28,7 @@ import datetime +import oracledb import pyarrow import pytest @@ -281,28 +282,34 @@ def test_9310(dtype, value_is_date, conn): @pytest.mark.parametrize( - "dtype,value_is_date", + "dtype", [ - (pyarrow.date32(), True), - (pyarrow.date64(), True), - (pyarrow.timestamp("s"), False), - (pyarrow.timestamp("us"), False), - (pyarrow.timestamp("ms"), False), - (pyarrow.timestamp("ns"), False), + pyarrow.date32(), + pyarrow.date64(), + pyarrow.timestamp("s"), + pyarrow.timestamp("us"), + pyarrow.timestamp("ms"), + pyarrow.timestamp("ns"), ], ) -def test_9311(dtype, value_is_date, conn): +def test_9311(dtype, conn): "9311 - fetch_df_all() for TIMESTAMP" requested_schema = pyarrow.schema([("TIMESTAMP_COL", dtype)]) - value = datetime.datetime(2025, 1, 15) - statement = "select cast(:1 as timestamp) from dual" + value = datetime.datetime(1974, 4, 4, 0, 57, 54, 15079) + var = conn.cursor().var(oracledb.DB_TYPE_TIMESTAMP) + var.setvalue(0, value) + statement = "select :1 from dual" ora_df = conn.fetch_df_all( - statement, [value], requested_schema=requested_schema + statement, [var], requested_schema=requested_schema ) tab = pyarrow.table(ora_df) assert tab.field("TIMESTAMP_COL").type == dtype - if value_is_date: + if not isinstance(dtype, pyarrow.TimestampType): value = value.date() + elif dtype.unit == "s": + value = value.replace(microsecond=0) + elif dtype.unit == "ms": + value = value.replace(microsecond=(value.microsecond // 1000) * 1000) assert tab["TIMESTAMP_COL"][0].as_py() == value diff --git a/tests/test_9400_dataframe_requested_schema_async.py b/tests/test_9400_dataframe_requested_schema_async.py index 1308b15d..aa69cfdf 100644 --- a/tests/test_9400_dataframe_requested_schema_async.py +++ b/tests/test_9400_dataframe_requested_schema_async.py @@ -28,6 +28,7 @@ import datetime +import oracledb import pyarrow import pytest @@ -298,28 +299,34 @@ async def test_9410(dtype, value_is_date, async_conn): @pytest.mark.parametrize( - "dtype,value_is_date", + "dtype", [ - (pyarrow.date32(), True), - (pyarrow.date64(), True), - (pyarrow.timestamp("s"), False), - (pyarrow.timestamp("us"), False), - (pyarrow.timestamp("ms"), False), - (pyarrow.timestamp("ns"), False), + pyarrow.date32(), + pyarrow.date64(), + pyarrow.timestamp("s"), + pyarrow.timestamp("us"), + pyarrow.timestamp("ms"), + pyarrow.timestamp("ns"), ], ) -async def test_9411(dtype, value_is_date, async_conn): +async def test_9411(dtype, async_conn): "9411 - fetch_df_all() for TIMESTAMP" requested_schema = pyarrow.schema([("TIMESTAMP_COL", dtype)]) - value = datetime.datetime(2025, 1, 15) - statement = "select cast(:1 as timestamp) from dual" + value = datetime.datetime(1974, 4, 4, 0, 57, 54, 15079) + var = async_conn.cursor().var(oracledb.DB_TYPE_TIMESTAMP) + var.setvalue(0, value) + statement = "select :1 from dual" ora_df = await async_conn.fetch_df_all( - statement, [value], requested_schema=requested_schema + statement, [var], requested_schema=requested_schema ) tab = pyarrow.table(ora_df) assert tab.field("TIMESTAMP_COL").type == dtype - if value_is_date: + if not isinstance(dtype, pyarrow.TimestampType): value = value.date() + elif dtype.unit == "s": + value = value.replace(microsecond=0) + elif dtype.unit == "ms": + value = value.replace(microsecond=(value.microsecond // 1000) * 1000) assert tab["TIMESTAMP_COL"][0].as_py() == value From c9ecb6ce9e1aebb106b1f86b21937923b96841a8 Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Fri, 7 Nov 2025 16:00:52 -0700 Subject: [PATCH 16/17] Fixed bug that failed to handle the KeyboardInterrupt exception correctly when the application caught this exception and then tried to reuse the connection. --- doc/src/release_notes.rst | 3 +++ src/oracledb/impl/thin/protocol.pyx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index ebbe0794..01eeaaf9 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -27,6 +27,9 @@ Thin Mode Changes #) Fixed bug when using bind variables with scrollable cursors. #) Fixed bug when setting ``SOURCE_ROUTE`` on the ``DESCRIPTION`` section of a full connect descriptor instead of the ``ADDRESS_LIST`` section. +#) Fixed bug that failed to handle the KeyboardInterrupt exception correctly + when the application caught this exception and then tried to reuse the + connection. Thick Mode Changes ++++++++++++++++++ diff --git a/src/oracledb/impl/thin/protocol.pyx b/src/oracledb/impl/thin/protocol.pyx index dfcfcad3..c34bcff6 100644 --- a/src/oracledb/impl/thin/protocol.pyx +++ b/src/oracledb/impl/thin/protocol.pyx @@ -460,7 +460,7 @@ cdef class Protocol(BaseProtocol): except MarkerDetected: self._reset() message.process(self._read_buf) - except Exception as e: + except BaseException as e: if not self._in_connect \ and self._write_buf._packet_sent \ and self._read_buf._transport is not None \ From f21d50544439ab41e191b98d79e1acc077d2621b Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Tue, 11 Nov 2025 17:59:26 -0700 Subject: [PATCH 17/17] Preparing to release python-oracledb 3.4.1. --- doc/src/release_notes.rst | 4 ++-- src/oracledb/impl/thick/odpi | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 01eeaaf9..fb0a4fd6 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -13,8 +13,8 @@ Release changes are listed as affecting Thin Mode (the default runtime behavior of python-oracledb), as affecting the optional :ref:`Thick Mode `, or as being 'Common' for changes that impact both modes. -oracledb `3.4.1 `__ (TBD) --------------------------------------------------------------------------------------------- +oracledb `3.4.1 `__ (November 2025) +------------------------------------------------------------------------------------------------------ Thin Mode Changes +++++++++++++++++ diff --git a/src/oracledb/impl/thick/odpi b/src/oracledb/impl/thick/odpi index a98ef338..27928bc2 160000 --- a/src/oracledb/impl/thick/odpi +++ b/src/oracledb/impl/thick/odpi @@ -1 +1 @@ -Subproject commit a98ef3384310081155e753063ef19c3e9abd9c23 +Subproject commit 27928bc299c7d132008406fa86c0982e1481f4ff