Opened 19 months ago

Last modified 3 months ago

#34533 new Bug

OuterRef not resolved as part oh ORDER BY clause

Reported by: REGNIER Guillaume Owned by:
Component: Database layer (models, ORM) Version: 3.2
Severity: Normal Keywords: OuterRef, OrderBy
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description

I feel like OuterRef are not resolved properly where used for ordering.

Here is a simple example showcasing the issue:

MyModel.objects.annotate(foo=Subquery(MyModel.objects.order_by("pk" - OuterRef("pk")).values("pk")[:1]))

The above fails with :
ValueError: This queryset contains a reference to an outer query and may only be used in a subquery.
Because the as_sql method of ResolvedOuterRef is called.

I think this a bug because it did not raise any notice regarding OuterRef and order_by not being compatible.

Change History (17)

comment:1 by David Sanders, 19 months ago

Triage Stage: UnreviewedAccepted

Thanks 👍

Just FYI this will work (note you must wrap 'pk' with an F):

MyModel.objects.annotate(foo=Subquery(MyModel.objects.annotate(order=F("pk") - OuterRef("pk")).order_by("order").values("pk")[:1]))

comment:2 by Jordan Bae, 18 months ago

Owner: changed from nobody to Jordan Bae
Status: newassigned

I will try to fix this!

comment:3 by REGNIER Guillaume, 18 months ago

I've been working in and out on this for the last three weeks. I'm sorry I didn't claim the ticket...

Here is a branch where the issue is solved: https://github.com/Alombaros/django/tree/ticket_34533_3_2

I'm unsure on how to proceed.
Previously, in the following query :
MyModel.objects.annotate(foo=Subquery(MyOtherModel.objects.order_by(F("pk")).values("pk")[:1]))
The F("pk") was resolved as the pk of the outer query (MyModel in this case) which does not seems right to me.
This was due to the order by expressions not being resolved until the last minute.

I force the resolution when annotating a SubQuery so that the OuterRef can do its job but this made so F objects are resolved as field of the inner query.
To me, this seems to be closer to the intended behavior because its what append when calling .filter or .annotate but if anyone used the prior behavior, thing will break

comment:4 by Jordan Bae, 18 months ago

Owner: Jordan Bae removed
Status: assignednew

no worry! i think it's better tyou bring this ticket. i deassign this ticket.

comment:5 by Jordan Bae, 18 months ago

Owner: set to REGNIER Guillaume
Status: newassigned

comment:6 by Umang Patel, 14 months ago

Owner: changed from REGNIER Guillaume to Umang Patel

comment:8 by Mariusz Felisiak, 14 months ago

Owner: changed from Umang Patel to REGNIER Guillaume
Patch needs improvement: set

comment:9 by Shafiya Adzhani, 10 months ago

Owner: changed from REGNIER Guillaume to Shafiya Adzhani

I will try to improve what can be done from previous PR.

comment:10 by Simon Charette, 10 months ago

The hard part is here is likely due to two factor

  1. OuterRef resolving is quite finicky and depends on a specific chain of resolving during query composition (subquery is resolved and then the outer query is)
  2. order_by members are unfortunately not resolved at query composition time (when QuerySet.order_by is called) but at query compilation time.

The reason for 2. is that QuerySet.order_by calls are not additive but destructive and that the query composition logic doesn't support a generic way of eliding joins that we previously created for some operations.

For example, say that you have a query that does qs = Book.objects.order_by("author__name"). If the order by clause is immediately resolved then a JOIN to author must be created. If the same query has then its ordering changed to qs.order_by("title") then we'd want to clear the join to author as it's no longer necessary.

In order to avoid implementing the logic to unreference relations the order_by method deferred the resolving to the very end of query compilation which is a different path than the normal resolving taking place in additive methods such as annotate and filter.

I suspect this ticket will be hard to solve without tackling the large problem of compile time resolving of order_by.

in reply to:  10 comment:11 by Shafiya Adzhani, 10 months ago

Replying to Simon Charette:

The hard part is here is likely due to two factor

  1. OuterRef resolving is quite finicky and depends on a specific chain of resolving during query composition (subquery is resolved and then the outer query is)
  2. order_by members are unfortunately not resolved at query composition time (when QuerySet.order_by is called) but at query compilation time.

The reason for 2. is that QuerySet.order_by calls are not additive but destructive and that the query composition logic doesn't support a generic way of eliding joins that we previously created for some operations.

For example, say that you have a query that does qs = Book.objects.order_by("author__name"). If the order by clause is immediately resolved then a JOIN to author must be created. If the same query has then its ordering changed to qs.order_by("title") then we'd want to clear the join to author as it's no longer necessary.

In order to avoid implementing the logic to unreference relations the order_by method deferred the resolving to the very end of query compilation which is a different path than the normal resolving taking place in additive methods such as annotate and filter.

I suspect this ticket will be hard to solve without tackling the large problem of compile time resolving of order_by.

Thank you for the pointer! Since this ticket is complicated, I'll leave it to someone who is interested in solving this problem.

comment:12 by Shafiya Adzhani, 10 months ago

Owner: Shafiya Adzhani removed
Status: assignednew

comment:13 by HeejunShin, 9 months ago

could i try fix this ticket?

comment:14 by David Sanders, 9 months ago

You're welcome to try with the disclaimer that you understand what Simon has said above about it being quite hard and it will take an in-depth understanding of how expressions are resolved & compiled :)

comment:15 by HeejunShin, 9 months ago

Thank you for answer.
I'll consider it further.

Version 0, edited 9 months ago by HeejunShin (next)

comment:16 by bcail, 8 months ago

@HeejunShin, you can try running the example from the ticket Description - see if that works with the main branch. I don't think this issue has been fixed, though.

comment:17 by Chris M, 3 months ago

I am able to reproduce this issue with a simple unit test still against the main branch.

  • tests/queries/tests.py

    diff --git a/tests/queries/tests.py b/tests/queries/tests.py
    index ec88fa558d..a9dd943ee8 100644
    a b class SubqueryTests(TestCase):  
    23772377        NamedCategory.objects.create(id=3, name="third")
    23782378        NamedCategory.objects.create(id=4, name="fourth")
    23792379
     2380    def test_outer_ref_order_by(self):
     2381        values = NamedCategory.objects.annotate(
     2382            foo=NamedCategory.objects.order_by(OuterRef("pk").desc()).values("name")
     2383        ).first()
     2384
     2385        self.assertEqual(["fourth", "third", "second", "first"], values.foo)
     2386
    23802387    def test_ordered_subselect(self):
    23812388        "Subselects honor any manual ordering"
    23822389        query = DumbCategory.objects.filter(
Note: See TracTickets for help on using tickets.
Back to Top