mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	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.
This commit is contained in:
		| @@ -240,6 +240,9 @@ class BaseDatabaseFeatures: | |||||||
|     create_test_procedure_without_params_sql = None |     create_test_procedure_without_params_sql = None | ||||||
|     create_test_procedure_with_int_param_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): |     def __init__(self, connection): | ||||||
|         self.connection = connection |         self.connection = connection | ||||||
|  |  | ||||||
|   | |||||||
| @@ -54,3 +54,4 @@ class DatabaseFeatures(BaseDatabaseFeatures): | |||||||
|             V_I := P_I; |             V_I := P_I; | ||||||
|         END; |         END; | ||||||
|     """ |     """ | ||||||
|  |     supports_callproc_kwargs = True | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import re | |||||||
| from time import time | from time import time | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.db.utils import NotSupportedError | ||||||
| from django.utils.encoding import force_bytes | from django.utils.encoding import force_bytes | ||||||
| from django.utils.timezone import utc | from django.utils.timezone import utc | ||||||
|  |  | ||||||
| @@ -45,13 +46,23 @@ class CursorWrapper: | |||||||
|     # The following methods cannot be implemented in __getattr__, because the |     # The following methods cannot be implemented in __getattr__, because the | ||||||
|     # code must run when the method is invoked, not just when it is accessed. |     # 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() |         self.db.validate_no_broken_transaction() | ||||||
|         with self.db.wrap_database_errors: |         with self.db.wrap_database_errors: | ||||||
|             if params is None: |             if params is None and kparams is None: | ||||||
|                 return self.cursor.callproc(procname) |                 return self.cursor.callproc(procname) | ||||||
|             else: |             elif kparams is None: | ||||||
|                 return self.cursor.callproc(procname, params) |                 return self.cursor.callproc(procname, params) | ||||||
|  |             else: | ||||||
|  |                 params = params or () | ||||||
|  |                 return self.cursor.callproc(procname, params, kparams) | ||||||
|  |  | ||||||
|     def execute(self, sql, params=None): |     def execute(self, sql, params=None): | ||||||
|         self.db.validate_no_broken_transaction() |         self.db.validate_no_broken_transaction() | ||||||
|   | |||||||
| @@ -269,6 +269,10 @@ Models | |||||||
| * The new ``field_name`` parameter of :meth:`.QuerySet.in_bulk` allows fetching | * The new ``field_name`` parameter of :meth:`.QuerySet.in_bulk` allows fetching | ||||||
|   results based on any unique model field. |   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 | Requests and Responses | ||||||
| ~~~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -350,10 +350,12 @@ is equivalent to:: | |||||||
| Calling stored procedures | 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 |     Calls a database stored procedure with the given name. A sequence | ||||||
|     of input parameters. |     (``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: |     For example, given this stored procedure in an Oracle database: | ||||||
|  |  | ||||||
| @@ -372,3 +374,7 @@ Calling stored procedures | |||||||
|  |  | ||||||
|         with connection.cursor() as cursor: |         with connection.cursor() as cursor: | ||||||
|             cursor.callproc('test_procedure', [1, 'test']) |             cursor.callproc('test_procedure', [1, 'test']) | ||||||
|  |  | ||||||
|  |     .. versionchanged:: 2.0 | ||||||
|  |  | ||||||
|  |         The ``kparams`` argument was added. | ||||||
|   | |||||||
| @@ -3,8 +3,9 @@ from decimal import Decimal, Rounded | |||||||
|  |  | ||||||
| from django.db import connection | from django.db import connection | ||||||
| from django.db.backends.utils import format_number, truncate_name | from django.db.backends.utils import format_number, truncate_name | ||||||
|  | from django.db.utils import NotSupportedError | ||||||
| from django.test import ( | from django.test import ( | ||||||
|     SimpleTestCase, TransactionTestCase, skipUnlessDBFeature, |     SimpleTestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -53,13 +54,13 @@ class TestUtils(SimpleTestCase): | |||||||
| class CursorWrapperTests(TransactionTestCase): | class CursorWrapperTests(TransactionTestCase): | ||||||
|     available_apps = [] |     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: |         with connection.cursor() as cursor: | ||||||
|             cursor.execute(procedure_sql) |             cursor.execute(procedure_sql) | ||||||
|         # Use a new cursor because in MySQL a procedure can't be used in the |         # Use a new cursor because in MySQL a procedure can't be used in the | ||||||
|         # same cursor in which it was created. |         # same cursor in which it was created. | ||||||
|         with connection.cursor() as cursor: |         with connection.cursor() as cursor: | ||||||
|             cursor.callproc('test_procedure', params) |             cursor.callproc('test_procedure', params, kparams) | ||||||
|         with connection.schema_editor() as editor: |         with connection.schema_editor() as editor: | ||||||
|             editor.remove_procedure('test_procedure', param_types) |             editor.remove_procedure('test_procedure', param_types) | ||||||
|  |  | ||||||
| @@ -70,3 +71,14 @@ class CursorWrapperTests(TransactionTestCase): | |||||||
|     @skipUnlessDBFeature('create_test_procedure_with_int_param_sql') |     @skipUnlessDBFeature('create_test_procedure_with_int_param_sql') | ||||||
|     def test_callproc_with_int_params(self): |     def test_callproc_with_int_params(self): | ||||||
|         self._test_procedure(connection.features.create_test_procedure_with_int_param_sql, [1], ['INTEGER']) |         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}) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user