Code

Ticket #10790: ticket10790_v5.diff

File ticket10790_v5.diff, 13.0 KB (added by milosu, 2 years ago)
Line 
1diff -urN Django-1.4b1.orig/django/db/models/sql/compiler.py Django-1.4b1/django/db/models/sql/compiler.py
2--- Django-1.4b1.orig/django/db/models/sql/compiler.py  2012-02-16 05:03:47.000000000 +0100
3+++ Django-1.4b1/django/db/models/sql/compiler.py       2012-03-04 23:39:03.982386400 +0100
4@@ -456,8 +456,8 @@
5         """
6         if not alias:
7             alias = self.query.get_initial_alias()
8-        field, target, opts, joins, _, _ = self.query.setup_joins(pieces,
9-                opts, alias, False)
10+        field, target, opts, joins, _, _, _ = self.query.setup_joins(
11+                pieces, opts, alias, False)
12         alias = joins[-1]
13         col = target.column
14         if not field.rel:
15@@ -511,7 +511,7 @@
16             if not self.query.alias_refcount[alias]:
17                 continue
18             try:
19-                name, alias, join_type, lhs, lhs_col, col, nullable = self.query.alias_map[alias]
20+                name, alias, join_type, lhs, lhs_col, col, nullable, demoted_join = self.query.alias_map[alias]
21             except KeyError:
22                 # Extra tables can end up in self.tables, but not in the
23                 # alias_map if they aren't in a join. That's OK. We skip them.
24diff -urN Django-1.4b1.orig/django/db/models/sql/constants.py Django-1.4b1/django/db/models/sql/constants.py
25--- Django-1.4b1.orig/django/db/models/sql/constants.py 2012-02-16 05:03:47.000000000 +0100
26+++ Django-1.4b1/django/db/models/sql/constants.py      2012-03-04 23:44:11.914999200 +0100
27@@ -24,6 +24,7 @@
28 LHS_JOIN_COL = 4
29 RHS_JOIN_COL = 5
30 NULLABLE = 6
31+DEMOTED_JOIN = 7
32 
33 # How many results to expect from a cursor.execute call
34 MULTI = 'multi'
35diff -urN Django-1.4b1.orig/django/db/models/sql/expressions.py Django-1.4b1/django/db/models/sql/expressions.py
36--- Django-1.4b1.orig/django/db/models/sql/expressions.py       2012-02-16 05:03:47.000000000 +0100
37+++ Django-1.4b1/django/db/models/sql/expressions.py    2012-03-04 23:40:00.806636600 +0100
38@@ -44,7 +44,7 @@
39             self.cols[node] = query.aggregate_select[node.name]
40         else:
41             try:
42-                field, source, opts, join_list, last, _ = query.setup_joins(
43+                field, source, opts, join_list, last, _, _ = query.setup_joins(
44                     field_list, query.get_meta(),
45                     query.get_initial_alias(), False)
46                 col, _, join_list = query.trim_joins(source, join_list, last, False)
47diff -urN Django-1.4b1.orig/django/db/models/sql/query.py Django-1.4b1/django/db/models/sql/query.py
48--- Django-1.4b1.orig/django/db/models/sql/query.py     2012-02-16 05:03:47.000000000 +0100
49+++ Django-1.4b1/django/db/models/sql/query.py  2012-03-04 23:47:38.154795400 +0100
50@@ -465,10 +465,12 @@
51         conjunction = (connector == AND)
52         first = True
53         for alias in rhs.tables:
54-            if not rhs.alias_refcount[alias]:
55+            demoted_alias = rhs.alias_map[alias][DEMOTED_JOIN]
56+            # the alias can be ignored only if it was not demoted due to the fkey trim join
57+            if not rhs.alias_refcount[alias] and not demoted_alias:
58                 # An unused alias.
59                 continue
60-            promote = (rhs.alias_map[alias][JOIN_TYPE] == self.LOUTER)
61+            promote = (rhs.alias_map[alias][JOIN_TYPE] == self.LOUTER or demoted_alias)
62             lhs, table, lhs_col, col = rhs.rev_join_map[alias]
63             # If the left side of the join was already relabeled, use the
64             # updated alias.
65@@ -700,6 +702,15 @@
66             return True
67         return False
68 
69+    def demote_alias(self, alias):
70+        """
71+        Demotes the join type of an alias to an inner join.
72+        """
73+        data = list(self.alias_map[alias])
74+        data[JOIN_TYPE] = self.INNER
75+        data[DEMOTED_JOIN] = True
76+        self.alias_map[alias] = tuple(data)
77+
78     def promote_alias_chain(self, chain, must_promote=False):
79         """
80         Walks along a chain of aliases, promoting the first nullable join and
81@@ -906,21 +917,29 @@
82                     if self.alias_map[alias][LHS_ALIAS] != lhs:
83                         continue
84                     self.ref_alias(alias)
85-                    if promote:
86+                    if promote or self.alias_map[alias][DEMOTED_JOIN]:
87                         self.promote_alias(alias)
88                     return alias
89 
90         # No reuse is possible, so we need a new alias.
91         alias, _ = self.table_alias(table, True)
92+
93         if not lhs:
94             # Not all tables need to be joined to anything. No join type
95             # means the later columns are ignored.
96             join_type = None
97         elif promote or outer_if_first:
98             join_type = self.LOUTER
99+        elif lhs in self.alias_map and self.alias_map[lhs][DEMOTED_JOIN]:
100+            # if the lhs is already present in the alias_map
101+            # and its join was DEMOTED earlier, change its join type to LOUTER
102+            # and promote LOUTER join
103+            if self.alias_map[lhs][JOIN_TYPE] == self.INNER:
104+                self.promote_alias(lhs, unconditional=True)
105+            join_type = self.LOUTER
106         else:
107             join_type = self.INNER
108-        join = (table, alias, join_type, lhs, lhs_col, col, nullable)
109+        join = (table, alias, join_type, lhs, lhs_col, col, nullable, False)
110         self.alias_map[alias] = join
111         if t_ident in self.join_map:
112             self.join_map[t_ident] += (alias,)
113@@ -1007,7 +1026,7 @@
114             #   - this is an annotation over a model field
115             # then we need to explore the joins that are required.
116 
117-            field, source, opts, join_list, last, _ = self.setup_joins(
118+            field, source, opts, join_list, last, _, allow_trim_join = self.setup_joins(
119                 field_list, opts, self.get_initial_alias(), False)
120 
121             # Process the join chain to see if it can be trimmed
122@@ -1119,7 +1138,7 @@
123         allow_many = trim or not negate
124 
125         try:
126-            field, target, opts, join_list, last, extra_filters = self.setup_joins(
127+            field, target, opts, join_list, last, extra_filters, allow_trim_join = self.setup_joins(
128                     parts, opts, alias, True, allow_many, allow_explicit_fk=True,
129                     can_reuse=can_reuse, negate=negate,
130                     process_extras=process_extras)
131@@ -1139,6 +1158,13 @@
132             self.promote_alias_chain(join_list)
133             join_promote = True
134 
135+            # If we have a one2one or many2one field, we can trim the left outer
136+            # join from the end of a list of joins.
137+            # In order to do this, we convert alias join type back to INNER and
138+            # trim_joins later will do the strip for us.
139+            if allow_trim_join and field.rel:
140+                self.demote_alias(join_list[-1])
141+
142         # Process the join list to see if we can remove any inner joins from
143         # the far end (fewer tables in a query is better).
144         nonnull_comparison = (lookup_type == 'isnull' and value is False)
145@@ -1295,6 +1321,7 @@
146         dupe_set = set()
147         exclusions = set()
148         extra_filters = []
149+        allow_trim_join = True
150         int_alias = None
151         for pos, name in enumerate(names):
152             if int_alias is not None:
153@@ -1318,6 +1345,11 @@
154                     raise FieldError("Cannot resolve keyword %r into field. "
155                             "Choices are: %s" % (name, ", ".join(names)))
156 
157+            # presence of indirect field in the filter requires
158+            # left outer join for isnull
159+            if not direct and allow_trim_join:
160+                allow_trim_join = False
161+
162             if not allow_many and (m2m or not direct):
163                 for alias in joins:
164                     self.unref_alias(alias)
165@@ -1359,6 +1391,8 @@
166                 extra_filters.extend(field.extra_filters(names, pos, negate))
167             if direct:
168                 if m2m:
169+                    # null query on m2mfield requires outer join
170+                    allow_trim_join = False
171                     # Many-to-many field defined on the current model.
172                     if cached_data:
173                         (table1, from_col1, to_col1, table2, from_col2,
174@@ -1479,7 +1513,7 @@
175             else:
176                 raise FieldError("Join on field %r not permitted." % name)
177 
178-        return field, target, opts, joins, last, extra_filters
179+        return field, target, opts, joins, last, extra_filters, allow_trim_join
180 
181     def trim_joins(self, target, join_list, last, trim, nonnull_check=False):
182         """
183@@ -1648,7 +1682,7 @@
184 
185         try:
186             for name in field_names:
187-                field, target, u2, joins, u3, u4 = self.setup_joins(
188+                field, target, u2, joins, u3, u4, allow_trim_join = self.setup_joins(
189                         name.split(LOOKUP_SEP), opts, alias, False, allow_m2m,
190                         True)
191                 final_alias = joins[-1]
192@@ -1930,7 +1964,7 @@
193         """
194         opts = self.model._meta
195         alias = self.get_initial_alias()
196-        field, col, opts, joins, last, extra = self.setup_joins(
197+        field, col, opts, joins, last, extra, allow_trim_join = self.setup_joins(
198                 start.split(LOOKUP_SEP), opts, alias, False)
199         select_col = self.alias_map[joins[1]][LHS_JOIN_COL]
200         select_alias = alias
201diff -urN Django-1.4b1.orig/tests/modeltests/null_trimjoin/__init__.py Django-1.4b1/tests/modeltests/null_trimjoin/__init__.py
202--- Django-1.4b1.orig/tests/modeltests/null_trimjoin/__init__.py        1970-01-01 01:00:00.000000000 +0100
203+++ Django-1.4b1/tests/modeltests/null_trimjoin/__init__.py     2012-03-04 20:49:30.323486400 +0100
204@@ -0,0 +1 @@
205+# dummy text for patch
206diff -urN Django-1.4b1.orig/tests/modeltests/null_trimjoin/models.py Django-1.4b1/tests/modeltests/null_trimjoin/models.py
207--- Django-1.4b1.orig/tests/modeltests/null_trimjoin/models.py  1970-01-01 01:00:00.000000000 +0100
208+++ Django-1.4b1/tests/modeltests/null_trimjoin/models.py       2012-03-04 20:49:30.334487100 +0100
209@@ -0,0 +1,31 @@
210+"""
211+Do not join table when querying on isnull
212+
213+"""
214+
215+from django.db import models
216+
217+class Category(models.Model):
218+    name = models.CharField(max_length=30)
219+
220+class ReporterType(models.Model):
221+    name = models.CharField(max_length=30)
222+
223+class Reporter(models.Model):
224+    name = models.CharField(max_length=30)
225+    type = models.ForeignKey(ReporterType, null=True)
226+    category = models.ManyToManyField(Category, null=True)
227+
228+    def __unicode__(self):
229+        return self.name
230+
231+class Article(models.Model):
232+    headline = models.CharField(max_length=100)
233+    reporter = models.ForeignKey(Reporter, null=True)
234+
235+    class Meta:
236+        ordering = ('headline',)
237+
238+    def __unicode__(self):
239+        return self.headline
240+
241diff -urN Django-1.4b1.orig/tests/modeltests/null_trimjoin/tests.py Django-1.4b1/tests/modeltests/null_trimjoin/tests.py
242--- Django-1.4b1.orig/tests/modeltests/null_trimjoin/tests.py   1970-01-01 01:00:00.000000000 +0100
243+++ Django-1.4b1/tests/modeltests/null_trimjoin/tests.py        2012-03-04 20:49:30.328486700 +0100
244@@ -0,0 +1,45 @@
245+from django.test import TestCase
246+from models import Article, Reporter
247+
248+class OneToOneTests(TestCase):
249+
250+    def setUp(self):
251+        self.r = Reporter(name='John Smith')
252+        self.r.save()
253+        self.a = Article(headline="First", reporter=self.r)
254+        self.a.save()
255+        self.a2 = Article(headline="Second")
256+        self.a2.save()
257+
258+    def test_query_with_isnull(self):
259+        """Querying with isnull should not join Reporter table."""
260+        q = Article.objects.filter(reporter=None)
261+        # check that reporter is not in the query's used_aliases
262+        self.assertFalse('null_trimjoin_reporter' in q.query.used_aliases)
263+        self.assertTrue('null_trimjoin_article' in q.query.used_aliases)
264+        # but it should still be in query.tables
265+        self.assertTrue('null_trimjoin_article' in q.query.tables)
266+        self.assertTrue('null_trimjoin_reporter' in q.query.tables)
267+
268+    def test_query_across_tables(self):
269+        """Querying across several tables should strip only the last join, while
270+        preserving the preceding left outer joins."""
271+        q = Article.objects.filter(reporter__type=None)
272+        self.assertEquals(len(q), 2)
273+        self.assertTrue('null_trimjoin_article' in q.query.used_aliases)
274+        self.assertTrue('null_trimjoin_reporter' in q.query.used_aliases)
275+        self.assertFalse('null_trimjoin_reportertype' in q.query.used_aliases)
276+
277+    def test_m2m_query(self):
278+        """Querying across m2m field should not strip the m2m table from join."""
279+        q = Article.objects.filter(reporter__category__isnull=True)
280+        self.assertTrue('null_trimjoin_article' in q.query.used_aliases)
281+        self.assertTrue('null_trimjoin_reporter' in q.query.used_aliases)
282+        self.assertTrue('null_trimjoin_category' in q.query.used_aliases)
283+
284+    def test_reverse_query(self):
285+        """Reverse querying with isnull should not strip the join."""
286+        q = Reporter.objects.filter(article__isnull=True)
287+        self.assertTrue('null_trimjoin_reporter' in q.query.used_aliases)
288+
289+