From 31e059c9b3a9014e08780eb564d990788d8f1cff Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Fri, 6 Mar 2009 02:09:59 +0000 Subject: [PATCH] [1.0.X] Improved table join handling for comparisons against NULL. This fixes a broad class of bugs involving filters that look for missing related models and fields. Most of them don't seem to have been reported (the added tests cover the root cause). The exception is that this has also fixed #9968. Backport of r9979 from trunk. git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.0.X@9980 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/models/sql/query.py | 27 +++++++++---------- tests/regressiontests/null_queries/models.py | 23 ++++++++++++++++ .../one_to_one_regress/models.py | 22 +++++++++++++++ 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 550e6e323c..e6d653a46e 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1197,14 +1197,21 @@ class BaseQuery(object): col = target.column alias = join_list[-1] + if (lookup_type == 'isnull' and value is True and not negate and + len(join_list) > 1): + # If the comparison is against NULL, we may need to use some left + # outer joins when creating the join chain. This is only done when + # needed, as it's less efficient at the database level. + self.promote_alias_chain(join_list) + while final > 1: - # An optimization: if the final join is against the same column as - # we are comparing against, we can go back one step in the join - # chain and compare against the lhs of the join instead (and then - # repeat the optimization). The result, potentially, involves less - # table joins. + # An optimization: if the final join is an inner join against the + # same column as we are comparing against, we can go back one step + # in the join chain and compare against the lhs of the join instead + # (and then repeat the optimization). The result, potentially, + # involves less table joins. join = self.alias_map[alias] - if col != join[RHS_JOIN_COL]: + if col != join[RHS_JOIN_COL] or join[JOIN_TYPE] != self.INNER: break self.unref_alias(alias) alias = join[LHS_ALIAS] @@ -1214,14 +1221,6 @@ class BaseQuery(object): if final == penultimate: penultimate = last.pop() - if (lookup_type == 'isnull' and value is True and not negate and - final > 1): - # If the comparison is against NULL, we need to use a left outer - # join when connecting to the previous model. We make that - # adjustment here. We don't do this unless needed as it's less - # efficient at the database level. - self.promote_alias(join_list[penultimate]) - if connector == OR: # 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 diff --git a/tests/regressiontests/null_queries/models.py b/tests/regressiontests/null_queries/models.py index df04f14eba..7cde0aac61 100644 --- a/tests/regressiontests/null_queries/models.py +++ b/tests/regressiontests/null_queries/models.py @@ -13,6 +13,17 @@ class Choice(models.Model): def __unicode__(self): return u"Choice: %s in poll %s" % (self.choice, self.poll) +# A set of models with an inner one pointing to two outer ones. +class OuterA(models.Model): + pass + +class OuterB(models.Model): + data = models.CharField(max_length=10) + +class Inner(models.Model): + first = models.ForeignKey(OuterA) + second = models.ForeignKey(OuterB, null=True) + __test__ = {'API_TESTS':""" # Regression test for the use of None as a query value. None is interpreted as # an SQL NULL, but only in __exact queries. @@ -56,4 +67,16 @@ ValueError: Cannot use None as a query value >>> p2.choice_set.all() [] +# Querying across reverse relations and then another relation should insert +# outer joins correctly so as not to exclude results. +>>> obj = OuterA.objects.create() +>>> OuterA.objects.filter(inner__second=None) +[] +>>> OuterA.objects.filter(inner__second__data=None) +[] +>>> _ = Inner.objects.create(first=obj) +>>> Inner.objects.filter(first__inner__second=None) +[] + + """} diff --git a/tests/regressiontests/one_to_one_regress/models.py b/tests/regressiontests/one_to_one_regress/models.py index 66d00d7223..5c89c55f45 100644 --- a/tests/regressiontests/one_to_one_regress/models.py +++ b/tests/regressiontests/one_to_one_regress/models.py @@ -33,6 +33,15 @@ class Favorites(models.Model): def __unicode__(self): return u"Favorites for %s" % self.name +class Target(models.Model): + pass + +class Pointer(models.Model): + other = models.OneToOneField(Target, primary_key=True) + +class Pointer2(models.Model): + other = models.OneToOneField(Target) + __test__ = {'API_TESTS':""" # Regression test for #1064 and #1506: Check that we create models via the m2m # relation if the remote model has a OneToOneField. @@ -119,4 +128,17 @@ False >>> r.place == p True +# Regression test for #9968: filtering reverse one-to-one relations with +# primary_key=True was misbehaving. We test both (primary_key=True & False) +# cases here to prevent any reappearance of the problem. +>>> _ = Target.objects.create() +>>> Target.objects.filter(pointer=None) +[] +>>> Target.objects.exclude(pointer=None) +[] +>>> Target.objects.filter(pointer2=None) +[] +>>> Target.objects.exclude(pointer2=None) +[] + """}