Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

queryset-refactor: Reworked exclude() handling to fix a few merging p…

…roblems.

Fixed #6704.


git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@7217 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit f2f933450f342058badce379b51b17eea81ad4de 1 parent d4d528e
Malcolm Tredinnick authored March 11, 2008
80  django/db/models/sql/query.py
@@ -20,6 +20,11 @@
20 20
 from datastructures import EmptyResultSet, Empty
21 21
 from constants import *
22 22
 
  23
+try:
  24
+    set
  25
+except NameError:
  26
+    from sets import Set as set     # Python 2.3 fallback
  27
+
23 28
 __all__ = ['Query']
24 29
 
25 30
 class Query(object):
@@ -573,13 +578,9 @@ def promote_alias(self, alias):
573 578
         """
574 579
         Promotes the join type of an alias to an outer join if it's possible
575 580
         for the join to contain NULL values on the left.
576  
-
577  
-        Returns True if the aliased join was promoted.
578 581
         """
579 582
         if self.alias_map[alias][ALIAS_NULLABLE]:
580 583
             self.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] = self.LOUTER
581  
-            return True
582  
-        return False
583 584
 
584 585
     def change_alias(self, old_alias, new_alias):
585 586
         """
@@ -753,7 +754,8 @@ def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
753 754
             self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1,
754 755
                     used, next, restricted)
755 756
 
756  
-    def add_filter(self, filter_expr, connector=AND, negate=False, trim=False):
  757
+    def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
  758
+            merge_negated=False):
757 759
         """
758 760
         Add a single filter to the query. The 'filter_expr' is a pair:
759 761
         (filter_string, value). E.g. ('name__contains', 'fred')
@@ -761,6 +763,10 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False):
761 763
         If 'negate' is True, this is an exclude() filter. If 'trim' is True, we
762 764
         automatically trim the final join group (used internally when
763 765
         constructing nested queries).
  766
+
  767
+        If 'merge_negated' is True, this negated filter will be merged with the
  768
+        existing negated where node (if it exists). This is used when
  769
+        constructing an exclude filter from combined subfilters.
764 770
         """
765 771
         arg, value = filter_expr
766 772
         parts = arg.split(LOOKUP_SEP)
@@ -813,9 +819,13 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False):
813 819
                 self.unref_alias(alias)
814 820
                 alias = join[LHS_ALIAS]
815 821
                 col = join[LHS_JOIN_COL]
  822
+                if len(join_list[-1]) == 1:
  823
+                    join_list = join_list[:-1]
  824
+                else:
  825
+                    join_list[-1] = join_list[-1][:-1]
816 826
 
817  
-        if lookup_type == 'isnull' and value is True and (len(join_list) > 1 or
818  
-                len(join_list[0]) > 1):
  827
+        if (lookup_type == 'isnull' and value is True and not negate and
  828
+                (len(join_list) > 1 or len(join_list[0]) > 1)):
819 829
             # If the comparison is against NULL, we need to use a left outer
820 830
             # join when connecting to the previous model. We make that
821 831
             # adjustment here. We don't do this unless needed as it's less
@@ -846,19 +856,32 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False):
846 856
                 # that's harmless.
847 857
                 self.promote_alias(table)
848 858
 
849  
-        self.where.add([alias, col, field, lookup_type, value], connector)
  859
+        entry = [alias, col, field, lookup_type, value]
  860
+        if merge_negated:
  861
+            # This case is when we're doing the Q2 filter in exclude(Q1, Q2).
  862
+            # It's different from exclude(Q1).exclude(Q2).
  863
+            for node in self.where.children:
  864
+                if getattr(node, 'negated', False):
  865
+                    node.add(entry, connector)
  866
+                    merged = True
  867
+                    break
  868
+        else:
  869
+            self.where.add(entry, connector)
  870
+            merged = False
850 871
 
851 872
         if negate:
852  
-            flag = False
853  
-            for seq in join_list:
854  
-                for join in seq:
855  
-                    if self.promote_alias(join):
856  
-                        flag = True
857  
-            self.where.negate()
858  
-            if flag:
859  
-                # XXX: Change this to the field we joined against to allow
860  
-                # for node sharing and where-tree optimisation?
861  
-                self.where.add([alias, col, field, 'isnull', True], OR)
  873
+            count = 0
  874
+            for join in join_list:
  875
+                count += len(join)
  876
+                for alias in join:
  877
+                    self.promote_alias(alias)
  878
+            if not merged:
  879
+                self.where.negate()
  880
+            if count > 1 and lookup_type != 'isnull':
  881
+                j_col = self.alias_map[alias][ALIAS_JOIN][RHS_JOIN_COL]
  882
+                entry = Node([[alias, j_col, None, 'isnull', True]])
  883
+                entry.negate()
  884
+                self.where.add(entry, AND)
862 885
 
863 886
     def add_q(self, q_object):
864 887
         """
@@ -877,13 +900,16 @@ def add_q(self, q_object):
877 900
         else:
878 901
             subtree = False
879 902
         connector = AND
  903
+        merge = False
880 904
         for child in q_object.children:
881 905
             if isinstance(child, Node):
882 906
                 self.where.start_subtree(connector)
883 907
                 self.add_q(child)
884 908
                 self.where.end_subtree()
885 909
             else:
886  
-                self.add_filter(child, connector, q_object.negated)
  910
+                self.add_filter(child, connector, q_object.negated,
  911
+                        merge_negated=merge)
  912
+                merge = q_object.negated
887 913
             connector = q_object.connector
888 914
         if subtree:
889 915
             self.where.end_subtree()
@@ -902,7 +928,9 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True):
902 928
         list of tables joined.
903 929
         """
904 930
         joins = [[alias]]
  931
+        used = set()
905 932
         for pos, name in enumerate(names):
  933
+            used.update(joins[-1])
906 934
             if name == 'pk':
907 935
                 name = opts.pk.name
908 936
 
@@ -924,7 +952,7 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True):
924 952
                     lhs_col = opts.parents[int_model].column
925 953
                     opts = int_model._meta
926 954
                     alias = self.join((alias, opts.db_table, lhs_col,
927  
-                            opts.pk.column))
  955
+                            opts.pk.column), exclusions=used)
928 956
                     alias_list.append(alias)
929 957
                 joins.append(alias_list)
930 958
             cached_data = opts._join_cache.get(name)
@@ -950,9 +978,9 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True):
950 978
                                 target)
951 979
 
952 980
                     int_alias = self.join((alias, table1, from_col1, to_col1),
953  
-                            dupe_multis, nullable=True)
  981
+                            dupe_multis, used, nullable=True)
954 982
                     alias = self.join((int_alias, table2, from_col2, to_col2),
955  
-                            dupe_multis, nullable=True)
  983
+                            dupe_multis, used, nullable=True)
956 984
                     joins.append([int_alias, alias])
957 985
                 elif field.rel:
958 986
                     # One-to-one or many-to-one field
@@ -968,7 +996,7 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True):
968 996
                                 opts, target)
969 997
 
970 998
                     alias = self.join((alias, table, from_col, to_col),
971  
-                            nullable=field.null)
  999
+                            exclusions=used, nullable=field.null)
972 1000
                     joins.append([alias])
973 1001
                 else:
974 1002
                     # Non-relation fields.
@@ -996,9 +1024,9 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True):
996 1024
                                 target)
997 1025
 
998 1026
                     int_alias = self.join((alias, table1, from_col1, to_col1),
999  
-                            dupe_multis, nullable=True)
  1027
+                            dupe_multis, used, nullable=True)
1000 1028
                     alias = self.join((int_alias, table2, from_col2, to_col2),
1001  
-                            dupe_multis, nullable=True)
  1029
+                            dupe_multis, used, nullable=True)
1002 1030
                     joins.append([int_alias, alias])
1003 1031
                 else:
1004 1032
                     # One-to-many field (ForeignKey defined on the target model)
@@ -1016,7 +1044,7 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True):
1016 1044
                                 opts, target)
1017 1045
 
1018 1046
                     alias = self.join((alias, table, from_col, to_col),
1019  
-                            dupe_multis, nullable=True)
  1047
+                            dupe_multis, used, nullable=True)
1020 1048
                     joins.append([alias])
1021 1049
 
1022 1050
         if pos != len(names) - 1:
6  django/db/models/sql/where.py
@@ -5,6 +5,7 @@
5 5
 
6 6
 from django.utils import tree
7 7
 from django.db import connection
  8
+from django.db.models.fields import Field
8 9
 from datastructures import EmptyResultSet, FullResultSet
9 10
 
10 11
 # Connection types
@@ -105,7 +106,10 @@ def make_atom(self, child, qn):
105 106
         else:
106 107
             cast_sql = '%s'
107 108
 
108  
-        params = field.get_db_prep_lookup(lookup_type, value)
  109
+        if field:
  110
+            params = field.get_db_prep_lookup(lookup_type, value)
  111
+        else:
  112
+            params = Field().get_db_prep_lookup(lookup_type, value)
109 113
         if isinstance(params, tuple):
110 114
             extra, params = params
111 115
         else:
15  django/utils/tree.py
@@ -16,6 +16,14 @@ class Node(object):
16 16
     default = 'DEFAULT'
17 17
 
18 18
     def __init__(self, children=None, connector=None, negated=False):
  19
+        """
  20
+        Constructs a new Node. If no connector is given, the default will be
  21
+        used.
  22
+
  23
+        Warning: You probably don't want to pass in the 'negated' parameter. It
  24
+        is NOT the same as constructing a node and calling negate() on the
  25
+        result.
  26
+        """
19 27
         self.children = children and children[:] or []
20 28
         self.connector = connector or self.default
21 29
         self.subtree_parents = []
@@ -63,6 +71,8 @@ def add(self, node, conn_type):
63 71
         Otherwise, the whole tree is pushed down one level and a new root
64 72
         connector is created, connecting the existing tree and the new node.
65 73
         """
  74
+        if node in self.children:
  75
+            return
66 76
         if len(self.children) < 2:
67 77
             self.connector = conn_type
68 78
         if self.connector == conn_type:
@@ -78,7 +88,10 @@ def add(self, node, conn_type):
78 88
 
79 89
     def negate(self):
80 90
         """
81  
-        Negate the sense of the root connector.
  91
+        Negate the sense of the root connector. This reorganises the children
  92
+        so that the current node has a single child: a negated node containing
  93
+        all the previous children. This slightly odd construction makes adding
  94
+        new children behave more intuitively.
82 95
 
83 96
         Interpreting the meaning of this negate is up to client code. This
84 97
         method is useful for implementing "not" arrangements.
16  tests/regressiontests/queries/models.py
@@ -312,7 +312,7 @@ class Meta:
312 312
 >>> Author.objects.filter(report__name='r1')
313 313
 [<Author: a1>]
314 314
 
315  
-Bug #5324
  315
+Bug #5324, #6704
316 316
 >>> Item.objects.filter(tags__name='t4')
317 317
 [<Item: four>]
318 318
 >>> Item.objects.exclude(tags__name='t4').order_by('name').distinct()
@@ -340,6 +340,20 @@ class Meta:
340 340
 >>> len([x[2][2] for x in qs.query.alias_map.values() if x[2][2] == query.LOUTER])
341 341
 1
342 342
 
  343
+The previous changes shouldn't affect nullable foreign key joins.
  344
+>>> Tag.objects.filter(parent__isnull=True).order_by('name')
  345
+[<Tag: t1>]
  346
+>>> Tag.objects.exclude(parent__isnull=True).order_by('name')
  347
+[<Tag: t2>, <Tag: t3>, <Tag: t4>, <Tag: t5>]
  348
+>>> Tag.objects.exclude(Q(parent__name='t1') | Q(parent__isnull=True)).order_by('name')
  349
+[<Tag: t4>, <Tag: t5>]
  350
+>>> Tag.objects.exclude(Q(parent__isnull=True) | Q(parent__name='t1')).order_by('name')
  351
+[<Tag: t4>, <Tag: t5>]
  352
+>>> Tag.objects.exclude(Q(parent__parent__isnull=True)).order_by('name')
  353
+[<Tag: t4>, <Tag: t5>]
  354
+>>> Tag.objects.filter(~Q(parent__parent__isnull=True)).order_by('name')
  355
+[<Tag: t4>, <Tag: t5>]
  356
+
343 357
 Bug #2091
344 358
 >>> t = Tag.objects.get(name='t4')
345 359
 >>> Item.objects.filter(tags__in=[t])

0 notes on commit f2f9334

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