Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

This fixes a group of problems in the SQL created by QuerySet.exclude…

…() when

used in a few situations where NULL results can appear.

Fixed #8921 (the only ticket I know of that noticed any of these).


git-svn-id: http://code.djangoproject.com/svn/django/trunk@9590 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 33c0f0de676ae8fd2c406beebb2f113aeb95fee7 1 parent d4f0ae4
Malcolm Tredinnick authored December 08, 2008
50  django/db/models/sql/query.py
@@ -54,7 +54,6 @@ def __init__(self, model, connection, where=WhereNode):
54 54
         self.default_ordering = True
55 55
         self.standard_ordering = True
56 56
         self.ordering_aliases = []
57  
-        self.start_meta = None
58 57
         self.select_fields = []
59 58
         self.related_select_fields = []
60 59
         self.dupe_avoidance = {}
@@ -125,10 +124,9 @@ def __setstate__(self, obj_dict):
125 124
     def get_meta(self):
126 125
         """
127 126
         Returns the Options instance (the model._meta) from which to start
128  
-        processing. Normally, this is self.model._meta, but it can change.
  127
+        processing. Normally, this is self.model._meta, but it can be changed
  128
+        by subclasses.
129 129
         """
130  
-        if self.start_meta:
131  
-            return self.start_meta
132 130
         return self.model._meta
133 131
 
134 132
     def quote_name_unless_alias(self, name):
@@ -166,7 +164,6 @@ def clone(self, klass=None, **kwargs):
166 164
         obj.default_ordering = self.default_ordering
167 165
         obj.standard_ordering = self.standard_ordering
168 166
         obj.ordering_aliases = []
169  
-        obj.start_meta = self.start_meta
170 167
         obj.select_fields = self.select_fields[:]
171 168
         obj.related_select_fields = self.related_select_fields[:]
172 169
         obj.dupe_avoidance = self.dupe_avoidance.copy()
@@ -1485,11 +1482,24 @@ def split_exclude(self, filter_expr, prefix, can_reuse):
1485 1482
         query = Query(self.model, self.connection)
1486 1483
         query.add_filter(filter_expr, can_reuse=can_reuse)
1487 1484
         query.bump_prefix()
1488  
-        query.set_start(prefix)
1489 1485
         query.clear_ordering(True)
  1486
+        query.set_start(prefix)
1490 1487
         self.add_filter(('%s__in' % prefix, query), negate=True, trim=True,
1491 1488
                 can_reuse=can_reuse)
1492 1489
 
  1490
+        # If there's more than one join in the inner query (before any initial
  1491
+        # bits were trimmed -- which means the last active table is more than
  1492
+        # two places into the alias list), we need to also handle the
  1493
+        # possibility that the earlier joins don't match anything by adding a
  1494
+        # comparison to NULL (e.g. in
  1495
+        # Tag.objects.exclude(parent__parent__name='t1'), a tag with no parent
  1496
+        # would otherwise be overlooked).
  1497
+        active_positions = [pos for (pos, count) in
  1498
+                enumerate(query.alias_refcount.itervalues()) if count]
  1499
+        if active_positions[-1] > 1:
  1500
+            self.add_filter(('%s__isnull' % prefix, False), negate=True,
  1501
+                    trim=True, can_reuse=can_reuse)
  1502
+
1493 1503
     def set_limits(self, low=None, high=None):
1494 1504
         """
1495 1505
         Adjusts the limits on the rows retrieved. We use low/high to set these,
@@ -1695,18 +1705,26 @@ def set_start(self, start):
1695 1705
         alias = self.get_initial_alias()
1696 1706
         field, col, opts, joins, last, extra = self.setup_joins(
1697 1707
                 start.split(LOOKUP_SEP), opts, alias, False)
1698  
-        alias = joins[last[-1]]
1699  
-        self.select = [(alias, self.alias_map[alias][RHS_JOIN_COL])]
1700  
-        self.select_fields = [field]
1701  
-        self.start_meta = opts
1702  
-
1703  
-        # The call to setup_joins add an extra reference to everything in
1704  
-        # joins. So we need to unref everything once, and everything prior to
1705  
-        # the final join a second time.
  1708
+        select_col = self.alias_map[joins[1]][LHS_JOIN_COL]
  1709
+        select_alias = alias
  1710
+
  1711
+        # The call to setup_joins added an extra reference to everything in
  1712
+        # joins. Reverse that.
1706 1713
         for alias in joins:
1707 1714
             self.unref_alias(alias)
1708  
-        for alias in joins[:last[-1]]:
1709  
-            self.unref_alias(alias)
  1715
+
  1716
+        # We might be able to trim some joins from the front of this query,
  1717
+        # providing that we only traverse "always equal" connections (i.e. rhs
  1718
+        # is *always* the same value as lhs).
  1719
+        for alias in joins[1:]:
  1720
+            join_info = self.alias_map[alias]
  1721
+            if (join_info[LHS_JOIN_COL] != select_col
  1722
+                    or join_info[JOIN_TYPE] != self.INNER):
  1723
+                break
  1724
+            self.unref_alias(select_alias)
  1725
+            select_alias = join_info[RHS_ALIAS]
  1726
+            select_col = join_info[RHS_JOIN_COL]
  1727
+        self.select = [(select_alias, select_col)]
1710 1728
 
1711 1729
     def execute_sql(self, result_type=MULTI):
1712 1730
         """
25  tests/regressiontests/queries/models.py
@@ -675,7 +675,7 @@ class PointerB(models.Model):
675 675
 ...     s.reverse()
676 676
 ...     params.reverse()
677 677
 
678  
-# This slightly odd comparison works aorund the fact that PostgreSQL will
  678
+# This slightly odd comparison works around the fact that PostgreSQL will
679 679
 # return 'one' and 'two' as strings, not Unicode objects. It's a side-effect of
680 680
 # using constants here and not a real concern.
681 681
 >>> d = Item.objects.extra(select=SortedDict(s), select_params=params).values('a', 'b')[0]
@@ -742,7 +742,7 @@ class PointerB(models.Model):
742 742
 ## only apparent much later when the full test suite runs. I don't understand
743 743
 ## what's going on here yet.
744 744
 ##
745  
-## # We need to mess with the implemenation internals a bit here to decrease the
  745
+## # We need to mess with the implementation internals a bit here to decrease the
746 746
 ## # cache fill size so that we don't read all the results at once.
747 747
 ## >>> from django.db.models import query
748 748
 ## >>> query.ITER_CHUNK_SIZE = 2
@@ -795,7 +795,7 @@ class PointerB(models.Model):
795 795
 [<Item: four>, <Item: one>, <Item: three>]
796 796
 
797 797
 Bug #7095
798  
-Updates that are filtered on the model being updated are somewhat tricky to get
  798
+Updates that are filtered on the model being updated are somewhat tricky
799 799
 in MySQL. This exercises that case.
800 800
 >>> mm = ManagedModel.objects.create(data='mm1', tag=t1, public=True)
801 801
 >>> ManagedModel.objects.update(data='mm')
@@ -998,13 +998,28 @@ class PointerB(models.Model):
998 998
 >>> 'ORDER BY' in qs.query.as_sql()[0]
999 999
 True
1000 1000
 
1001  
-Bug #9188 -- incorrect SQL was being generated for certain types of
1002  
-exclude() queries that crossed multi-valued relations.
  1001
+Incorrect SQL was being generated for certain types of exclude() queries that
  1002
+crossed multi-valued relations (#8921, #9188 and some pre-emptively discovered
  1003
+cases).
1003 1004
 
1004 1005
 >>> PointerA.objects.filter(connection__pointerb__id=1)
1005 1006
 []
1006 1007
 >>> PointerA.objects.exclude(connection__pointerb__id=1)
1007 1008
 []
  1009
+
  1010
+>>> Tag.objects.exclude(children=None)
  1011
+[<Tag: t1>, <Tag: t3>]
  1012
+
  1013
+# This example is tricky because the parent could be NULL, so only checking
  1014
+# parents with annotations omits some results (tag t1, in this case).
  1015
+>>> Tag.objects.exclude(parent__annotation__name="a1")
  1016
+[<Tag: t1>, <Tag: t4>, <Tag: t5>]
  1017
+
  1018
+# The annotation->tag link is single values and tag->children links is
  1019
+# multi-valued. So we have to split the exclude filter in the middle and then
  1020
+# optimise the inner query without losing results.
  1021
+>>> Annotation.objects.exclude(tag__children__name="t2")
  1022
+[<Annotation: a2>]
1008 1023
 """}
1009 1024
 
1010 1025
 # In Python 2.3 and the Python 2.6 beta releases, exceptions raised in __len__

0 notes on commit 33c0f0d

Please sign in to comment.
Something went wrong with that request. Please try again.