Skip to content

Commit 99ad710

Browse files
Add check for bind variables duplicated in DML returning statement.
1 parent 3fbee66 commit 99ad710

File tree

5 files changed

+56
-4
lines changed

5 files changed

+56
-4
lines changed

Diff for: doc/src/release_notes.rst

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ Thin Mode Changes
1616
#) Fixed bug when calling :meth:`~Connection.gettype()` with an object type
1717
name containing ``%ROWTYPE``
1818
(`issue 304 <https://github.com/oracle/python-oracledb/issues/304>`__).
19+
#) Error ``DPY-2048: the bind variable placeholder ":{name}" cannot be used
20+
both before and after the RETURNING clause in a DML RETURNING statement``
21+
is now raised when the same bind variable placeholder name is used both
22+
before and after the RETURNING clause in a
23+
:ref:`DML RETURNING statement <dml-returning-bind>`. Previously, various
24+
internal errors were raised.
1925
#) Tightened up code looking for the end of a database request.
2026
#) Packet output is now immediately flushed in order to avoid losing output
2127
due to buffering when multiple threads are running.

Diff for: doc/src/user_guide/bind.rst

+24-3
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@ UROWID can be bound with type :attr:`oracledb.DB_TYPE_ROWID`.
291291
See :ref:`querymetadatadiff`.
292292

293293

294+
.. _dml-returning-bind:
295+
294296
DML RETURNING Bind Variables
295297
============================
296298

@@ -315,9 +317,28 @@ In the above example, since the WHERE clause matches only one row, the output
315317
contains a single item in the list. If the WHERE clause matched multiple rows,
316318
the output would contain as many items as there were rows that were updated.
317319

318-
No duplicate binds are allowed in a DML statement with a RETURNING clause, and
319-
no duplication is allowed between bind variables in the DML section and the
320-
RETURNING section of the statement.
320+
The same bind variable placeholder name cannot be used both before and after
321+
the RETURNING clause. For example, if the ``:dept_name`` bind variable is used
322+
both before and after the RETURNING clause:
323+
324+
.. code-block:: python
325+
326+
# a variable cannot be used for both input and output in a DML returning
327+
# statement
328+
dept_name_var = cursor.var(str)
329+
dept_name_var.setvalue(0, 'Input Department')
330+
cursor.execute("""
331+
update departments set
332+
department_name = :dept_name || ' EXTRA TEXT'
333+
returning department_name into :dept_name""",
334+
dept_name=dept_name_var)
335+
336+
The above example will not update the bind variable as expected, but no error
337+
will be raised if you are using python-oracledb Thick mode. With
338+
python-oracledb Thin mode, the above example returns the following error::
339+
340+
DPY-2048: the bind variable placeholder ":dept_name" cannot be used
341+
both before and after the RETURNING clause in a DML RETURNING statement
321342

322343

323344
LOB Bind Variables

Diff for: src/oracledb/errors.py

+5
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def _raise_err(
241241
ERR_INVALID_ARRAYSIZE = 2045
242242
ERR_CURSOR_HAS_BEEN_CLOSED = 2046
243243
ERR_INVALID_LOB_AMOUNT = 2047
244+
ERR_DML_RETURNING_DUP_BINDS = 2048
244245

245246
# error numbers that result in NotSupportedError
246247
ERR_TIME_NOT_SUPPORTED = 3000
@@ -463,6 +464,10 @@ def _raise_err(
463464
"(actual: {actual_size}, maximum: {max_size})"
464465
),
465466
ERR_DB_TYPE_NOT_SUPPORTED: 'database type "{name}" is not supported',
467+
ERR_DML_RETURNING_DUP_BINDS: (
468+
'the bind variable placeholder ":{name}" cannot be used both before '
469+
"and after the RETURNING clause in a DML RETURNING statement"
470+
),
466471
ERR_DUPLICATED_PARAMETER: (
467472
'"{deprecated_name}" and "{new_name}" cannot be specified together'
468473
),

Diff for: src/oracledb/impl/thin/statement.pyx

+6-1
Original file line numberDiff line numberDiff line change
@@ -353,11 +353,16 @@ cdef class Statement:
353353
Add bind information to the statement by examining the passed SQL for
354354
bind variable names.
355355
"""
356-
cdef BindInfo info
356+
cdef BindInfo info, orig_info
357357
if not self._is_plsql or name not in self._bind_info_dict:
358358
info = BindInfo(name, self._is_returning)
359359
self._bind_info_list.append(info)
360360
if info._bind_name in self._bind_info_dict:
361+
if self._is_returning:
362+
orig_info = self._bind_info_dict[info._bind_name][-1]
363+
if not orig_info._is_return_bind:
364+
errors._raise_err(errors.ERR_DML_RETURNING_DUP_BINDS,
365+
name=info._bind_name)
361366
self._bind_info_dict[info._bind_name].append(info)
362367
else:
363368
self._bind_info_dict[info._bind_name] = [info]

Diff for: tests/test_1600_dml_returning.py

+15
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"""
2828

2929
import datetime
30+
import unittest
3031

3132
import oracledb
3233
import test_env
@@ -525,6 +526,20 @@ def test_1622(self):
525526
self.cursor.execute(sql, in_val=25, out_val=out_val)
526527
self.assertEqual(out_val.getvalue(), [25])
527528

529+
@unittest.skipUnless(test_env.get_is_thin(), "cannot be checked")
530+
def test_1623(self):
531+
"1623 - execute DML returning with duplicated binds"
532+
self.cursor.execute("truncate table TestTempTable")
533+
str_val = self.cursor.var(str)
534+
str_val.setvalue(0, "Test Data")
535+
sql = """
536+
insert into TestTempTable (IntCol, StringCol1)
537+
values (:id_val, :str_val || ' (Additional String)')
538+
returning StringCol1 into :str_val
539+
"""
540+
with self.assertRaisesFullCode("DPY-2048"):
541+
self.cursor.execute(sql, id_val=1, str_val=str_val)
542+
528543

529544
if __name__ == "__main__":
530545
test_env.run_test_cases()

0 commit comments

Comments
 (0)