diff --git a/doc/src/api_manual/connection.rst b/doc/src/api_manual/connection.rst index 323f1eda..76482f4f 100644 --- a/doc/src/api_manual/connection.rst +++ b/doc/src/api_manual/connection.rst @@ -760,7 +760,11 @@ Connection Attributes in bytes supported by the database to which the connection has been established. See `Database Object Naming Rules `__. + id=GUID-75337742-67FD-4EC0-985F-741C93D918DA>`__. The value may be + ``None``, 30, or 128. The value ``None`` indicates the size cannot be + reliably determined by python-oracledb, which occurs when using Thick mode + with Oracle Client libraries 12.1 (or older) to connect to Oracle Database + 12.2, or later. .. versionadded:: 2.5.0 diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 8d4392da..f880f6e2 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -11,6 +11,47 @@ 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 2.5.1 (December 2024) +------------------------------ + +Thin Mode Changes ++++++++++++++++++ + +#) Fixed bug when table recreation changes the data type of a column from + :data:`oracledb.DB_TYPE_LONG` or :data:`oracledb.DB_TYPE_LONG_RAW` to a + different compatible type + (`issue 424 `__). +#) If the database states that an out-of-band break check should not take + place during connect (by setting the `DISABLE_OOB_AUTO + `__ parameter to TRUE), + python-oracledb no longer attempts to do so + (`issue 419 `__). +#) All exceptions subclassed from ``OSError`` now cause connection retry + attempts, subject to the connection ``retry_count`` and ``retry_delay`` + parameters + (`issue 420 `__). + +Thick Mode Changes +++++++++++++++++++ + +#) Fixed bug calculating property :data:`Connection.max_identifier_length` + when using Oracle Client libraries 12.1, or older. The returned value may + now be ``None`` when the size cannot be reliably determined by + python-oracledb, which occurs when using Oracle Client libraries 12.1 (or + older) to connect to Oracle Database 12.2, or later. + (`ODPI-C `__ dependency update). +#) Fixed bug resulting in a segfault when using external authentication + (`issue 425 `__). + +Common Changes +++++++++++++++ + +#) Fixed bug when fetching empty data from CLOB or BLOB columns marked with + the ``IS JSON`` constraint + (`issue 429 `__). + + oracledb 2.5.0 (November 2024) ------------------------------ diff --git a/src/oracledb/impl/base/cursor.pyx b/src/oracledb/impl/base/cursor.pyx index 409d960d..41845fd8 100644 --- a/src/oracledb/impl/base/cursor.pyx +++ b/src/oracledb/impl/base/cursor.pyx @@ -136,7 +136,8 @@ cdef class BaseCursorImpl: value = value.read() if isinstance(value, bytes): value = value.decode() - return json.loads(value) + if value: + return json.loads(value) return converter cdef int _check_binds(self, uint32_t num_execs) except -1: diff --git a/src/oracledb/impl/thick/connection.pyx b/src/oracledb/impl/thick/connection.pyx index 27eab106..2adc5ca3 100644 --- a/src/oracledb/impl/thick/connection.pyx +++ b/src/oracledb/impl/thick/connection.pyx @@ -666,7 +666,8 @@ cdef class ThickConnImpl(BaseConnImpl): cdef dpiConnInfo info if dpiConn_getInfo(self._handle, &info) < 0: _raise_from_odpi() - return info.maxIdentifierLength + if info.maxIdentifierLength != 0: + return info.maxIdentifierLength def get_max_open_cursors(self): cdef uint32_t value diff --git a/src/oracledb/impl/thick/odpi b/src/oracledb/impl/thick/odpi index c9be4cce..56f155ed 160000 --- a/src/oracledb/impl/thick/odpi +++ b/src/oracledb/impl/thick/odpi @@ -1 +1 @@ -Subproject commit c9be4cceeb004435409379851e13eb7fc50a5c6c +Subproject commit 56f155ed070c0b6ed44942aea12fab7ef9d07dc3 diff --git a/src/oracledb/impl/thin/capabilities.pyx b/src/oracledb/impl/thin/capabilities.pyx index 882d702c..70321239 100644 --- a/src/oracledb/impl/thin/capabilities.pyx +++ b/src/oracledb/impl/thin/capabilities.pyx @@ -41,6 +41,7 @@ cdef class Capabilities: uint32_t max_string_size bint supports_fast_auth bint supports_oob + bint supports_oob_check bint supports_end_of_response bint supports_pipelining uint32_t sdu @@ -60,6 +61,8 @@ cdef class Capabilities: self.supports_oob = protocol_options & TNS_GSO_CAN_RECV_ATTENTION if flags & TNS_ACCEPT_FLAG_FAST_AUTH: self.supports_fast_auth = True + if flags & TNS_ACCEPT_FLAG_CHECK_OOB: + self.supports_oob_check = True if protocol_version >= TNS_VERSION_MIN_END_OF_RESPONSE: if flags & TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE: self.compile_caps[TNS_CCAP_TTC4] |= TNS_CCAP_END_OF_RESPONSE diff --git a/src/oracledb/impl/thin/connection.pyx b/src/oracledb/impl/thin/connection.pyx index d6d58aea..e1627798 100644 --- a/src/oracledb/impl/thin/connection.pyx +++ b/src/oracledb/impl/thin/connection.pyx @@ -331,8 +331,7 @@ cdef class ThinConnImpl(BaseThinConnImpl): try: protocol._connect_phase_one(self, params, description, address, connect_string) - except (exceptions.DatabaseError, socket.gaierror, - ConnectionRefusedError) as e: + except (exceptions.DatabaseError, socket.gaierror, OSError) as e: if raise_exception: errors._raise_err(errors.ERR_CONNECTION_FAILED, cause=e, connection_id=description.connection_id) diff --git a/src/oracledb/impl/thin/constants.pxi b/src/oracledb/impl/thin/constants.pxi index ede6b48f..845b91c9 100644 --- a/src/oracledb/impl/thin/constants.pxi +++ b/src/oracledb/impl/thin/constants.pxi @@ -762,6 +762,7 @@ cdef enum: # accept flags cdef enum: + TNS_ACCEPT_FLAG_CHECK_OOB = 0x00000001 TNS_ACCEPT_FLAG_FAST_AUTH = 0x10000000 TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE = 0x02000000 diff --git a/src/oracledb/impl/thin/cursor.pyx b/src/oracledb/impl/thin/cursor.pyx index 33bc330f..6f9e57c9 100644 --- a/src/oracledb/impl/thin/cursor.pyx +++ b/src/oracledb/impl/thin/cursor.pyx @@ -256,7 +256,8 @@ cdef class AsyncThinCursorImpl(BaseThinCursorImpl): value = await value.read() if isinstance(value, bytes): value = value.decode() - return json.loads(value) + if value: + return json.loads(value) return converter async def _fetch_rows_async(self, object cursor): diff --git a/src/oracledb/impl/thin/messages.pyx b/src/oracledb/impl/thin/messages.pyx index 1cc9042a..812035f7 100644 --- a/src/oracledb/impl/thin/messages.pyx +++ b/src/oracledb/impl/thin/messages.pyx @@ -375,21 +375,31 @@ cdef class MessageWithData(Message): """ cdef: FetchInfoImpl prev_fetch_info = prev_var_impl._fetch_info - uint8_t csfrm = prev_var_impl.dbtype._csfrm - uint8_t type_num - if fetch_info.dbtype._ora_type_num == TNS_DATA_TYPE_CLOB \ + uint8_t type_num, csfrm + if fetch_info.dbtype._ora_type_num == TNS_DATA_TYPE_VARCHAR \ + and prev_fetch_info.dbtype._ora_type_num == TNS_DATA_TYPE_LONG: + type_num = TNS_DATA_TYPE_LONG + csfrm = fetch_info.dbtype._csfrm + fetch_info.dbtype = DbType._from_ora_type_and_csfrm(type_num, csfrm) + + elif fetch_info.dbtype._ora_type_num == TNS_DATA_TYPE_RAW \ + and prev_fetch_info.dbtype._ora_type_num == \ + TNS_DATA_TYPE_LONG_RAW: + type_num = TNS_DATA_TYPE_LONG_RAW + fetch_info.dbtype = DbType._from_ora_type_and_csfrm(type_num, 0) + elif fetch_info.dbtype._ora_type_num == TNS_DATA_TYPE_CLOB \ and prev_fetch_info.dbtype._ora_type_num in \ (TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_VARCHAR, TNS_DATA_TYPE_LONG): type_num = TNS_DATA_TYPE_LONG + csfrm = prev_var_impl.dbtype._csfrm fetch_info.dbtype = DbType._from_ora_type_and_csfrm(type_num, csfrm) elif fetch_info.dbtype._ora_type_num == TNS_DATA_TYPE_BLOB \ and prev_fetch_info.dbtype._ora_type_num in \ (TNS_DATA_TYPE_RAW, TNS_DATA_TYPE_LONG_RAW): type_num = TNS_DATA_TYPE_LONG_RAW - fetch_info.dbtype = DbType._from_ora_type_and_csfrm(type_num, - csfrm) + fetch_info.dbtype = DbType._from_ora_type_and_csfrm(type_num, 0) cdef object _create_cursor_from_describe(self, ReadBuffer buf, object cursor=None): diff --git a/src/oracledb/impl/thin/protocol.pyx b/src/oracledb/impl/thin/protocol.pyx index e28de23c..0b73086c 100644 --- a/src/oracledb/impl/thin/protocol.pyx +++ b/src/oracledb/impl/thin/protocol.pyx @@ -291,8 +291,7 @@ cdef class Protocol(BaseProtocol): # if we can use OOB, send an urgent message now followed by a reset # marker to see if the server understands it - if self._caps.supports_oob \ - and self._caps.protocol_version >= TNS_VERSION_MIN_OOB_CHECK: + if self._caps.supports_oob and self._caps.supports_oob_check: self._transport.send_oob_break() self._send_marker(self._write_buf, TNS_MARKER_TYPE_RESET) diff --git a/src/oracledb/version.py b/src/oracledb/version.py index 8ae6bf18..26b5ad36 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__ = "2.5.0" +__version__ = "2.5.1" diff --git a/tests/sql/create_schema.sql b/tests/sql/create_schema.sql index cd7f21a7..14074d5b 100644 --- a/tests/sql/create_schema.sql +++ b/tests/sql/create_schema.sql @@ -613,6 +613,10 @@ insert into &main_user..TestJsonCols values (1, '[1, 2, 3]', '[4, 5, 6]', utl_raw.cast_to_raw('[7, 8, 9]')) / +insert into &main_user..TestJsonCols values (2, + 'null', empty_clob(), empty_blob()) +/ + commit / diff --git a/tests/test_4300_cursor_other.py b/tests/test_4300_cursor_other.py index 816ad0d0..ae88f793 100644 --- a/tests/test_4300_cursor_other.py +++ b/tests/test_4300_cursor_other.py @@ -887,9 +887,12 @@ def test_4359(self): ) def test_4360(self): "4360 - fetch JSON columns as Python objects" - expected_data = (1, [1, 2, 3], [4, 5, 6], [7, 8, 9]) - self.cursor.execute("select * from TestJsonCols") - self.assertEqual(self.cursor.fetchone(), expected_data) + expected_data = [ + (1, [1, 2, 3], [4, 5, 6], [7, 8, 9]), + (2, None, None, None), + ] + self.cursor.execute("select * from TestJsonCols order by IntCol") + self.assertEqual(self.cursor.fetchall(), expected_data) @unittest.skipIf( test_env.get_server_version() < (23, 1), "unsupported database" diff --git a/tests/test_4600_type_changes.py b/tests/test_4600_type_changes.py index 05479516..a11882c1 100644 --- a/tests/test_4600_type_changes.py +++ b/tests/test_4600_type_changes.py @@ -29,6 +29,7 @@ import datetime import unittest +import oracledb import test_env @@ -40,27 +41,33 @@ def __test_type_change( query_frag_2, query_value_2, table_name="dual", + type_handler=None, ): if test_env.get_is_implicit_pooling(): self.skipTest("sessions can change with implicit pooling") - self.cursor.execute( - f""" - create or replace view TestTypesChanged as - select {query_frag_1} as value - from {table_name} - """ - ) - self.cursor.execute("select * from TestTypesChanged") - self.assertEqual(self.cursor.fetchall(), [(query_value_1,)]) - self.cursor.execute( - f""" - create or replace view TestTypesChanged as - select {query_frag_2} as value - from dual - """ - ) - self.cursor.execute("select * from TestTypesChanged") - self.assertEqual(self.cursor.fetchall(), [(query_value_2,)]) + orig_type_handler = self.conn.outputtypehandler + self.conn.outputtypehandler = type_handler + try: + self.cursor.execute( + f""" + create or replace view TestTypesChanged as + select {query_frag_1} as value + from {table_name} + """ + ) + self.cursor.execute("select * from TestTypesChanged") + self.assertEqual(self.cursor.fetchall(), [(query_value_1,)]) + self.cursor.execute( + f""" + create or replace view TestTypesChanged as + select {query_frag_2} as value + from dual + """ + ) + self.cursor.execute("select * from TestTypesChanged") + self.assertEqual(self.cursor.fetchall(), [(query_value_2,)]) + finally: + self.conn.outputtypehandler = orig_type_handler @unittest.skipIf( not test_env.get_is_thin(), @@ -273,6 +280,111 @@ def test_4616(self): datetime.datetime(2022, 1, 5, 0, 0), ) + @unittest.skipIf( + not test_env.get_is_thin(), + "thick mode doesn't support this type change", + ) + def test_4617(self): + "4617 - test data type changing from CLOB to VARCHAR" + + def type_handler(cursor, metadata): + if metadata.type_code is oracledb.DB_TYPE_CLOB: + return cursor.var( + oracledb.DB_TYPE_VARCHAR, + size=32768, + arraysize=cursor.arraysize, + ) + + self.__test_type_change( + "to_clob('clob_4617')", + "clob_4617", + "cast('string_4617' as VARCHAR2(15))", + "string_4617", + type_handler=type_handler, + ) + + @unittest.skipIf( + not test_env.get_is_thin(), + "thick mode doesn't support this type change", + ) + def test_4618(self): + "4618 - test data type changing from NCLOB to NVARCHAR" + + def type_handler(cursor, metadata): + if metadata.type_code is oracledb.DB_TYPE_NCLOB: + return cursor.var( + oracledb.DB_TYPE_NVARCHAR, + size=32768, + arraysize=cursor.arraysize, + ) + + self.__test_type_change( + "to_nclob('nclob_4618')", + "nclob_4618", + "cast('nstring_4618' as NVARCHAR2(15))", + "nstring_4618", + type_handler=type_handler, + ) + + @unittest.skipIf( + not test_env.get_is_thin(), + "thick mode doesn't support this type change", + ) + def test_4619(self): + "4619 - test data type changing from CLOB to NVARCHAR" + + def type_handler(cursor, metadata): + if metadata.type_code is oracledb.DB_TYPE_CLOB: + return cursor.var( + oracledb.DB_TYPE_NVARCHAR, + size=32768, + arraysize=cursor.arraysize, + ) + + self.__test_type_change( + "to_clob('clob_4619')", + "clob_4619", + "cast('string_4619' as VARCHAR2(15))", + "string_4619", + type_handler=type_handler, + ) + + @unittest.skipIf( + not test_env.get_is_thin(), + "thick mode doesn't support this type change", + ) + def test_4620(self): + "4620 - test data type changing from BLOB to RAW" + + def type_handler(cursor, metadata): + if metadata.type_code is oracledb.DB_TYPE_BLOB: + return cursor.var( + oracledb.DB_TYPE_RAW, + size=32768, + arraysize=cursor.arraysize, + ) + + self.__test_type_change( + "to_blob(utl_raw.cast_to_raw('blob_4620'))", + b"blob_4620", + "utl_raw.cast_to_raw('string_4620')", + b"string_4620", + type_handler=type_handler, + ) + + @unittest.skipIf( + not test_env.get_is_thin(), + "thick mode doesn't support this type change", + ) + def test_4621(self): + "4621 - test data type changing from NVARCHAR to CLOB" + self.__test_type_change( + "cast('string_4621' as NVARCHAR2(15))", + "string_4621", + "to_clob('clob_4621')", + "clob_4621", + ) + if __name__ == "__main__": test_env.run_test_cases() diff --git a/tests/test_6300_cursor_other_async.py b/tests/test_6300_cursor_other_async.py index 1901ec21..16bdc5e5 100644 --- a/tests/test_6300_cursor_other_async.py +++ b/tests/test_6300_cursor_other_async.py @@ -863,6 +863,15 @@ async def test_6350(self): (fetched_value,) = await self.cursor.fetchone() self.assertEqual(fetched_value, value) + async def test_6351(self): + "4360 - fetch JSON columns as Python objects" + expected_data = [ + (1, [1, 2, 3], [4, 5, 6], [7, 8, 9]), + (2, None, None, None), + ] + await self.cursor.execute("select * from TestJsonCols order by IntCol") + self.assertEqual(await self.cursor.fetchall(), expected_data) + if __name__ == "__main__": test_env.run_test_cases()