Ticket #10790: ticket10790_v5.diff
File ticket10790_v5.diff, 13.0 KB (added by , 13 years ago) |
---|
-
django/db/models/sql/compiler.py
diff -urN Django-1.4b1.orig/django/db/models/sql/compiler.py Django-1.4b1/django/db/models/sql/compiler.py
old new 456 456 """ 457 457 if not alias: 458 458 alias = self.query.get_initial_alias() 459 field, target, opts, joins, _, _ = self.query.setup_joins(pieces,460 opts, alias, False)459 field, target, opts, joins, _, _, _ = self.query.setup_joins( 460 pieces, opts, alias, False) 461 461 alias = joins[-1] 462 462 col = target.column 463 463 if not field.rel: … … 511 511 if not self.query.alias_refcount[alias]: 512 512 continue 513 513 try: 514 name, alias, join_type, lhs, lhs_col, col, nullable = self.query.alias_map[alias]514 name, alias, join_type, lhs, lhs_col, col, nullable, demoted_join = self.query.alias_map[alias] 515 515 except KeyError: 516 516 # Extra tables can end up in self.tables, but not in the 517 517 # alias_map if they aren't in a join. That's OK. We skip them. -
django/db/models/sql/constants.py
diff -urN Django-1.4b1.orig/django/db/models/sql/constants.py Django-1.4b1/django/db/models/sql/constants.py
old new 24 24 LHS_JOIN_COL = 4 25 25 RHS_JOIN_COL = 5 26 26 NULLABLE = 6 27 DEMOTED_JOIN = 7 27 28 28 29 # How many results to expect from a cursor.execute call 29 30 MULTI = 'multi' -
django/db/models/sql/expressions.py
diff -urN Django-1.4b1.orig/django/db/models/sql/expressions.py Django-1.4b1/django/db/models/sql/expressions.py
old new 44 44 self.cols[node] = query.aggregate_select[node.name] 45 45 else: 46 46 try: 47 field, source, opts, join_list, last, _ = query.setup_joins(47 field, source, opts, join_list, last, _, _ = query.setup_joins( 48 48 field_list, query.get_meta(), 49 49 query.get_initial_alias(), False) 50 50 col, _, join_list = query.trim_joins(source, join_list, last, False) -
django/db/models/sql/query.py
diff -urN Django-1.4b1.orig/django/db/models/sql/query.py Django-1.4b1/django/db/models/sql/query.py
old new 465 465 conjunction = (connector == AND) 466 466 first = True 467 467 for alias in rhs.tables: 468 if not rhs.alias_refcount[alias]: 468 demoted_alias = rhs.alias_map[alias][DEMOTED_JOIN] 469 # the alias can be ignored only if it was not demoted due to the fkey trim join 470 if not rhs.alias_refcount[alias] and not demoted_alias: 469 471 # An unused alias. 470 472 continue 471 promote = (rhs.alias_map[alias][JOIN_TYPE] == self.LOUTER )473 promote = (rhs.alias_map[alias][JOIN_TYPE] == self.LOUTER or demoted_alias) 472 474 lhs, table, lhs_col, col = rhs.rev_join_map[alias] 473 475 # If the left side of the join was already relabeled, use the 474 476 # updated alias. … … 700 702 return True 701 703 return False 702 704 705 def demote_alias(self, alias): 706 """ 707 Demotes the join type of an alias to an inner join. 708 """ 709 data = list(self.alias_map[alias]) 710 data[JOIN_TYPE] = self.INNER 711 data[DEMOTED_JOIN] = True 712 self.alias_map[alias] = tuple(data) 713 703 714 def promote_alias_chain(self, chain, must_promote=False): 704 715 """ 705 716 Walks along a chain of aliases, promoting the first nullable join and … … 906 917 if self.alias_map[alias][LHS_ALIAS] != lhs: 907 918 continue 908 919 self.ref_alias(alias) 909 if promote :920 if promote or self.alias_map[alias][DEMOTED_JOIN]: 910 921 self.promote_alias(alias) 911 922 return alias 912 923 913 924 # No reuse is possible, so we need a new alias. 914 925 alias, _ = self.table_alias(table, True) 926 915 927 if not lhs: 916 928 # Not all tables need to be joined to anything. No join type 917 929 # means the later columns are ignored. 918 930 join_type = None 919 931 elif promote or outer_if_first: 920 932 join_type = self.LOUTER 933 elif lhs in self.alias_map and self.alias_map[lhs][DEMOTED_JOIN]: 934 # if the lhs is already present in the alias_map 935 # and its join was DEMOTED earlier, change its join type to LOUTER 936 # and promote LOUTER join 937 if self.alias_map[lhs][JOIN_TYPE] == self.INNER: 938 self.promote_alias(lhs, unconditional=True) 939 join_type = self.LOUTER 921 940 else: 922 941 join_type = self.INNER 923 join = (table, alias, join_type, lhs, lhs_col, col, nullable )942 join = (table, alias, join_type, lhs, lhs_col, col, nullable, False) 924 943 self.alias_map[alias] = join 925 944 if t_ident in self.join_map: 926 945 self.join_map[t_ident] += (alias,) … … 1007 1026 # - this is an annotation over a model field 1008 1027 # then we need to explore the joins that are required. 1009 1028 1010 field, source, opts, join_list, last, _ = self.setup_joins(1029 field, source, opts, join_list, last, _, allow_trim_join = self.setup_joins( 1011 1030 field_list, opts, self.get_initial_alias(), False) 1012 1031 1013 1032 # Process the join chain to see if it can be trimmed … … 1119 1138 allow_many = trim or not negate 1120 1139 1121 1140 try: 1122 field, target, opts, join_list, last, extra_filters = self.setup_joins(1141 field, target, opts, join_list, last, extra_filters, allow_trim_join = self.setup_joins( 1123 1142 parts, opts, alias, True, allow_many, allow_explicit_fk=True, 1124 1143 can_reuse=can_reuse, negate=negate, 1125 1144 process_extras=process_extras) … … 1139 1158 self.promote_alias_chain(join_list) 1140 1159 join_promote = True 1141 1160 1161 # If we have a one2one or many2one field, we can trim the left outer 1162 # join from the end of a list of joins. 1163 # In order to do this, we convert alias join type back to INNER and 1164 # trim_joins later will do the strip for us. 1165 if allow_trim_join and field.rel: 1166 self.demote_alias(join_list[-1]) 1167 1142 1168 # Process the join list to see if we can remove any inner joins from 1143 1169 # the far end (fewer tables in a query is better). 1144 1170 nonnull_comparison = (lookup_type == 'isnull' and value is False) … … 1295 1321 dupe_set = set() 1296 1322 exclusions = set() 1297 1323 extra_filters = [] 1324 allow_trim_join = True 1298 1325 int_alias = None 1299 1326 for pos, name in enumerate(names): 1300 1327 if int_alias is not None: … … 1318 1345 raise FieldError("Cannot resolve keyword %r into field. " 1319 1346 "Choices are: %s" % (name, ", ".join(names))) 1320 1347 1348 # presence of indirect field in the filter requires 1349 # left outer join for isnull 1350 if not direct and allow_trim_join: 1351 allow_trim_join = False 1352 1321 1353 if not allow_many and (m2m or not direct): 1322 1354 for alias in joins: 1323 1355 self.unref_alias(alias) … … 1359 1391 extra_filters.extend(field.extra_filters(names, pos, negate)) 1360 1392 if direct: 1361 1393 if m2m: 1394 # null query on m2mfield requires outer join 1395 allow_trim_join = False 1362 1396 # Many-to-many field defined on the current model. 1363 1397 if cached_data: 1364 1398 (table1, from_col1, to_col1, table2, from_col2, … … 1479 1513 else: 1480 1514 raise FieldError("Join on field %r not permitted." % name) 1481 1515 1482 return field, target, opts, joins, last, extra_filters 1516 return field, target, opts, joins, last, extra_filters, allow_trim_join 1483 1517 1484 1518 def trim_joins(self, target, join_list, last, trim, nonnull_check=False): 1485 1519 """ … … 1648 1682 1649 1683 try: 1650 1684 for name in field_names: 1651 field, target, u2, joins, u3, u4 = self.setup_joins(1685 field, target, u2, joins, u3, u4, allow_trim_join = self.setup_joins( 1652 1686 name.split(LOOKUP_SEP), opts, alias, False, allow_m2m, 1653 1687 True) 1654 1688 final_alias = joins[-1] … … 1930 1964 """ 1931 1965 opts = self.model._meta 1932 1966 alias = self.get_initial_alias() 1933 field, col, opts, joins, last, extra = self.setup_joins(1967 field, col, opts, joins, last, extra, allow_trim_join = self.setup_joins( 1934 1968 start.split(LOOKUP_SEP), opts, alias, False) 1935 1969 select_col = self.alias_map[joins[1]][LHS_JOIN_COL] 1936 1970 select_alias = alias -
tests/modeltests/null_trimjoin/__init__.py
diff -urN Django-1.4b1.orig/tests/modeltests/null_trimjoin/__init__.py Django-1.4b1/tests/modeltests/null_trimjoin/__init__.py
old new 1 # dummy text for patch -
tests/modeltests/null_trimjoin/models.py
diff -urN Django-1.4b1.orig/tests/modeltests/null_trimjoin/models.py Django-1.4b1/tests/modeltests/null_trimjoin/models.py
old new 1 """ 2 Do not join table when querying on isnull 3 4 """ 5 6 from django.db import models 7 8 class Category(models.Model): 9 name = models.CharField(max_length=30) 10 11 class ReporterType(models.Model): 12 name = models.CharField(max_length=30) 13 14 class Reporter(models.Model): 15 name = models.CharField(max_length=30) 16 type = models.ForeignKey(ReporterType, null=True) 17 category = models.ManyToManyField(Category, null=True) 18 19 def __unicode__(self): 20 return self.name 21 22 class Article(models.Model): 23 headline = models.CharField(max_length=100) 24 reporter = models.ForeignKey(Reporter, null=True) 25 26 class Meta: 27 ordering = ('headline',) 28 29 def __unicode__(self): 30 return self.headline 31 -
tests/modeltests/null_trimjoin/tests.py
diff -urN Django-1.4b1.orig/tests/modeltests/null_trimjoin/tests.py Django-1.4b1/tests/modeltests/null_trimjoin/tests.py
old new 1 from django.test import TestCase 2 from models import Article, Reporter 3 4 class OneToOneTests(TestCase): 5 6 def setUp(self): 7 self.r = Reporter(name='John Smith') 8 self.r.save() 9 self.a = Article(headline="First", reporter=self.r) 10 self.a.save() 11 self.a2 = Article(headline="Second") 12 self.a2.save() 13 14 def test_query_with_isnull(self): 15 """Querying with isnull should not join Reporter table.""" 16 q = Article.objects.filter(reporter=None) 17 # check that reporter is not in the query's used_aliases 18 self.assertFalse('null_trimjoin_reporter' in q.query.used_aliases) 19 self.assertTrue('null_trimjoin_article' in q.query.used_aliases) 20 # but it should still be in query.tables 21 self.assertTrue('null_trimjoin_article' in q.query.tables) 22 self.assertTrue('null_trimjoin_reporter' in q.query.tables) 23 24 def test_query_across_tables(self): 25 """Querying across several tables should strip only the last join, while 26 preserving the preceding left outer joins.""" 27 q = Article.objects.filter(reporter__type=None) 28 self.assertEquals(len(q), 2) 29 self.assertTrue('null_trimjoin_article' in q.query.used_aliases) 30 self.assertTrue('null_trimjoin_reporter' in q.query.used_aliases) 31 self.assertFalse('null_trimjoin_reportertype' in q.query.used_aliases) 32 33 def test_m2m_query(self): 34 """Querying across m2m field should not strip the m2m table from join.""" 35 q = Article.objects.filter(reporter__category__isnull=True) 36 self.assertTrue('null_trimjoin_article' in q.query.used_aliases) 37 self.assertTrue('null_trimjoin_reporter' in q.query.used_aliases) 38 self.assertTrue('null_trimjoin_category' in q.query.used_aliases) 39 40 def test_reverse_query(self): 41 """Reverse querying with isnull should not strip the join.""" 42 q = Reporter.objects.filter(article__isnull=True) 43 self.assertTrue('null_trimjoin_reporter' in q.query.used_aliases) 44 45