1
0
mirror of https://github.com/django/django.git synced 2025-03-03 05:24:24 +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.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
@ -19,47 +19,11 @@ __all__ = [
]
# RemovedInDjango50Warning
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):
class ArrayAgg(OrderableAggMixin, Aggregate):
function = "ARRAY_AGG"
template = "%(function)s(%(distinct)s%(expressions)s %(ordering)s)"
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
def output_field(self):
return ArrayField(self.source_expressions[0].output_field)
@ -87,27 +51,14 @@ class BoolOr(Aggregate):
output_field = BooleanField()
class JSONBAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
class JSONBAgg(OrderableAggMixin, Aggregate):
function = "JSONB_AGG"
template = "%(function)s(%(distinct)s%(expressions)s %(ordering)s)"
allow_distinct = True
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__().
#
# RemovedInDjango50Warning: When the deprecation ends, replace with:
# def __init__(self, *expressions, default=None, **extra):
def __init__(self, *expressions, default=NOT_PROVIDED, **extra):
def __init__(self, *expressions, default=None, **extra):
super().__init__(*expressions, default=default, **extra)
if (
isinstance(default, Value)
@ -136,20 +87,12 @@ class JSONBAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
)
class StringAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
class StringAgg(OrderableAggMixin, Aggregate):
function = "STRING_AGG"
template = "%(function)s(%(distinct)s%(expressions)s %(ordering)s)"
allow_distinct = True
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):
delimiter_expr = Value(str(delimiter))
super().__init__(expression, delimiter_expr, **extra)

View File

@ -52,12 +52,11 @@ General-purpose aggregation functions
from django.db.models import F
F('some_field').desc()
.. deprecated:: 4.0
.. versionchanged:: 5.0
If there are no rows and ``default`` is not provided, ``ArrayAgg``
returns an empty list instead of ``None``. This behavior is deprecated
and will be removed in Django 5.0. If you need it, explicitly set
``default`` to ``Value([])``.
In older versions, if there are no rows and ``default`` is not
provided, ``ArrayAgg`` returned an empty list instead of ``None``. If
you need it, explicitly set ``default`` to ``Value([])``.
``BitAnd``
----------
@ -173,12 +172,11 @@ General-purpose aggregation functions
{'parking': True, 'double_bed': True}
]}]>
.. deprecated:: 4.0
.. versionchanged:: 5.0
If there are no rows and ``default`` is not provided, ``JSONBAgg``
returns an empty list instead of ``None``. This behavior is deprecated
and will be removed in Django 5.0. If you need it, explicitly set
``default`` to ``Value('[]')``.
In older versions, if there are no rows and ``default`` is not
provided, ``JSONBAgg`` returned an empty list instead of ``None``. If
you need it, explicitly set ``default`` to ``Value([])``.
``StringAgg``
-------------
@ -232,12 +230,11 @@ General-purpose aggregation functions
'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``
returns an empty string instead of ``None``. This behavior is
deprecated and will be removed in Django 5.0. If you need it,
explicitly set ``default`` to ``Value('')``.
In older versions, if there are no rows and ``default`` is not
provided, ``StringAgg`` returned an empty string instead of ``None``.
If you need it, explicitly set ``default`` to ``Value("")``.
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
``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
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.utils import Approximate, ignore_warnings
from django.utils import timezone
from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning
from django.utils.deprecation import RemovedInDjango51Warning
from . import PostgreSQLTestCase
from .models import AggregateTestModel, HotelReservation, Room, StatTestModel
@ -84,36 +84,35 @@ class TestGeneralAggregate(PostgreSQLTestCase):
]
)
@ignore_warnings(category=RemovedInDjango50Warning)
def test_empty_result_set(self):
AggregateTestModel.objects.all().delete()
tests = [
(ArrayAgg("char_field"), []),
(ArrayAgg("integer_field"), []),
(ArrayAgg("boolean_field"), []),
(BitAnd("integer_field"), None),
(BitOr("integer_field"), None),
(BoolAnd("boolean_field"), None),
(BoolOr("boolean_field"), None),
(JSONBAgg("integer_field"), []),
(StringAgg("char_field", delimiter=";"), ""),
ArrayAgg("char_field"),
ArrayAgg("integer_field"),
ArrayAgg("boolean_field"),
BitAnd("integer_field"),
BitOr("integer_field"),
BoolAnd("boolean_field"),
BoolOr("boolean_field"),
JSONBAgg("integer_field"),
StringAgg("char_field", delimiter=";"),
]
if connection.features.has_bit_xor:
tests.append((BitXor("integer_field"), None))
for aggregation, expected_result in tests:
for aggregation in tests:
with self.subTest(aggregation=aggregation):
# Empty result with non-execution optimization.
with self.assertNumQueries(0):
values = AggregateTestModel.objects.none().aggregate(
aggregation=aggregation,
)
self.assertEqual(values, {"aggregation": expected_result})
self.assertEqual(values, {"aggregation": None})
# Empty result when query must be executed.
with self.assertNumQueries(1):
values = AggregateTestModel.objects.aggregate(
aggregation=aggregation,
)
self.assertEqual(values, {"aggregation": expected_result})
self.assertEqual(values, {"aggregation": None})
def test_default_argument(self):
AggregateTestModel.objects.all().delete()
@ -153,57 +152,6 @@ class TestGeneralAggregate(PostgreSQLTestCase):
)
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)
def test_jsonb_agg_default_str_value(self):
AggregateTestModel.objects.all().delete()