Fixed #30349 -- Fixed QuerySet.exclude() on FilteredRelation.

Using annotated FilteredRelations raised a FieldError when coupled with
exclude(). This is due to not passing filtered relation fields to the
subquery created in split_exclude(). We fixed this issue by passing the
filtered relation data to the newly created subquery.

Secondly, in the case where an INNER JOIN is used in the excluded
subquery, the ORM would trim the filtered relation INNER JOIN in attempt
to simplify the query. This will also remove the ON clause filters
generated by the FilteredRelation. We added logic to not trim the INNER
JOIN if it is from FilteredRelation.
This commit is contained in:
Rob 2019-05-07 00:42:56 +10:00 committed by Mariusz Felisiak
parent 21aa2a5e78
commit 6b736dd074
2 changed files with 16 additions and 3 deletions

View File

@ -1666,6 +1666,7 @@ class Query(BaseExpression):
filter_expr = (filter_lhs, OuterRef(filter_rhs.name)) filter_expr = (filter_lhs, OuterRef(filter_rhs.name))
# Generate the inner query. # Generate the inner query.
query = Query(self.model) query = Query(self.model)
query._filtered_relations = self._filtered_relations
query.add_filter(filter_expr) query.add_filter(filter_expr)
query.clear_ordering(True) query.clear_ordering(True)
# Try to have as simple as possible subquery -> trim leading joins from # Try to have as simple as possible subquery -> trim leading joins from
@ -2140,9 +2141,13 @@ class Query(BaseExpression):
join_field.foreign_related_fields[0].name) join_field.foreign_related_fields[0].name)
trimmed_prefix = LOOKUP_SEP.join(trimmed_prefix) trimmed_prefix = LOOKUP_SEP.join(trimmed_prefix)
# Lets still see if we can trim the first join from the inner query # Lets still see if we can trim the first join from the inner query
# (that is, self). We can't do this for LEFT JOINs because we would # (that is, self). We can't do this for:
# miss those rows that have nothing on the outer side. # - LEFT JOINs because we would miss those rows that have nothing on
if self.alias_map[lookup_tables[trimmed_paths + 1]].join_type != LOUTER: # the outer side,
# - INNER JOINs from filtered relations because we would miss their
# filters.
first_join = self.alias_map[lookup_tables[trimmed_paths + 1]]
if first_join.join_type != LOUTER and not first_join.filtered_relation:
select_fields = [r[0] for r in join_field.related_fields] select_fields = [r[0] for r in join_field.related_fields]
select_alias = lookup_tables[trimmed_paths + 1] select_alias = lookup_tables[trimmed_paths + 1]
self.unref_alias(lookup_tables[trimmed_paths]) self.unref_alias(lookup_tables[trimmed_paths])

View File

@ -98,6 +98,14 @@ class FilteredRelationTests(TestCase):
[self.author1] [self.author1]
) )
def test_with_exclude(self):
self.assertSequenceEqual(
Author.objects.annotate(
book_alice=FilteredRelation('book', condition=Q(book__title__iexact='poem by alice')),
).exclude(book_alice__isnull=False),
[self.author2],
)
def test_with_join_and_complex_condition(self): def test_with_join_and_complex_condition(self):
self.assertSequenceEqual( self.assertSequenceEqual(
Author.objects.annotate( Author.objects.annotate(