Browse Source

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.
Mariusz Felisiak 7 years ago
parent
commit
489421b015

+ 3 - 0
django/db/backends/base/features.py

@@ -240,6 +240,9 @@ class BaseDatabaseFeatures:
     create_test_procedure_without_params_sql = None
     create_test_procedure_with_int_param_sql = None
 
+    # Does the backend support keyword parameters for cursor.callproc()?
+    supports_callproc_kwargs = False
+
     def __init__(self, connection):
         self.connection = connection
 

+ 1 - 0
django/db/backends/oracle/features.py

@@ -54,3 +54,4 @@ class DatabaseFeatures(BaseDatabaseFeatures):
             V_I := P_I;
         END;
     """
+    supports_callproc_kwargs = True

+ 14 - 3
django/db/backends/utils.py

@@ -6,6 +6,7 @@ import re
 from time import time
 
 from django.conf import settings
+from django.db.utils import NotSupportedError
 from django.utils.encoding import force_bytes
 from django.utils.timezone import utc
 
@@ -45,13 +46,23 @@ class CursorWrapper:
     # The following methods cannot be implemented in __getattr__, because the
     # code must run when the method is invoked, not just when it is accessed.
 
-    def callproc(self, procname, params=None):
+    def callproc(self, procname, params=None, kparams=None):
+        # Keyword parameters for callproc aren't supported in PEP 249, but the
+        # database driver may support them (e.g. cx_Oracle).
+        if kparams is not None and not self.db.features.supports_callproc_kwargs:
+            raise NotSupportedError(
+                'Keyword parameters for callproc are not supported on this '
+                'database backend.'
+            )
         self.db.validate_no_broken_transaction()
         with self.db.wrap_database_errors:
-            if params is None:
+            if params is None and kparams is None:
                 return self.cursor.callproc(procname)
-            else:
+            elif kparams is None:
                 return self.cursor.callproc(procname, params)
+            else:
+                params = params or ()
+                return self.cursor.callproc(procname, params, kparams)
 
     def execute(self, sql, params=None):
         self.db.validate_no_broken_transaction()

+ 4 - 0
docs/releases/2.0.txt

@@ -269,6 +269,10 @@ Models
 * The new ``field_name`` parameter of :meth:`.QuerySet.in_bulk` allows fetching
   results based on any unique model field.
 
+* :meth:`.CursorWrapper.callproc()` now takes an optional dictionary of keyword
+  parameters, if the backend supports this feature. Of Django's built-in
+  backends, only Oracle supports it.
+
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 

+ 9 - 3
docs/topics/db/sql.txt

@@ -350,10 +350,12 @@ is equivalent to::
 Calling stored procedures
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. method:: CursorWrapper.callproc(procname, params=None)
+.. method:: CursorWrapper.callproc(procname, params=None, kparams=None)
 
-    Calls a database stored procedure with the given name and optional sequence
-    of input parameters.
+    Calls a database stored procedure with the given name. A sequence
+    (``params``) or dictionary (``kparams``) of input parameters may be
+    provided. Most databases don't support ``kparams``. Of Django's built-in
+    backends, only Oracle supports it.
 
     For example, given this stored procedure in an Oracle database:
 
@@ -372,3 +374,7 @@ Calling stored procedures
 
         with connection.cursor() as cursor:
             cursor.callproc('test_procedure', [1, 'test'])
+
+    .. versionchanged:: 2.0
+
+        The ``kparams`` argument was added.

+ 15 - 3
tests/backends/test_utils.py

@@ -3,8 +3,9 @@ from decimal import Decimal, Rounded
 
 from django.db import connection
 from django.db.backends.utils import format_number, truncate_name
+from django.db.utils import NotSupportedError
 from django.test import (
-    SimpleTestCase, TransactionTestCase, skipUnlessDBFeature,
+    SimpleTestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
 )
 
 
@@ -53,13 +54,13 @@ class TestUtils(SimpleTestCase):
 class CursorWrapperTests(TransactionTestCase):
     available_apps = []
 
-    def _test_procedure(self, procedure_sql, params, param_types):
+    def _test_procedure(self, procedure_sql, params, param_types, kparams=None):
         with connection.cursor() as cursor:
             cursor.execute(procedure_sql)
         # Use a new cursor because in MySQL a procedure can't be used in the
         # same cursor in which it was created.
         with connection.cursor() as cursor:
-            cursor.callproc('test_procedure', params)
+            cursor.callproc('test_procedure', params, kparams)
         with connection.schema_editor() as editor:
             editor.remove_procedure('test_procedure', param_types)
 
@@ -70,3 +71,14 @@ class CursorWrapperTests(TransactionTestCase):
     @skipUnlessDBFeature('create_test_procedure_with_int_param_sql')
     def test_callproc_with_int_params(self):
         self._test_procedure(connection.features.create_test_procedure_with_int_param_sql, [1], ['INTEGER'])
+
+    @skipUnlessDBFeature('create_test_procedure_with_int_param_sql', 'supports_callproc_kwargs')
+    def test_callproc_kparams(self):
+        self._test_procedure(connection.features.create_test_procedure_with_int_param_sql, [], ['INTEGER'], {'P_I': 1})
+
+    @skipIfDBFeature('supports_callproc_kwargs')
+    def test_unsupported_callproc_kparams_raises_error(self):
+        msg = 'Keyword parameters for callproc are not supported on this database backend.'
+        with self.assertRaisesMessage(NotSupportedError, msg):
+            with connection.cursor() as cursor:
+                cursor.callproc('test_procedure', [], {'P_I': 1})