Skip to content

Commit e7f49cd

Browse files
Correct handling of binding closed cursors as REF CURSORS (#368);
variables containing cursors, LOBs or DbObject values now return the same instances when calling getvalue(), matching Thin mode behavior. Previously, new instances were created for each call in Thick mode.
1 parent 4c37300 commit e7f49cd

File tree

10 files changed

+189
-36
lines changed

10 files changed

+189
-36
lines changed

doc/src/release_notes.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,24 @@ oracledb 2.4.0 (TBD)
1717
Thin Mode Changes
1818
+++++++++++++++++
1919

20+
#) Fixed bug resulting in a segfault when a closed cursor is bound as a REF
21+
CURSOR.
22+
2023
Thick Mode Changes
2124
++++++++++++++++++
2225

26+
#) Variables containing cursors, LOBs or DbObject values now return the same
27+
instances when calling :meth:`Variable.getvalue()`, matching Thin mode
28+
behavior. Previously, new instances were created for each call in Thick
29+
mode.
30+
2331
Common Changes
2432
++++++++++++++
2533

34+
#) Error ``DPY-1006: cursor is not open`` is now raised consistently when
35+
attempting to bind a closed cursor. Previously, thin mode would result in a
36+
segfault and thick mode would result in unusual errors.
37+
2638

2739
oracledb 2.3.0 (July 2024)
2840
--------------------------

src/oracledb/base_impl.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,7 @@ cdef class BaseVarImpl:
643643
BaseConnImpl _conn_impl
644644
int _preferred_num_type
645645
FetchInfoImpl _fetch_info
646+
list _values
646647
bint _is_value_set
647648

648649
cdef int _bind(self, object conn, BaseCursorImpl cursor,

src/oracledb/impl/base/connection.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ cdef class BaseConnImpl:
133133
return value
134134
elif db_type_num == DB_TYPE_NUM_CURSOR:
135135
if isinstance(value, (PY_TYPE_CURSOR, PY_TYPE_ASYNC_CURSOR)):
136+
value._verify_open()
136137
return value
137138
elif db_type_num == DB_TYPE_NUM_BOOLEAN:
138139
return bool(value)

src/oracledb/impl/thick/utils.pyx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,8 @@ cdef object _convert_from_json_node(dpiJsonNode *node):
104104

105105
cdef int _convert_from_python(object value, DbType dbtype,
106106
ThickDbObjectTypeImpl obj_type_impl,
107-
dpiDataBuffer *dbvalue, StringBuffer buf,
108-
ThickVarImpl var_impl=None,
109-
uint32_t pos=0) except -1:
107+
dpiDataBuffer *dbvalue,
108+
StringBuffer buf) except -1:
110109
cdef:
111110
uint32_t oracle_type = dbtype.num
112111
ThickDbObjectImpl obj_impl
@@ -183,24 +182,14 @@ cdef int _convert_from_python(object value, DbType dbtype,
183182
if not isinstance(value, PY_TYPE_DB_OBJECT):
184183
raise TypeError("expecting DbObject")
185184
obj_impl = <ThickDbObjectImpl> value._impl
186-
if var_impl is not None:
187-
if dpiVar_setFromObject(var_impl._handle, pos,
188-
obj_impl._handle) < 0:
189-
_raise_from_odpi()
190-
else:
191-
dbvalue.asObject = obj_impl._handle
185+
dbvalue.asObject = obj_impl._handle
192186
elif oracle_type == DPI_ORACLE_TYPE_CLOB \
193187
or oracle_type == DPI_ORACLE_TYPE_BLOB \
194188
or oracle_type == DPI_ORACLE_TYPE_NCLOB \
195189
or oracle_type == DPI_ORACLE_TYPE_BFILE:
196190
if isinstance(value, PY_TYPE_LOB):
197191
lob_impl = value._impl
198-
if var_impl is not None:
199-
if dpiVar_setFromLob(var_impl._handle, pos,
200-
lob_impl._handle) < 0:
201-
_raise_from_odpi()
202-
else:
203-
dbvalue.asLOB = lob_impl._handle
192+
dbvalue.asLOB = lob_impl._handle
204193
else:
205194
buf.set_value(value)
206195
dbvalue.asBytes.ptr = buf.ptr

src/oracledb/impl/thick/var.pyx

Lines changed: 107 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,11 @@ cdef class ThickVarImpl(BaseVarImpl):
4949
dpiDataBuffer *dbvalue
5050
const char *name_ptr
5151
bytes name_bytes
52+
object cursor
5253
if self.dbtype.num == DB_TYPE_NUM_CURSOR:
53-
for i in range(self.num_elements):
54+
for i, cursor in enumerate(self._values):
55+
if cursor is not None and cursor._impl is None:
56+
errors._raise_err(errors.ERR_CURSOR_NOT_OPEN)
5457
if self._data[i].isNull:
5558
continue
5659
dbvalue = &self._data[i].value
@@ -93,6 +96,12 @@ cdef class ThickVarImpl(BaseVarImpl):
9396
Internal method that finalizes initialization of the variable.
9497
"""
9598
BaseVarImpl._finalize_init(self)
99+
if self.dbtype._native_num in (
100+
DPI_NATIVE_TYPE_LOB,
101+
DPI_NATIVE_TYPE_OBJECT,
102+
DPI_NATIVE_TYPE_STMT,
103+
):
104+
self._values = [None] * self.num_elements
96105
self._create_handle()
97106

98107
cdef list _get_array_value(self):
@@ -103,18 +112,66 @@ cdef class ThickVarImpl(BaseVarImpl):
103112
return [self._get_scalar_value(i) \
104113
for i in range(self.num_elements_in_array)]
105114

106-
cdef object _get_cursor_value(self, dpiDataBuffer *dbvalue):
115+
cdef object _get_cursor_value(self, dpiDataBuffer *dbvalue, uint32_t pos):
116+
"""
117+
Returns the cursor stored in the variable at the given position. If a
118+
cursor was previously available, use it instead of creating a new one.
119+
"""
107120
cdef:
108121
ThickCursorImpl cursor_impl
109122
object cursor
110-
cursor = self._conn.cursor()
123+
cursor = self._values[pos]
124+
if cursor is None:
125+
cursor = self._values[pos] = self._conn.cursor()
111126
cursor_impl = <ThickCursorImpl> cursor._impl
112127
if dpiStmt_addRef(dbvalue.asStmt) < 0:
113128
_raise_from_odpi()
114129
cursor_impl._handle = dbvalue.asStmt
115130
cursor_impl._fixup_ref_cursor = True
116131
return cursor
117132

133+
cdef object _get_dbobject_value(self, dpiDataBuffer *dbvalue,
134+
uint32_t pos):
135+
"""
136+
Returns the DbObject stored in the variable at the given position. If a
137+
DbObject was previously available, use it instead of creating a new
138+
one, unless the handle has changed.
139+
"""
140+
cdef:
141+
ThickDbObjectImpl obj_impl
142+
object obj
143+
obj = self._values[pos]
144+
if obj is not None:
145+
obj_impl = <ThickDbObjectImpl> obj._impl
146+
if obj_impl._handle == dbvalue.asObject:
147+
return obj
148+
obj_impl = ThickDbObjectImpl.__new__(ThickDbObjectImpl)
149+
obj_impl.type = self.objtype
150+
if dpiObject_addRef(dbvalue.asObject) < 0:
151+
_raise_from_odpi()
152+
obj_impl._handle = dbvalue.asObject
153+
obj = self._values[pos] = PY_TYPE_DB_OBJECT._from_impl(obj_impl)
154+
return obj
155+
156+
cdef object _get_lob_value(self, dpiDataBuffer *dbvalue, uint32_t pos):
157+
"""
158+
Returns the LOB stored in the variable at the given position. If a LOB
159+
was previously created, use it instead of creating a new one, unless
160+
the handle has changed.
161+
"""
162+
cdef:
163+
ThickLobImpl lob_impl
164+
object lob
165+
lob = self._values[pos]
166+
if lob is not None:
167+
lob_impl = <ThickLobImpl> lob._impl
168+
if lob_impl._handle == dbvalue.asLOB:
169+
return lob
170+
lob_impl = ThickLobImpl._create(self._conn_impl, self.dbtype,
171+
dbvalue.asLOB)
172+
lob = self._values[pos] = PY_TYPE_LOB._from_impl(lob_impl)
173+
return lob
174+
118175
cdef object _get_scalar_value(self, uint32_t pos):
119176
"""
120177
Internal method to return the value of the variable at the given
@@ -182,6 +239,10 @@ cdef class ThickVarImpl(BaseVarImpl):
182239
dpiVar_release(orig_handle)
183240

184241
cdef int _set_cursor_value(self, object cursor, uint32_t pos) except -1:
242+
"""
243+
Sets a cursor value in the variable. If the cursor does not have a
244+
statement handle already, associate the one created by the variable.
245+
"""
185246
cdef:
186247
ThickCursorImpl cursor_impl = cursor._impl
187248
dpiData *data
@@ -197,9 +258,30 @@ cdef class ThickVarImpl(BaseVarImpl):
197258
if dpiStmt_addRef(data.value.asStmt) < 0:
198259
_raise_from_odpi()
199260
cursor_impl._handle = data.value.asStmt
261+
self._values[pos] = cursor
200262
cursor_impl._fixup_ref_cursor = True
201263
cursor_impl.statement = None
202264

265+
cdef int _set_dbobject_value(self, object obj, uint32_t pos) except -1:
266+
"""
267+
Sets an object value in the variable. The object is retained so that
268+
multiple calls to getvalue() return the same instance.
269+
"""
270+
cdef ThickDbObjectImpl obj_impl = <ThickDbObjectImpl> obj._impl
271+
if dpiVar_setFromObject(self._handle, pos, obj_impl._handle) < 0:
272+
_raise_from_odpi()
273+
self._values[pos] = obj
274+
275+
cdef int _set_lob_value(self, object lob, uint32_t pos) except -1:
276+
"""
277+
Sets a LOB value in the variable. The LOB is retained so that multiple
278+
calls to getvalue() return the same instance.
279+
"""
280+
cdef ThickLobImpl lob_impl = <ThickLobImpl> lob._impl
281+
if dpiVar_setFromLob(self._handle, pos, lob_impl._handle) < 0:
282+
_raise_from_odpi()
283+
self._values[pos] = lob
284+
203285
cdef int _set_num_elements_in_array(self, uint32_t num_elements) except -1:
204286
"""
205287
Sets the number of elements in the array.
@@ -217,26 +299,26 @@ cdef class ThickVarImpl(BaseVarImpl):
217299
dpiDataBuffer temp_dbvalue
218300
dpiDataBuffer *dbvalue
219301
dpiBytes *as_bytes
220-
bint needs_set
221302
dpiData *data
222303
data = &self._data[pos]
223304
data.isNull = (value is None)
224305
if not data.isNull:
225-
if self.dbtype.num == DB_TYPE_NUM_CURSOR:
306+
if self.dbtype._native_num == DPI_NATIVE_TYPE_STMT:
226307
self._set_cursor_value(value, pos)
308+
elif self.dbtype._native_num == DPI_NATIVE_TYPE_LOB:
309+
self._set_lob_value(value, pos)
310+
elif self.dbtype._native_num == DPI_NATIVE_TYPE_OBJECT:
311+
self._set_dbobject_value(value, pos)
227312
else:
228-
needs_set = self.dbtype._native_num == DPI_NATIVE_TYPE_BYTES \
229-
or (self.dbtype._native_num == DPI_NATIVE_TYPE_LOB \
230-
and not isinstance(value, PY_TYPE_LOB))
231-
if needs_set:
313+
if self.dbtype._native_num == DPI_NATIVE_TYPE_BYTES:
232314
dbvalue = &temp_dbvalue
233315
else:
234316
dbvalue = &data.value
235317
if self._buf is None:
236318
self._buf = StringBuffer.__new__(StringBuffer)
237319
_convert_from_python(value, self.dbtype, self.objtype, dbvalue,
238-
self._buf, self, pos)
239-
if needs_set:
320+
self._buf)
321+
if self.dbtype._native_num == DPI_NATIVE_TYPE_BYTES:
240322
as_bytes = &dbvalue.asBytes
241323
if dpiVar_setFromBytes(self._handle, pos, as_bytes.ptr,
242324
as_bytes.length) < 0:
@@ -270,15 +352,20 @@ cdef class ThickVarImpl(BaseVarImpl):
270352
object value
271353
data = &data[pos]
272354
if not data.isNull:
273-
if self.dbtype.num == DB_TYPE_NUM_CURSOR:
274-
return self._get_cursor_value(&data.value)
275-
if self.encoding_errors is not None:
276-
encoding_errors_bytes = self.encoding_errors.encode()
277-
encoding_errors = encoding_errors_bytes
278-
value = _convert_to_python(self._conn_impl, self.dbtype,
279-
self.objtype, &data.value,
280-
self._preferred_num_type,
281-
self.bypass_decode, encoding_errors)
355+
if self.dbtype._native_num == DPI_NATIVE_TYPE_STMT:
356+
value = self._get_cursor_value(&data.value, pos)
357+
elif self.dbtype._native_num == DPI_NATIVE_TYPE_LOB:
358+
value = self._get_lob_value(&data.value, pos)
359+
elif self.dbtype._native_num == DPI_NATIVE_TYPE_OBJECT:
360+
value = self._get_dbobject_value(&data.value, pos)
361+
else:
362+
if self.encoding_errors is not None:
363+
encoding_errors_bytes = self.encoding_errors.encode()
364+
encoding_errors = encoding_errors_bytes
365+
value = _convert_to_python(self._conn_impl, self.dbtype,
366+
self.objtype, &data.value,
367+
self._preferred_num_type,
368+
self.bypass_decode, encoding_errors)
282369
if self.outconverter is not None:
283370
value = self.outconverter(value)
284371
return value

src/oracledb/impl/thin/messages.pyx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,8 @@ cdef class MessageWithData(Message):
10821082
buf.write_binary_float(value)
10831083
elif ora_type_num == TNS_DATA_TYPE_CURSOR:
10841084
cursor_impl = value._impl
1085+
if cursor_impl is None:
1086+
errors._raise_err(errors.ERR_CURSOR_NOT_OPEN)
10851087
if cursor_impl._statement is None:
10861088
cursor_impl._statement = self.conn_impl._get_statement()
10871089
if cursor_impl._statement._cursor_id == 0:

src/oracledb/impl/thin/var.pyx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131

3232
cdef class ThinVarImpl(BaseVarImpl):
3333
cdef:
34-
list _values
3534
object _conv_func
3635
object _last_raw_value
3736

tests/test_1300_cursor_var.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ def type_handler(cursor, metadata):
230230
)
231231
ref_cursor = var.getvalue()
232232
self.assertEqual(ref_cursor.fetchall(), [(string_val,)])
233+
self.assertIs(var.getvalue(), ref_cursor)
233234

234235
def test_1310(self):
235236
"1310 - bind a REF cursor but never open it"
@@ -400,6 +401,47 @@ def test_1315(self):
400401
],
401402
)
402403

404+
def test_1316(self):
405+
"1316 - test using a closed ref cursor for OUT bind"
406+
value = "test 1316a"
407+
sql = """
408+
declare
409+
t_Cursor sys_refcursor;
410+
begin
411+
open t_Cursor for
412+
select :value
413+
from dual;
414+
:cursor := t_Cursor;
415+
end;
416+
"""
417+
var = self.cursor.var(oracledb.DB_TYPE_CURSOR)
418+
self.cursor.execute(sql, [value, var])
419+
ref_cursor = var.getvalue()
420+
self.assertEqual(ref_cursor.fetchall(), [(value,)])
421+
ref_cursor.close()
422+
with self.assertRaisesFullCode("DPY-1006"):
423+
self.cursor.execute(sql, [value, var])
424+
425+
def test_1317(self):
426+
"1317 - test binding a closed cursor"
427+
ref_cursor = self.conn.cursor()
428+
ref_cursor.close()
429+
with self.assertRaisesFullCode("DPY-1006"):
430+
self.cursor.callproc(
431+
"pkg_testRefCursors.TestInCursor", [ref_cursor]
432+
)
433+
434+
def test_1318(self):
435+
"1318 - test ref cursor doesn't work after connection is closed"
436+
conn = test_env.get_connection()
437+
cursor = conn.cursor()
438+
var = cursor.var(oracledb.DB_TYPE_CURSOR)
439+
cursor.callproc("myrefcursorproc", [var])
440+
conn.close()
441+
with self.assertRaisesFullCode("DPY-1001"):
442+
ref_cursor = var.getvalue()
443+
ref_cursor.fetchall()
444+
403445

404446
if __name__ == "__main__":
405447
test_env.run_test_cases()

tests/test_1900_lob_var.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,18 @@ def test_1937(self):
602602
with self.assertRaisesFullCode("DPY-3026"):
603603
lob.fileexists()
604604

605+
def test_1938(self):
606+
"1938 - confirm that LOB objects are retained across getvalue() calls"
607+
for typ in (
608+
oracledb.DB_TYPE_BLOB,
609+
oracledb.DB_TYPE_CLOB,
610+
oracledb.DB_TYPE_NCLOB,
611+
):
612+
var = self.cursor.var(typ)
613+
lob = self.conn.createlob(typ, "Some data for test 1938")
614+
var.setvalue(0, lob)
615+
self.assertIs(var.getvalue(), lob)
616+
605617

606618
if __name__ == "__main__":
607619
test_env.run_test_cases()

tests/test_2300_object_var.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,14 @@ def test_2339(self):
803803
self.assertEqual(obj.XMLVALUE, xml_val)
804804
self.assertEqual(obj.STRINGVALUE, str_val)
805805

806+
def test_2340(self):
807+
"2340 - test DbObject instances are retained across getvalue() calls"
808+
typ = self.conn.gettype("UDT_OBJECT")
809+
obj = typ.newobject()
810+
var = self.cursor.var(typ)
811+
var.setvalue(0, obj)
812+
self.assertIs(var.getvalue(), obj)
813+
806814

807815
if __name__ == "__main__":
808816
test_env.run_test_cases()

0 commit comments

Comments
 (0)