From 32536b1324e98768dd892980408a8c6b26c23fd9 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Wed, 7 Sep 2022 22:30:06 -0400 Subject: [PATCH] Fixed #33992 -- Fixed queryset crash when aggregating over a group containing Exists. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A more in-depth solution is likely to make sure that we always GROUP BY selected annotations or revisit how we use Query.exists() in the Exists expression but that requires extra work that isn't suitable for a backport. Regression in e5a92d400acb4ca6a8e1375d1ab8121f2c7220be. Thanks Fernando Flores Villaça for the report. --- django/db/models/expressions.py | 8 ++++++++ django/db/models/sql/compiler.py | 5 ++++- docs/releases/4.1.2.txt | 4 ++++ tests/aggregation/tests.py | 11 +++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 58e945dd44..5e3c7cab82 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -1501,6 +1501,14 @@ class Exists(Subquery): clone.negated = not self.negated return clone + def get_group_by_cols(self, alias=None): + # self.query only gets limited to a single row in the .exists() call + # from self.as_sql() so deferring to Query.get_group_by_cols() is + # inappropriate. + if alias is None: + return [self] + return super().get_group_by_cols(alias) + def as_sql(self, compiler, connection, template=None, **extra_context): query = self.query.exists(using=connection.alias) try: diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 96d10b9eda..27310e5d9f 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -160,7 +160,10 @@ class SQLCompiler: expressions = self.collapse_group_by(expressions, having_group_by) for expr in expressions: - sql, params = self.compile(expr) + try: + sql, params = self.compile(expr) + except EmptyResultSet: + continue sql, params = expr.select_format(self, sql, params) params_hash = make_hashable(params) if (sql, params_hash) not in seen: diff --git a/docs/releases/4.1.2.txt b/docs/releases/4.1.2.txt index 4659558f3d..96c2fefcdc 100644 --- a/docs/releases/4.1.2.txt +++ b/docs/releases/4.1.2.txt @@ -11,3 +11,7 @@ Bugfixes * Fixed a regression in Django 4.1 that caused a migration crash on PostgreSQL when adding a model with ``ExclusionConstraint`` (:ticket:`33982`). + +* Fixed a regression in Django 4.1 that caused aggregation over a queryset that + contained an ``Exists`` annotation to crash due to too many selected columns + (:ticket:`33992`). diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py index 75a4e0d022..5ec12ccd64 100644 --- a/tests/aggregation/tests.py +++ b/tests/aggregation/tests.py @@ -1663,6 +1663,17 @@ class AggregateTestCase(TestCase): ).values_list("publisher_count", flat=True) self.assertSequenceEqual(books_breakdown, [1] * 6) + def test_aggregation_exists_multivalued_outeref(self): + self.assertCountEqual( + Publisher.objects.annotate( + books_exists=Exists( + Book.objects.filter(publisher=OuterRef("book__publisher")) + ), + books_count=Count("book"), + ), + Publisher.objects.all(), + ) + def test_filter_in_subquery_or_aggregation(self): """ Filtering against an aggregate requires the usage of the HAVING clause.