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

This commit is contained in:
Anssi Kääriäinen 2014-04-28 15:27:36 +03:00
parent 13ec89f267
commit 5e1f4656b9
4 changed files with 50 additions and 10 deletions

View File

@ -1159,6 +1159,9 @@ class Query(object):
try: try:
field, sources, opts, join_list, path = self.setup_joins( field, sources, opts, join_list, path = self.setup_joins(
parts, opts, alias, can_reuse, allow_many) 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: except MultiJoin as e:
return self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]), return self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]),
can_reuse, e.names_with_path) can_reuse, e.names_with_path)
@ -1899,17 +1902,21 @@ class Query(object):
for _, paths in names_with_path: for _, paths in names_with_path:
all_paths.extend(paths) all_paths.extend(paths)
contains_louter = False 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: if path.m2m:
break 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 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 # The path.join_field is a Rel, lets get the other side's field
join_field = path.join_field.field join_field = path.join_field.field
# Build the filter prefix. # Build the filter prefix.
paths_in_prefix = trimmed_paths
trimmed_prefix = [] trimmed_prefix = []
paths_in_prefix = pos
for name, path in names_with_path: for name, path in names_with_path:
if paths_in_prefix - len(path) < 0: if paths_in_prefix - len(path) < 0:
break break
@ -1921,12 +1928,12 @@ class Query(object):
# 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 LEFT JOINs because we would
# miss those rows that have nothing on the outer side. # 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_fields = [r[0] for r in join_field.related_fields]
select_alias = self.tables[pos + 1] select_alias = lookup_tables[trimmed_paths + 1]
self.unref_alias(self.tables[pos]) self.unref_alias(lookup_tables[trimmed_paths])
extra_restriction = join_field.get_extra_restriction( 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: if extra_restriction:
self.where.add(extra_restriction, AND) self.where.add(extra_restriction, AND)
else: else:
@ -1934,7 +1941,7 @@ class Query(object):
# inner query if it happens to have a longer join chain containing the # inner query if it happens to have a longer join chain containing the
# values in select_fields. Lets punt this one for now. # values in select_fields. Lets punt this one for now.
select_fields = [r[1] for r in join_field.related_fields] 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] self.select = [SelectInfo((select_alias, f.column), f) for f in select_fields]
return trimmed_prefix, contains_louter return trimmed_prefix, contains_louter

View File

@ -14,3 +14,6 @@ Bugfixes
* Fixed ``pgettext_lazy`` crash when receiving bytestring content on Python 2 * Fixed ``pgettext_lazy`` crash when receiving bytestring content on Python 2
(`#22565 <http://code.djangoproject.com/ticket/22565>`_). (`#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>`_).

View File

@ -660,3 +660,18 @@ class Employment(models.Model):
employer = models.ForeignKey(Company) employer = models.ForeignKey(Company)
employee = models.ForeignKey(Person) employee = models.ForeignKey(Person)
title = models.CharField(max_length=128) 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')

View File

@ -28,7 +28,7 @@ from .models import (
BaseA, FK1, Identifier, Program, Channel, Page, Paragraph, Chapter, Book, BaseA, FK1, Identifier, Program, Channel, Page, Paragraph, Chapter, Book,
MyObject, Order, OrderItem, SharedConnection, Task, Staff, StaffUser, MyObject, Order, OrderItem, SharedConnection, Task, Staff, StaffUser,
CategoryRelationship, Ticket21203Parent, Ticket21203Child, Person, CategoryRelationship, Ticket21203Parent, Ticket21203Child, Person,
Company, Employment, CustomPk, CustomPkTag) Company, Employment, CustomPk, CustomPkTag, Classroom, School, Student)
class BaseQuerysetTest(TestCase): class BaseQuerysetTest(TestCase):
@ -3345,3 +3345,18 @@ class ReverseM2MCustomPkTests(TestCase):
self.assertQuerysetEqual( self.assertQuerysetEqual(
CustomPkTag.objects.filter(custom_pk=cp1), [cpt1], CustomPkTag.objects.filter(custom_pk=cp1), [cpt1],
lambda x: x) 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)