Added support for transforming SYS.XMLTYPE into strings as is done in
thick mode.
This commit is contained in:
parent
12c1da25f3
commit
30e554b922
|
@ -17,6 +17,9 @@ Thin Mode Changes
|
||||||
:data:`~oracledb.DB_TYPE_OBJECT`. Note that some of the error codes and
|
: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
|
messages have changed as a result (DPY errors are raised instead of ones
|
||||||
specific to ODPI-C and OCI).
|
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
|
#) Added support for using a wallet for one-way TLS connections, rather than
|
||||||
requiring OS recognition of certificates
|
requiring OS recognition of certificates
|
||||||
(`issue 65 <https://github.com/oracle/python-oracledb/issues/65>`__).
|
(`issue 65 <https://github.com/oracle/python-oracledb/issues/65>`__).
|
||||||
|
|
|
@ -367,9 +367,9 @@ see :ref:`driverdiff` and :ref:`compatibility`.
|
||||||
- Yes
|
- Yes
|
||||||
- Yes
|
- Yes
|
||||||
* - XMLType data type (see :ref:`xmldatatype`)
|
* - XMLType data type (see :ref:`xmldatatype`)
|
||||||
- No
|
- Yes
|
||||||
- No
|
- Yes - may need to fetch as CLOB
|
||||||
- No
|
- Yes - may need to fetch as CLOB
|
||||||
* - BFILE data type (see :data:`~oracledb.DB_TYPE_BFILE`)
|
* - BFILE data type (see :data:`~oracledb.DB_TYPE_BFILE`)
|
||||||
- No
|
- No
|
||||||
- Yes
|
- Yes
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
Using XMLTYPE Data
|
Using XMLTYPE Data
|
||||||
******************
|
******************
|
||||||
|
|
||||||
Oracle XMLType columns are fetched as strings by default. This is currently
|
Oracle XMLType columns are fetched as strings by default in Thin and Thick
|
||||||
limited to the maximum length of a ``VARCHAR2`` column. To return longer XML
|
mode. Note that in Thick mode you may need to use ``XMLTYPE.GETCLOBVAL()`` as
|
||||||
values, they must be queried as LOB values instead.
|
discussed below.
|
||||||
|
|
||||||
The examples below demonstrate using XMLType data with python-oracledb. The
|
The examples below demonstrate using XMLType data with python-oracledb. The
|
||||||
following table will be used in these examples:
|
following table will be used in these examples:
|
||||||
|
@ -18,7 +18,7 @@ following table will be used in these examples:
|
||||||
xml_data SYS.XMLTYPE
|
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
|
.. 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)
|
id=1, xml=xml_data)
|
||||||
|
|
||||||
This approach works with XML strings up to 1 GB in size. For longer strings, a
|
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
|
temporary CLOB must be created using :meth:`Connection.createlob()` and cast
|
||||||
as shown:
|
when bound:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -43,17 +43,17 @@ as shown:
|
||||||
cursor.execute("insert into xml_table values (:id, sys.xmltype(:xml))",
|
cursor.execute("insert into xml_table values (:id, sys.xmltype(:xml))",
|
||||||
id=2, xml=clob)
|
id=2, xml=clob)
|
||||||
|
|
||||||
Fetching XML data can be done simply for values that are shorter than the
|
Fetching XML data can be done directly in Thin mode. This also works in Thick
|
||||||
length of a VARCHAR2 column as shown:
|
mode for values that are shorter than the length of a VARCHAR2 column:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
cursor.execute("select xml_data from xml_table where id = :id", id=1)
|
cursor.execute("select xml_data from xml_table where id = :id", id=1)
|
||||||
xml_data, = cursor.fetchone()
|
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
|
In Thick mode, for values that exceed the length of a VARCHAR2 column, a CLOB
|
||||||
instead by using the function ``XMLTYPE.GETCLOBVAL()`` as shown:
|
must be returned by using the function ``XMLTYPE.GETCLOBVAL()``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -64,5 +64,5 @@ instead by using the function ``XMLTYPE.GETCLOBVAL()`` as shown:
|
||||||
clob, = cursor.fetchone()
|
clob, = cursor.fetchone()
|
||||||
print(clob.read())
|
print(clob.read())
|
||||||
|
|
||||||
The LOB that is returned can be streamed or a string can be returned instead of
|
The LOB that is returned can be streamed, as shown. Alternatively a string can
|
||||||
a CLOB. See :ref:`lobdata` for more information about processing LOBs.
|
be returned. See :ref:`lobdata` for more information.
|
||||||
|
|
|
@ -791,6 +791,31 @@ cdef class Buffer:
|
||||||
cdef const char_type *ptr = self._get_raw(4)
|
cdef const char_type *ptr = self._get_raw(4)
|
||||||
value[0] = unpack_uint32(ptr, byte_order)
|
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:
|
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
|
Skip the specified number of bytes in the buffer. In order to avoid
|
||||||
|
|
|
@ -539,6 +539,7 @@ cdef class ThinDbObjectTypeImpl(BaseDbObjectTypeImpl):
|
||||||
cdef:
|
cdef:
|
||||||
uint8_t collection_type, collection_flags, version
|
uint8_t collection_type, collection_flags, version
|
||||||
uint32_t max_num_elements
|
uint32_t max_num_elements
|
||||||
|
bint is_xml_type
|
||||||
bytes oid
|
bytes oid
|
||||||
|
|
||||||
def create_new_object(self):
|
def create_new_object(self):
|
||||||
|
@ -835,6 +836,8 @@ cdef class ThinDbObjectTypeCache:
|
||||||
typ_impl.schema = self.schema_var.getvalue()
|
typ_impl.schema = self.schema_var.getvalue()
|
||||||
typ_impl.package_name = self.package_name_var.getvalue()
|
typ_impl.package_name = self.package_name_var.getvalue()
|
||||||
typ_impl.name = self.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())
|
self._parse_tds(typ_impl, self.tds_var.getvalue())
|
||||||
typ_impl.attrs = []
|
typ_impl.attrs = []
|
||||||
typ_impl.attrs_by_name = {}
|
typ_impl.attrs_by_name = {}
|
||||||
|
@ -901,6 +904,7 @@ cdef class ThinDbObjectTypeCache:
|
||||||
typ_impl.schema = schema
|
typ_impl.schema = schema
|
||||||
typ_impl.package_name = package_name
|
typ_impl.package_name = package_name
|
||||||
typ_impl.name = name
|
typ_impl.name = name
|
||||||
|
typ_impl.is_xml_type = (schema == "SYS" and name == "XMLTYPE")
|
||||||
if oid is not None:
|
if oid is not None:
|
||||||
self.types_by_oid[oid] = typ_impl
|
self.types_by_oid[oid] = typ_impl
|
||||||
self.types_by_name[full_name] = typ_impl
|
self.types_by_name[full_name] = typ_impl
|
||||||
|
|
|
@ -520,6 +520,7 @@ cdef class MessageWithData(Message):
|
||||||
ThinVarImpl var_impl, uint32_t pos):
|
ThinVarImpl var_impl, uint32_t pos):
|
||||||
cdef:
|
cdef:
|
||||||
uint8_t num_bytes, ora_type_num, csfrm
|
uint8_t num_bytes, ora_type_num, csfrm
|
||||||
|
ThinDbObjectTypeImpl typ_impl
|
||||||
ThinCursorImpl cursor_impl
|
ThinCursorImpl cursor_impl
|
||||||
object column_value = None
|
object column_value = None
|
||||||
ThinDbObjectImpl obj_impl
|
ThinDbObjectImpl obj_impl
|
||||||
|
@ -597,7 +598,11 @@ cdef class MessageWithData(Message):
|
||||||
column_value = buf.read_lob_with_length(self.conn_impl,
|
column_value = buf.read_lob_with_length(self.conn_impl,
|
||||||
var_impl.dbtype)
|
var_impl.dbtype)
|
||||||
elif ora_type_num == TNS_DATA_TYPE_INT_NAMED:
|
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 obj_impl is not None:
|
||||||
if not self.in_fetch:
|
if not self.in_fetch:
|
||||||
column_value = var_impl._values[pos]
|
column_value = var_impl._values[pos]
|
||||||
|
|
|
@ -399,10 +399,8 @@ class TestCase(test_env.BaseTestCase):
|
||||||
self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3004:",
|
self.assertRaisesRegex(oracledb.NotSupportedError, "^DPY-3004:",
|
||||||
var.setvalue, 0, "ABDHRYTHFJGKDKKDH")
|
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):
|
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("""
|
self.cursor.execute("""
|
||||||
select XMLElement("string", stringCol)
|
select XMLElement("string", stringCol)
|
||||||
from TestStrings
|
from TestStrings
|
||||||
|
@ -411,13 +409,11 @@ class TestCase(test_env.BaseTestCase):
|
||||||
expected_value = "<string>String 1</string>"
|
expected_value = "<string>String 1</string>"
|
||||||
self.assertEqual(actual_value, expected_value)
|
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):
|
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
|
chars = string.ascii_uppercase + string.ascii_lowercase
|
||||||
random_string = ''.join(random.choice(chars) for _ in range(1024))
|
random_string = ''.join(random.choice(chars) for _ in range(1024))
|
||||||
int_val = 200
|
int_val = 2531
|
||||||
xml_string = '<data>' + random_string + '</data>'
|
xml_string = '<data>' + random_string + '</data>'
|
||||||
self.cursor.execute("truncate table TestTempXML")
|
self.cursor.execute("truncate table TestTempXML")
|
||||||
self.cursor.execute("""
|
self.cursor.execute("""
|
||||||
|
@ -458,5 +454,24 @@ class TestCase(test_env.BaseTestCase):
|
||||||
cursor.execute("select IntCol, StringCol1 from TestTempTable")
|
cursor.execute("select IntCol, StringCol1 from TestTempTable")
|
||||||
self.assertEqual(cursor.fetchone(), (1, string_val))
|
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__":
|
if __name__ == "__main__":
|
||||||
test_env.run_test_cases()
|
test_env.run_test_cases()
|
||||||
|
|
|
@ -226,17 +226,15 @@ class TestCase(test_env.BaseTestCase):
|
||||||
exp = "udt_Object(28, 'Bind obj out', null, null, null, null, null)"
|
exp = "udt_Object(28, 'Bind obj out', null, null, null, null, null)"
|
||||||
self.assertEqual(result, exp)
|
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):
|
def test_4320_fetch_xmltype(self):
|
||||||
"4320 - test that fetching an XMLType returns a string"
|
"4320 - test that fetching an XMLType returns a string"
|
||||||
int_val = 5
|
int_val = 5
|
||||||
label = "IntCol"
|
label = "IntCol"
|
||||||
expected_result = "<%s>%s</%s>" % (label, int_val, label)
|
expected_result = f"<{label}>{int_val}</{label}>"
|
||||||
self.cursor.execute("""
|
self.cursor.execute(f"""
|
||||||
select XMLElement("%s", IntCol)
|
select XMLElement("{label}", IntCol)
|
||||||
from TestStrings
|
from TestStrings
|
||||||
where IntCol = :int_val""" % label,
|
where IntCol = :int_val""",
|
||||||
int_val=int_val)
|
int_val=int_val)
|
||||||
result, = self.cursor.fetchone()
|
result, = self.cursor.fetchone()
|
||||||
self.assertEqual(result, expected_result)
|
self.assertEqual(result, expected_result)
|
||||||
|
|
Loading…
Reference in New Issue