diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 86646806..fb0a4fd6 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -13,6 +13,49 @@ 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 `__ (November 2025) +------------------------------------------------------------------------------------------------------ + +Thin Mode Changes ++++++++++++++++++ + +#) Fixed bug when fetching a timestamp with nanosecond precision into a data + frame + (`issue 538 `__). +#) 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. +#) 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 +++++++++++++++++++ + +#) Fixed segfault on some platforms when trying to execute queries returning + vector columns + (`ODPI-C `__ dependency update). + +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. + + oracledb `3.4.0 `__ (October 2025) ----------------------------------------------------------------------------------------------------- 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") 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..44d406f7 100644 --- a/src/oracledb/errors.py +++ b/src/oracledb/errors.py @@ -291,6 +291,8 @@ 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 +ERR_WRONG_REQUESTED_SCHEMA_LENGTH = 2069 # error numbers that result in NotSupportedError ERR_TIME_NOT_SUPPORTED = 3000 @@ -894,6 +896,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" ), @@ -1003,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/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/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/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/src/oracledb/impl/thick/odpi b/src/oracledb/impl/thick/odpi index a6ac6133..27928bc2 160000 --- a/src/oracledb/impl/thick/odpi +++ b/src/oracledb/impl/thick/odpi @@ -1 +1 @@ -Subproject commit a6ac6133646856ea1563bd09aa57a0aeac9b40c8 +Subproject commit 27928bc299c7d132008406fa86c0982e1481f4ff diff --git a/src/oracledb/impl/thin/connection.pyx b/src/oracledb/impl/thin/connection.pyx index 6a820a52..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 @@ -721,6 +711,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 +723,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 +975,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 +1208,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/src/oracledb/impl/thin/constants.pxi b/src/oracledb/impl/thin/constants.pxi index 89aa0b47..252e7d6e 100644 --- a/src/oracledb/impl/thin/constants.pxi +++ b/src/oracledb/impl/thin/constants.pxi @@ -240,6 +240,7 @@ cdef enum: cdef enum: TNS_EXEC_FLAGS_DML_ROWCOUNTS = 0x4000 TNS_EXEC_FLAGS_IMPLICIT_RESULTSET = 0x8000 + TNS_EXEC_FLAGS_NO_CANCEL_ON_EOF = 0x80 TNS_EXEC_FLAGS_SCROLLABLE = 0x02 # fetch orientations diff --git a/src/oracledb/impl/thin/cursor.pyx b/src/oracledb/impl/thin/cursor.pyx index 3310a514..525ff47b 100644 --- a/src/oracledb/impl/thin/cursor.pyx +++ b/src/oracledb/impl/thin/cursor.pyx @@ -72,7 +72,7 @@ cdef class BaseThinCursorImpl(BaseCursorImpl): message.num_execs = 1 if self.scrollable: message.fetch_orientation = TNS_FETCH_ORIENTATION_CURRENT - message.fetch_pos = 1 + message.fetch_pos = self.rowcount + 1 return message cdef ExecuteMessage _create_scroll_message(self, object cursor, @@ -116,7 +116,7 @@ cdef class BaseThinCursorImpl(BaseCursorImpl): # build message message = self._create_message(ExecuteMessage, cursor) - message.scroll_operation = self._more_rows_to_fetch + message.scroll_operation = True message.fetch_orientation = orientation message.fetch_pos = 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 cda1183a..c5381c21 100644 --- a/src/oracledb/impl/thin/messages/execute.pyx +++ b/src/oracledb/impl/thin/messages/execute.pyx @@ -81,8 +81,9 @@ 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 + 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/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 \ 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" diff --git a/tests/conftest.py b/tests/conftest.py index 40014370..b5e784ca 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() @@ -664,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): """ 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_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() diff --git a/tests/test_4200_cursor_scrollable.py b/tests/test_4200_cursor_scrollable.py index 7a341731..dc34c50c 100644 --- a/tests/test_4200_cursor_scrollable.py +++ b/tests/test_4200_cursor_scrollable.py @@ -223,3 +223,53 @@ 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 + + +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 + + +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_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)))" 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 diff --git a/tests/test_8600_cursor_scrollable_async.py b/tests/test_8600_cursor_scrollable_async.py index 636d72c9..5bd47140 100644 --- a/tests/test_8600_cursor_scrollable_async.py +++ b/tests/test_8600_cursor_scrollable_async.py @@ -226,3 +226,53 @@ 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 + + +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 + + +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") diff --git a/tests/test_9300_dataframe_requested_schema.py b/tests/test_9300_dataframe_requested_schema.py index c851ea11..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 @@ -755,3 +762,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..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 @@ -778,3 +785,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 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