From a8473b4d348776d823b7a83c1795279279cf3ab5 Mon Sep 17 00:00:00 2001 From: John Parton Date: Fri, 12 Jun 2020 09:55:22 -0500 Subject: [PATCH] Fixed #31691 -- Added ordering support to JSONBAgg. --- django/contrib/postgres/aggregates/general.py | 3 +- docs/ref/contrib/postgres/aggregates.txt | 13 ++++++- docs/releases/3.2.txt | 3 ++ tests/postgres_tests/test_aggregates.py | 36 +++++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/django/contrib/postgres/aggregates/general.py b/django/contrib/postgres/aggregates/general.py index 12cba627019..00957bdab0a 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 ad070906885..b59555cb10a 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 2e9c87e5672..3b831ee8ec7 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 c97ed8e62d4..8f3d8bb6ef8 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')):