diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 5b776ed..1fadd8c 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -17,6 +17,9 @@ Thin Mode Changes :data:`~oracledb.DB_TYPE_OBJECT`. Note that some of the error codes and messages have changed as a result (DPY errors are raised instead of ones specific to ODPI-C and OCI). +#) Added support for fetching SYS.XMLTYPE data as strings. Note that unlike + in Thick mode, fetching longer values does not require using + ``XMLTYPE.GETCLOBVAL()``. #) Added support for using a wallet for one-way TLS connections, rather than requiring OS recognition of certificates (`issue 65 `__). diff --git a/doc/src/user_guide/appendix_a.rst b/doc/src/user_guide/appendix_a.rst index 7b9578e..299a481 100644 --- a/doc/src/user_guide/appendix_a.rst +++ b/doc/src/user_guide/appendix_a.rst @@ -367,9 +367,9 @@ see :ref:`driverdiff` and :ref:`compatibility`. - Yes - Yes * - XMLType data type (see :ref:`xmldatatype`) - - No - - No - - No + - Yes + - Yes - may need to fetch as CLOB + - Yes - may need to fetch as CLOB * - BFILE data type (see :data:`~oracledb.DB_TYPE_BFILE`) - No - Yes diff --git a/doc/src/user_guide/xml_data_type.rst b/doc/src/user_guide/xml_data_type.rst index 0fc4824..e95b2a4 100644 --- a/doc/src/user_guide/xml_data_type.rst +++ b/doc/src/user_guide/xml_data_type.rst @@ -4,9 +4,9 @@ Using XMLTYPE Data ****************** -Oracle XMLType columns are fetched as strings by default. This is currently -limited to the maximum length of a ``VARCHAR2`` column. To return longer XML -values, they must be queried as LOB values instead. +Oracle XMLType columns are fetched as strings by default in Thin and Thick +mode. Note that in Thick mode you may need to use ``XMLTYPE.GETCLOBVAL()`` as +discussed below. The examples below demonstrate using XMLType data with python-oracledb. The following table will be used in these examples: @@ -18,7 +18,7 @@ following table will be used in these examples: xml_data SYS.XMLTYPE ); -Inserting into the table can be done by simply binding a string as shown: +Inserting into the table can be done by simply binding a string: .. code-block:: python @@ -33,8 +33,8 @@ Inserting into the table can be done by simply binding a string as shown: id=1, xml=xml_data) This approach works with XML strings up to 1 GB in size. For longer strings, a -temporary CLOB must be created using :meth:`Connection.createlob()` and bound -as shown: +temporary CLOB must be created using :meth:`Connection.createlob()` and cast +when bound: .. code-block:: python @@ -43,17 +43,17 @@ as shown: cursor.execute("insert into xml_table values (:id, sys.xmltype(:xml))", id=2, xml=clob) -Fetching XML data can be done simply for values that are shorter than the -length of a VARCHAR2 column as shown: +Fetching XML data can be done directly in Thin mode. This also works in Thick +mode for values that are shorter than the length of a VARCHAR2 column: .. code-block:: python cursor.execute("select xml_data from xml_table where id = :id", id=1) xml_data, = cursor.fetchone() - print(xml_data) # will print the string that was originally stored + print(xml_data) -For values that exceed the length of a VARCHAR2 column, a CLOB must be returned -instead by using the function ``XMLTYPE.GETCLOBVAL()`` as shown: +In Thick mode, for values that exceed the length of a VARCHAR2 column, a CLOB +must be returned by using the function ``XMLTYPE.GETCLOBVAL()``: .. code-block:: python @@ -64,5 +64,5 @@ instead by using the function ``XMLTYPE.GETCLOBVAL()`` as shown: clob, = cursor.fetchone() print(clob.read()) -The LOB that is returned can be streamed or a string can be returned instead of -a CLOB. See :ref:`lobdata` for more information about processing LOBs. +The LOB that is returned can be streamed, as shown. Alternatively a string can +be returned. See :ref:`lobdata` for more information. diff --git a/src/oracledb/impl/thin/buffer.pyx b/src/oracledb/impl/thin/buffer.pyx index 68cd2bb..7801976 100644 --- a/src/oracledb/impl/thin/buffer.pyx +++ b/src/oracledb/impl/thin/buffer.pyx @@ -791,6 +791,31 @@ cdef class Buffer: cdef const char_type *ptr = self._get_raw(4) value[0] = unpack_uint32(ptr, byte_order) + cdef object read_xmltype(self): + """ + Reads an XMLType value from the buffer and returns the string value. + The XMLType object is a special DbObjectType and is handled separately + since the structure is a bit different. + """ + cdef: + uint32_t num_bytes + bytes packed_data + self.read_ub4(&num_bytes) + if num_bytes > 0: # type OID + self.read_bytes() + self.read_ub4(&num_bytes) + if num_bytes > 0: # OID + self.read_bytes() + self.read_ub4(&num_bytes) + if num_bytes > 0: # snapshot + self.read_bytes() + self.skip_ub2() # version + self.read_ub4(&num_bytes) # length of data + self.skip_ub2() # flags + if num_bytes > 0: + packed_data = self.read_bytes() + return packed_data[12:].decode() + cdef int skip_raw_bytes(self, ssize_t num_bytes) except -1: """ Skip the specified number of bytes in the buffer. In order to avoid diff --git a/src/oracledb/impl/thin/dbobject.pyx b/src/oracledb/impl/thin/dbobject.pyx index 7229375..aadee13 100644 --- a/src/oracledb/impl/thin/dbobject.pyx +++ b/src/oracledb/impl/thin/dbobject.pyx @@ -539,6 +539,7 @@ cdef class ThinDbObjectTypeImpl(BaseDbObjectTypeImpl): cdef: uint8_t collection_type, collection_flags, version uint32_t max_num_elements + bint is_xml_type bytes oid def create_new_object(self): @@ -835,6 +836,8 @@ cdef class ThinDbObjectTypeCache: typ_impl.schema = self.schema_var.getvalue() typ_impl.package_name = self.package_name_var.getvalue() typ_impl.name = self.name_var.getvalue() + typ_impl.is_xml_type = \ + (typ_impl.schema == "SYS" and typ_impl.name == "XMLTYPE") self._parse_tds(typ_impl, self.tds_var.getvalue()) typ_impl.attrs = [] typ_impl.attrs_by_name = {} @@ -901,6 +904,7 @@ cdef class ThinDbObjectTypeCache: typ_impl.schema = schema typ_impl.package_name = package_name typ_impl.name = name + typ_impl.is_xml_type = (schema == "SYS" and name == "XMLTYPE") if oid is not None: self.types_by_oid[oid] = typ_impl self.types_by_name[full_name] = typ_impl diff --git a/src/oracledb/impl/thin/messages.pyx b/src/oracledb/impl/thin/messages.pyx index 0e74576..970446e 100644 --- a/src/oracledb/impl/thin/messages.pyx +++ b/src/oracledb/impl/thin/messages.pyx @@ -520,6 +520,7 @@ cdef class MessageWithData(Message): ThinVarImpl var_impl, uint32_t pos): cdef: uint8_t num_bytes, ora_type_num, csfrm + ThinDbObjectTypeImpl typ_impl ThinCursorImpl cursor_impl object column_value = None ThinDbObjectImpl obj_impl @@ -597,14 +598,18 @@ cdef class MessageWithData(Message): column_value = buf.read_lob_with_length(self.conn_impl, var_impl.dbtype) elif ora_type_num == TNS_DATA_TYPE_INT_NAMED: - obj_impl = buf.read_dbobject(var_impl.objtype) - if obj_impl is not None: - if not self.in_fetch: - column_value = var_impl._values[pos] - if column_value is not None: - column_value._impl = obj_impl - else: - column_value = PY_TYPE_DB_OBJECT._from_impl(obj_impl) + typ_impl = var_impl.objtype + if typ_impl.is_xml_type: + column_value = buf.read_xmltype() + else: + obj_impl = buf.read_dbobject(typ_impl) + if obj_impl is not None: + if not self.in_fetch: + column_value = var_impl._values[pos] + if column_value is not None: + column_value._impl = obj_impl + else: + column_value = PY_TYPE_DB_OBJECT._from_impl(obj_impl) else: errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED, name=var_impl.dbtype.name) diff --git a/tests/test_2500_string_var.py b/tests/test_2500_string_var.py index be4e790..59b1ce5 100644 --- a/tests/test_2500_string_var.py +++ b/tests/test_2500_string_var.py @@ -399,10 +399,8 @@ class TestCase(test_env.BaseTestCase): self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3004:", var.setvalue, 0, "ABDHRYTHFJGKDKKDH") - @unittest.skipIf(test_env.get_is_thin(), - "thin mode doesn't support XML type objects yet") def test_2530_short_xml_as_string(self): - "2530 - test fetching XMLType object as a string" + "2530 - test fetching XMLType (< 1K) as a string" self.cursor.execute(""" select XMLElement("string", stringCol) from TestStrings @@ -411,13 +409,11 @@ class TestCase(test_env.BaseTestCase): expected_value = "String 1" self.assertEqual(actual_value, expected_value) - @unittest.skipIf(test_env.get_is_thin(), - "thin mode doesn't support XML type objects yet") def test_2531_long_xml_as_string(self): - "2531 - test inserting and fetching an XMLType object (1K) as a string" + "2531 - test inserting and fetching XMLType (1K) as a string" chars = string.ascii_uppercase + string.ascii_lowercase random_string = ''.join(random.choice(chars) for _ in range(1024)) - int_val = 200 + int_val = 2531 xml_string = '' + random_string + '' self.cursor.execute("truncate table TestTempXML") self.cursor.execute(""" @@ -458,5 +454,24 @@ class TestCase(test_env.BaseTestCase): cursor.execute("select IntCol, StringCol1 from TestTempTable") self.assertEqual(cursor.fetchone(), (1, string_val)) + @unittest.skipIf(not test_env.get_is_thin(), + "thick mode doesn't support fetching XMLType > VARCHAR2") + def test_2534_very_long_xml_as_string(self): + "2534 - test inserting and fetching XMLType (32K) as a string" + chars = string.ascii_uppercase + string.ascii_lowercase + random_string = ''.join(random.choice(chars) for _ in range(32768)) + int_val = 2534 + xml_string = f"{random_string}" + lob = self.connection.createlob(oracledb.DB_TYPE_CLOB) + lob.write(xml_string) + self.cursor.execute("truncate table TestTempXML") + self.cursor.execute(""" + insert into TestTempXML (IntCol, XMLCol) + values (:1, sys.xmltype(:2))""", (int_val, lob)) + self.cursor.execute("select XMLCol from TestTempXML where intCol = :1", + (int_val,)) + actual_value, = self.cursor.fetchone() + self.assertEqual(actual_value.strip(), xml_string) + if __name__ == "__main__": test_env.run_test_cases() diff --git a/tests/test_4300_cursor_other.py b/tests/test_4300_cursor_other.py index cf78ced..c3628fd 100644 --- a/tests/test_4300_cursor_other.py +++ b/tests/test_4300_cursor_other.py @@ -226,17 +226,15 @@ class TestCase(test_env.BaseTestCase): exp = "udt_Object(28, 'Bind obj out', null, null, null, null, null)" self.assertEqual(result, exp) - @unittest.skipIf(test_env.get_is_thin(), - "thin mode doesn't support XML type objects yet") def test_4320_fetch_xmltype(self): "4320 - test that fetching an XMLType returns a string" int_val = 5 label = "IntCol" - expected_result = "<%s>%s" % (label, int_val, label) - self.cursor.execute(""" - select XMLElement("%s", IntCol) + expected_result = f"<{label}>{int_val}" + self.cursor.execute(f""" + select XMLElement("{label}", IntCol) from TestStrings - where IntCol = :int_val""" % label, + where IntCol = :int_val""", int_val=int_val) result, = self.cursor.fetchone() self.assertEqual(result, expected_result)