Added support for calling the outconverter when null values are fetched

from the database (#107).
This commit is contained in:
Anthony Tuininga 2023-08-10 15:21:33 -06:00
parent 428622760e
commit 1ffaa7fe00
10 changed files with 66 additions and 5 deletions

View File

@ -371,7 +371,7 @@ Cursor Methods
.. method:: Cursor.var(typ, [size, arraysize, inconverter, outconverter, \
typename, encoding_errors, bypass_decode])
typename, encoding_errors, bypass_decode, convert_nulls])
Creates a variable with the specified characteristics. This method was
designed for use with PL/SQL in/out variables where the length or type
@ -442,10 +442,20 @@ Cursor Methods
meaning that python-oracledb does not do any decoding. See :ref:`Fetching raw
data <fetching-raw-data>` for more information.
The ``convert_nulls`` parameter, if specified, should be passed a boolean
value. Passing the value ``True`` causes the ``outconverter`` to be called
when a null value is fetched from the database; otherwise, the
``outconverter`` is only called when non-null values are fetched from the
database.
For consistency and compliance with the PEP 8 naming style, the
parameter `encodingErrors` was renamed to `encoding_errors`. The old
name will continue to work as a keyword parameter for a period of time.
.. versionchanged:: 1.4
The ``convert_nulls`` parameter was added.
.. note::
The DB API definition does not define this method.

View File

@ -50,6 +50,13 @@ Variable Attributes
name will continue to work for a period of time.
.. attribute:: Variable.convert_nulls
This read-only attribute returns whether the :data:`~Variable.outconverter`
method is called when null values are fetched from the database.
.. versionadded:: 1.4
.. attribute:: Variable.inconverter
This read-write attribute specifies the method used to convert data from

View File

@ -74,6 +74,11 @@ Thick Mode Changes
Common Changes
++++++++++++++
#) Added support for the :attr:`~Variable.outconverter` being called when a
null value is fetched from the database and the new parameter
``convert_nulls`` to the method :meth:`Cursor.var()` is passed the value
``True``
(`issue 107 <https://github.com/oracle/python-oracledb/issues/107>`__).
#) Replaced fixed 7-tuple for the cursor metadata found in
:data:`Cursor.description` with a class which provides additional
information such as the database object type and whether the column

View File

@ -333,6 +333,7 @@ cdef class BaseVarImpl:
readonly bint bypass_decode
readonly bint is_array
readonly bint nulls_allowed
readonly bint convert_nulls
public uint32_t num_elements_in_array
readonly DbType dbtype
readonly BaseDbObjectTypeImpl objtype

View File

@ -743,6 +743,7 @@ class Cursor:
typename: str=None,
encoding_errors: str=None,
bypass_decode: bool=False,
convert_nulls: bool=False,
*,
encodingErrors: str=None) -> "Var":
"""
@ -794,6 +795,9 @@ class Cursor:
DB_TYPE_VARCHAR, DB_TYPE_CHAR, DB_TYPE_NVARCHAR, DB_TYPE_NCHAR and
DB_TYPE_LONG to be returned as bytes instead of str, meaning that
oracledb doesn't do any decoding.
The convert_nulls parameter specifies whether the outconverter should
be called when null values are fetched from the database.
"""
self._verify_open()
if typename is not None:
@ -808,4 +812,5 @@ class Cursor:
encoding_errors = encodingErrors
return self._impl.create_var(self.connection, typ, size, arraysize,
inconverter, outconverter,
encoding_errors, bypass_decode)
encoding_errors, bypass_decode,
convert_nulls=convert_nulls)

View File

@ -403,7 +403,8 @@ cdef class BaseCursorImpl:
def create_var(self, object conn, object typ, uint32_t size=0,
uint32_t num_elements=1, object inconverter=None,
object outconverter=None, str encoding_errors=None,
bint bypass_decode=False, bint is_array=False):
bint bypass_decode=False, bint is_array=False,
bint convert_nulls=False):
cdef BaseVarImpl var_impl
var_impl = self._create_var_impl(conn)
var_impl._set_type_info_from_type(typ)
@ -413,6 +414,7 @@ cdef class BaseCursorImpl:
var_impl.outconverter = outconverter
var_impl.bypass_decode = bypass_decode
var_impl.is_array = is_array
var_impl.convert_nulls = convert_nulls
var_impl._finalize_init()
return PY_TYPE_VAR._from_impl(var_impl)

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------
# Copyright (c) 2020, 2022, Oracle and/or its affiliates.
# Copyright (c) 2020, 2023, Oracle and/or its affiliates.
#
# This software is dual-licensed to you under the Universal Permissive License
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@ -276,3 +276,5 @@ cdef class ThickVarImpl(BaseVarImpl):
if self.outconverter is not None:
value = self.outconverter(value)
return value
elif self.convert_nulls:
return self.outconverter(None)

View File

@ -494,7 +494,7 @@ cdef class MessageWithData(Message):
num_elements = self.row_index
for i in range(num_elements):
value = var_impl._values[i]
if value is None:
if value is None and not var_impl.convert_nulls:
continue
if isinstance(value, list):
for j, element_value in enumerate(value):

View File

@ -90,6 +90,14 @@ class Var:
"""
return self.buffer_size
@property
def convert_nulls(self) -> bool:
"""
This read-only attribute returns whether null values are converted
using the supplied ``outconverter``.
"""
return self._impl.convert_nulls
def getvalue(self, pos: int=0) -> Any:
"""
Return the value at the given position in the variable. For variables

View File

@ -433,5 +433,26 @@ class TestCase(test_env.BaseTestCase):
self.assertEqual(var.actualElements, 200)
self.assertEqual(var.numElements, 200)
def test_3730_convert_nulls(self):
"3730 - test calling of outconverter with null values"
def type_handler(cursor, metadata):
return cursor.var(metadata.type_code,
outconverter=lambda v: f"|{v}|" if v else '',
convert_nulls=True, arraysize=cursor.arraysize)
self.cursor.outputtypehandler = type_handler
self.cursor.execute("""
select 'First - A', 'First - B' from dual
union all
select 'Second - A', null from dual
union all
select null, 'Third - B' from dual""")
rows = self.cursor.fetchall()
expected_rows = [
('|First - A|', '|First - B|'),
('|Second - A|', ''),
('', '|Third - B|')
]
self.assertEqual(rows, expected_rows)
if __name__ == "__main__":
test_env.run_test_cases()