Skip to content

Commit 0060aa8

Browse files
Remove subqueries from group by. Fixes #343 (#344)
* collapse group by considers subexpressions in order to remove subquery expressions and avoid query failing * method renamed to make it "protected". * constants are removed from group by because sql server does not allow them * a django test that now passes is not excluded anymore
1 parent 67b61be commit 0060aa8

File tree

3 files changed

+68
-5
lines changed

3 files changed

+68
-5
lines changed

mssql/compiler.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def _as_sql_window(self, compiler, connection, template=None):
155155
else:
156156
# MSSQL window functions require an OVER clause with ORDER BY
157157
window_sql.append('ORDER BY (SELECT NULL)')
158-
158+
159159
if self.frame:
160160
frame_sql, frame_params = compiler.compile(self.frame)
161161
window_sql.append(frame_sql)
@@ -443,7 +443,45 @@ def compile(self, node, *args, **kwargs):
443443

444444
def collapse_group_by(self, expressions, having):
445445
expressions = super().collapse_group_by(expressions, having)
446-
return [e for e in expressions if not isinstance(e, Subquery)]
446+
# SQL server does not allow subqueries or constant expressions in the group by
447+
# For constants: Each GROUP BY expression must contain at least one column that is not an outer reference.
448+
# For subqueries: Cannot use an aggregate or a subquery in an expression used for the group by list of a GROUP BY clause.
449+
return self._filter_subquery_and_constant_expressions(expressions)
450+
451+
def _is_constant_expression(self, expression):
452+
if isinstance(expression, Value):
453+
return True
454+
sub_exprs = expression.get_source_expressions()
455+
if not sub_exprs:
456+
return False
457+
for each in sub_exprs:
458+
if not self._is_constant_expression(each):
459+
return False
460+
return True
461+
462+
463+
464+
def _filter_subquery_and_constant_expressions(self, expressions):
465+
ret = []
466+
for expression in expressions:
467+
if self._is_subquery(expression):
468+
continue
469+
if self._is_constant_expression(expression):
470+
continue
471+
if not self._has_nested_subquery(expression):
472+
ret.append(expression)
473+
return ret
474+
475+
def _has_nested_subquery(self, expression):
476+
if self._is_subquery(expression):
477+
return True
478+
for sub_expr in expression.get_source_expressions():
479+
if self._has_nested_subquery(sub_expr):
480+
return True
481+
return False
482+
483+
def _is_subquery(self, expression):
484+
return isinstance(expression, Subquery)
447485

448486
def _as_microsoft(self, node):
449487
as_microsoft = None

testapp/settings.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@
168168
'expressions.tests.FTimeDeltaTests.test_time_subquery_subtraction',
169169
'migrations.test_operations.OperationTests.test_alter_field_reloads_state_on_fk_with_to_field_target_type_change',
170170
'schema.tests.SchemaTests.test_alter_smallint_pk_to_smallautofield_pk',
171-
'annotations.tests.NonAggregateAnnotationTestCase.test_combined_expression_annotation_with_aggregation',
172171
'db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_func',
173172
'db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_iso_weekday_func',
174173
'db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_extract_func',

testapp/tests/test_expressions.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
from django import VERSION
77
from django.db.models import IntegerField, F
8-
from django.db.models.expressions import Case, Exists, OuterRef, Subquery, Value, When
8+
from django.db.models.expressions import Case, Exists, OuterRef, Subquery, Value, When, ExpressionWrapper
99
from django.test import TestCase, skipUnlessDBFeature
1010

11-
11+
from django.db.models.aggregates import Count
1212
from ..models import Author, Comment, Post, Editor
1313

1414
DJANGO3 = VERSION[0] >= 3
@@ -47,6 +47,32 @@ def test_with_case_when(self):
4747
).get()
4848
self.assertEqual(author.has_post, 1)
4949

50+
def test_unnecessary_exists_group_by(self):
51+
author = Author.objects.annotate(
52+
has_post=Case(
53+
When(Exists(Post.objects.filter(author=OuterRef('pk')).values('pk')), then=Value(1)),
54+
default=Value(0),
55+
output_field=IntegerField(),
56+
)).annotate(
57+
amount=Count("post")
58+
).get()
59+
self.assertEqual(author.amount, 1)
60+
self.assertEqual(author.has_post, 1)
61+
62+
def test_combined_expression_annotation_with_aggregation(self):
63+
book = Author.objects.annotate(
64+
combined=ExpressionWrapper(
65+
Value(2) * Value(5), output_field=IntegerField()
66+
),
67+
null_value=ExpressionWrapper(
68+
Value(None), output_field=IntegerField()
69+
),
70+
rating_count=Count("post"),
71+
).first()
72+
self.assertEqual(book.combined, 10)
73+
self.assertEqual(book.null_value, None)
74+
75+
5076
@skipUnless(DJANGO3, "Django 3 specific tests")
5177
def test_order_by_exists(self):
5278
author_without_posts = Author.objects.create(name="other author")

0 commit comments

Comments
 (0)