Skip to content

Commit 489421b

Browse files
authored
Fixed #23546 -- Added kwargs support for CursorWrapper.callproc() on Oracle.
Thanks Shai Berger, Tim Graham and Aymeric Augustin for reviews and Renbi Yu for the initial patch.
1 parent 47ccefe commit 489421b

File tree

6 files changed

+46
-9
lines changed

6 files changed

+46
-9
lines changed

django/db/backends/base/features.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ class BaseDatabaseFeatures:
240240
create_test_procedure_without_params_sql = None
241241
create_test_procedure_with_int_param_sql = None
242242

243+
# Does the backend support keyword parameters for cursor.callproc()?
244+
supports_callproc_kwargs = False
245+
243246
def __init__(self, connection):
244247
self.connection = connection
245248

django/db/backends/oracle/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ class DatabaseFeatures(BaseDatabaseFeatures):
5454
V_I := P_I;
5555
END;
5656
"""
57+
supports_callproc_kwargs = True

django/db/backends/utils.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from time import time
77

88
from django.conf import settings
9+
from django.db.utils import NotSupportedError
910
from django.utils.encoding import force_bytes
1011
from django.utils.timezone import utc
1112

@@ -45,13 +46,23 @@ def __exit__(self, type, value, traceback):
4546
# The following methods cannot be implemented in __getattr__, because the
4647
# code must run when the method is invoked, not just when it is accessed.
4748

48-
def callproc(self, procname, params=None):
49+
def callproc(self, procname, params=None, kparams=None):
50+
# Keyword parameters for callproc aren't supported in PEP 249, but the
51+
# database driver may support them (e.g. cx_Oracle).
52+
if kparams is not None and not self.db.features.supports_callproc_kwargs:
53+
raise NotSupportedError(
54+
'Keyword parameters for callproc are not supported on this '
55+
'database backend.'
56+
)
4957
self.db.validate_no_broken_transaction()
5058
with self.db.wrap_database_errors:
51-
if params is None:
59+
if params is None and kparams is None:
5260
return self.cursor.callproc(procname)
53-
else:
61+
elif kparams is None:
5462
return self.cursor.callproc(procname, params)
63+
else:
64+
params = params or ()
65+
return self.cursor.callproc(procname, params, kparams)
5566

5667
def execute(self, sql, params=None):
5768
self.db.validate_no_broken_transaction()

docs/releases/2.0.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,10 @@ Models
269269
* The new ``field_name`` parameter of :meth:`.QuerySet.in_bulk` allows fetching
270270
results based on any unique model field.
271271

272+
* :meth:`.CursorWrapper.callproc()` now takes an optional dictionary of keyword
273+
parameters, if the backend supports this feature. Of Django's built-in
274+
backends, only Oracle supports it.
275+
272276
Requests and Responses
273277
~~~~~~~~~~~~~~~~~~~~~~
274278

docs/topics/db/sql.txt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,10 +350,12 @@ is equivalent to::
350350
Calling stored procedures
351351
~~~~~~~~~~~~~~~~~~~~~~~~~
352352

353-
.. method:: CursorWrapper.callproc(procname, params=None)
353+
.. method:: CursorWrapper.callproc(procname, params=None, kparams=None)
354354

355-
Calls a database stored procedure with the given name and optional sequence
356-
of input parameters.
355+
Calls a database stored procedure with the given name. A sequence
356+
(``params``) or dictionary (``kparams``) of input parameters may be
357+
provided. Most databases don't support ``kparams``. Of Django's built-in
358+
backends, only Oracle supports it.
357359

358360
For example, given this stored procedure in an Oracle database:
359361

@@ -372,3 +374,7 @@ Calling stored procedures
372374

373375
with connection.cursor() as cursor:
374376
cursor.callproc('test_procedure', [1, 'test'])
377+
378+
.. versionchanged:: 2.0
379+
380+
The ``kparams`` argument was added.

tests/backends/test_utils.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
from django.db import connection
55
from django.db.backends.utils import format_number, truncate_name
6+
from django.db.utils import NotSupportedError
67
from django.test import (
7-
SimpleTestCase, TransactionTestCase, skipUnlessDBFeature,
8+
SimpleTestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
89
)
910

1011

@@ -53,13 +54,13 @@ def equal(value, max_d, places, result):
5354
class CursorWrapperTests(TransactionTestCase):
5455
available_apps = []
5556

56-
def _test_procedure(self, procedure_sql, params, param_types):
57+
def _test_procedure(self, procedure_sql, params, param_types, kparams=None):
5758
with connection.cursor() as cursor:
5859
cursor.execute(procedure_sql)
5960
# Use a new cursor because in MySQL a procedure can't be used in the
6061
# same cursor in which it was created.
6162
with connection.cursor() as cursor:
62-
cursor.callproc('test_procedure', params)
63+
cursor.callproc('test_procedure', params, kparams)
6364
with connection.schema_editor() as editor:
6465
editor.remove_procedure('test_procedure', param_types)
6566

@@ -70,3 +71,14 @@ def test_callproc_without_params(self):
7071
@skipUnlessDBFeature('create_test_procedure_with_int_param_sql')
7172
def test_callproc_with_int_params(self):
7273
self._test_procedure(connection.features.create_test_procedure_with_int_param_sql, [1], ['INTEGER'])
74+
75+
@skipUnlessDBFeature('create_test_procedure_with_int_param_sql', 'supports_callproc_kwargs')
76+
def test_callproc_kparams(self):
77+
self._test_procedure(connection.features.create_test_procedure_with_int_param_sql, [], ['INTEGER'], {'P_I': 1})
78+
79+
@skipIfDBFeature('supports_callproc_kwargs')
80+
def test_unsupported_callproc_kparams_raises_error(self):
81+
msg = 'Keyword parameters for callproc are not supported on this database backend.'
82+
with self.assertRaisesMessage(NotSupportedError, msg):
83+
with connection.cursor() as cursor:
84+
cursor.callproc('test_procedure', [], {'P_I': 1})

0 commit comments

Comments
 (0)