Added support for transforming SYS.XMLTYPE into strings as is done in

thick mode.
This commit is contained in:
Anthony Tuininga 2022-11-07 10:27:37 -07:00
parent 12c1da25f3
commit 30e554b922
8 changed files with 87 additions and 37 deletions

View File

@ -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 <https://github.com/oracle/python-oracledb/issues/65>`__).

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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,7 +598,11 @@ 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)
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]

View File

@ -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>String 1</string>"
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 = '<data>' + random_string + '</data>'
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"<data>{random_string}</data>"
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()

View File

@ -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</%s>" % (label, int_val, label)
self.cursor.execute("""
select XMLElement("%s", IntCol)
expected_result = f"<{label}>{int_val}</{label}>"
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)