Author: mtredinnick
Date: 2007-10-13 21:15:28 -0500 (Sat, 13 Oct 2007)
New Revision: 6494

Modified:
   django/branches/queryset-refactor/django/db/models/sql/query.py
   django/branches/queryset-refactor/tests/regressiontests/queries/models.py
Log:
queryset-refactor: Fixed the SQL construction when excluding items across
nullable joins. This is #5324 plus a few more complex variations on that theme.


Modified: django/branches/queryset-refactor/django/db/models/sql/query.py
===================================================================
--- django/branches/queryset-refactor/django/db/models/sql/query.py     
2007-10-14 02:15:03 UTC (rev 6493)
+++ django/branches/queryset-refactor/django/db/models/sql/query.py     
2007-10-14 02:15:28 UTC (rev 6494)
@@ -558,13 +558,14 @@
         alias = self.join((None, opts.db_table, None, None))
         dupe_multis = (connection == AND)
         join_list = []
-        done_split = not self.where
+        split = not self.where
+        null_point = None
 
         # FIXME: Using enumerate() here is expensive. We only need 'i' to
         # check we aren't joining against a non-joinable field. Find a
         # better way to do this!
         for i, name in enumerate(parts):
-            joins, opts, orig_field, target_field, target_col = \
+            joins, opts, orig_field, target_field, target_col, nullable = \
                     self.get_next_join(name, opts, alias, dupe_multis)
             if name == 'pk':
                 name = target_field.name
@@ -572,9 +573,11 @@
                 join_list.append(joins)
                 last = joins
                 alias = joins[-1]
-                if connection == OR and not done_split:
+                if not null_point and nullable:
+                    null_point = len(join_list)
+                if connection == OR and not split:
                     if self.alias_map[joins[0]][ALIAS_REFCOUNT] == 1:
-                        done_split = True
+                        split = True
                         self.promote_alias(joins[0])
                         all_aliases = []
                         for a in join_list:
@@ -611,10 +614,12 @@
 
         self.where.add([alias, col, orig_field, lookup_type, value],
                 connection)
-        if negate:
+        if negate and null_point:
             if join_list:
-                self.promote_alias(last[0])
+                for join in last:
+                    self.promote_alias(join)
             self.where.negate()
+            self.where.add([alias, col, orig_field, 'isnull', True], OR)
 
     def add_q(self, q_object):
         """
@@ -644,8 +649,9 @@
         always create a new alias (necessary for disjunctive filters).
 
         Returns a list of aliases involved in the join, the next value for
-        'opts' and the field class that was matched. For a non-joining field,
-        the first value (join alias) is None.
+        'opts', the field instance that was matched, the new field to include
+        in the join, the column name on the rhs of the join and whether the
+        join can include NULL results.
         """
         if name == 'pk':
             name = opts.pk.name
@@ -660,7 +666,7 @@
                     field.m2m_reverse_name(), remote_opts.pk.column),
                     dupe_multis, merge_separate=True)
             return ([int_alias, far_alias], remote_opts, field, remote_opts.pk,
-                    None)
+                    None, field.null)
 
         field = find_field(name, opts.get_all_related_many_to_many_objects(),
                 True)
@@ -675,7 +681,7 @@
                     dupe_multis, merge_separate=True)
             # XXX: Why is the final component able to be None here?
             return ([int_alias, far_alias], remote_opts, field, remote_opts.pk,
-                    None)
+                    None, True)
 
         field = find_field(name, opts.get_all_related_objects(), True)
         if field:
@@ -686,7 +692,8 @@
             alias = self.join((root_alias, remote_opts.db_table,
                     local_field.column, field.column), dupe_multis,
                     merge_separate=True)
-            return ([alias], remote_opts, field, field, remote_opts.pk.column)
+            return ([alias], remote_opts, field, field, remote_opts.pk.column,
+                    True)
 
 
         field = find_field(name, opts.fields, False)
@@ -701,11 +708,12 @@
             target = field.rel.get_related_field()
             alias = self.join((root_alias, remote_opts.db_table, field.column,
                     target.column))
-            return [alias], remote_opts, field, target, target.column
+            return ([alias], remote_opts, field, target, target.column,
+                    field.null)
 
         # Only remaining possibility is a normal (direct lookup) field. No
         # join is required.
-        return None, opts, field, field, None
+        return None, opts, field, field, None, False
 
     def set_limits(self, low=None, high=None):
         """

Modified: 
django/branches/queryset-refactor/tests/regressiontests/queries/models.py
===================================================================
--- django/branches/queryset-refactor/tests/regressiontests/queries/models.py   
2007-10-14 02:15:03 UTC (rev 6493)
+++ django/branches/queryset-refactor/tests/regressiontests/queries/models.py   
2007-10-14 02:15:28 UTC (rev 6494)
@@ -3,7 +3,7 @@
 """
 
 from django.db import models
-from django.db.models.query import Q
+from django.db.models.query import Q, QNot
 
 class Tag(models.Model):
     name = models.CharField(maxlength=10)
@@ -14,7 +14,7 @@
 
 class Author(models.Model):
     name = models.CharField(maxlength=10)
-    num = models.IntegerField()
+    num = models.IntegerField(unique=True)
 
     def __unicode__(self):
         return self.name
@@ -69,6 +69,8 @@
 
 >>> r1 = Report(name='r1', creator=a1)
 >>> r1.save()
+>>> r2 = Report(name='r2', creator=a3)
+>>> r2.save()
 
 Bug #1050
 >>> Item.objects.filter(tags__isnull=True)
@@ -149,12 +151,27 @@
 Bug #5324
 >>> Item.objects.filter(tags__name='t4')
 [<Item: four>]
+>>> Item.objects.exclude(tags__name='t4').order_by('name').distinct()
+[<Item: one>, <Item: three>, <Item: two>]
+>>> Author.objects.exclude(item__name='one').distinct().order_by('name')
+[<Author: a2>, <Author: a3>, <Author: a4>]
 
-# FIXME: We seem to be constructing the right SQL here, but maybe a NULL test
-# for the pk of Tag is needed or something?
-# >>> Item.objects.exclude(tags__name='t4').order_by('name').distinct()
-# [<Item: one>, <Item: three>, <Item: two>]
+# Excluding from a relation that cannot be NULL should not use outer joins.
+>>> query = Item.objects.exclude(creator__in=[a1, a2]).query
+>>> query.LOUTER not in [x[2][2] for x in query.alias_map.values()]
+True
 
+# When only one of the joins is nullable (here, the Author -> Item join), we
+# should only get outer joins after that point (one, in this case). We also
+# show that three tables (so, two joins) are involved.
+>>> qs = Report.objects.exclude(creator__item__name='one')
+>>> list(qs)
+[<Report: r2>]
+>>> len([x[2][2] for x in qs.query.alias_map.values() if x[2][2] == 
query.LOUTER])
+1
+>>> len(qs.query.alias_map)
+3
+
 Bug #2091
 >>> t = Tag.objects.get(name='t4')
 >>> Item.objects.filter(tags__in=[t])


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to