New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed #35135 -- Made FilteredRelation raise ValueError on querysets as rhs. #17771
Fixed #35135 -- Made FilteredRelation raise ValueError on querysets as rhs. #17771
Conversation
1cc4d5a
to
5b18b5d
Compare
All bugfixes require an accepted ticket. Is it a regression in Django 5.0? |
Looks like a regression in 59f4754. |
All good, I will create one. Yes, it is a regression introduced by DJango 5.0, probably introduced by d660cee and subsequent improvements in afc8805 and 59f4754 |
django/db/models/sql/query.py
Outdated
@@ -104,6 +106,8 @@ def get_child_with_renamed_prefix(prefix, replacement, child): | |||
child = child.copy() | |||
if child.name.startswith(prefix + LOOKUP_SEP): | |||
child.name = child.name.replace(prefix, replacement, 1) | |||
elif isinstance(child, QuerySet): | |||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it enough to skip querysets? I think this will make the original issue (ticket-34362) unfixed for querysets 🤔 For example:
qs = Author.objects.annotate(
poem_book=FilteredRelation(
"book",
condition=Q(
book__in=Book.objects.filter(book__title="Poem by Alice")
),
),
).filter(poem_book__isnull=False)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, was not as easy as I thought.
Thanks for the regression test, I will add it to the PR and work on trying to find the proper fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lookup inside the rhs's QuerySet was incorrect (book__title
instead of title
).
It's working fine now.
(I added your test case in the PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised that subquery in join condition are even supported on all engines.
I guess the question now becomes what should we do if the queryset need to refer to the filtered relation itself or any one of its right-hand-side lookup? I think that's what @felixxm was referring to.
OuterRef
are resolved relative to the main model of the query (Author
in this case) so OuterRef("book__*")
is allowed and without repointing book
to poem_book
like it's the case for pretty much everything else in the condition it will spawn a different join.
qs = Author.objects.annotate(
poem_book=FilteredRelation(
"book",
condition=Q(
book__in=Book.objects.filter(title=OuterRef("book__title"))
),
),
).filter(poem_book__isnull=False)
Which should produce the equivalent of
SELECT *
FROM author
LEFT JOIN book poem_book ON (
poem_book.author_id = author.id
AND poem_book.id IN (
SELECT id
FROM book
WHERE title = poem_book.title
)
)
WHERE poem_book.id IS NOT NULL
Maybe it's okay to support queryset but not querysets that have outer references?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's what @felixxm was referring to.
Yes, sorry, I wrote it in a hurry.
cc9f796
to
e8442e6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't checked the branch locally but I'm curious what the original crash was given QuerySet
is also resolvable to sql.Query
?
django/db/models/sql/query.py
Outdated
@@ -104,6 +106,8 @@ def get_child_with_renamed_prefix(prefix, replacement, child): | |||
child = child.copy() | |||
if child.name.startswith(prefix + LOOKUP_SEP): | |||
child.name = child.name.replace(prefix, replacement, 1) | |||
elif isinstance(child, QuerySet): | |||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised that subquery in join condition are even supported on all engines.
I guess the question now becomes what should we do if the queryset need to refer to the filtered relation itself or any one of its right-hand-side lookup? I think that's what @felixxm was referring to.
OuterRef
are resolved relative to the main model of the query (Author
in this case) so OuterRef("book__*")
is allowed and without repointing book
to poem_book
like it's the case for pretty much everything else in the condition it will spawn a different join.
qs = Author.objects.annotate(
poem_book=FilteredRelation(
"book",
condition=Q(
book__in=Book.objects.filter(title=OuterRef("book__title"))
),
),
).filter(poem_book__isnull=False)
Which should produce the equivalent of
SELECT *
FROM author
LEFT JOIN book poem_book ON (
poem_book.author_id = author.id
AND poem_book.id IN (
SELECT id
FROM book
WHERE title = poem_book.title
)
)
WHERE poem_book.id IS NOT NULL
Maybe it's okay to support queryset but not querysets that have outer references?
django/db/models/sql/query.py
Outdated
elif isinstance(child, QuerySet): | ||
pass |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah it's possibly not doable because we're dealing with unresolved expression at this point.
Without this patch, it crashes with:
|
My understanding is that the ...
elif hasattr(child, "resolve_expression"):
... but the nature of the QuerySet doesn't allow it to be treated like an Expression. Hence the |
The presence of
It's treated as an expression in any other context so I don't think your assessment is right. I think that the usage of To me the problem is more than Do the tests pass with elif hasattr(child, "resolve_expression"):
if isinstance(child, QuerySet):
child = child._clone()
else:
child = child.copy()
... |
We end up with another missing method expected by the Expression's API.
|
The problem is that That brings the question what exactly should be done here.
My take is that we should either go with 1. and raise a more appropriate error (it can always be worked around by materializing the queryset by users by doing I have to admit I have a slight preference for 1. because of the can of worms 2. opens with regards to supporting some querysets and not others as whether a subquery is supported or not is not something that can be easily determined at condition replacement time. Maybe we can try testing out what the message emitted when making use of |
Thanks for the clarifications. I agree with your strategy. I'll give it a try to option |
For the record this is what the orm would produce if we would let QuerySet passed as it is. qs = Author.objects.annotate(
poem_book=FilteredRelation(
"book",
condition=Q(
book__in=Book.objects.filter(title=OuterRef("book__title"))
),
),
).filter(poem_book__isnull=False) Outputs SELECT "filtered_relation_author"."id", "filtered_relation_author"."name", "filtered_relation_author"."content_type_id", "filtered_relation_author"."object_id"
FROM "filtered_relation_author
LEFT OUTER JOIN "filtered_relation_book" T3 ON ("filtered_relation_author"."id" = T3."author_id")
INNER JOIN "filtered_relation_book" poem_book ON ("filtered_relation_author"."id" = poem_book."author_id" AND (poem_book."id" IN (SELECT U0."id" FROM "filtered_relation_book" U0 WHERE U0."title" = (T3."title"))))
WHERE poem_book."id" IS NOT NULL Which is clearly not what we want, and letting it passed, silently, would be damaging to the users. I'm still continuing to make progress on option |
3be6b5d
to
f99e02b
Compare
django/db/models/sql/query.py
Outdated
isinstance(where_child.rhs, ResolvedOuterRef) | ||
for where_child in queryset.query.where.children | ||
): | ||
raise ValueError("Passing OuterRef within a FilteredRelation is not supported.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure it's worth going this route. Does it catch nested OuterRef()
calls?
I'd prefer not to support querysets at all. Especially since it's intended for backporting. |
9827ebc
to
44ee313
Compare
I changed the direction of the PR to prevent passing a QuerySet within a FilteredRelation. |
0581289
to
2a5e593
Compare
…s rhs. Regression in 59f4754.
2a5e593
to
820c5f1
Compare
@ticosax Thanks 👍 I pushed edits to the release note and comment. |
@charettes Does it work for you? |
@felixxm it does, thanks for the patch @ticosax! I wonder if we should suggest that users materialize the queryset to workaround the problem in the error message. e.g. |
Thanks for checking 👍 I think it's fine without a hint. |
I didn't found an existing Ticket and I think the patch is small enough to avoid one.Please let me know otherwise.
https://code.djangoproject.com/ticket/35135#ticket