Loading django/db/models/sql/query.py +16 −9 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 Loading @@ -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: Loading @@ -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 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 @@ -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') tests/queries/tests.py +16 −1 Original line number Diff line number Diff line Loading @@ -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): Loading Loading @@ -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) Loading
django/db/models/sql/query.py +16 −9 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 Loading @@ -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: Loading @@ -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 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 @@ -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')
tests/queries/tests.py +16 −1 Original line number Diff line number Diff line Loading @@ -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): Loading Loading @@ -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)