Fixed an isnull=False filtering edge-case. Fixes #15316.
The bulk of this patch is due to some fine analysis from Aleksandra Sendecka. git-svn-id: http://code.djangoproject.com/svn/django/trunk@16656 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
c3a0dcf6e9
commit
2e56066a5b
1
AUTHORS
1
AUTHORS
|
@ -448,6 +448,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
schwank@gmail.com
|
schwank@gmail.com
|
||||||
scott@staplefish.com
|
scott@staplefish.com
|
||||||
Ilya Semenov <semenov@inetss.com>
|
Ilya Semenov <semenov@inetss.com>
|
||||||
|
Aleksandra Sendecka <asendecka@hauru.eu>
|
||||||
serbaut@gmail.com
|
serbaut@gmail.com
|
||||||
John Shaffer <jshaffer2112@gmail.com>
|
John Shaffer <jshaffer2112@gmail.com>
|
||||||
Pete Shinners <pete@shinners.org>
|
Pete Shinners <pete@shinners.org>
|
||||||
|
|
|
@ -1105,7 +1105,10 @@ class Query(object):
|
||||||
|
|
||||||
# Process the join list to see if we can remove any inner joins from
|
# Process the join list to see if we can remove any inner joins from
|
||||||
# the far end (fewer tables in a query is better).
|
# the far end (fewer tables in a query is better).
|
||||||
col, alias, join_list = self.trim_joins(target, join_list, last, trim)
|
nonnull_comparison = (lookup_type == 'isnull' and value is False)
|
||||||
|
col, alias, join_list = self.trim_joins(target, join_list, last, trim,
|
||||||
|
nonnull_comparison)
|
||||||
|
|
||||||
if connector == OR:
|
if connector == OR:
|
||||||
# Some joins may need to be promoted when adding a new filter to a
|
# Some joins may need to be promoted when adding a new filter to a
|
||||||
# disjunction. We walk the list of new joins and where it diverges
|
# disjunction. We walk the list of new joins and where it diverges
|
||||||
|
@ -1442,7 +1445,7 @@ class Query(object):
|
||||||
|
|
||||||
return field, target, opts, joins, last, extra_filters
|
return field, target, opts, joins, last, extra_filters
|
||||||
|
|
||||||
def trim_joins(self, target, join_list, last, trim):
|
def trim_joins(self, target, join_list, last, trim, nonnull_check=False):
|
||||||
"""
|
"""
|
||||||
Sometimes joins at the end of a multi-table sequence can be trimmed. If
|
Sometimes joins at the end of a multi-table sequence can be trimmed. If
|
||||||
the final join is against the same column as we are comparing against,
|
the final join is against the same column as we are comparing against,
|
||||||
|
@ -1463,6 +1466,11 @@ class Query(object):
|
||||||
trimmed before anything. See the documentation of add_filter() for
|
trimmed before anything. See the documentation of add_filter() for
|
||||||
details about this.
|
details about this.
|
||||||
|
|
||||||
|
The 'nonnull_check' parameter is True when we are using inner joins
|
||||||
|
between tables explicitly to exclude NULL entries. In that case, the
|
||||||
|
tables shouldn't be trimmed, because the very action of joining to them
|
||||||
|
alters the result set.
|
||||||
|
|
||||||
Returns the final active column and table alias and the new active
|
Returns the final active column and table alias and the new active
|
||||||
join_list.
|
join_list.
|
||||||
"""
|
"""
|
||||||
|
@ -1470,7 +1478,7 @@ class Query(object):
|
||||||
penultimate = last.pop()
|
penultimate = last.pop()
|
||||||
if penultimate == final:
|
if penultimate == final:
|
||||||
penultimate = last.pop()
|
penultimate = last.pop()
|
||||||
if trim and len(join_list) > 1:
|
if trim and final > 1:
|
||||||
extra = join_list[penultimate:]
|
extra = join_list[penultimate:]
|
||||||
join_list = join_list[:penultimate]
|
join_list = join_list[:penultimate]
|
||||||
final = penultimate
|
final = penultimate
|
||||||
|
@ -1483,12 +1491,13 @@ class Query(object):
|
||||||
alias = join_list[-1]
|
alias = join_list[-1]
|
||||||
while final > 1:
|
while final > 1:
|
||||||
join = self.alias_map[alias]
|
join = self.alias_map[alias]
|
||||||
if col != join[RHS_JOIN_COL] or join[JOIN_TYPE] != self.INNER:
|
if (col != join[RHS_JOIN_COL] or join[JOIN_TYPE] != self.INNER or
|
||||||
|
nonnull_check):
|
||||||
break
|
break
|
||||||
self.unref_alias(alias)
|
self.unref_alias(alias)
|
||||||
alias = join[LHS_ALIAS]
|
alias = join[LHS_ALIAS]
|
||||||
col = join[LHS_JOIN_COL]
|
col = join[LHS_JOIN_COL]
|
||||||
join_list = join_list[:-1]
|
join_list.pop()
|
||||||
final -= 1
|
final -= 1
|
||||||
if final == penultimate:
|
if final == penultimate:
|
||||||
penultimate = last.pop()
|
penultimate = last.pop()
|
||||||
|
|
|
@ -317,3 +317,29 @@ class ObjectC(models.Model):
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
class SimpleCategory(models.Model):
|
||||||
|
name = models.CharField(max_length=10)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class SpecialCategory(SimpleCategory):
|
||||||
|
special_name = models.CharField(max_length=10)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name + " " + self.special_name
|
||||||
|
|
||||||
|
class CategoryItem(models.Model):
|
||||||
|
category = models.ForeignKey(SimpleCategory)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return "category item: " + str(self.category)
|
||||||
|
|
||||||
|
class OneToOneCategory(models.Model):
|
||||||
|
new_name = models.CharField(max_length=10)
|
||||||
|
category = models.OneToOneField(SimpleCategory)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return "one2one " + self.new_name
|
||||||
|
|
|
@ -15,7 +15,7 @@ from models import (Annotation, Article, Author, Celebrity, Child, Cover, Detail
|
||||||
DumbCategory, ExtraInfo, Fan, Item, LeafA, LoopX, LoopZ, ManagedModel,
|
DumbCategory, ExtraInfo, Fan, Item, LeafA, LoopX, LoopZ, ManagedModel,
|
||||||
Member, NamedCategory, Note, Number, Plaything, PointerA, Ranking, Related,
|
Member, NamedCategory, Note, Number, Plaything, PointerA, Ranking, Related,
|
||||||
Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA, ObjectB,
|
Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA, ObjectB,
|
||||||
ObjectC)
|
ObjectC, CategoryItem, SimpleCategory, SpecialCategory, OneToOneCategory)
|
||||||
|
|
||||||
|
|
||||||
class BaseQuerysetTest(TestCase):
|
class BaseQuerysetTest(TestCase):
|
||||||
|
@ -1043,11 +1043,135 @@ class Queries4Tests(BaseQuerysetTest):
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_ticket15316_filter_false(self):
|
||||||
|
c1 = SimpleCategory.objects.create(name="category1")
|
||||||
|
c2 = SpecialCategory.objects.create(name="named category1",
|
||||||
|
special_name="special1")
|
||||||
|
c3 = SpecialCategory.objects.create(name="named category2",
|
||||||
|
special_name="special2")
|
||||||
|
|
||||||
|
ci1 = CategoryItem.objects.create(category=c1)
|
||||||
|
ci2 = CategoryItem.objects.create(category=c2)
|
||||||
|
ci3 = CategoryItem.objects.create(category=c3)
|
||||||
|
|
||||||
|
qs = CategoryItem.objects.filter(category__specialcategory__isnull=False)
|
||||||
|
self.assertEqual(qs.count(), 2)
|
||||||
|
self.assertQuerysetEqual(qs, [ci2.pk, ci3.pk], lambda x: x.pk, False)
|
||||||
|
|
||||||
|
def test_ticket15316_exclude_false(self):
|
||||||
|
c1 = SimpleCategory.objects.create(name="category1")
|
||||||
|
c2 = SpecialCategory.objects.create(name="named category1",
|
||||||
|
special_name="special1")
|
||||||
|
c3 = SpecialCategory.objects.create(name="named category2",
|
||||||
|
special_name="special2")
|
||||||
|
|
||||||
|
ci1 = CategoryItem.objects.create(category=c1)
|
||||||
|
ci2 = CategoryItem.objects.create(category=c2)
|
||||||
|
ci3 = CategoryItem.objects.create(category=c3)
|
||||||
|
|
||||||
|
qs = CategoryItem.objects.exclude(category__specialcategory__isnull=False)
|
||||||
|
self.assertEqual(qs.count(), 1)
|
||||||
|
self.assertQuerysetEqual(qs, [ci1.pk], lambda x: x.pk)
|
||||||
|
|
||||||
|
def test_ticket15316_filter_true(self):
|
||||||
|
c1 = SimpleCategory.objects.create(name="category1")
|
||||||
|
c2 = SpecialCategory.objects.create(name="named category1",
|
||||||
|
special_name="special1")
|
||||||
|
c3 = SpecialCategory.objects.create(name="named category2",
|
||||||
|
special_name="special2")
|
||||||
|
|
||||||
|
ci1 = CategoryItem.objects.create(category=c1)
|
||||||
|
ci2 = CategoryItem.objects.create(category=c2)
|
||||||
|
ci3 = CategoryItem.objects.create(category=c3)
|
||||||
|
|
||||||
|
qs = CategoryItem.objects.filter(category__specialcategory__isnull=True)
|
||||||
|
self.assertEqual(qs.count(), 1)
|
||||||
|
self.assertQuerysetEqual(qs, [ci1.pk], lambda x: x.pk)
|
||||||
|
|
||||||
|
def test_ticket15316_exclude_true(self):
|
||||||
|
c1 = SimpleCategory.objects.create(name="category1")
|
||||||
|
c2 = SpecialCategory.objects.create(name="named category1",
|
||||||
|
special_name="special1")
|
||||||
|
c3 = SpecialCategory.objects.create(name="named category2",
|
||||||
|
special_name="special2")
|
||||||
|
|
||||||
|
ci1 = CategoryItem.objects.create(category=c1)
|
||||||
|
ci2 = CategoryItem.objects.create(category=c2)
|
||||||
|
ci3 = CategoryItem.objects.create(category=c3)
|
||||||
|
|
||||||
|
qs = CategoryItem.objects.exclude(category__specialcategory__isnull=True)
|
||||||
|
self.assertEqual(qs.count(), 2)
|
||||||
|
self.assertQuerysetEqual(qs, [ci2.pk, ci3.pk], lambda x: x.pk, False)
|
||||||
|
|
||||||
|
def test_ticket15316_one2one_filter_false(self):
|
||||||
|
c = SimpleCategory.objects.create(name="cat")
|
||||||
|
c0 = SimpleCategory.objects.create(name="cat0")
|
||||||
|
c1 = SimpleCategory.objects.create(name="category1")
|
||||||
|
|
||||||
|
c2 = OneToOneCategory.objects.create(category = c1, new_name="new1")
|
||||||
|
c3 = OneToOneCategory.objects.create(category = c0, new_name="new2")
|
||||||
|
|
||||||
|
ci1 = CategoryItem.objects.create(category=c)
|
||||||
|
ci2 = CategoryItem.objects.create(category=c0)
|
||||||
|
ci3 = CategoryItem.objects.create(category=c1)
|
||||||
|
|
||||||
|
qs = CategoryItem.objects.filter(category__onetoonecategory__isnull=False)
|
||||||
|
self.assertEqual(qs.count(), 2)
|
||||||
|
self.assertQuerysetEqual(qs, [ci2.pk, ci3.pk], lambda x: x.pk, False)
|
||||||
|
|
||||||
|
def test_ticket15316_one2one_exclude_false(self):
|
||||||
|
c = SimpleCategory.objects.create(name="cat")
|
||||||
|
c0 = SimpleCategory.objects.create(name="cat0")
|
||||||
|
c1 = SimpleCategory.objects.create(name="category1")
|
||||||
|
|
||||||
|
c2 = OneToOneCategory.objects.create(category = c1, new_name="new1")
|
||||||
|
c3 = OneToOneCategory.objects.create(category = c0, new_name="new2")
|
||||||
|
|
||||||
|
ci1 = CategoryItem.objects.create(category=c)
|
||||||
|
ci2 = CategoryItem.objects.create(category=c0)
|
||||||
|
ci3 = CategoryItem.objects.create(category=c1)
|
||||||
|
|
||||||
|
qs = CategoryItem.objects.exclude(category__onetoonecategory__isnull=False)
|
||||||
|
self.assertEqual(qs.count(), 1)
|
||||||
|
self.assertQuerysetEqual(qs, [ci1.pk], lambda x: x.pk)
|
||||||
|
|
||||||
|
def test_ticket15316_one2one_filter_true(self):
|
||||||
|
c = SimpleCategory.objects.create(name="cat")
|
||||||
|
c0 = SimpleCategory.objects.create(name="cat0")
|
||||||
|
c1 = SimpleCategory.objects.create(name="category1")
|
||||||
|
|
||||||
|
c2 = OneToOneCategory.objects.create(category = c1, new_name="new1")
|
||||||
|
c3 = OneToOneCategory.objects.create(category = c0, new_name="new2")
|
||||||
|
|
||||||
|
ci1 = CategoryItem.objects.create(category=c)
|
||||||
|
ci2 = CategoryItem.objects.create(category=c0)
|
||||||
|
ci3 = CategoryItem.objects.create(category=c1)
|
||||||
|
|
||||||
|
qs = CategoryItem.objects.filter(category__onetoonecategory__isnull=True)
|
||||||
|
self.assertEqual(qs.count(), 1)
|
||||||
|
self.assertQuerysetEqual(qs, [ci1.pk], lambda x: x.pk)
|
||||||
|
|
||||||
|
def test_ticket15316_one2one_exclude_true(self):
|
||||||
|
c = SimpleCategory.objects.create(name="cat")
|
||||||
|
c0 = SimpleCategory.objects.create(name="cat0")
|
||||||
|
c1 = SimpleCategory.objects.create(name="category1")
|
||||||
|
|
||||||
|
c2 = OneToOneCategory.objects.create(category = c1, new_name="new1")
|
||||||
|
c3 = OneToOneCategory.objects.create(category = c0, new_name="new2")
|
||||||
|
|
||||||
|
ci1 = CategoryItem.objects.create(category=c)
|
||||||
|
ci2 = CategoryItem.objects.create(category=c0)
|
||||||
|
ci3 = CategoryItem.objects.create(category=c1)
|
||||||
|
|
||||||
|
qs = CategoryItem.objects.exclude(category__onetoonecategory__isnull=True)
|
||||||
|
self.assertEqual(qs.count(), 2)
|
||||||
|
self.assertQuerysetEqual(qs, [ci2.pk, ci3.pk], lambda x: x.pk, False)
|
||||||
|
|
||||||
|
|
||||||
class Queries5Tests(TestCase):
|
class Queries5Tests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Ordering by 'rank' gives us rank2, rank1, rank3. Ordering by the Meta.ordering
|
# Ordering by 'rank' gives us rank2, rank1, rank3. Ordering by the
|
||||||
# will be rank3, rank2, rank1.
|
# Meta.ordering will be rank3, rank2, rank1.
|
||||||
n1 = Note.objects.create(note='n1', misc='foo', id=1)
|
n1 = Note.objects.create(note='n1', misc='foo', id=1)
|
||||||
n2 = Note.objects.create(note='n2', misc='bar', id=2)
|
n2 = Note.objects.create(note='n2', misc='bar', id=2)
|
||||||
e1 = ExtraInfo.objects.create(info='e1', note=n1)
|
e1 = ExtraInfo.objects.create(info='e1', note=n1)
|
||||||
|
|
Loading…
Reference in New Issue