Loading django/db/models/sql/query.py +19 −9 Original line number Diff line number Diff line Loading @@ -780,6 +780,9 @@ class Query(object): if lhs in change_map: data = data._replace(lhs_alias=change_map[lhs]) self.alias_map[alias] = data # 4. Update the temporary _lookup_joins list if hasattr(self, '_lookup_joins'): self._lookup_joins = [change_map.get(lj, lj) for lj in self._lookup_joins] def bump_prefix(self, exceptions=()): """ Loading Loading @@ -1100,6 +1103,9 @@ class Query(object): allow_explicit_fk=True) if can_reuse is not None: can_reuse.update(join_list) # 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) Loading Loading @@ -1822,17 +1828,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 Loading @@ -1844,12 +1854,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: Loading @@ -1857,7 +1867,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 Loading docs/releases/1.6.5.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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>`_). tests/queries/models.py +15 −0 Original line number Diff line number Diff line Loading @@ -536,3 +536,18 @@ class Ticket21203Parent(models.Model): class Ticket21203Child(models.Model): childid = models.AutoField(primary_key=True) parent = models.ForeignKey(Ticket21203Parent) # 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') tests/queries/tests.py +17 −1 Original line number Diff line number Diff line Loading @@ -27,7 +27,8 @@ from .models import ( ModelA, ModelB, ModelC, ModelD, Responsibility, Job, JobResponsibilities, BaseA, FK1, Identifier, Program, Channel, Page, Paragraph, Chapter, Book, MyObject, Order, OrderItem, Task, Staff, StaffUser, Ticket21203Parent, Ticket21203Child) Ticket21203Child, Classroom, School, Student) class BaseQuerysetTest(TestCase): def assertValueQuerysetEqual(self, qs, values): Loading Loading @@ -3105,3 +3106,18 @@ class ForeignKeyToBaseExcludeTests(TestCase): SpecialCategory.objects.filter(categoryitem__id=c1.pk), [sc1], 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) Loading
django/db/models/sql/query.py +19 −9 Original line number Diff line number Diff line Loading @@ -780,6 +780,9 @@ class Query(object): if lhs in change_map: data = data._replace(lhs_alias=change_map[lhs]) self.alias_map[alias] = data # 4. Update the temporary _lookup_joins list if hasattr(self, '_lookup_joins'): self._lookup_joins = [change_map.get(lj, lj) for lj in self._lookup_joins] def bump_prefix(self, exceptions=()): """ Loading Loading @@ -1100,6 +1103,9 @@ class Query(object): allow_explicit_fk=True) if can_reuse is not None: can_reuse.update(join_list) # 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) Loading Loading @@ -1822,17 +1828,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 Loading @@ -1844,12 +1854,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: Loading @@ -1857,7 +1867,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 Loading
docs/releases/1.6.5.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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>`_).
tests/queries/models.py +15 −0 Original line number Diff line number Diff line Loading @@ -536,3 +536,18 @@ class Ticket21203Parent(models.Model): class Ticket21203Child(models.Model): childid = models.AutoField(primary_key=True) parent = models.ForeignKey(Ticket21203Parent) # 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')
tests/queries/tests.py +17 −1 Original line number Diff line number Diff line Loading @@ -27,7 +27,8 @@ from .models import ( ModelA, ModelB, ModelC, ModelD, Responsibility, Job, JobResponsibilities, BaseA, FK1, Identifier, Program, Channel, Page, Paragraph, Chapter, Book, MyObject, Order, OrderItem, Task, Staff, StaffUser, Ticket21203Parent, Ticket21203Child) Ticket21203Child, Classroom, School, Student) class BaseQuerysetTest(TestCase): def assertValueQuerysetEqual(self, qs, values): Loading Loading @@ -3105,3 +3106,18 @@ class ForeignKeyToBaseExcludeTests(TestCase): SpecialCategory.objects.filter(categoryitem__id=c1.pk), [sc1], 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)