diff --git a/django/contrib/postgres/aggregates/mixins.py b/django/contrib/postgres/aggregates/mixins.py index 6c1ce53b75..4625738beb 100644 --- a/django/contrib/postgres/aggregates/mixins.py +++ b/django/contrib/postgres/aggregates/mixins.py @@ -33,6 +33,12 @@ class OrderableAggMixin: return sql, sql_params + ordering_params return super().as_sql(compiler, connection, ordering='') + def set_source_expressions(self, exprs): + # Extract the ordering expressions because ORDER BY clause is handled + # in a custom way. + self.ordering = exprs[self._get_ordering_expressions_index():] + return super().set_source_expressions(exprs[:self._get_ordering_expressions_index()]) + def get_source_expressions(self): return self.source_expressions + self.ordering diff --git a/docs/releases/2.2.2.txt b/docs/releases/2.2.2.txt index fc47100844..08af175caa 100644 --- a/docs/releases/2.2.2.txt +++ b/docs/releases/2.2.2.txt @@ -21,3 +21,7 @@ Bugfixes * Fixed a regression in Django 2.2 where auto-reloader doesn't detect changes in ``manage.py`` file when using ``StatReloader`` (:ticket:`30479`). + +* Fixed crash of :class:`~django.contrib.postgres.aggregates.ArrayAgg` and + :class:`~django.contrib.postgres.aggregates.StringAgg` with ``ordering`` + argument when used in a ``Subquery`` (:ticket:`30315`). diff --git a/tests/postgres_tests/test_aggregates.py b/tests/postgres_tests/test_aggregates.py index 23f3c59ded..9bd5b70a9e 100644 --- a/tests/postgres_tests/test_aggregates.py +++ b/tests/postgres_tests/test_aggregates.py @@ -1,7 +1,8 @@ import json -from django.db.models.expressions import F, Value -from django.db.models.functions import Concat, Substr +from django.db.models import CharField +from django.db.models.expressions import F, OuterRef, Subquery, Value +from django.db.models.functions import Cast, Concat, Substr from django.test.utils import Approximate from . import PostgreSQLTestCase @@ -202,6 +203,36 @@ class TestGeneralAggregate(PostgreSQLTestCase): values = AggregateTestModel.objects.none().aggregate(jsonagg=JSONBAgg('integer_field')) self.assertEqual(values, json.loads('{"jsonagg": []}')) + def test_string_agg_array_agg_ordering_in_subquery(self): + stats = [] + for i, agg in enumerate(AggregateTestModel.objects.order_by('char_field')): + stats.append(StatTestModel(related_field=agg, int1=i, int2=i + 1)) + stats.append(StatTestModel(related_field=agg, int1=i + 1, int2=i)) + StatTestModel.objects.bulk_create(stats) + + for aggregate, expected_result in ( + ( + ArrayAgg('stattestmodel__int1', ordering='-stattestmodel__int2'), + [('Foo1', [0, 1]), ('Foo2', [1, 2]), ('Foo3', [2, 3]), ('Foo4', [3, 4])], + ), + ( + StringAgg( + Cast('stattestmodel__int1', CharField()), + delimiter=';', + ordering='-stattestmodel__int2', + ), + [('Foo1', '0;1'), ('Foo2', '1;2'), ('Foo3', '2;3'), ('Foo4', '3;4')], + ), + ): + with self.subTest(aggregate=aggregate.__class__.__name__): + subquery = AggregateTestModel.objects.filter( + pk=OuterRef('pk'), + ).annotate(agg=aggregate).values('agg') + values = AggregateTestModel.objects.annotate( + agg=Subquery(subquery), + ).order_by('char_field').values_list('char_field', 'agg') + self.assertEqual(list(values), expected_result) + class TestAggregateDistinct(PostgreSQLTestCase): @classmethod