From 292b9e6fe8f23491680d9cc60f328562e2b1c823 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Fri, 21 Mar 2025 21:50:54 -0400 Subject: [PATCH] Refs #27222 -- Adapted RETURNING handling to be usable for UPDATE queries. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed existing methods and abstractions used for INSERT … RETURNING to be generic enough to be used in the context of UPDATEs as well. This also consolidates SQL compliant implementations on BaseDatabaseOperations. --- django/db/backends/base/operations.py | 22 +++++------- django/db/backends/oracle/operations.py | 48 ++++++++++++------------- django/db/backends/oracle/utils.py | 2 +- django/db/models/sql/compiler.py | 20 +++++------ docs/releases/6.0.txt | 12 +++++++ 5 files changed, 51 insertions(+), 53 deletions(-) diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py index f1b0c09abd..16a6296f9b 100644 --- a/django/db/backends/base/operations.py +++ b/django/db/backends/base/operations.py @@ -208,13 +208,6 @@ class BaseDatabaseOperations: else: return ["DISTINCT"], [] - def fetch_returned_insert_columns(self, cursor, returning_params): - """ - Given a cursor object that has just performed an INSERT...RETURNING - statement into a table, return the newly created data. - """ - return cursor.fetchone() - def force_group_by(self): """ Return a GROUP BY clause to use with a HAVING clause when no grouping @@ -358,11 +351,12 @@ class BaseDatabaseOperations: """ return value - def return_insert_columns(self, fields): + def returning_columns(self, fields): """ - For backends that support returning columns as part of an insert query, - return the SQL and params to append to the INSERT query. The returned - fragment should contain a format string to hold the appropriate column. + For backends that support returning columns as part of an insert or + update query, return the SQL and params to append to the query. + The returned fragment should contain a format string to hold the + appropriate column. """ if not fields: return "", () @@ -376,10 +370,10 @@ class BaseDatabaseOperations: ] return "RETURNING %s" % ", ".join(columns), () - def fetch_returned_insert_rows(self, cursor): + def fetch_returned_rows(self, cursor, returning_params): """ - Given a cursor object that has just performed an INSERT...RETURNING - statement into a table, return the tuple of returned data. + Given a cursor object for a DML query with a RETURNING statement, + return the selected returning rows of tuples. """ return cursor.fetchall() diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index ce9ed7288d..bc152c4e6e 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -22,7 +22,7 @@ from django.utils.functional import cached_property from django.utils.regex_helper import _lazy_re_compile from .base import Database -from .utils import BulkInsertMapper, InsertVar, Oracle_datetime +from .utils import BoundVar, BulkInsertMapper, Oracle_datetime class DatabaseOperations(BaseDatabaseOperations): @@ -298,12 +298,27 @@ END; def deferrable_sql(self): return " DEFERRABLE INITIALLY DEFERRED" - def fetch_returned_insert_columns(self, cursor, returning_params): - columns = [] - for param in returning_params: - value = param.get_value() - columns.append(value[0]) - return tuple(columns) + def returning_columns(self, fields): + if not fields: + return "", () + field_names = [] + params = [] + for field in fields: + field_names.append( + "%s.%s" + % ( + self.quote_name(field.model._meta.db_table), + self.quote_name(field.column), + ) + ) + params.append(BoundVar(field)) + return "RETURNING %s INTO %s" % ( + ", ".join(field_names), + ", ".join(["%s"] * len(params)), + ), tuple(params) + + def fetch_returned_rows(self, cursor, returning_params): + return list(zip(*(param.get_value() for param in returning_params))) def no_limit_value(self): return None @@ -391,25 +406,6 @@ END; match_option = "'i'" return "REGEXP_LIKE(%%s, %%s, %s)" % match_option - def return_insert_columns(self, fields): - if not fields: - return "", () - field_names = [] - params = [] - for field in fields: - field_names.append( - "%s.%s" - % ( - self.quote_name(field.model._meta.db_table), - self.quote_name(field.column), - ) - ) - params.append(InsertVar(field)) - return "RETURNING %s INTO %s" % ( - ", ".join(field_names), - ", ".join(["%s"] * len(params)), - ), tuple(params) - def __foreign_key_constraints(self, table_name, recursive): with self.connection.cursor() as cursor: if recursive: diff --git a/django/db/backends/oracle/utils.py b/django/db/backends/oracle/utils.py index 57d97b3f77..fd7deab9a2 100644 --- a/django/db/backends/oracle/utils.py +++ b/django/db/backends/oracle/utils.py @@ -4,7 +4,7 @@ import decimal from .base import Database -class InsertVar: +class BoundVar: """ A late-binding cursor variable that can be passed to Cursor.execute as a parameter, in order to receive the id of the row created by an diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index f72ba907ad..73dfa5b87c 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1890,7 +1890,7 @@ class SQLInsertCompiler(SQLCompiler): result.append(on_conflict_suffix_sql) # Skip empty r_sql to allow subclasses to customize behavior for # 3rd party backends. Refs #19096. - r_sql, self.returning_params = self.connection.ops.return_insert_columns( + r_sql, self.returning_params = self.connection.ops.returning_columns( self.returning_fields ) if r_sql: @@ -1925,20 +1925,16 @@ class SQLInsertCompiler(SQLCompiler): cursor.execute(sql, params) if not self.returning_fields: return [] + obj_len = len(self.query.objs) if ( self.connection.features.can_return_rows_from_bulk_insert - and len(self.query.objs) > 1 + and obj_len > 1 + ) or ( + self.connection.features.can_return_columns_from_insert and obj_len == 1 ): - rows = self.connection.ops.fetch_returned_insert_rows(cursor) - cols = [field.get_col(opts.db_table) for field in self.returning_fields] - elif self.connection.features.can_return_columns_from_insert: - assert len(self.query.objs) == 1 - rows = [ - self.connection.ops.fetch_returned_insert_columns( - cursor, - self.returning_params, - ) - ] + rows = self.connection.ops.fetch_returned_rows( + cursor, self.returning_params + ) cols = [field.get_col(opts.db_table) for field in self.returning_fields] elif returning_fields and isinstance( returning_field := returning_fields[0], AutoField diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index 94376b9994..a9fe1873aa 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -402,6 +402,18 @@ backends. * :class:`~django.db.backends.base.schema.BaseDatabaseSchemaEditor` and PostgreSQL backends no longer use ``CASCADE`` when dropping a column. +* ``DatabaseOperations.return_insert_columns()`` and + ``DatabaseOperations.fetch_returned_insert_rows()`` methods are renamed to + ``returning_columns()`` and ``fetch_returned_rows()``, respectively, to + denote they can be used in the context ``UPDATE … RETURNING`` statements as + well as ``INSERT … RETURNING``. + +* The ``DatabaseOperations.fetch_returned_insert_columns()`` method is removed + and the ``fetch_returned_rows()`` method replacing + ``fetch_returned_insert_rows()`` expects both a ``cursor`` and + ``returning_params`` to be provided just like + ``fetch_returned_insert_columns()`` did. + Dropped support for MariaDB 10.5 --------------------------------