#34967: Django ORM not generating GROUP BY clause with static annotation
-------------------------------------+-------------------------------------
               Reporter:  Simon      |          Owner:  nobody
  Legtenborg                         |
                   Type:  Bug        |         Status:  new
              Component:  Database   |        Version:  4.2
  layer (models, ORM)                |
               Severity:  Normal     |       Keywords:
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 Django ORM is not generating a GROUP BY clause with static annotation.
 After filtering, Django ORM inserts a HAVING clause, but GROUP BY is
 missing.

 **Expected Behavior**
 The Django ORM should generate a SQL query with a GROUP_BY clause when
 using `.values()` and `.annotate()` methods on a queryset.

 **Actual Behvaior**
 The ORM is not generating a GROUP BY clause when a static annotation is
 used. However, it does generate it when a non-trivial annotation is used.


 **Steps to reproduce**

 With a small model
 {{{
 class Book(models.Model):
     title = models.CharField(max_length=100)
     pages = models.IntegerField(default=0)
 }}}

 and the corresponding view


 {{{
 def bug_view(request):
     queryset = Book.objects.all()
     print(f"query: {queryset.query}")

     # trivial annotation
     annotated_queryset = queryset.annotate(group=Value('all'))
     print(f"annotated_query: {annotated_queryset.query}")

     # grouped_and_annotated_query won't have a GROUP BY clause
     grouped_and_annotated_queryset =
     annotated_queryset.values('group').annotate(sum=models.Sum('pages'))
     print(f"grouped_and_annotated_query:
 {grouped_and_annotated_queryset.query}")

     # filtered_query contains HAVING clause but no GROUP BY clause
     filtered_queryset = grouped_and_annotated_queryset.filter(sum__gt=10)
     print(f"filtered_query: {filtered_queryset.query}")
     return HttpResponse(filtered_queryset)
 }}}

 Django raises a django.db.utils.OperationalError. The (printed) querries
 are


 {{{
 query: SELECT "books_book"."id", "books_book"."title",
 "books_book"."pages" FROM "books_book"

 annotated_query: SELECT "books_book"."id", "books_book"."title",
 "books_book"."pages", all AS "group" FROM "books_book"

 grouped_and_annotated_query: SELECT all AS "group",
 SUM("books_book"."pages") AS "sum" FROM "books_book"

 filtered_query: SELECT all AS "group", SUM("books_book"."pages") AS "sum"
 FROM "books_book" HAVING SUM("books_book"."pages") > 10
 }}}

 As you can see, there is no GROUP BY keyword in the
 grouped_and_annotated_query. But after filtering, a HAVING keyword is
 inserted, without a GROUP BY.  This is the main reason for this error.
 This happens only to static annotations. If i evaluate a more complex
 annotation, the grouping works as intended:

 {{{
 def without_bug_view(request):
     queryset = Book.objects.all()
     print(f"query: {queryset.query}")

     # non-trivial annotation
     annotated_queryset = queryset.annotate(large=Case(
         When(pages__gt=650, then=Value(True)),
         default=Value(False),
         output_field=BooleanField()))
     print(f"annotated_query: {annotated_queryset.query}")
     grouped_and_annotated_queryset =
 annotated_queryset.values('large').annotate(sum=models.Sum('pages'))
     print(f"grouped_and_annotated_query:
 {grouped_and_annotated_queryset.query}")
     filtered_queryset = grouped_and_annotated_queryset.filter(sum__gt=0)
     print(f"filtered_query: {filtered_queryset.query}")
     return HttpResponse(filtered_queryset)
 }}}

 querries:
 {{{
 query: SELECT "books_book"."id", "books_book"."title",
 "books_book"."pages" FROM "books_book"

 annotated_query: SELECT "books_book"."id", "books_book"."title",
 "books_book"."pages", CASE WHEN "books_book"."pages" > 650 THEN True ELSE
 False END AS "large" FROM "books_book"

 grouped_and_annotated_query: SELECT CASE WHEN "books_book"."pages" > 650
 THEN True ELSE False END AS "large", SUM("books_book"."pages") AS "sum"
 FROM "books_book" GROUP BY 1

 filtered_query: SELECT CASE WHEN "books_book"."pages" > 650 THEN True ELSE
 False END AS "large", SUM("books_book"."pages") AS "sum" FROM "books_book"
 GROUP BY 1 HAVING SUM("books_book"."pages") > 0
 }}}

 The GROUP BY keyword is inserted as is should.


 Here is the Stack Trace for completeness:


 {{{
 Error
 Traceback (most recent call last):
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/backends/utils.py", line 89, in _execute
     return self.cursor.execute(sql, params)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/backends/sqlite3/base.py", line 328, in execute
     return super().execute(query, params)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 sqlite3.OperationalError: a GROUP BY clause is required before HAVING

 The above exception was the direct cause of the following exception:

 Traceback (most recent call last):
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/books/tests.py", line
 8, in test_bug_view
     response = client.get("/books/bug")
                ^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/test/client.py", line 927, in get
     response = super().get(path, data=data, secure=secure,
 headers=headers, **extra)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/test/client.py", line 457, in get
     return self.generic(
            ^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/test/client.py", line 609, in generic
     return self.request(**r)
            ^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/test/client.py", line 891, in request
     self.check_exception(response)
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/test/client.py", line 738, in check_exception
     raise exc_value
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/core/handlers/exception.py", line 55, in inner
     response = get_response(request)
                ^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/core/handlers/base.py", line 197, in _get_response
     response = wrapped_callback(request, *callback_args,
 **callback_kwargs)
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/books/views.py", line
 23, in bug_view
     return HttpResponse(filtered_queryset)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/http/response.py", line 376, in __init__
     self.content = content
     ^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/http/response.py", line 401, in content
     content = b"".join(self.make_bytes(chunk) for chunk in value)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/models/query.py", line 398, in __iter__
     self._fetch_all()
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/models/query.py", line 1881, in _fetch_all
     self._result_cache = list(self._iterable_class(self))
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/models/query.py", line 208, in __iter__
     for row in compiler.results_iter(
                ^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/models/sql/compiler.py", line 1513, in
 results_iter
     results = self.execute_sql(
               ^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/models/sql/compiler.py", line 1562, in
 execute_sql
     cursor.execute(sql, params)
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/backends/utils.py", line 67, in execute
     return self._execute_with_wrappers(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/backends/utils.py", line 80, in
 _execute_with_wrappers
     return executor(sql, params, many, context)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/backends/utils.py", line 84, in _execute
     with self.db.wrap_database_errors:
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/utils.py", line 91, in __exit__
     raise dj_exc_value.with_traceback(traceback) from exc_value
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/backends/utils.py", line 89, in _execute
     return self.cursor.execute(sql, params)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File
 "/home/simonlegtenborg/PycharmProjects/djangoProject/venv/lib/python3.11
 /site-packages/django/db/backends/sqlite3/base.py", line 328, in execute
     return super().execute(query, params)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 django.db.utils.OperationalError: a GROUP BY clause is required before
 HAVING
 }}}

-- 
Ticket URL: <https://code.djangoproject.com/ticket/34967>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-updates+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/0107018bc3899c99-f747acc1-abfb-4787-b68f-1cff94e85062-000000%40eu-central-1.amazonses.com.

Reply via email to