Browse Source

Fixed #31691 -- Added ordering support to JSONBAgg.

John Parton 4 years ago
parent
commit
a8473b4d34

+ 2 - 1
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):

+ 12 - 1
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``
 -------------
 

+ 3 - 0
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`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 36 - 0
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')):