Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions django/db/backends/base/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 22 additions & 5 deletions django/db/backends/base/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down
14 changes: 0 additions & 14 deletions django/db/backends/mysql/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
24 changes: 1 addition & 23 deletions django/db/backends/oracle/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions django/db/backends/postgresql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions django/db/models/lookups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 1 addition & 3 deletions django/http/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 6 additions & 17 deletions docs/ref/request-response.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://262.ecma-international.org/5.1/#sec-11.1.4>`_ 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

Expand Down
66 changes: 48 additions & 18 deletions tests/lookup/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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):
"""
Expand Down
Loading