#33766: FilteredRelation doesn't resolve its conditions resulting in unknown
alias
references at SQL compilation time
-------------------------------------+-------------------------------------
Reporter: Daniel Schaffer | Owner: nobody
Type: Bug | Status: new
Component: Database layer | Version: 3.2
(models, ORM) |
Severity: Normal | Resolution:
Keywords: filteredrelation | Triage Stage: Accepted
coalesce |
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Simon Charette):
* stage: Unreviewed => Accepted
Comment:
`FilteredRelation` annotations are implemented in a somewhat awkward way
in the sense that they are accepted by `Queryset.annotate` even they are
not truly resolvable.
From my understanding the reason behind that is to somewhat mimic what
`Queryset.alias` does for annotations to avoid incurring an extraneous
`JOIN` if the filtered relation end up not being referenced in the final
queryset composition.
This is achieved by having `Queryset.annotate` take an entirely different
route when provided a `FilteredRelation` which ''defers'' expression
resolving by stashing the instance and its alias in
`Query._filtered_relations`. This stash is then later used to create an
actual filtered join when `Query.names_to_path` detects a reference to a
previously stashed ''name'' in `._filtered_relations`.
What the actual implementation of `FilteredRelation` failed to account for
when making the latter rely on a special JIT resolving instead of using
the usual `resolve_expression` path is that `.conditions` right-hand-sides
are allowed to span joins (as in the reporter's case here with
`F("worker_substitutions__worker")`) and thus must be resolved before the
query is passed to the compiler. This is problematic because the
`.condition` of `FilteredRelation` is ''resolved'' via the
`SQLCompiler.compile -> .get_from_clause -> Join.as_sql ->
FilteredRelation.as_sql` chain but at the time `FilteredRelation.as_sql`
calls `compiler.query.build_filtered_relation_q`, which happens to augment
`query.alias_map` with the proper joins associated with the right-hand-
sides, it's already too late because the list of aliases to be considered
for compilation in `get_from_clause`
[https://github.com/django/django/blob/49b470b9187b6be60e573fed08c8f4a87f133750/django/db/models/sql/compiler.py#L960
is already fixed].
I don't have all the details at hand here but I wonder why we chose to go
this way instead of immediately creating the `alias_map` entry with a
corresponding `alias_refcount` of `0` which would have resulted in the
joins being referenceable but
[https://github.com/django/django/blob/49b470b9187b6be60e573fed08c8f4a87f133750/django/db/models/sql/compiler.py#L961-L962
eventually pruned if unused]. We have the machinery in place to track
dependency chains between joins so it would only have been a matter of
incrementing the transitive chain of joins in `names_to_path` when the
filtered relation was referenced and we should have been set.
I didn't have time to commit in exploring this route, which seems like a
good way of cleaning all the filtered relation logic, but here's at least
a small patch reproduces the issue in our current test suite.
{{{#!diff
diff --git a/tests/filtered_relation/tests.py
b/tests/filtered_relation/tests.py
index 7d77e31b51..825d150830 100644
--- a/tests/filtered_relation/tests.py
+++ b/tests/filtered_relation/tests.py
@@ -685,6 +685,20 @@ def test_eq(self):
FilteredRelation("book", condition=Q(book__title="b")),
mock.ANY
)
+ def test_condition_spans_join(self):
+ self.assertSequenceEqual(
+ Book.objects.annotate(
+ contains_editor_author=FilteredRelation(
+ "author",
condition=Q(author__name__contains=F("editor__name"))
+ )
+ )
+ .filter(
+ contains_editor_author__isnull=False,
+ )
+ .order_by("id"),
+ [cls.book1, cls.book4],
+ )
+
class FilteredRelationAggregationTests(TestCase):
@classmethod
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/33766#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/01070181321f888a-ee1a0b61-3767-4562-b43f-83507bfc45dd-000000%40eu-central-1.amazonses.com.