mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Fixed #35918 -- Refactored execute_sql to reduce cursor management.
This in particular adds support for execute_sql to return row counts directly
This commit is contained in:
parent
857b1048d5
commit
1469952b44
@ -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.expressions import Case, F, Value, When
|
||||||
from django.db.models.functions import Cast, Trunc
|
from django.db.models.functions import Cast, Trunc
|
||||||
from django.db.models.query_utils import FilteredRelation, Q
|
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 (
|
from django.db.models.utils import (
|
||||||
AltersData,
|
AltersData,
|
||||||
create_namedtuple_class,
|
create_namedtuple_class,
|
||||||
@ -1206,11 +1206,7 @@ class QuerySet(AltersData):
|
|||||||
"""
|
"""
|
||||||
query = self.query.clone()
|
query = self.query.clone()
|
||||||
query.__class__ = sql.DeleteQuery
|
query.__class__ = sql.DeleteQuery
|
||||||
cursor = query.get_compiler(using).execute_sql(CURSOR)
|
return query.get_compiler(using).execute_sql(ROW_COUNT)
|
||||||
if cursor:
|
|
||||||
with cursor:
|
|
||||||
return cursor.rowcount
|
|
||||||
return 0
|
|
||||||
|
|
||||||
_raw_delete.alters_data = True
|
_raw_delete.alters_data = True
|
||||||
|
|
||||||
@ -1249,7 +1245,7 @@ class QuerySet(AltersData):
|
|||||||
# Clear any annotations so that they won't be present in subqueries.
|
# Clear any annotations so that they won't be present in subqueries.
|
||||||
query.annotations = {}
|
query.annotations = {}
|
||||||
with transaction.mark_for_rollback_on_error(using=self.db):
|
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
|
self._result_cache = None
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@ -1274,7 +1270,7 @@ class QuerySet(AltersData):
|
|||||||
# Clear any annotations so that they won't be present in subqueries.
|
# Clear any annotations so that they won't be present in subqueries.
|
||||||
query.annotations = {}
|
query.annotations = {}
|
||||||
self._result_cache = None
|
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.alters_data = True
|
||||||
_update.queryset_only = False
|
_update.queryset_only = False
|
||||||
|
@ -17,6 +17,7 @@ from django.db.models.sql.constants import (
|
|||||||
MULTI,
|
MULTI,
|
||||||
NO_RESULTS,
|
NO_RESULTS,
|
||||||
ORDER_DIR,
|
ORDER_DIR,
|
||||||
|
ROW_COUNT,
|
||||||
SINGLE,
|
SINGLE,
|
||||||
)
|
)
|
||||||
from django.db.models.sql.query import Query, get_order_dir
|
from django.db.models.sql.query import Query, get_order_dir
|
||||||
@ -1560,12 +1561,19 @@ class SQLCompiler:
|
|||||||
return value is a single data item if result_type is SINGLE, or an
|
return value is a single data item if result_type is SINGLE, or an
|
||||||
iterator over the results if the result_type is MULTI.
|
iterator over the results if the result_type is MULTI.
|
||||||
|
|
||||||
result_type is either MULTI (use fetchmany() to retrieve all rows),
|
result_type can be one of the following:
|
||||||
SINGLE (only retrieve a single row), or None. In this last case, the
|
- MULTI
|
||||||
cursor is returned if any query is executed, since it's used by
|
uses fetchmany() to retrieve all rows, potentially wrapping
|
||||||
subclasses such as InsertQuery). It's possible, however, that no query
|
it in an iterator for chunked reads for connections that
|
||||||
is needed, as the filters describe an empty set. In that case, None is
|
support it
|
||||||
returned, to avoid any unnecessary database interaction.
|
- SINGLE
|
||||||
|
retrieves a single row using fetchone()
|
||||||
|
- ROW_COUNT
|
||||||
|
retrieve the number of rows in the result
|
||||||
|
- CURSOR
|
||||||
|
run the query, and then return the cursor object. In this case
|
||||||
|
it is the caller's responsibility to close the cursor when they
|
||||||
|
are done with it
|
||||||
"""
|
"""
|
||||||
result_type = result_type or NO_RESULTS
|
result_type = result_type or NO_RESULTS
|
||||||
try:
|
try:
|
||||||
@ -1588,10 +1596,15 @@ class SQLCompiler:
|
|||||||
cursor.close()
|
cursor.close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if result_type == CURSOR:
|
if result_type == ROW_COUNT:
|
||||||
# Give the caller the cursor to process and close.
|
try:
|
||||||
|
return cursor.rowcount
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
elif result_type == CURSOR:
|
||||||
|
# here we are returning the cursor without closing it
|
||||||
return cursor
|
return cursor
|
||||||
if result_type == SINGLE:
|
elif result_type == SINGLE:
|
||||||
try:
|
try:
|
||||||
val = cursor.fetchone()
|
val = cursor.fetchone()
|
||||||
if val:
|
if val:
|
||||||
@ -1600,23 +1613,26 @@ class SQLCompiler:
|
|||||||
finally:
|
finally:
|
||||||
# done with the cursor
|
# done with the cursor
|
||||||
cursor.close()
|
cursor.close()
|
||||||
if result_type == NO_RESULTS:
|
elif result_type == NO_RESULTS:
|
||||||
cursor.close()
|
cursor.close()
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
result = cursor_iter(
|
assert result_type == MULTI
|
||||||
cursor,
|
# NB: cursor is now managed by cursor_iter, which
|
||||||
self.connection.features.empty_fetchmany_value,
|
# will close the cursor if/when everything is consumed
|
||||||
self.col_count if self.has_extra_select else None,
|
result = cursor_iter(
|
||||||
chunk_size,
|
cursor,
|
||||||
)
|
self.connection.features.empty_fetchmany_value,
|
||||||
if not chunked_fetch or not self.connection.features.can_use_chunked_reads:
|
self.col_count if self.has_extra_select else None,
|
||||||
# If we are using non-chunked reads, we return the same data
|
chunk_size,
|
||||||
# structure as normally, but ensure it is all read into memory
|
)
|
||||||
# before going any further. Use chunked_fetch if requested,
|
if not chunked_fetch or not self.connection.features.can_use_chunked_reads:
|
||||||
# unless the database doesn't support it.
|
# If we are using non-chunked reads, we return the same data
|
||||||
return list(result)
|
# structure as normally, but ensure it is all read into memory
|
||||||
return result
|
# before going any further. Use chunked_fetch if requested,
|
||||||
|
# unless the database doesn't support it.
|
||||||
|
return list(result)
|
||||||
|
return result
|
||||||
|
|
||||||
def as_subquery_condition(self, alias, columns, compiler):
|
def as_subquery_condition(self, alias, columns, compiler):
|
||||||
qn = compiler.quote_name_unless_alias
|
qn = compiler.quote_name_unless_alias
|
||||||
@ -2012,19 +2028,21 @@ class SQLUpdateCompiler(SQLCompiler):
|
|||||||
non-empty query that is executed. Row counts for any subsequent,
|
non-empty query that is executed. Row counts for any subsequent,
|
||||||
related queries are not available.
|
related queries are not available.
|
||||||
"""
|
"""
|
||||||
cursor = super().execute_sql(result_type)
|
if result_type not in {ROW_COUNT, NO_RESULTS}:
|
||||||
try:
|
raise ValueError(f"Unknown cursor type for update {repr(result_type)}")
|
||||||
rows = cursor.rowcount if cursor else 0
|
row_count = super().execute_sql(result_type)
|
||||||
is_empty = cursor is None
|
is_empty = row_count is None
|
||||||
finally:
|
row_count = row_count or 0
|
||||||
if cursor:
|
|
||||||
cursor.close()
|
|
||||||
for query in self.query.get_related_updates():
|
for query in self.query.get_related_updates():
|
||||||
aux_rows = query.get_compiler(self.using).execute_sql(result_type)
|
# NB: if result_type == NO_RESULTS then aux_row_count is None
|
||||||
if is_empty and aux_rows:
|
aux_row_count = query.get_compiler(self.using).execute_sql(result_type)
|
||||||
rows = aux_rows
|
if is_empty and aux_row_count:
|
||||||
|
# this will return the row count for any related updates as
|
||||||
|
# the number of rows updated
|
||||||
|
row_count = aux_row_count
|
||||||
is_empty = False
|
is_empty = False
|
||||||
return rows
|
return row_count
|
||||||
|
|
||||||
def pre_sql_setup(self):
|
def pre_sql_setup(self):
|
||||||
"""
|
"""
|
||||||
|
@ -9,9 +9,15 @@ GET_ITERATOR_CHUNK_SIZE = 100
|
|||||||
# Namedtuples for sql.* internal use.
|
# Namedtuples for sql.* internal use.
|
||||||
|
|
||||||
# How many results to expect from a cursor.execute call
|
# How many results to expect from a cursor.execute call
|
||||||
|
# multiple rows are expected
|
||||||
MULTI = "multi"
|
MULTI = "multi"
|
||||||
|
# a single row is expected
|
||||||
SINGLE = "single"
|
SINGLE = "single"
|
||||||
|
# do not return the rows, instead return the cursor
|
||||||
|
# used for the query
|
||||||
CURSOR = "cursor"
|
CURSOR = "cursor"
|
||||||
|
# instead of returning the rows, return the row count
|
||||||
|
ROW_COUNT = "row count"
|
||||||
NO_RESULTS = "no results"
|
NO_RESULTS = "no results"
|
||||||
|
|
||||||
ORDER_DIR = {
|
ORDER_DIR = {
|
||||||
|
@ -3,7 +3,11 @@ Query subclasses which provide extra functionality beyond simple data retrieval.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from django.core.exceptions import FieldError
|
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
|
from django.db.models.sql.query import Query
|
||||||
|
|
||||||
__all__ = ["DeleteQuery", "UpdateQuery", "InsertQuery", "AggregateQuery"]
|
__all__ = ["DeleteQuery", "UpdateQuery", "InsertQuery", "AggregateQuery"]
|
||||||
@ -17,11 +21,7 @@ class DeleteQuery(Query):
|
|||||||
def do_query(self, table, where, using):
|
def do_query(self, table, where, using):
|
||||||
self.alias_map = {table: self.alias_map[table]}
|
self.alias_map = {table: self.alias_map[table]}
|
||||||
self.where = where
|
self.where = where
|
||||||
cursor = self.get_compiler(using).execute_sql(CURSOR)
|
return self.get_compiler(using).execute_sql(ROW_COUNT)
|
||||||
if cursor:
|
|
||||||
with cursor:
|
|
||||||
return cursor.rowcount
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def delete_batch(self, pk_list, using):
|
def delete_batch(self, pk_list, using):
|
||||||
"""
|
"""
|
||||||
|
@ -389,6 +389,17 @@ Dropped support for PostgreSQL 13
|
|||||||
Upstream support for PostgreSQL 13 ends in November 2025. Django 5.2 supports
|
Upstream support for PostgreSQL 13 ends in November 2025. Django 5.2 supports
|
||||||
PostgreSQL 14 and higher.
|
PostgreSQL 14 and higher.
|
||||||
|
|
||||||
|
Models
|
||||||
|
------
|
||||||
|
|
||||||
|
* Multiple changes have been made to the undocumented ``django.db.models.sql.compiler.SQLCompiler.execute_sql``
|
||||||
|
method.
|
||||||
|
|
||||||
|
* ``ROW_COUNT`` has been added as a result type, which returns the number of rows
|
||||||
|
returned by the query directly, closing the cursor in the process.
|
||||||
|
* ``SQLUpdateCompiler.execute_sql`` now only accepts ``NO_RESULT`` and ``ROW_COUNT``
|
||||||
|
as result types.
|
||||||
|
|
||||||
Changed MySQL connection character set default
|
Changed MySQL connection character set default
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
|
@ -472,6 +472,8 @@ Sorani
|
|||||||
sortable
|
sortable
|
||||||
Spectre
|
Spectre
|
||||||
Springmeyer
|
Springmeyer
|
||||||
|
sql
|
||||||
|
SQLCompiler
|
||||||
SSL
|
SSL
|
||||||
stacktrace
|
stacktrace
|
||||||
stateful
|
stateful
|
||||||
|
Loading…
Reference in New Issue
Block a user