From 1ffaa7fe00d8d121d563e99a9166b0042833ff7c Mon Sep 17 00:00:00 2001 From: Anthony Tuininga Date: Thu, 10 Aug 2023 15:21:33 -0600 Subject: [PATCH] Added support for calling the outconverter when null values are fetched from the database (#107). --- doc/src/api_manual/cursor.rst | 12 +++++++++++- doc/src/api_manual/variable.rst | 7 +++++++ doc/src/release_notes.rst | 5 +++++ src/oracledb/base_impl.pxd | 1 + src/oracledb/cursor.py | 7 ++++++- src/oracledb/impl/base/cursor.pyx | 4 +++- src/oracledb/impl/thick/var.pyx | 4 +++- src/oracledb/impl/thin/messages.pyx | 2 +- src/oracledb/var.py | 8 ++++++++ tests/test_3700_var.py | 21 +++++++++++++++++++++ 10 files changed, 66 insertions(+), 5 deletions(-) diff --git a/doc/src/api_manual/cursor.rst b/doc/src/api_manual/cursor.rst index fe21d19..8bb4ea6 100644 --- a/doc/src/api_manual/cursor.rst +++ b/doc/src/api_manual/cursor.rst @@ -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 ` 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. diff --git a/doc/src/api_manual/variable.rst b/doc/src/api_manual/variable.rst index 4388d76..f3d7e4c 100644 --- a/doc/src/api_manual/variable.rst +++ b/doc/src/api_manual/variable.rst @@ -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 diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index 0896d43..4d0f8d3 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -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 `__). #) 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 diff --git a/src/oracledb/base_impl.pxd b/src/oracledb/base_impl.pxd index d4b1466..7a82965 100644 --- a/src/oracledb/base_impl.pxd +++ b/src/oracledb/base_impl.pxd @@ -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 diff --git a/src/oracledb/cursor.py b/src/oracledb/cursor.py index 0b645e3..0a5bac1 100644 --- a/src/oracledb/cursor.py +++ b/src/oracledb/cursor.py @@ -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) diff --git a/src/oracledb/impl/base/cursor.pyx b/src/oracledb/impl/base/cursor.pyx index b54b032..f7c3484 100644 --- a/src/oracledb/impl/base/cursor.pyx +++ b/src/oracledb/impl/base/cursor.pyx @@ -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) diff --git a/src/oracledb/impl/thick/var.pyx b/src/oracledb/impl/thick/var.pyx index bf89259..40b3a0c 100644 --- a/src/oracledb/impl/thick/var.pyx +++ b/src/oracledb/impl/thick/var.pyx @@ -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) diff --git a/src/oracledb/impl/thin/messages.pyx b/src/oracledb/impl/thin/messages.pyx index 8614aed..37d71e2 100644 --- a/src/oracledb/impl/thin/messages.pyx +++ b/src/oracledb/impl/thin/messages.pyx @@ -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): diff --git a/src/oracledb/var.py b/src/oracledb/var.py index 31ce022..a3fadec 100644 --- a/src/oracledb/var.py +++ b/src/oracledb/var.py @@ -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 diff --git a/tests/test_3700_var.py b/tests/test_3700_var.py index db3a097..458ea0f 100644 --- a/tests/test_3700_var.py +++ b/tests/test_3700_var.py @@ -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()