#33772: Queryset first() returns different result than queryset[0] when using
ExpressionWrapper
-------------------------------------+-------------------------------------
     Reporter:  Pablo Pissi          |                    Owner:  nobody
         Type:  Uncategorized        |                   Status:  closed
    Component:  Database layer       |                  Version:  3.2
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:  needsinfo
     Keywords:                       |             Triage Stage:
  ExpressionWrapper,first,queryset   |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------

Comment (by Simon Charette):

 The addition of `order_by` columns to the `GROUP BY` clause specified by
 `values().annotate()`
 [https://docs.djangoproject.com/en/4.0/topics/db/aggregation/#interaction-
 with-order-by is documented].

 The crux of the issue remain; you are asking for an element at a
 particular index of a collection with an undefined ordering. Adding
 `order_by("example_model")` to your test case addresses the irregularity
 you are experiencing with `first()`.

 `first()` could arguably be adjusted to raise an exception when attempted
 to be used on an unordered collection to ''refuse the temptation to
 guess'' but that would require a deprecation period. Another approach
 could be to only raise an exception when `first()` is called on a
 unordered queryset with an explicit `values()` that doesn't include `pk`
 which is the case reported here.

 Here's what the code could look like for a systematic error on attempts to
 use `first()` on a undefined ordering queryset performing aggregation over
 specific columns

 {{{#!diff
 diff --git a/django/db/models/query.py b/django/db/models/query.py
 index 67fcb411eb..da3d1ff3a7 100644
 --- a/django/db/models/query.py
 +++ b/django/db/models/query.py
 @@ -1032,7 +1032,14 @@ async def alatest(self, *fields):

      def first(self):
          """Return the first object of a query or None if no match is
 found."""
 -        for obj in (self if self.ordered else self.order_by("pk"))[:1]:
 +        queryset = self
 +        if not self.ordered:
 +            if isinstance(self.query.group_by, tuple):
 +                raise TypeError(
 +                    "Cannot use first() on an unordered queryset
 performing aggregation"
 +                )
 +            queryset = self.order_by("pk")
 +        for obj in queryset[:1]:
              return obj

      async def afirst(self):
 }}}

 And a somewhat more complex implementation that still allow the `pk`

 {{{#!diff
 diff --git a/django/db/models/query.py b/django/db/models/query.py
 index 67fcb411eb..0534a2cc16 100644
 --- a/django/db/models/query.py
 +++ b/django/db/models/query.py
 @@ -1032,7 +1032,17 @@ async def alatest(self, *fields):

      def first(self):
          """Return the first object of a query or None if no match is
 found."""
 -        for obj in (self if self.ordered else self.order_by("pk"))[:1]:
 +        queryset = self
 +        if not self.ordered:
 +            if isinstance(self.query.group_by, tuple) and not any(
 +                col.output_field is self.model._meta.pk for col in
 self.query.group_by
 +            ):
 +                raise TypeError(
 +                    "Cannot use first() on an unordered queryset
 performing aggregation "
 +                    "over a set of expressions that doesn't include the
 base model's primary key"
 +                )
 +            queryset = self.order_by("pk")
 +        for obj in queryset[:1]:
              return obj

      async def afirst(self):
 }}}

 The latter patch seems to be passing the test suite.

 Thoughts on the value of such adjustment Carlton?

-- 
Ticket URL: <https://code.djangoproject.com/ticket/33772#comment:5>
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 [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/0107018146af4e5a-d9aae070-472e-4cf0-8ca1-84e27d249bb0-000000%40eu-central-1.amazonses.com.

Reply via email to