mirror of https://github.com/django/django.git
[3.0.x] Fixed #31094 -- Included columns referenced by subqueries in GROUP BY on aggregations.
Thanks Johannes Hoppe for the report. Regression infb3f034f1c
. Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com> Backport of5a4d7285bd
from master
This commit is contained in:
parent
fb260ad777
commit
fd9050589f
|
@ -1068,7 +1068,7 @@ class Subquery(Expression):
|
||||||
def get_group_by_cols(self, alias=None):
|
def get_group_by_cols(self, alias=None):
|
||||||
if alias:
|
if alias:
|
||||||
return [Ref(alias, self)]
|
return [Ref(alias, self)]
|
||||||
return []
|
return self.query.get_external_cols()
|
||||||
|
|
||||||
|
|
||||||
class Exists(Subquery):
|
class Exists(Subquery):
|
||||||
|
|
|
@ -110,7 +110,14 @@ class SQLCompiler:
|
||||||
# Note that even if the group_by is set, it is only the minimal
|
# Note that even if the group_by is set, it is only the minimal
|
||||||
# set to group by. So, we need to add cols in select, order_by, and
|
# set to group by. So, we need to add cols in select, order_by, and
|
||||||
# having into the select in any case.
|
# having into the select in any case.
|
||||||
|
ref_sources = {
|
||||||
|
expr.source for expr in expressions if isinstance(expr, Ref)
|
||||||
|
}
|
||||||
for expr, _, _ in select:
|
for expr, _, _ in select:
|
||||||
|
# Skip members of the select clause that are already included
|
||||||
|
# by reference.
|
||||||
|
if expr in ref_sources:
|
||||||
|
continue
|
||||||
cols = expr.get_group_by_cols()
|
cols = expr.get_group_by_cols()
|
||||||
for col in cols:
|
for col in cols:
|
||||||
expressions.append(col)
|
expressions.append(col)
|
||||||
|
@ -400,7 +407,7 @@ class SQLCompiler:
|
||||||
return self.quote_cache[name]
|
return self.quote_cache[name]
|
||||||
if ((name in self.query.alias_map and name not in self.query.table_map) or
|
if ((name in self.query.alias_map and name not in self.query.table_map) or
|
||||||
name in self.query.extra_select or (
|
name in self.query.extra_select or (
|
||||||
name in self.query.external_aliases and name not in self.query.table_map)):
|
self.query.external_aliases.get(name) and name not in self.query.table_map)):
|
||||||
self.quote_cache[name] = name
|
self.quote_cache[name] = name
|
||||||
return name
|
return name
|
||||||
r = self.connection.ops.quote_name(name)
|
r = self.connection.ops.quote_name(name)
|
||||||
|
|
|
@ -163,7 +163,8 @@ class Query(BaseExpression):
|
||||||
# Sometimes the query contains references to aliases in outer queries (as
|
# Sometimes the query contains references to aliases in outer queries (as
|
||||||
# a result of split_exclude). Correct alias quoting needs to know these
|
# a result of split_exclude). Correct alias quoting needs to know these
|
||||||
# aliases too.
|
# aliases too.
|
||||||
self.external_aliases = set()
|
# Map external tables to whether they are aliased.
|
||||||
|
self.external_aliases = {}
|
||||||
self.table_map = {} # Maps table names to list of aliases.
|
self.table_map = {} # Maps table names to list of aliases.
|
||||||
self.default_cols = True
|
self.default_cols = True
|
||||||
self.default_ordering = True
|
self.default_ordering = True
|
||||||
|
@ -856,8 +857,11 @@ class Query(BaseExpression):
|
||||||
if alias == old_alias:
|
if alias == old_alias:
|
||||||
table_aliases[pos] = new_alias
|
table_aliases[pos] = new_alias
|
||||||
break
|
break
|
||||||
self.external_aliases = {change_map.get(alias, alias)
|
self.external_aliases = {
|
||||||
for alias in self.external_aliases}
|
# Table is aliased or it's being changed and thus is aliased.
|
||||||
|
change_map.get(alias, alias): (aliased or alias in change_map)
|
||||||
|
for alias, aliased in self.external_aliases.items()
|
||||||
|
}
|
||||||
|
|
||||||
def bump_prefix(self, outer_query):
|
def bump_prefix(self, outer_query):
|
||||||
"""
|
"""
|
||||||
|
@ -1031,19 +1035,23 @@ class Query(BaseExpression):
|
||||||
for key, value in clone.annotations.items():
|
for key, value in clone.annotations.items():
|
||||||
resolved = value.resolve_expression(query, *args, **kwargs)
|
resolved = value.resolve_expression(query, *args, **kwargs)
|
||||||
if hasattr(resolved, 'external_aliases'):
|
if hasattr(resolved, 'external_aliases'):
|
||||||
resolved.external_aliases.update(clone.alias_map)
|
resolved.external_aliases.update(clone.external_aliases)
|
||||||
clone.annotations[key] = resolved
|
clone.annotations[key] = resolved
|
||||||
# Outer query's aliases are considered external.
|
# Outer query's aliases are considered external.
|
||||||
clone.external_aliases.update(
|
for alias, table in query.alias_map.items():
|
||||||
alias for alias, table in query.alias_map.items()
|
clone.external_aliases[alias] = (
|
||||||
if (
|
(isinstance(table, Join) and table.join_field.related_model._meta.db_table != alias) or
|
||||||
isinstance(table, Join) and table.join_field.related_model._meta.db_table != alias
|
(isinstance(table, BaseTable) and table.table_name != table.table_alias)
|
||||||
) or (
|
|
||||||
isinstance(table, BaseTable) and table.table_name != table.table_alias
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
return clone
|
return clone
|
||||||
|
|
||||||
|
def get_external_cols(self):
|
||||||
|
exprs = chain(self.annotations.values(), self.where.children)
|
||||||
|
return [
|
||||||
|
col for col in self._gen_cols(exprs)
|
||||||
|
if col.alias in self.external_aliases
|
||||||
|
]
|
||||||
|
|
||||||
def as_sql(self, compiler, connection):
|
def as_sql(self, compiler, connection):
|
||||||
sql, params = self.get_compiler(connection=connection).as_sql()
|
sql, params = self.get_compiler(connection=connection).as_sql()
|
||||||
if self.subquery:
|
if self.subquery:
|
||||||
|
@ -1638,12 +1646,16 @@ class Query(BaseExpression):
|
||||||
return targets, joins[-1], joins
|
return targets, joins[-1], joins
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _gen_col_aliases(cls, exprs):
|
def _gen_cols(cls, exprs):
|
||||||
for expr in exprs:
|
for expr in exprs:
|
||||||
if isinstance(expr, Col):
|
if isinstance(expr, Col):
|
||||||
yield expr.alias
|
yield expr
|
||||||
else:
|
else:
|
||||||
yield from cls._gen_col_aliases(expr.get_source_expressions())
|
yield from cls._gen_cols(expr.get_source_expressions())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _gen_col_aliases(cls, exprs):
|
||||||
|
yield from (expr.alias for expr in cls._gen_cols(exprs))
|
||||||
|
|
||||||
def resolve_ref(self, name, allow_joins=True, reuse=None, summarize=False, simple_col=False):
|
def resolve_ref(self, name, allow_joins=True, reuse=None, summarize=False, simple_col=False):
|
||||||
if not allow_joins and LOOKUP_SEP in name:
|
if not allow_joins and LOOKUP_SEP in name:
|
||||||
|
@ -1737,7 +1749,7 @@ class Query(BaseExpression):
|
||||||
lookup = lookup_class(pk.get_col(query.select[0].alias),
|
lookup = lookup_class(pk.get_col(query.select[0].alias),
|
||||||
pk.get_col(alias))
|
pk.get_col(alias))
|
||||||
query.where.add(lookup, AND)
|
query.where.add(lookup, AND)
|
||||||
query.external_aliases.add(alias)
|
query.external_aliases[alias] = True
|
||||||
|
|
||||||
condition, needed_inner = self.build_filter(
|
condition, needed_inner = self.build_filter(
|
||||||
('%s__in' % trimmed_prefix, query),
|
('%s__in' % trimmed_prefix, query),
|
||||||
|
|
|
@ -9,4 +9,5 @@ Django 3.0.2 fixes several bugs in 3.0.1.
|
||||||
Bugfixes
|
Bugfixes
|
||||||
========
|
========
|
||||||
|
|
||||||
* ...
|
* Fixed a regression in Django 3.0 that didn't include columns referenced by a
|
||||||
|
``Subquery()`` in the ``GROUP BY`` clause (:ticket:`31094`).
|
||||||
|
|
|
@ -1170,3 +1170,23 @@ class AggregateTestCase(TestCase):
|
||||||
Exists(long_books_qs),
|
Exists(long_books_qs),
|
||||||
).annotate(total=Count('*'))
|
).annotate(total=Count('*'))
|
||||||
self.assertEqual(dict(has_long_books_breakdown), {True: 2, False: 3})
|
self.assertEqual(dict(has_long_books_breakdown), {True: 2, False: 3})
|
||||||
|
|
||||||
|
def test_aggregation_subquery_annotation_related_field(self):
|
||||||
|
publisher = Publisher.objects.create(name=self.a9.name, num_awards=2)
|
||||||
|
book = Book.objects.create(
|
||||||
|
isbn='159059999', name='Test book.', pages=819, rating=2.5,
|
||||||
|
price=Decimal('14.44'), contact=self.a9, publisher=publisher,
|
||||||
|
pubdate=datetime.date(2019, 12, 6),
|
||||||
|
)
|
||||||
|
book.authors.add(self.a5, self.a6, self.a7)
|
||||||
|
books_qs = Book.objects.annotate(
|
||||||
|
contact_publisher=Subquery(
|
||||||
|
Publisher.objects.filter(
|
||||||
|
pk=OuterRef('publisher'),
|
||||||
|
name=OuterRef('contact__name'),
|
||||||
|
).values('name')[:1],
|
||||||
|
)
|
||||||
|
).filter(
|
||||||
|
contact_publisher__isnull=False,
|
||||||
|
).annotate(count=Count('authors'))
|
||||||
|
self.assertSequenceEqual(books_qs, [book])
|
||||||
|
|
Loading…
Reference in New Issue