Commit 5e1f4656 authored by Anssi Kääriäinen's avatar Anssi Kääriäinen
Browse files

Fixed #22429 -- Incorrect SQL when using ~Q and F

parent 13ec89f2
Loading
Loading
Loading
Loading
+16 −9
Original line number Diff line number Diff line
@@ -1159,6 +1159,9 @@ class Query(object):
        try:
            field, sources, opts, join_list, path = self.setup_joins(
                parts, opts, alias, can_reuse, allow_many)
            # split_exclude() needs to know which joins were generated for the
            # lookup parts
            self._lookup_joins = join_list
        except MultiJoin as e:
            return self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]),
                                      can_reuse, e.names_with_path)
@@ -1899,17 +1902,21 @@ class Query(object):
        for _, paths in names_with_path:
            all_paths.extend(paths)
        contains_louter = False
        for pos, path in enumerate(all_paths):
        # Trim and operate only on tables that were generated for
        # the lookup part of the query. That is, avoid trimming
        # joins generated for F() expressions.
        lookup_tables = [t for t in self.tables if t in self._lookup_joins or t == self.tables[0]]
        for trimmed_paths, path in enumerate(all_paths):
            if path.m2m:
                break
            if self.alias_map[self.tables[pos + 1]].join_type == self.LOUTER:
            if self.alias_map[lookup_tables[trimmed_paths + 1]].join_type == self.LOUTER:
                contains_louter = True
            self.unref_alias(self.tables[pos])
            self.unref_alias(lookup_tables[trimmed_paths])
        # The path.join_field is a Rel, lets get the other side's field
        join_field = path.join_field.field
        # Build the filter prefix.
        paths_in_prefix = trimmed_paths
        trimmed_prefix = []
        paths_in_prefix = pos
        for name, path in names_with_path:
            if paths_in_prefix - len(path) < 0:
                break
@@ -1921,12 +1928,12 @@ class Query(object):
        # 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
        # miss those rows that have nothing on the outer side.
        if self.alias_map[self.tables[pos + 1]].join_type != self.LOUTER:
        if self.alias_map[lookup_tables[trimmed_paths + 1]].join_type != self.LOUTER:
            select_fields = [r[0] for r in join_field.related_fields]
            select_alias = self.tables[pos + 1]
            self.unref_alias(self.tables[pos])
            select_alias = lookup_tables[trimmed_paths + 1]
            self.unref_alias(lookup_tables[trimmed_paths])
            extra_restriction = join_field.get_extra_restriction(
                self.where_class, None, self.tables[pos + 1])
                self.where_class, None, lookup_tables[trimmed_paths + 1])
            if extra_restriction:
                self.where.add(extra_restriction, AND)
        else:
@@ -1934,7 +1941,7 @@ class Query(object):
            # inner query if it happens to have a longer join chain containing the
            # values in select_fields. Lets punt this one for now.
            select_fields = [r[1] for r in join_field.related_fields]
            select_alias = self.tables[pos]
            select_alias = lookup_tables[trimmed_paths]
        self.select = [SelectInfo((select_alias, f.column), f) for f in select_fields]
        return trimmed_prefix, contains_louter

+3 −0
Original line number Diff line number Diff line
@@ -14,3 +14,6 @@ Bugfixes

* Fixed ``pgettext_lazy`` crash when receiving bytestring content on Python 2
  (`#22565 <http://code.djangoproject.com/ticket/22565>`_).

* Fixed the SQL generated when filtering by a negated ``Q`` object that contains
  a ``F`` object. (`#22429 <http://code.djangoproject.com/ticket/22429>`_).
+15 −0
Original line number Diff line number Diff line
@@ -660,3 +660,18 @@ class Employment(models.Model):
    employer = models.ForeignKey(Company)
    employee = models.ForeignKey(Person)
    title = models.CharField(max_length=128)


# Bug #22429

class School(models.Model):
    pass


class Student(models.Model):
    school = models.ForeignKey(School)


class Classroom(models.Model):
    school = models.ForeignKey(School)
    students = models.ManyToManyField(Student, related_name='classroom')
+16 −1
Original line number Diff line number Diff line
@@ -28,7 +28,7 @@ from .models import (
    BaseA, FK1, Identifier, Program, Channel, Page, Paragraph, Chapter, Book,
    MyObject, Order, OrderItem, SharedConnection, Task, Staff, StaffUser,
    CategoryRelationship, Ticket21203Parent, Ticket21203Child, Person,
    Company, Employment, CustomPk, CustomPkTag)
    Company, Employment, CustomPk, CustomPkTag, Classroom, School, Student)


class BaseQuerysetTest(TestCase):
@@ -3345,3 +3345,18 @@ class ReverseM2MCustomPkTests(TestCase):
        self.assertQuerysetEqual(
            CustomPkTag.objects.filter(custom_pk=cp1), [cpt1],
            lambda x: x)


class Ticket22429Tests(TestCase):
    def test_ticket_22429(self):
        sc1 = School.objects.create()
        st1 = Student.objects.create(school=sc1)

        sc2 = School.objects.create()
        st2 = Student.objects.create(school=sc2)

        cr = Classroom.objects.create(school=sc1)
        cr.students.add(st1)

        queryset = Student.objects.filter(~Q(classroom__school=F('school')))
        self.assertQuerysetEqual(queryset, [st2], lambda x: x)