From b5d9dbdf2bba8df4c85cd0db308b3a467d763d02 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 1 Jun 2026 10:38:44 -0400 Subject: [PATCH 1/2] Refs #36905 -- Moved JSONResponse safe param discussion to versionchanged box. Follow-up to 6e15ac8066312328de279e3e072667416c205bfc. --- django/http/response.py | 4 +--- docs/ref/request-response.txt | 23 ++++++----------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/django/http/response.py b/django/http/response.py index dbefb173593c..fe267f700ff9 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -747,9 +747,7 @@ class JsonResponse(HttpResponse): """ An HTTP response class that consumes data to be serialized to JSON. - :param data: Data to be dumped into json. By default only ``dict`` objects - are allowed to be passed due to a security flaw before ECMAScript 5. See - the ``safe`` parameter for more information. + :param data: Data to be dumped into json. :param encoder: Should be a json encoder class. Defaults to ``django.core.serializers.json.DjangoJSONEncoder``. :param safe: Controls if only ``dict`` objects may be serialized. Defaults diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 1ca5cb0c7824..9ab3127df0b0 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -1267,32 +1267,21 @@ Typical usage could look like: Serializing non-dictionary objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In order to serialize objects other than ``dict`` you must set the ``safe`` -parameter to ``False``: +Objects other than ``dict`` can be serialized: .. code-block:: pycon - >>> response = JsonResponse([1, 2, 3], safe=False) - -Without passing ``safe=False``, a :exc:`TypeError` will be raised. + >>> response = JsonResponse([1, 2, 3]) Note that an API based on ``dict`` objects is more extensible, flexible, and makes it easier to maintain forwards compatibility. Therefore, you should avoid -using non-dict objects in JSON-encoded response. - -.. warning:: - - Before the `5th edition of ECMAScript - `_ it was possible to - poison the JavaScript ``Array`` constructor. For this reason, Django does - not allow passing non-dict objects to the - :class:`~django.http.JsonResponse` constructor by default. However, most - modern browsers implement ECMAScript 5 which removes this attack vector. - Therefore it is possible to disable this security precaution. +using non-dict objects in JSON-encoded responses. .. versionchanged:: 6.2 - In earlier versions, the ``safe`` parameter defaulted to ``True``. + In earlier versions, it was necessary to pass ``safe=False`` to serialize + other objects besides dictionaries, as the (now deprecated) ``safe`` + parameter defaulted to ``True``, raising :exc:`TypeError`. .. deprecated:: 6.2 From 4bbc27c8686f10f9556cef02dbfa9f5157fbcf56 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sat, 5 Jul 2025 21:19:05 -0400 Subject: [PATCH 2/2] Fixed #36492 -- Restored exact boolean lookup against literals on SQLite. Performance regression in 37e6c5b on SQLite. Just like MySQL, and presumably Oracle, which don't have a native boolean type and incidently store booleans in integer columns, indices on such columns cannot be used when explicit boolean literal equalities are omitted. Adapt the logic introduced by refs #32691 for MySQL to be used for all backends that don't support native boolean fields instead of special casing MySQL, SQLite, and Oracle in their own special way. Note that review of this work surfaced that SQLite's query planner also cannot make use of indices when dealing with expressions of form WHERE NOT (indexed_bool_field = false) but that's a long standing problem unrelated to the restorative work performed in this patch. Thanks Klaas van Schelven for the report. --- django/db/backends/base/features.py | 3 ++ django/db/backends/base/operations.py | 27 ++++++++-- django/db/backends/mysql/operations.py | 14 ----- django/db/backends/oracle/operations.py | 24 +-------- django/db/backends/postgresql/features.py | 1 + django/db/models/lookups.py | 15 ++++-- tests/lookup/tests.py | 66 ++++++++++++++++------- 7 files changed, 85 insertions(+), 65 deletions(-) diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 6770c177c172..c9e78b574650 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -78,6 +78,9 @@ class BaseDatabaseFeatures: # Does the backend ignore unnecessary ORDER BY clauses in subqueries? ignores_unnecessary_order_by_in_subqueries = True + # Is there a true datatype for boolean? + has_native_boolean_field = False + # Is there a true datatype for uuid? has_native_uuid_field = False diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py index 29ef7d93d166..db45d6922e97 100644 --- a/django/db/backends/base/operations.py +++ b/django/db/backends/base/operations.py @@ -8,8 +8,10 @@ from django.conf import settings from django.db import NotSupportedError, models, transaction -from django.db.models.expressions import Col +from django.db.models import Exists, ExpressionWrapper, Lookup +from django.db.models.expressions import Col, RawSQL from django.db.models.fields.composite import CompositePrimaryKey +from django.db.models.sql.where import WhereNode from django.utils import timezone from django.utils.duration import duration_microseconds from django.utils.encoding import force_str @@ -716,10 +718,25 @@ def check_expression_support(self, expression): def conditional_expression_supported_in_where_clause(self, expression): """ - Return True, if the conditional expression is supported in the WHERE - clause. - """ - return True + Return True, if the conditional expression is directly supported in the + WHERE clause. + """ + # If the backend supports native boolean field it can accept any + # direct conditional expression usage. + if self.connection.features.has_native_boolean_field: + return True + # Most backends support direct EXISTS and lookups usage. + if isinstance(expression, (Exists, Lookup, WhereNode)): + return True + # Nested expression wrappers should be unwrapped. + if isinstance(expression, ExpressionWrapper) and expression.conditional: + return self.conditional_expression_supported_in_where_clause( + expression.expression + ) + # Trust that direct usage of RawSQL can be used by itself. + if isinstance(expression, RawSQL) and expression.conditional: + return True + return False def combine_expression(self, connector, sub_expressions): """ diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index 61fc9da3f49e..7dee707820f8 100644 --- a/django/db/backends/mysql/operations.py +++ b/django/db/backends/mysql/operations.py @@ -3,7 +3,6 @@ from django.conf import settings from django.db.backends.base.operations import BaseDatabaseOperations from django.db.backends.utils import split_tzname_delta -from django.db.models import Exists, ExpressionWrapper, Lookup from django.db.models.constants import OnConflict from django.utils import timezone from django.utils.encoding import force_str @@ -393,19 +392,6 @@ def lookup_cast(self, lookup_type, internal_type=None): lookup = "JSON_UNQUOTE(%s)" return lookup - def conditional_expression_supported_in_where_clause(self, expression): - # MySQL ignores indexes with boolean fields unless they're compared - # directly to a boolean value. - if isinstance(expression, (Exists, Lookup)): - return True - if isinstance(expression, ExpressionWrapper) and expression.conditional: - return self.conditional_expression_supported_in_where_clause( - expression.expression - ) - if getattr(expression, "conditional", False): - return False - return super().conditional_expression_supported_in_where_clause(expression) - def on_conflict_suffix_sql(self, fields, on_conflict, update_fields, unique_fields): if on_conflict == OnConflict.UPDATE: conflict_suffix_sql = "ON DUPLICATE KEY UPDATE %(fields)s" diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 802f27a3c6a2..d946e37fac38 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -6,14 +6,7 @@ from django.db import NotSupportedError from django.db.backends.base.operations import BaseDatabaseOperations from django.db.backends.utils import split_tzname_delta, strip_quotes, truncate_name -from django.db.models import ( - AutoField, - Exists, - ExpressionWrapper, - Lookup, -) -from django.db.models.expressions import RawSQL -from django.db.models.sql.where import WhereNode +from django.db.models import AutoField from django.utils import timezone from django.utils.encoding import force_bytes, force_str from django.utils.functional import cached_property @@ -705,21 +698,6 @@ def subtract_temporals(self, internal_type, lhs, rhs): ) return super().subtract_temporals(internal_type, lhs, rhs) - def conditional_expression_supported_in_where_clause(self, expression): - """ - Oracle supports only EXISTS(...) or filters in the WHERE clause, others - must be compared with True. - """ - if isinstance(expression, (Exists, Lookup, WhereNode)): - return True - if isinstance(expression, ExpressionWrapper) and expression.conditional: - return self.conditional_expression_supported_in_where_clause( - expression.expression - ) - if isinstance(expression, RawSQL) and expression.conditional: - return True - return False - def format_json_path_numeric_index(self, num): if num < 0: return "[last-%s]" % abs(num + 1) # Indexing is zero-based. diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index d3fae82a10b6..b4a257547522 100644 --- a/django/db/backends/postgresql/features.py +++ b/django/db/backends/postgresql/features.py @@ -13,6 +13,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): can_return_rows_from_bulk_insert = True can_return_rows_from_update = True has_real_datatype = True + has_native_boolean_field = True has_native_uuid_field = True has_native_duration_field = True has_native_json_field = True diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index 4c08999fb6df..eef7bc93a5ce 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -151,11 +151,16 @@ def as_oracle(self, compiler, connection): # expression unless they're wrapped in a CASE WHEN. wrapped = False exprs = [] - for expr in (self.lhs, self.rhs): - if connection.ops.conditional_expression_supported_in_where_clause(expr): - expr = Case(When(expr, then=True), default=False) - wrapped = True - exprs.append(expr) + if getattr(self.lhs, "conditional", False) and getattr( + self.rhs, "conditional", False + ): + for expr in (self.lhs, self.rhs): + if connection.ops.conditional_expression_supported_in_where_clause( + expr + ): + expr = Case(When(expr, then=True), default=False) + wrapped = True + exprs.append(expr) lookup = type(self)(*exprs) if wrapped else self return lookup.as_sql(compiler, connection) diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py index b154541e788b..9314fa05b082 100644 --- a/tests/lookup/tests.py +++ b/tests/lookup/tests.py @@ -2,7 +2,7 @@ from datetime import datetime from math import ceil from operator import attrgetter -from unittest import mock, skipUnless +from unittest import mock from django.core.exceptions import FieldError from django.db import connection, models @@ -19,6 +19,7 @@ Value, When, ) +from django.db.models.expressions import RawSQL from django.db.models.functions import Abs, Cast, Length, Substr from django.db.models.lookups import ( Exact, @@ -29,7 +30,7 @@ LessThan, LessThanOrEqual, ) -from django.test import TestCase, skipUnlessDBFeature +from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from django.test.utils import ignore_warnings, isolate_apps, register_lookup from django.utils.deprecation import RemovedInDjango70Warning @@ -1591,10 +1592,10 @@ def test_exact_sliced_queryset_not_limited_to_one(self): with self.assertRaisesMessage(ValueError, msg): list(Article.objects.filter(author=Author.objects.all()[1:])) - @skipUnless(connection.vendor == "mysql", "MySQL-specific workaround.") + @skipIfDBFeature("has_native_boolean_field") def test_exact_booleanfield(self): - # MySQL ignores indexes with boolean fields unless they're compared - # directly to a boolean value. + # Most databases without a native boolean type ignore indexes on them + # unless they're compared directly to a literal value. product = Product.objects.create(name="Paper", qty_target=5000) Stock.objects.create(product=product, short=False, qty_available=5100) stock_1 = Stock.objects.create(product=product, short=True, qty_available=180) @@ -1605,31 +1606,60 @@ def test_exact_booleanfield(self): str(qs.query), ) - @skipUnless(connection.vendor == "mysql", "MySQL-specific workaround.") + @skipIfDBFeature("has_native_boolean_field") def test_exact_booleanfield_annotation(self): - # MySQL ignores indexes with boolean fields unless they're compared - # directly to a boolean value. - qs = Author.objects.annotate( - case=Case( - When(alias="a1", then=True), - default=False, + # Most databases without a native boolean type ignore indexes on them + # unless they're compared directly to a literal value. + product = Product.objects.create(name="Paper", qty_target=5000) + Stock.objects.create(product=product, short=False, qty_available=5100) + stock_1 = Stock.objects.create(product=product, short=True, qty_available=180) + qs = Stock.objects.annotate( + short_annotation=F("short"), + ).filter(short_annotation=True) + self.assertSequenceEqual(qs, [stock_1]) + self.assertIn(" = True", str(qs.query)) + # ExpressionWrapper should be unwrapped. + qs = Stock.objects.annotate( + short_wrapper=ExpressionWrapper( + F("short"), output_field=BooleanField(), ) - ).filter(case=True) - self.assertSequenceEqual(qs, [self.au1]) + ).filter(short_wrapper=True) + self.assertSequenceEqual(qs, [stock_1]) self.assertIn(" = True", str(qs.query)) - + # Q which resolve to WhereNode should not be compared to a boolean + # value as it's compatible by definition. qs = Author.objects.annotate( - wrapped=ExpressionWrapper(Q(alias="a1"), output_field=BooleanField()), - ).filter(wrapped=True) + node=Q(alias="a1"), + ).filter(node=True) self.assertSequenceEqual(qs, [self.au1]) - self.assertIn(" = True", str(qs.query)) + self.assertNotIn(" = True", str(qs.query)) # EXISTS(...) shouldn't be compared to a boolean value. qs = Author.objects.annotate( exists=Exists(Author.objects.filter(alias="a1", pk=OuterRef("pk"))), ).filter(exists=True) self.assertSequenceEqual(qs, [self.au1]) self.assertNotIn(" = True", str(qs.query)) + # CASE shouldn't be compared to a boolean value. + qs = Author.objects.annotate( + case=Case( + When(alias="a1", then=True), + default=False, + output_field=BooleanField(), + ) + ).filter(case=True) + self.assertSequenceEqual(qs, [self.au1]) + self.assertEqual(str(qs.query).count(" = True"), 1) + # Conditional usage of RawSQL usage should not be compared to a boolean + # value. + queryset = Author.objects.all() + compiler = queryset.query.get_compiler(connection=connection) + sql, params = compiler.compile(Q(alias="a1").resolve_expression(queryset.query)) + qs = Author.objects.alias( + raw=RawSQL(sql, params, BooleanField()), + ).filter(raw=True) + self.assertSequenceEqual(qs, [self.au1]) + self.assertNotIn(" = True", str(qs.query)) def test_custom_field_none_rhs(self): """