mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Refs #10929 -- Stopped forcing empty result value by PostgreSQL aggregates.
Per deprecation timeline.
This commit is contained in:
		| @@ -3,7 +3,7 @@ import warnings | |||||||
|  |  | ||||||
| from django.contrib.postgres.fields import ArrayField | from django.contrib.postgres.fields import ArrayField | ||||||
| from django.db.models import Aggregate, BooleanField, JSONField, TextField, Value | from django.db.models import Aggregate, BooleanField, JSONField, TextField, Value | ||||||
| from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning | from django.utils.deprecation import RemovedInDjango51Warning | ||||||
|  |  | ||||||
| from .mixins import OrderableAggMixin | from .mixins import OrderableAggMixin | ||||||
|  |  | ||||||
| @@ -19,47 +19,11 @@ __all__ = [ | |||||||
| ] | ] | ||||||
|  |  | ||||||
|  |  | ||||||
| # RemovedInDjango50Warning | class ArrayAgg(OrderableAggMixin, Aggregate): | ||||||
| NOT_PROVIDED = object() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class DeprecatedConvertValueMixin: |  | ||||||
|     def __init__(self, *expressions, default=NOT_PROVIDED, **extra): |  | ||||||
|         if default is NOT_PROVIDED: |  | ||||||
|             default = None |  | ||||||
|             self._default_provided = False |  | ||||||
|         else: |  | ||||||
|             self._default_provided = True |  | ||||||
|         super().__init__(*expressions, default=default, **extra) |  | ||||||
|  |  | ||||||
|     def resolve_expression(self, *args, **kwargs): |  | ||||||
|         resolved = super().resolve_expression(*args, **kwargs) |  | ||||||
|         if not self._default_provided: |  | ||||||
|             resolved.empty_result_set_value = getattr( |  | ||||||
|                 self, "deprecation_empty_result_set_value", self.deprecation_value |  | ||||||
|             ) |  | ||||||
|         return resolved |  | ||||||
|  |  | ||||||
|     def convert_value(self, value, expression, connection): |  | ||||||
|         if value is None and not self._default_provided: |  | ||||||
|             warnings.warn(self.deprecation_msg, category=RemovedInDjango50Warning) |  | ||||||
|             return self.deprecation_value |  | ||||||
|         return value |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ArrayAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate): |  | ||||||
|     function = "ARRAY_AGG" |     function = "ARRAY_AGG" | ||||||
|     template = "%(function)s(%(distinct)s%(expressions)s %(ordering)s)" |     template = "%(function)s(%(distinct)s%(expressions)s %(ordering)s)" | ||||||
|     allow_distinct = True |     allow_distinct = True | ||||||
|  |  | ||||||
|     # RemovedInDjango50Warning |  | ||||||
|     deprecation_value = property(lambda self: []) |  | ||||||
|     deprecation_msg = ( |  | ||||||
|         "In Django 5.0, ArrayAgg() will return None instead of an empty list " |  | ||||||
|         "if there are no rows. Pass default=None to opt into the new behavior " |  | ||||||
|         "and silence this warning or default=[] to keep the previous behavior." |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def output_field(self): |     def output_field(self): | ||||||
|         return ArrayField(self.source_expressions[0].output_field) |         return ArrayField(self.source_expressions[0].output_field) | ||||||
| @@ -87,27 +51,14 @@ class BoolOr(Aggregate): | |||||||
|     output_field = BooleanField() |     output_field = BooleanField() | ||||||
|  |  | ||||||
|  |  | ||||||
| class JSONBAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate): | class JSONBAgg(OrderableAggMixin, Aggregate): | ||||||
|     function = "JSONB_AGG" |     function = "JSONB_AGG" | ||||||
|     template = "%(function)s(%(distinct)s%(expressions)s %(ordering)s)" |     template = "%(function)s(%(distinct)s%(expressions)s %(ordering)s)" | ||||||
|     allow_distinct = True |     allow_distinct = True | ||||||
|     output_field = JSONField() |     output_field = JSONField() | ||||||
|  |  | ||||||
|     # RemovedInDjango50Warning |  | ||||||
|     deprecation_value = "[]" |  | ||||||
|     deprecation_empty_result_set_value = property(lambda self: []) |  | ||||||
|     deprecation_msg = ( |  | ||||||
|         "In Django 5.0, JSONBAgg() will return None instead of an empty list " |  | ||||||
|         "if there are no rows. Pass default=None to opt into the new behavior " |  | ||||||
|         "and silence this warning or default=[] to keep the previous " |  | ||||||
|         "behavior." |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     # RemovedInDjango51Warning: When the deprecation ends, remove __init__(). |     # RemovedInDjango51Warning: When the deprecation ends, remove __init__(). | ||||||
|     # |     def __init__(self, *expressions, default=None, **extra): | ||||||
|     # RemovedInDjango50Warning: When the deprecation ends, replace with: |  | ||||||
|     # def __init__(self, *expressions, default=None, **extra): |  | ||||||
|     def __init__(self, *expressions, default=NOT_PROVIDED, **extra): |  | ||||||
|         super().__init__(*expressions, default=default, **extra) |         super().__init__(*expressions, default=default, **extra) | ||||||
|         if ( |         if ( | ||||||
|             isinstance(default, Value) |             isinstance(default, Value) | ||||||
| @@ -136,20 +87,12 @@ class JSONBAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate): | |||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class StringAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate): | class StringAgg(OrderableAggMixin, Aggregate): | ||||||
|     function = "STRING_AGG" |     function = "STRING_AGG" | ||||||
|     template = "%(function)s(%(distinct)s%(expressions)s %(ordering)s)" |     template = "%(function)s(%(distinct)s%(expressions)s %(ordering)s)" | ||||||
|     allow_distinct = True |     allow_distinct = True | ||||||
|     output_field = TextField() |     output_field = TextField() | ||||||
|  |  | ||||||
|     # RemovedInDjango50Warning |  | ||||||
|     deprecation_value = "" |  | ||||||
|     deprecation_msg = ( |  | ||||||
|         "In Django 5.0, StringAgg() will return None instead of an empty " |  | ||||||
|         "string if there are no rows. Pass default=None to opt into the new " |  | ||||||
|         'behavior and silence this warning or default="" to keep the previous behavior.' |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     def __init__(self, expression, delimiter, **extra): |     def __init__(self, expression, delimiter, **extra): | ||||||
|         delimiter_expr = Value(str(delimiter)) |         delimiter_expr = Value(str(delimiter)) | ||||||
|         super().__init__(expression, delimiter_expr, **extra) |         super().__init__(expression, delimiter_expr, **extra) | ||||||
|   | |||||||
| @@ -52,12 +52,11 @@ General-purpose aggregation functions | |||||||
|             from django.db.models import F |             from django.db.models import F | ||||||
|             F('some_field').desc() |             F('some_field').desc() | ||||||
|  |  | ||||||
|     .. deprecated:: 4.0 |     .. versionchanged:: 5.0 | ||||||
|  |  | ||||||
|         If there are no rows and ``default`` is not provided, ``ArrayAgg`` |         In older versions, if there are no rows and ``default`` is not | ||||||
|         returns an empty list instead of ``None``. This behavior is deprecated |         provided, ``ArrayAgg`` returned an empty list instead of ``None``. If | ||||||
|         and will be removed in Django 5.0. If you need it, explicitly set |         you need it, explicitly set ``default`` to ``Value([])``. | ||||||
|         ``default`` to ``Value([])``. |  | ||||||
|  |  | ||||||
| ``BitAnd`` | ``BitAnd`` | ||||||
| ---------- | ---------- | ||||||
| @@ -173,12 +172,11 @@ General-purpose aggregation functions | |||||||
|             {'parking': True, 'double_bed': True} |             {'parking': True, 'double_bed': True} | ||||||
|         ]}]> |         ]}]> | ||||||
|  |  | ||||||
|     .. deprecated:: 4.0 |     .. versionchanged:: 5.0 | ||||||
|  |  | ||||||
|         If there are no rows and ``default`` is not provided, ``JSONBAgg`` |         In older versions, if there are no rows and ``default`` is not | ||||||
|         returns an empty list instead of ``None``. This behavior is deprecated |         provided, ``JSONBAgg`` returned an empty list instead of ``None``. If | ||||||
|         and will be removed in Django 5.0. If you need it, explicitly set |         you need it, explicitly set ``default`` to ``Value([])``. | ||||||
|         ``default`` to ``Value('[]')``. |  | ||||||
|  |  | ||||||
| ``StringAgg`` | ``StringAgg`` | ||||||
| ------------- | ------------- | ||||||
| @@ -232,12 +230,11 @@ General-purpose aggregation functions | |||||||
|             'headline': 'NASA uses Python', 'publication_names': 'Science News, The Python Journal' |             'headline': 'NASA uses Python', 'publication_names': 'Science News, The Python Journal' | ||||||
|         }]> |         }]> | ||||||
|  |  | ||||||
|     .. deprecated:: 4.0 |     .. versionchanged:: 5.0 | ||||||
|  |  | ||||||
|         If there are no rows and ``default`` is not provided, ``StringAgg`` |         In older versions, if there are no rows and ``default`` is not | ||||||
|         returns an empty string instead of ``None``. This behavior is |         provided, ``StringAgg`` returned an empty string instead of ``None``. | ||||||
|         deprecated and will be removed in Django 5.0. If you need it, |         If you need it, explicitly set ``default`` to ``Value("")``. | ||||||
|         explicitly set ``default`` to ``Value('')``. |  | ||||||
|  |  | ||||||
| Aggregate functions for statistics | Aggregate functions for statistics | ||||||
| ================================== | ================================== | ||||||
|   | |||||||
| @@ -270,6 +270,10 @@ to remove usage of these features. | |||||||
| * The ``extra_tests`` argument for ``DiscoverRunner.build_suite()`` and | * The ``extra_tests`` argument for ``DiscoverRunner.build_suite()`` and | ||||||
|   ``DiscoverRunner.run_tests()`` is removed. |   ``DiscoverRunner.run_tests()`` is removed. | ||||||
|  |  | ||||||
|  | * The ``django.contrib.postgres.aggregates.ArrayAgg``, ``JSONBAgg``, and | ||||||
|  |   ``StringAgg`` aggregates no longer return ``[]``, ``[]``, and ``''``, | ||||||
|  |   respectively, when there are no rows. | ||||||
|  |  | ||||||
| See :ref:`deprecated-features-4.1` for details on these changes, including how | See :ref:`deprecated-features-4.1` for details on these changes, including how | ||||||
| to remove usage of these features. | to remove usage of these features. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ from django.db.models.functions import Cast, Concat, Substr | |||||||
| from django.test import skipUnlessDBFeature | from django.test import skipUnlessDBFeature | ||||||
| from django.test.utils import Approximate, ignore_warnings | from django.test.utils import Approximate, ignore_warnings | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning | from django.utils.deprecation import RemovedInDjango51Warning | ||||||
|  |  | ||||||
| from . import PostgreSQLTestCase | from . import PostgreSQLTestCase | ||||||
| from .models import AggregateTestModel, HotelReservation, Room, StatTestModel | from .models import AggregateTestModel, HotelReservation, Room, StatTestModel | ||||||
| @@ -84,36 +84,35 @@ class TestGeneralAggregate(PostgreSQLTestCase): | |||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @ignore_warnings(category=RemovedInDjango50Warning) |  | ||||||
|     def test_empty_result_set(self): |     def test_empty_result_set(self): | ||||||
|         AggregateTestModel.objects.all().delete() |         AggregateTestModel.objects.all().delete() | ||||||
|         tests = [ |         tests = [ | ||||||
|             (ArrayAgg("char_field"), []), |             ArrayAgg("char_field"), | ||||||
|             (ArrayAgg("integer_field"), []), |             ArrayAgg("integer_field"), | ||||||
|             (ArrayAgg("boolean_field"), []), |             ArrayAgg("boolean_field"), | ||||||
|             (BitAnd("integer_field"), None), |             BitAnd("integer_field"), | ||||||
|             (BitOr("integer_field"), None), |             BitOr("integer_field"), | ||||||
|             (BoolAnd("boolean_field"), None), |             BoolAnd("boolean_field"), | ||||||
|             (BoolOr("boolean_field"), None), |             BoolOr("boolean_field"), | ||||||
|             (JSONBAgg("integer_field"), []), |             JSONBAgg("integer_field"), | ||||||
|             (StringAgg("char_field", delimiter=";"), ""), |             StringAgg("char_field", delimiter=";"), | ||||||
|         ] |         ] | ||||||
|         if connection.features.has_bit_xor: |         if connection.features.has_bit_xor: | ||||||
|             tests.append((BitXor("integer_field"), None)) |             tests.append((BitXor("integer_field"), None)) | ||||||
|         for aggregation, expected_result in tests: |         for aggregation in tests: | ||||||
|             with self.subTest(aggregation=aggregation): |             with self.subTest(aggregation=aggregation): | ||||||
|                 # Empty result with non-execution optimization. |                 # Empty result with non-execution optimization. | ||||||
|                 with self.assertNumQueries(0): |                 with self.assertNumQueries(0): | ||||||
|                     values = AggregateTestModel.objects.none().aggregate( |                     values = AggregateTestModel.objects.none().aggregate( | ||||||
|                         aggregation=aggregation, |                         aggregation=aggregation, | ||||||
|                     ) |                     ) | ||||||
|                     self.assertEqual(values, {"aggregation": expected_result}) |                     self.assertEqual(values, {"aggregation": None}) | ||||||
|                 # Empty result when query must be executed. |                 # Empty result when query must be executed. | ||||||
|                 with self.assertNumQueries(1): |                 with self.assertNumQueries(1): | ||||||
|                     values = AggregateTestModel.objects.aggregate( |                     values = AggregateTestModel.objects.aggregate( | ||||||
|                         aggregation=aggregation, |                         aggregation=aggregation, | ||||||
|                     ) |                     ) | ||||||
|                     self.assertEqual(values, {"aggregation": expected_result}) |                     self.assertEqual(values, {"aggregation": None}) | ||||||
|  |  | ||||||
|     def test_default_argument(self): |     def test_default_argument(self): | ||||||
|         AggregateTestModel.objects.all().delete() |         AggregateTestModel.objects.all().delete() | ||||||
| @@ -153,57 +152,6 @@ class TestGeneralAggregate(PostgreSQLTestCase): | |||||||
|                     ) |                     ) | ||||||
|                     self.assertEqual(values, {"aggregation": expected_result}) |                     self.assertEqual(values, {"aggregation": expected_result}) | ||||||
|  |  | ||||||
|     def test_convert_value_deprecation(self): |  | ||||||
|         AggregateTestModel.objects.all().delete() |  | ||||||
|         queryset = AggregateTestModel.objects.all() |  | ||||||
|  |  | ||||||
|         with self.assertWarnsMessage( |  | ||||||
|             RemovedInDjango50Warning, ArrayAgg.deprecation_msg |  | ||||||
|         ): |  | ||||||
|             queryset.aggregate(aggregation=ArrayAgg("boolean_field")) |  | ||||||
|  |  | ||||||
|         with self.assertWarnsMessage( |  | ||||||
|             RemovedInDjango50Warning, JSONBAgg.deprecation_msg |  | ||||||
|         ): |  | ||||||
|             queryset.aggregate(aggregation=JSONBAgg("integer_field")) |  | ||||||
|  |  | ||||||
|         with self.assertWarnsMessage( |  | ||||||
|             RemovedInDjango50Warning, StringAgg.deprecation_msg |  | ||||||
|         ): |  | ||||||
|             queryset.aggregate(aggregation=StringAgg("char_field", delimiter=";")) |  | ||||||
|  |  | ||||||
|         # No warnings raised if default argument provided. |  | ||||||
|         self.assertEqual( |  | ||||||
|             queryset.aggregate(aggregation=ArrayAgg("boolean_field", default=None)), |  | ||||||
|             {"aggregation": None}, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             queryset.aggregate(aggregation=JSONBAgg("integer_field", default=None)), |  | ||||||
|             {"aggregation": None}, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             queryset.aggregate( |  | ||||||
|                 aggregation=StringAgg("char_field", delimiter=";", default=None), |  | ||||||
|             ), |  | ||||||
|             {"aggregation": None}, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             queryset.aggregate( |  | ||||||
|                 aggregation=ArrayAgg("boolean_field", default=Value([])) |  | ||||||
|             ), |  | ||||||
|             {"aggregation": []}, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             queryset.aggregate(aggregation=JSONBAgg("integer_field", default=[])), |  | ||||||
|             {"aggregation": []}, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual( |  | ||||||
|             queryset.aggregate( |  | ||||||
|                 aggregation=StringAgg("char_field", delimiter=";", default=Value("")), |  | ||||||
|             ), |  | ||||||
|             {"aggregation": ""}, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     @ignore_warnings(category=RemovedInDjango51Warning) |     @ignore_warnings(category=RemovedInDjango51Warning) | ||||||
|     def test_jsonb_agg_default_str_value(self): |     def test_jsonb_agg_default_str_value(self): | ||||||
|         AggregateTestModel.objects.all().delete() |         AggregateTestModel.objects.all().delete() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user