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()