Skip to content

Commit 30e554b

Browse files
Added support for transforming SYS.XMLTYPE into strings as is done in
thick mode.
1 parent 12c1da2 commit 30e554b

File tree

8 files changed

+87
-37
lines changed

8 files changed

+87
-37
lines changed

doc/src/release_notes.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Thin Mode Changes
1717
:data:`~oracledb.DB_TYPE_OBJECT`. Note that some of the error codes and
1818
messages have changed as a result (DPY errors are raised instead of ones
1919
specific to ODPI-C and OCI).
20+
#) Added support for fetching SYS.XMLTYPE data as strings. Note that unlike
21+
in Thick mode, fetching longer values does not require using
22+
``XMLTYPE.GETCLOBVAL()``.
2023
#) Added support for using a wallet for one-way TLS connections, rather than
2124
requiring OS recognition of certificates
2225
(`issue 65 <https://github.com/oracle/python-oracledb/issues/65>`__).

doc/src/user_guide/appendix_a.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,9 @@ see :ref:`driverdiff` and :ref:`compatibility`.
367367
- Yes
368368
- Yes
369369
* - XMLType data type (see :ref:`xmldatatype`)
370-
- No
371-
- No
372-
- No
370+
- Yes
371+
- Yes - may need to fetch as CLOB
372+
- Yes - may need to fetch as CLOB
373373
* - BFILE data type (see :data:`~oracledb.DB_TYPE_BFILE`)
374374
- No
375375
- Yes

doc/src/user_guide/xml_data_type.rst

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
Using XMLTYPE Data
55
******************
66

7-
Oracle XMLType columns are fetched as strings by default. This is currently
8-
limited to the maximum length of a ``VARCHAR2`` column. To return longer XML
9-
values, they must be queried as LOB values instead.
7+
Oracle XMLType columns are fetched as strings by default in Thin and Thick
8+
mode. Note that in Thick mode you may need to use ``XMLTYPE.GETCLOBVAL()`` as
9+
discussed below.
1010

1111
The examples below demonstrate using XMLType data with python-oracledb. The
1212
following table will be used in these examples:
@@ -18,7 +18,7 @@ following table will be used in these examples:
1818
xml_data SYS.XMLTYPE
1919
);
2020
21-
Inserting into the table can be done by simply binding a string as shown:
21+
Inserting into the table can be done by simply binding a string:
2222

2323
.. code-block:: python
2424
@@ -33,8 +33,8 @@ Inserting into the table can be done by simply binding a string as shown:
3333
id=1, xml=xml_data)
3434
3535
This approach works with XML strings up to 1 GB in size. For longer strings, a
36-
temporary CLOB must be created using :meth:`Connection.createlob()` and bound
37-
as shown:
36+
temporary CLOB must be created using :meth:`Connection.createlob()` and cast
37+
when bound:
3838

3939
.. code-block:: python
4040
@@ -43,17 +43,17 @@ as shown:
4343
cursor.execute("insert into xml_table values (:id, sys.xmltype(:xml))",
4444
id=2, xml=clob)
4545
46-
Fetching XML data can be done simply for values that are shorter than the
47-
length of a VARCHAR2 column as shown:
46+
Fetching XML data can be done directly in Thin mode. This also works in Thick
47+
mode for values that are shorter than the length of a VARCHAR2 column:
4848

4949
.. code-block:: python
5050
5151
cursor.execute("select xml_data from xml_table where id = :id", id=1)
5252
xml_data, = cursor.fetchone()
53-
print(xml_data) # will print the string that was originally stored
53+
print(xml_data)
5454
55-
For values that exceed the length of a VARCHAR2 column, a CLOB must be returned
56-
instead by using the function ``XMLTYPE.GETCLOBVAL()`` as shown:
55+
In Thick mode, for values that exceed the length of a VARCHAR2 column, a CLOB
56+
must be returned by using the function ``XMLTYPE.GETCLOBVAL()``:
5757

5858
.. code-block:: python
5959
@@ -64,5 +64,5 @@ instead by using the function ``XMLTYPE.GETCLOBVAL()`` as shown:
6464
clob, = cursor.fetchone()
6565
print(clob.read())
6666
67-
The LOB that is returned can be streamed or a string can be returned instead of
68-
a CLOB. See :ref:`lobdata` for more information about processing LOBs.
67+
The LOB that is returned can be streamed, as shown. Alternatively a string can
68+
be returned. See :ref:`lobdata` for more information.

src/oracledb/impl/thin/buffer.pyx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,31 @@ cdef class Buffer:
791791
cdef const char_type *ptr = self._get_raw(4)
792792
value[0] = unpack_uint32(ptr, byte_order)
793793

794+
cdef object read_xmltype(self):
795+
"""
796+
Reads an XMLType value from the buffer and returns the string value.
797+
The XMLType object is a special DbObjectType and is handled separately
798+
since the structure is a bit different.
799+
"""
800+
cdef:
801+
uint32_t num_bytes
802+
bytes packed_data
803+
self.read_ub4(&num_bytes)
804+
if num_bytes > 0: # type OID
805+
self.read_bytes()
806+
self.read_ub4(&num_bytes)
807+
if num_bytes > 0: # OID
808+
self.read_bytes()
809+
self.read_ub4(&num_bytes)
810+
if num_bytes > 0: # snapshot
811+
self.read_bytes()
812+
self.skip_ub2() # version
813+
self.read_ub4(&num_bytes) # length of data
814+
self.skip_ub2() # flags
815+
if num_bytes > 0:
816+
packed_data = self.read_bytes()
817+
return packed_data[12:].decode()
818+
794819
cdef int skip_raw_bytes(self, ssize_t num_bytes) except -1:
795820
"""
796821
Skip the specified number of bytes in the buffer. In order to avoid

src/oracledb/impl/thin/dbobject.pyx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ cdef class ThinDbObjectTypeImpl(BaseDbObjectTypeImpl):
539539
cdef:
540540
uint8_t collection_type, collection_flags, version
541541
uint32_t max_num_elements
542+
bint is_xml_type
542543
bytes oid
543544

544545
def create_new_object(self):
@@ -835,6 +836,8 @@ cdef class ThinDbObjectTypeCache:
835836
typ_impl.schema = self.schema_var.getvalue()
836837
typ_impl.package_name = self.package_name_var.getvalue()
837838
typ_impl.name = self.name_var.getvalue()
839+
typ_impl.is_xml_type = \
840+
(typ_impl.schema == "SYS" and typ_impl.name == "XMLTYPE")
838841
self._parse_tds(typ_impl, self.tds_var.getvalue())
839842
typ_impl.attrs = []
840843
typ_impl.attrs_by_name = {}
@@ -901,6 +904,7 @@ cdef class ThinDbObjectTypeCache:
901904
typ_impl.schema = schema
902905
typ_impl.package_name = package_name
903906
typ_impl.name = name
907+
typ_impl.is_xml_type = (schema == "SYS" and name == "XMLTYPE")
904908
if oid is not None:
905909
self.types_by_oid[oid] = typ_impl
906910
self.types_by_name[full_name] = typ_impl

src/oracledb/impl/thin/messages.pyx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ cdef class MessageWithData(Message):
520520
ThinVarImpl var_impl, uint32_t pos):
521521
cdef:
522522
uint8_t num_bytes, ora_type_num, csfrm
523+
ThinDbObjectTypeImpl typ_impl
523524
ThinCursorImpl cursor_impl
524525
object column_value = None
525526
ThinDbObjectImpl obj_impl
@@ -597,14 +598,18 @@ cdef class MessageWithData(Message):
597598
column_value = buf.read_lob_with_length(self.conn_impl,
598599
var_impl.dbtype)
599600
elif ora_type_num == TNS_DATA_TYPE_INT_NAMED:
600-
obj_impl = buf.read_dbobject(var_impl.objtype)
601-
if obj_impl is not None:
602-
if not self.in_fetch:
603-
column_value = var_impl._values[pos]
604-
if column_value is not None:
605-
column_value._impl = obj_impl
606-
else:
607-
column_value = PY_TYPE_DB_OBJECT._from_impl(obj_impl)
601+
typ_impl = var_impl.objtype
602+
if typ_impl.is_xml_type:
603+
column_value = buf.read_xmltype()
604+
else:
605+
obj_impl = buf.read_dbobject(typ_impl)
606+
if obj_impl is not None:
607+
if not self.in_fetch:
608+
column_value = var_impl._values[pos]
609+
if column_value is not None:
610+
column_value._impl = obj_impl
611+
else:
612+
column_value = PY_TYPE_DB_OBJECT._from_impl(obj_impl)
608613
else:
609614
errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED,
610615
name=var_impl.dbtype.name)

tests/test_2500_string_var.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -399,10 +399,8 @@ def test_2529_set_rowid_to_string(self):
399399
self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3004:",
400400
var.setvalue, 0, "ABDHRYTHFJGKDKKDH")
401401

402-
@unittest.skipIf(test_env.get_is_thin(),
403-
"thin mode doesn't support XML type objects yet")
404402
def test_2530_short_xml_as_string(self):
405-
"2530 - test fetching XMLType object as a string"
403+
"2530 - test fetching XMLType (< 1K) as a string"
406404
self.cursor.execute("""
407405
select XMLElement("string", stringCol)
408406
from TestStrings
@@ -411,13 +409,11 @@ def test_2530_short_xml_as_string(self):
411409
expected_value = "<string>String 1</string>"
412410
self.assertEqual(actual_value, expected_value)
413411

414-
@unittest.skipIf(test_env.get_is_thin(),
415-
"thin mode doesn't support XML type objects yet")
416412
def test_2531_long_xml_as_string(self):
417-
"2531 - test inserting and fetching an XMLType object (1K) as a string"
413+
"2531 - test inserting and fetching XMLType (1K) as a string"
418414
chars = string.ascii_uppercase + string.ascii_lowercase
419415
random_string = ''.join(random.choice(chars) for _ in range(1024))
420-
int_val = 200
416+
int_val = 2531
421417
xml_string = '<data>' + random_string + '</data>'
422418
self.cursor.execute("truncate table TestTempXML")
423419
self.cursor.execute("""
@@ -458,5 +454,24 @@ def test_2533_bypass_decode(self):
458454
cursor.execute("select IntCol, StringCol1 from TestTempTable")
459455
self.assertEqual(cursor.fetchone(), (1, string_val))
460456

457+
@unittest.skipIf(not test_env.get_is_thin(),
458+
"thick mode doesn't support fetching XMLType > VARCHAR2")
459+
def test_2534_very_long_xml_as_string(self):
460+
"2534 - test inserting and fetching XMLType (32K) as a string"
461+
chars = string.ascii_uppercase + string.ascii_lowercase
462+
random_string = ''.join(random.choice(chars) for _ in range(32768))
463+
int_val = 2534
464+
xml_string = f"<data>{random_string}</data>"
465+
lob = self.connection.createlob(oracledb.DB_TYPE_CLOB)
466+
lob.write(xml_string)
467+
self.cursor.execute("truncate table TestTempXML")
468+
self.cursor.execute("""
469+
insert into TestTempXML (IntCol, XMLCol)
470+
values (:1, sys.xmltype(:2))""", (int_val, lob))
471+
self.cursor.execute("select XMLCol from TestTempXML where intCol = :1",
472+
(int_val,))
473+
actual_value, = self.cursor.fetchone()
474+
self.assertEqual(actual_value.strip(), xml_string)
475+
461476
if __name__ == "__main__":
462477
test_env.run_test_cases()

tests/test_4300_cursor_other.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -226,17 +226,15 @@ def test_4319_var_type_with_object_type(self):
226226
exp = "udt_Object(28, 'Bind obj out', null, null, null, null, null)"
227227
self.assertEqual(result, exp)
228228

229-
@unittest.skipIf(test_env.get_is_thin(),
230-
"thin mode doesn't support XML type objects yet")
231229
def test_4320_fetch_xmltype(self):
232230
"4320 - test that fetching an XMLType returns a string"
233231
int_val = 5
234232
label = "IntCol"
235-
expected_result = "<%s>%s</%s>" % (label, int_val, label)
236-
self.cursor.execute("""
237-
select XMLElement("%s", IntCol)
233+
expected_result = f"<{label}>{int_val}</{label}>"
234+
self.cursor.execute(f"""
235+
select XMLElement("{label}", IntCol)
238236
from TestStrings
239-
where IntCol = :int_val""" % label,
237+
where IntCol = :int_val""",
240238
int_val=int_val)
241239
result, = self.cursor.fetchone()
242240
self.assertEqual(result, expected_result)

0 commit comments

Comments
 (0)