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