summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClifford Gama <cliffygamy@gmail.com>2025-05-26 16:44:08 +0200
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-06-06 17:40:35 +0200
commitc1fa3fdd040718356e5a3b9a0fe699d73f47a940 (patch)
tree7ca4b4a2a4a5bf1149c4bc7ee13c8533c61865c2
parent957951755259b412d5113333b32bf85871d29814 (diff)
Fixed #36419 -- Ensured for_save was propagated when resolving expressions.
The for_save flag wasn't properly propagated when resolving expressions, which prevented get_db_prep_save() from being called in some cases. This affected fields like JSONField where None would be saved as JSON null instead of SQL NULL. Regression in 00c690efbc0b10f67924687f24a7b30397bf47d9. Thanks to David Sanders and Simon Charette for reviews. Co-authored-by: Adam Johnson <me@adamj.eu>
-rw-r--r--django/db/models/expressions.py2
-rw-r--r--docs/releases/5.2.3.txt4
-rw-r--r--tests/queries/test_bulk_update.py17
3 files changed, 21 insertions, 2 deletions
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index 54e372ea01..dc070114c9 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -297,7 +297,7 @@ class BaseExpression:
c.is_summary = summarize
source_expressions = [
(
- expr.resolve_expression(query, allow_joins, reuse, summarize)
+ expr.resolve_expression(query, allow_joins, reuse, summarize, for_save)
if expr is not None
else None
)
diff --git a/docs/releases/5.2.3.txt b/docs/releases/5.2.3.txt
index 5aaa7fd2dd..927ec16d34 100644
--- a/docs/releases/5.2.3.txt
+++ b/docs/releases/5.2.3.txt
@@ -13,3 +13,7 @@ Bugfixes
* Fixed a log injection possibility by migrating remaining response logging
to ``django.utils.log.log_response()``, which safely escapes arguments such
as the request path to prevent unsafe log output (:cve:`2025-48432`).
+
+* Fixed a regression in Django 5.2 that caused :meth:`.QuerySet.bulk_update` to
+ incorrectly convert ``None`` to JSON ``null`` instead of SQL ``NULL`` for
+ ``JSONField`` (:ticket:`36419`).
diff --git a/tests/queries/test_bulk_update.py b/tests/queries/test_bulk_update.py
index 765fa934ca..480fac6784 100644
--- a/tests/queries/test_bulk_update.py
+++ b/tests/queries/test_bulk_update.py
@@ -4,7 +4,7 @@ from math import ceil
from django.core.exceptions import FieldDoesNotExist
from django.db import connection
from django.db.models import F
-from django.db.models.functions import Lower
+from django.db.models.functions import Coalesce, Lower
from django.db.utils import IntegrityError
from django.test import TestCase, override_settings, skipUnlessDBFeature
@@ -300,6 +300,21 @@ class BulkUpdateTests(TestCase):
JSONFieldNullable.objects.filter(json_field__has_key="c"), objs
)
+ @skipUnlessDBFeature("supports_json_field")
+ def test_json_field_sql_null(self):
+ obj = JSONFieldNullable.objects.create(json_field={})
+ test_cases = [
+ ("direct_none_assignment", None),
+ ("expression_none_assignment", Coalesce(None, None)),
+ ]
+ for label, value in test_cases:
+ with self.subTest(case=label):
+ obj.json_field = value
+ JSONFieldNullable.objects.bulk_update([obj], fields=["json_field"])
+ obj.refresh_from_db()
+ sql_null_qs = JSONFieldNullable.objects.filter(json_field__isnull=True)
+ self.assertSequenceEqual(sql_null_qs, [obj])
+
def test_nullable_fk_after_related_save(self):
parent = RelatedObject.objects.create()
child = SingleObject()