diff --git a/django/db/models/query.py b/django/db/models/query.py index e1f785f714..1a44523374 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -26,7 +26,7 @@ from django.db.models.deletion import Collector from django.db.models.expressions import Case, F, Value, When from django.db.models.functions import Cast, Trunc from django.db.models.query_utils import FilteredRelation, Q -from django.db.models.sql.constants import CURSOR, GET_ITERATOR_CHUNK_SIZE +from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, ROW_COUNT from django.db.models.utils import ( AltersData, create_namedtuple_class, @@ -1209,11 +1209,7 @@ class QuerySet(AltersData): """ query = self.query.clone() query.__class__ = sql.DeleteQuery - cursor = query.get_compiler(using).execute_sql(CURSOR) - if cursor: - with cursor: - return cursor.rowcount - return 0 + return query.get_compiler(using).execute_sql(ROW_COUNT) _raw_delete.alters_data = True @@ -1252,7 +1248,7 @@ class QuerySet(AltersData): # Clear any annotations so that they won't be present in subqueries. query.annotations = {} with transaction.mark_for_rollback_on_error(using=self.db): - rows = query.get_compiler(self.db).execute_sql(CURSOR) + rows = query.get_compiler(self.db).execute_sql(ROW_COUNT) self._result_cache = None return rows @@ -1277,7 +1273,7 @@ class QuerySet(AltersData): # Clear any annotations so that they won't be present in subqueries. query.annotations = {} self._result_cache = None - return query.get_compiler(self.db).execute_sql(CURSOR) + return query.get_compiler(self.db).execute_sql(ROW_COUNT) _update.alters_data = True _update.queryset_only = False diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 49c5d301cc..5bb491d823 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -19,6 +19,7 @@ from django.db.models.sql.constants import ( MULTI, NO_RESULTS, ORDER_DIR, + ROW_COUNT, SINGLE, ) from django.db.models.sql.query import Query, get_order_dir @@ -1596,15 +1597,15 @@ class SQLCompiler: ): """ Run the query against the database and return the result(s). The - return value is a single data item if result_type is SINGLE, or an - iterator over the results if the result_type is MULTI. + return value depends on the value of result_type. - result_type is either MULTI (use fetchmany() to retrieve all rows), - SINGLE (only retrieve a single row), or None. In this last case, the - cursor is returned if any query is executed, since it's used by - subclasses such as InsertQuery). It's possible, however, that no query - is needed, as the filters describe an empty set. In that case, None is - returned, to avoid any unnecessary database interaction. + When result_type is: + - MULTI: Retrieves all rows using fetchmany(). Wraps in an iterator for + chunked reads when supported. + - SINGLE: Retrieves a single row using fetchone(). + - ROW_COUNT: Retrieves the number of rows in the result. + - CURSOR: Runs the query, and returns the cursor object. It is the + caller's responsibility to close the cursor. """ result_type = result_type or NO_RESULTS try: @@ -1627,6 +1628,11 @@ class SQLCompiler: cursor.close() raise + if result_type == ROW_COUNT: + try: + return cursor.rowcount + finally: + cursor.close() if result_type == CURSOR: # Give the caller the cursor to process and close. return cursor @@ -2069,19 +2075,19 @@ class SQLUpdateCompiler(SQLCompiler): non-empty query that is executed. Row counts for any subsequent, related queries are not available. """ - cursor = super().execute_sql(result_type) - try: - rows = cursor.rowcount if cursor else 0 - is_empty = cursor is None - finally: - if cursor: - cursor.close() + row_count = super().execute_sql(result_type) + is_empty = row_count is None + row_count = row_count or 0 + for query in self.query.get_related_updates(): - aux_rows = query.get_compiler(self.using).execute_sql(result_type) - if is_empty and aux_rows: - rows = aux_rows + # If the result_type is NO_RESULTS then the aux_row_count is None. + aux_row_count = query.get_compiler(self.using).execute_sql(result_type) + if is_empty and aux_row_count: + # Returns the row count for any related updates as the number of + # rows updated. + row_count = aux_row_count is_empty = False - return rows + return row_count def pre_sql_setup(self): """ diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index fdfb2ea891..709405b0df 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -11,8 +11,10 @@ GET_ITERATOR_CHUNK_SIZE = 100 # How many results to expect from a cursor.execute call MULTI = "multi" SINGLE = "single" -CURSOR = "cursor" NO_RESULTS = "no results" +# Rather than returning results, returns: +CURSOR = "cursor" +ROW_COUNT = "row count" ORDER_DIR = { "ASC": ("ASC", "DESC"), diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index f639eb8b82..b2810c8413 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -3,7 +3,11 @@ Query subclasses which provide extra functionality beyond simple data retrieval. """ from django.core.exceptions import FieldError -from django.db.models.sql.constants import CURSOR, GET_ITERATOR_CHUNK_SIZE, NO_RESULTS +from django.db.models.sql.constants import ( + GET_ITERATOR_CHUNK_SIZE, + NO_RESULTS, + ROW_COUNT, +) from django.db.models.sql.query import Query __all__ = ["DeleteQuery", "UpdateQuery", "InsertQuery", "AggregateQuery"] @@ -17,11 +21,7 @@ class DeleteQuery(Query): def do_query(self, table, where, using): self.alias_map = {table: self.alias_map[table]} self.where = where - cursor = self.get_compiler(using).execute_sql(CURSOR) - if cursor: - with cursor: - return cursor.rowcount - return 0 + return self.get_compiler(using).execute_sql(ROW_COUNT) def delete_batch(self, pk_list, using): """