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:
parent
43b01300b7
commit
0be8095b25
@ -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)
|
||||
|
@ -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
|
||||
==================================
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user