1
0
mirror of https://github.com/django/django.git synced 2025-03-03 13:34:26 +00:00

Refs #10929 -- Stopped forcing empty result value by PostgreSQL aggregates.

Per deprecation timeline.
This commit is contained in:
Mariusz Felisiak 2023-01-06 13:53:42 +01:00
parent 43b01300b7
commit 0be8095b25
4 changed files with 34 additions and 142 deletions

View File

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

View File

@ -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
================================== ==================================

View File

@ -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.

View File

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