diff --git a/django/contrib/postgres/fields/array.py b/django/contrib/postgres/fields/array.py index 078428416c..a76598a9bf 100644 --- a/django/contrib/postgres/fields/array.py +++ b/django/contrib/postgres/fields/array.py @@ -135,6 +135,11 @@ class ArrayField(CheckPostgresInstalledMixin, CheckFieldDefaultMixin, Field): ] return value + def get_db_prep_save(self, value, connection): + if isinstance(value, (list, tuple)): + return [self.base_field.get_db_prep_save(i, connection) for i in value] + return value + def deconstruct(self): name, path, args, kwargs = super().deconstruct() if path == "django.contrib.postgres.fields.array.ArrayField": diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index bb8f686f84..412ec692e3 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -319,6 +319,15 @@ backends. * Support for PostGIS 3.1 is removed. +:mod:`django.contrib.postgres` +------------------------------ + +* Top-level elements set to ``None`` in an + :class:`~django.contrib.postgres.fields.ArrayField` with a + :class:`~django.db.models.JSONField` base field are now saved as SQL ``NULL`` + instead of the JSON ``null`` primitive. This matches the behavior of a + standalone :class:`~django.db.models.JSONField` when storing ``None`` values. + Dropped support for PostgreSQL 14 --------------------------------- diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py index f07f4492b8..6a3d25a6af 100644 --- a/tests/postgres_tests/models.py +++ b/tests/postgres_tests/models.py @@ -79,7 +79,7 @@ class OtherTypesArrayModel(PostgreSQLModel): models.DecimalField(max_digits=5, decimal_places=2), default=list ) tags = ArrayField(TagField(), blank=True, null=True) - json = ArrayField(models.JSONField(default=dict), default=list) + json = ArrayField(models.JSONField(default=dict), default=list, null=True) int_ranges = ArrayField(IntegerRangeField(), blank=True, null=True) bigint_ranges = ArrayField(BigIntegerRangeField(), blank=True, null=True) diff --git a/tests/postgres_tests/test_array.py b/tests/postgres_tests/test_array.py index 392b8f946c..e65009ad83 100644 --- a/tests/postgres_tests/test_array.py +++ b/tests/postgres_tests/test_array.py @@ -10,6 +10,7 @@ from django.core import checks, exceptions, serializers, validators from django.core.exceptions import FieldError from django.core.management import call_command from django.db import IntegrityError, connection, models +from django.db.models import JSONNull from django.db.models.expressions import Exists, F, OuterRef, RawSQL, Value from django.db.models.functions import Cast, JSONObject, Upper from django.test import TransactionTestCase, override_settings, skipUnlessDBFeature @@ -1577,3 +1578,29 @@ class TestAdminUtils(PostgreSQLTestCase): self.empty_value, ) self.assertEqual(display_value, self.empty_value) + + +class TestJSONFieldQuerying(PostgreSQLTestCase): + def test_saving_and_querying_for_sql_null(self): + obj = OtherTypesArrayModel.objects.create(json=[None, None]) + self.assertSequenceEqual( + OtherTypesArrayModel.objects.filter(json__1__isnull=True), [obj] + ) + + def test_saving_and_querying_for_json_null(self): + obj = OtherTypesArrayModel.objects.create(json=[JSONNull(), JSONNull()]) + self.assertSequenceEqual( + OtherTypesArrayModel.objects.filter(json__1=JSONNull()), [obj] + ) + self.assertSequenceEqual( + OtherTypesArrayModel.objects.filter(json__1__isnull=True), [] + ) + + def test_saving_and_querying_for_nested_json_nulls(self): + obj = OtherTypesArrayModel.objects.create(json=[[None, 1], [None, 2]]) + self.assertSequenceEqual( + OtherTypesArrayModel.objects.filter(json__1__0=None), [obj] + ) + self.assertSequenceEqual( + OtherTypesArrayModel.objects.filter(json__1__0__isnull=True), [] + )