diff --git a/django/contrib/postgres/aggregates/general.py b/django/contrib/postgres/aggregates/general.py index 12cba62701..00957bdab0 100644 --- a/django/contrib/postgres/aggregates/general.py +++ b/django/contrib/postgres/aggregates/general.py @@ -39,8 +39,9 @@ class BoolOr(Aggregate): function = 'BOOL_OR' -class JSONBAgg(Aggregate): +class JSONBAgg(OrderableAggMixin, Aggregate): function = 'JSONB_AGG' + template = '%(function)s(%(expressions)s %(ordering)s)' output_field = JSONField() def convert_value(self, value, expression, connection): diff --git a/docs/ref/contrib/postgres/aggregates.txt b/docs/ref/contrib/postgres/aggregates.txt index ad07090688..b59555cb10 100644 --- a/docs/ref/contrib/postgres/aggregates.txt +++ b/docs/ref/contrib/postgres/aggregates.txt @@ -86,10 +86,21 @@ General-purpose aggregation functions ``JSONBAgg`` ------------ -.. class:: JSONBAgg(expressions, filter=None, **extra) +.. class:: JSONBAgg(expressions, filter=None, ordering=(), **extra) Returns the input values as a ``JSON`` array. + .. attribute:: ordering + + .. versionadded:: 3.2 + + An optional string of a field name (with an optional ``"-"`` prefix + which indicates descending order) or an expression (or a tuple or list + of strings and/or expressions) that specifies the ordering of the + elements in the result list. + + Examples are the same as for :attr:`ArrayAgg.ordering`. + ``StringAgg`` ------------- diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index 2e9c87e567..3b831ee8ec 100644 --- a/docs/releases/3.2.txt +++ b/docs/releases/3.2.txt @@ -73,6 +73,9 @@ Minor features * The new :attr:`.ExclusionConstraint.include` attribute allows creating covering exclusion constraints on PostgreSQL 12+. +* The new :attr:`.JSONBAgg.ordering` attribute determines the ordering of the + aggregated elements. + :mod:`django.contrib.redirects` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/postgres_tests/test_aggregates.py b/tests/postgres_tests/test_aggregates.py index c97ed8e62d..8f3d8bb6ef 100644 --- a/tests/postgres_tests/test_aggregates.py +++ b/tests/postgres_tests/test_aggregates.py @@ -222,6 +222,42 @@ class TestGeneralAggregate(PostgreSQLTestCase): values = AggregateTestModel.objects.none().aggregate(jsonagg=JSONBAgg('integer_field')) self.assertEqual(values, json.loads('{"jsonagg": []}')) + def test_json_agg_charfield_ordering(self): + ordering_test_cases = ( + (F('char_field').desc(), ['Foo4', 'Foo3', 'Foo2', 'Foo1']), + (F('char_field').asc(), ['Foo1', 'Foo2', 'Foo3', 'Foo4']), + (F('char_field'), ['Foo1', 'Foo2', 'Foo3', 'Foo4']), + ('char_field', ['Foo1', 'Foo2', 'Foo3', 'Foo4']), + ('-char_field', ['Foo4', 'Foo3', 'Foo2', 'Foo1']), + (Concat('char_field', Value('@')), ['Foo1', 'Foo2', 'Foo3', 'Foo4']), + (Concat('char_field', Value('@')).desc(), ['Foo4', 'Foo3', 'Foo2', 'Foo1']), + ) + for ordering, expected_output in ordering_test_cases: + with self.subTest(ordering=ordering, expected_output=expected_output): + values = AggregateTestModel.objects.aggregate( + jsonagg=JSONBAgg('char_field', ordering=ordering), + ) + self.assertEqual(values, {'jsonagg': expected_output}) + + def test_json_agg_integerfield_ordering(self): + values = AggregateTestModel.objects.aggregate( + jsonagg=JSONBAgg('integer_field', ordering=F('integer_field').desc()), + ) + self.assertEqual(values, {'jsonagg': [2, 1, 0, 0]}) + + def test_json_agg_booleanfield_ordering(self): + ordering_test_cases = ( + (F('boolean_field').asc(), [False, False, True, True]), + (F('boolean_field').desc(), [True, True, False, False]), + (F('boolean_field'), [False, False, True, True]), + ) + for ordering, expected_output in ordering_test_cases: + with self.subTest(ordering=ordering, expected_output=expected_output): + values = AggregateTestModel.objects.aggregate( + jsonagg=JSONBAgg('boolean_field', ordering=ordering), + ) + self.assertEqual(values, {'jsonagg': expected_output}) + def test_string_agg_array_agg_ordering_in_subquery(self): stats = [] for i, agg in enumerate(AggregateTestModel.objects.order_by('char_field')):