Skip to content

Commit 500187f

Browse files
Correct fetching of LOBs with asyncio prior to Oracle Database 23ai
(#500).
1 parent 3fcd580 commit 500187f

File tree

5 files changed

+124
-55
lines changed

5 files changed

+124
-55
lines changed

doc/src/release_notes.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Thin Mode Changes
2626
closed connection
2727
(`issue 482 <https://github.com/oracle/python-oracledb/issues/482>`__).
2828
#) Fixed bug when connecting with asyncio using the parameter ``https_proxy``.
29+
#) Fixed bug when fetching LOBs with asyncio from databases prior to Oracle
30+
Database 23ai
31+
(`issue 500 <https://github.com/oracle/python-oracledb/issues/500>`__).
2932
#) Fixed regression when connecting where only the host specified by the
3033
``https_proxy`` parameter can successfully perform name resolution.
3134
#) Fixed bug resulting in explicit request boundaries to aid planned database

src/oracledb/impl/thin/connection.pyx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,11 +596,13 @@ cdef class AsyncThinConnImpl(BaseThinConnImpl):
596596
cursor_impl = message_with_data.cursor_impl
597597
if message.resend:
598598
await protocol._process_message(message)
599+
await message.postprocess_async()
599600
if op_type in (
600601
PIPELINE_OP_TYPE_FETCH_ONE,
601602
PIPELINE_OP_TYPE_FETCH_MANY,
602603
PIPELINE_OP_TYPE_FETCH_ALL,
603604
):
605+
result_impl.rows = []
604606
while cursor_impl._buffer_rowcount > 0:
605607
result_impl.rows.append(cursor_impl._create_row())
606608
result_impl.fetch_metadata = cursor_impl.fetch_metadata
@@ -870,7 +872,7 @@ cdef class AsyncThinConnImpl(BaseThinConnImpl):
870872
Message message
871873
for message in messages:
872874
result_impl = message.pipeline_result_impl
873-
if result_impl.error is not None:
875+
if result_impl.error is not None or message.resend:
874876
continue
875877
try:
876878
self._populate_pipeline_op_result(message)

src/oracledb/impl/thin/messages/base.pyx

Lines changed: 113 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,32 @@ cdef class _OracleErrorInfo:
4242
list batcherrors
4343

4444

45+
@cython.freelist(20)
46+
cdef class _PostProcessFn:
47+
cdef:
48+
object fn
49+
bint convert_nulls
50+
bint check_awaitable
51+
uint32_t num_elements
52+
list values
53+
54+
@staticmethod
55+
cdef _PostProcessFn from_info(object fn, uint32_t num_elements,
56+
list values, bint convert_nulls=False,
57+
bint check_awaitable=False):
58+
"""
59+
Create a post process function object and return it.
60+
"""
61+
cdef _PostProcessFn fn_obj
62+
fn_obj = _PostProcessFn.__new__(_PostProcessFn)
63+
fn_obj.fn = fn
64+
fn_obj.convert_nulls = convert_nulls
65+
fn_obj.check_awaitable = check_awaitable
66+
fn_obj.num_elements = num_elements
67+
fn_obj.values = values
68+
return fn_obj
69+
70+
4571
cdef class Message:
4672
cdef:
4773
BaseThinConnImpl conn_impl
@@ -736,6 +762,70 @@ cdef class MessageWithData(Message):
736762
self.bit_vector = <const char_type*> self.bit_vector_buf.data.as_chars
737763
memcpy(<void*> self.bit_vector, ptr, num_bytes)
738764

765+
cdef list _get_post_process_fns(self):
766+
"""
767+
Returns a list of functions that need to be run after the database
768+
response has been completely received. These functions can be
769+
internally defined (for wrapping implementation objects with user
770+
facing objects) or user defined (out converters). This prevents
771+
multiple executions of functions (reparsing of database responses for
772+
older databases without the end of response indicator) or interference
773+
with any ongoing database response. Returning a list allows this
774+
process to be determined commonly across sync and async in order to
775+
avoid duplicating code.
776+
"""
777+
cdef:
778+
OracleMetadata metadata
779+
uint32_t num_elements
780+
uint8_t ora_type_num
781+
ThinVarImpl var_impl
782+
_PostProcessFn fn
783+
list fns = []
784+
bint is_async
785+
object cls
786+
is_async = self.conn_impl._protocol._transport._is_async
787+
if self.out_var_impls is not None:
788+
for var_impl in self.out_var_impls:
789+
if var_impl is None:
790+
continue
791+
792+
# retain last raw value when not fetching Arrow (for handling
793+
# duplicate rows)
794+
if not self.cursor_impl.fetching_arrow:
795+
var_impl._last_raw_value = \
796+
var_impl._values[self.cursor_impl._last_row_index]
797+
798+
# determine the number of elements to process, if needed
799+
if var_impl.is_array:
800+
num_elements = var_impl.num_elements_in_array
801+
else:
802+
num_elements = self.row_index
803+
804+
# perform post conversion to user-facing objects, if applicable
805+
if self.in_fetch:
806+
metadata = var_impl._fetch_metadata
807+
else:
808+
metadata = var_impl.metadata
809+
ora_type_num = metadata.dbtype._ora_type_num
810+
if ora_type_num in (ORA_TYPE_NUM_CLOB,
811+
ORA_TYPE_NUM_BLOB,
812+
ORA_TYPE_NUM_BFILE):
813+
cls = PY_TYPE_ASYNC_LOB if is_async else PY_TYPE_LOB
814+
fn = _PostProcessFn.from_info(cls._from_impl, num_elements,
815+
var_impl._values)
816+
fns.append(fn)
817+
818+
# perform post conversion via user out converter, if applicable
819+
if var_impl.outconverter is None:
820+
continue
821+
fn = _PostProcessFn.from_info(var_impl.outconverter,
822+
num_elements, var_impl._values,
823+
var_impl.convert_nulls,
824+
check_awaitable=True)
825+
fns.append(fn)
826+
827+
return fns
828+
739829
cdef bint _is_duplicate_data(self, uint32_t column_num):
740830
"""
741831
Returns a boolean indicating if the given column contains data
@@ -1366,32 +1456,21 @@ cdef class MessageWithData(Message):
13661456
database round-trip.
13671457
"""
13681458
cdef:
1369-
uint32_t i, j, num_elements
13701459
object value, element_value
1371-
ThinVarImpl var_impl
1372-
if self.out_var_impls is None:
1373-
return 0
1374-
for var_impl in self.out_var_impls:
1375-
if var_impl is None or var_impl.outconverter is None:
1376-
continue
1377-
if not self.cursor_impl.fetching_arrow:
1378-
var_impl._last_raw_value = \
1379-
var_impl._values[self.cursor_impl._last_row_index]
1380-
if var_impl.is_array:
1381-
num_elements = var_impl.num_elements_in_array
1382-
else:
1383-
num_elements = self.row_index
1384-
for i in range(num_elements):
1385-
value = var_impl._values[i]
1386-
if value is None and not var_impl.convert_nulls:
1460+
_PostProcessFn fn
1461+
uint32_t i, j
1462+
for fn in self._get_post_process_fns():
1463+
for i in range(fn.num_elements):
1464+
value = fn.values[i]
1465+
if value is None and not fn.convert_nulls:
13871466
continue
13881467
if isinstance(value, list):
13891468
for j, element_value in enumerate(value):
1390-
if element_value is None:
1469+
if element_value is None and not fn.convert_nulls:
13911470
continue
1392-
value[j] = var_impl.outconverter(element_value)
1471+
value[j] = fn.fn(element_value)
13931472
else:
1394-
var_impl._values[i] = var_impl.outconverter(value)
1473+
fn.values[i] = fn.fn(value)
13951474

13961475
async def postprocess_async(self):
13971476
"""
@@ -1401,39 +1480,28 @@ cdef class MessageWithData(Message):
14011480
database round-trip.
14021481
"""
14031482
cdef:
1404-
object value, element_value, fn
1405-
uint32_t i, j, num_elements
1406-
ThinVarImpl var_impl
1407-
if self.out_var_impls is None:
1408-
return 0
1409-
for var_impl in self.out_var_impls:
1410-
if var_impl is None or var_impl.outconverter is None:
1411-
continue
1412-
if not self.cursor_impl.fetching_arrow:
1413-
var_impl._last_raw_value = \
1414-
var_impl._values[self.cursor_impl._last_row_index]
1415-
if var_impl.is_array:
1416-
num_elements = var_impl.num_elements_in_array
1417-
else:
1418-
num_elements = self.row_index
1419-
fn = var_impl.outconverter
1420-
for i in range(num_elements):
1421-
value = var_impl._values[i]
1422-
if value is None and not var_impl.convert_nulls:
1483+
object value, element_value
1484+
_PostProcessFn fn
1485+
uint32_t i, j
1486+
for fn in self._get_post_process_fns():
1487+
for i in range(fn.num_elements):
1488+
value = fn.values[i]
1489+
if value is None and not fn.convert_nulls:
14231490
continue
14241491
if isinstance(value, list):
14251492
for j, element_value in enumerate(value):
1426-
if element_value is None:
1493+
if element_value is None and not fn.convert_nulls:
14271494
continue
1428-
element_value = fn(element_value)
1429-
if inspect.isawaitable(element_value):
1495+
element_value = fn.fn(element_value)
1496+
if fn.check_awaitable \
1497+
and inspect.isawaitable(element_value):
14301498
element_value = await element_value
14311499
value[j] = element_value
14321500
else:
1433-
value = fn(value)
1434-
if inspect.isawaitable(value):
1501+
value = fn.fn(value)
1502+
if fn.check_awaitable and inspect.isawaitable(value):
14351503
value = await value
1436-
var_impl._values[i] = value
1504+
fn.values[i] = value
14371505

14381506
cdef int preprocess(self) except -1:
14391507
cdef:

src/oracledb/impl/thin/packet.pyx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,6 @@ cdef class ReadBuffer(Buffer):
487487
BaseThinLobImpl lob_impl
488488
uint64_t size
489489
bytes locator
490-
type cls
491490
self.read_ub4(&num_bytes)
492491
if num_bytes > 0:
493492
if dbtype._ora_type_num == ORA_TYPE_NUM_BFILE:
@@ -497,18 +496,13 @@ cdef class ReadBuffer(Buffer):
497496
self.read_ub4(&chunk_size)
498497
locator = self.read_bytes()
499498
if lob is None:
500-
lob_impl = conn_impl._create_lob_impl(dbtype, locator)
501-
cls = PY_TYPE_ASYNC_LOB \
502-
if conn_impl._protocol._transport._is_async \
503-
else PY_TYPE_LOB
504-
lob = cls._from_impl(lob_impl)
499+
lob = lob_impl = conn_impl._create_lob_impl(dbtype, locator)
505500
else:
506501
lob_impl = lob._impl
507502
lob_impl._locator = locator
508503
lob_impl._size = size
509504
lob_impl._chunk_size = chunk_size
510-
lob_impl._has_metadata = \
511-
dbtype._ora_type_num != ORA_TYPE_NUM_BFILE
505+
lob_impl._has_metadata = dbtype._ora_type_num != ORA_TYPE_NUM_BFILE
512506
return lob
513507

514508
cdef const char_type* read_raw_bytes(self, ssize_t num_bytes) except NULL:

src/oracledb/lob.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -----------------------------------------------------------------------------
2-
# Copyright (c) 2021, 2024, Oracle and/or its affiliates.
2+
# Copyright (c) 2021, 2025, Oracle and/or its affiliates.
33
#
44
# This software is dual-licensed to you under the Universal Permissive License
55
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@@ -70,6 +70,8 @@ def _check_value_to_write(self, value):
7070

7171
@classmethod
7272
def _from_impl(cls, impl):
73+
if isinstance(impl, BaseLOB):
74+
return impl
7375
lob = cls.__new__(cls)
7476
lob._impl = impl
7577
return lob

0 commit comments

Comments
 (0)