Commit db656609 authored by Anssi Kääriäinen's avatar Anssi Kääriäinen Committed by Tim Graham
Browse files

[1.8.x] Fixed #24705 -- Fixed negated Q objects in expressions.

Avoided split_exclude() for Q when used as an expression.

Backport of bc87061a from master
parent 63d60dfe
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -88,7 +88,7 @@ class Q(tree.Node):
        # We must promote any new joins to left outer joins so that when Q is
        # used as an expression, rows aren't filtered due to joins.
        joins_before = query.tables[:]
        clause, joins = query._add_q(self, reuse, allow_joins=allow_joins)
        clause, joins = query._add_q(self, reuse, allow_joins=allow_joins, split_subq=False)
        joins_to_promote = [j for j in joins if j not in joins_before]
        query.promote_joins(joins_to_promote)
        return clause
+7 −5
Original line number Diff line number Diff line
@@ -1112,7 +1112,7 @@ class Query(object):
                (name, lhs.output_field.__class__.__name__))

    def build_filter(self, filter_expr, branch_negated=False, current_negated=False,
                     can_reuse=None, connector=AND, allow_joins=True):
                     can_reuse=None, connector=AND, allow_joins=True, split_subq=True):
        """
        Builds a WhereNode for a single filter clause, but doesn't add it
        to this Query. Query.add_q() will then add this filter to the where
@@ -1161,7 +1161,7 @@ class Query(object):

        opts = self.get_meta()
        alias = self.get_initial_alias()
        allow_many = not branch_negated
        allow_many = not branch_negated or not split_subq

        try:
            field, sources, opts, join_list, path = self.setup_joins(
@@ -1309,7 +1309,7 @@ class Query(object):
        self.demote_joins(existing_inner)

    def _add_q(self, q_object, used_aliases, branch_negated=False,
               current_negated=False, allow_joins=True):
               current_negated=False, allow_joins=True, split_subq=True):
        """
        Adds a Q-object to the current filter.
        """
@@ -1323,12 +1323,14 @@ class Query(object):
            if isinstance(child, Node):
                child_clause, needed_inner = self._add_q(
                    child, used_aliases, branch_negated,
                    current_negated, allow_joins)
                    current_negated, allow_joins, split_subq)
                joinpromoter.add_votes(needed_inner)
            else:
                child_clause, needed_inner = self.build_filter(
                    child, can_reuse=used_aliases, branch_negated=branch_negated,
                    current_negated=current_negated, connector=connector, allow_joins=allow_joins)
                    current_negated=current_negated, connector=connector,
                    allow_joins=allow_joins, split_subq=split_subq,
                )
                joinpromoter.add_votes(needed_inner)
            target_clause.add(child_clause, connector)
        needed_inner = joinpromoter.update_join_types(self)
+4 −0
Original line number Diff line number Diff line
@@ -18,6 +18,10 @@ Bugfixes
  query with a  ``Case`` expression could unexpectedly filter out results
  (:ticket:`24766`).

* Fixed negated ``Q`` objects in expressions. Cases like
  ``Case(When(~Q(friends__age__lte=30)))`` tried to generate a subquery which
  resulted in a crash (:ticket:`24705`).

* Fixed incorrect GROUP BY clause generation on MySQL when the query's model
  has a self-referential foreign key (:ticket:`24748`).

+42 −0
Original line number Diff line number Diff line
@@ -1063,6 +1063,48 @@ class CaseExpressionTests(TestCase):
            lambda x: (x, x.foo)
        )

    def test_m2m_exclude(self):
        CaseTestModel.objects.create(integer=10, integer2=1, string='1')
        qs = CaseTestModel.objects.values_list('id', 'integer').annotate(
            cnt=models.Sum(
                Case(When(~Q(fk_rel__integer=1), then=1), default=2),
                output_field=models.IntegerField()
            ),
        ).order_by('integer')
        # The first o has 2 as its fk_rel__integer=1, thus it hits the
        # default=2 case. The other ones have 2 as the result as they have 2
        # fk_rel objects, except for integer=4 and integer=10 (created above).
        # The integer=4 case has one integer, thus the result is 1, and
        # integer=10 doesn't have any and this too generates 1 (instead of 0)
        # as ~Q() also matches nulls.
        self.assertQuerysetEqual(
            qs,
            [(1, 2), (2, 2), (2, 2), (3, 2), (3, 2), (3, 2), (4, 1), (10, 1)],
            lambda x: x[1:]
        )

    def test_m2m_reuse(self):
        CaseTestModel.objects.create(integer=10, integer2=1, string='1')
        # Need to use values before annotate so that Oracle will not group
        # by fields it isn't capable of grouping by.
        qs = CaseTestModel.objects.values_list('id', 'integer').annotate(
            cnt=models.Sum(
                Case(When(~Q(fk_rel__integer=1), then=1), default=2),
                output_field=models.IntegerField()
            ),
        ).annotate(
            cnt2=models.Sum(
                Case(When(~Q(fk_rel__integer=1), then=1), default=2),
                output_field=models.IntegerField()
            ),
        ).order_by('integer')
        self.assertEqual(str(qs.query).count(' JOIN '), 1)
        self.assertQuerysetEqual(
            qs,
            [(1, 2, 2), (2, 2, 2), (2, 2, 2), (3, 2, 2), (3, 2, 2), (3, 2, 2), (4, 1, 1), (10, 1, 1)],
            lambda x: x[1:]
        )


class CaseDocumentationExamples(TestCase):
    @classmethod