Ticket #16715: 16715_alternate-redux-r16928.diff
File 16715_alternate-redux-r16928.diff, 20.2 KB (added by , 13 years ago) |
---|
-
tests/regressiontests/nested_foreign_keys/tests.py
1 from django.test import TestCase 2 3 from models import Person, Movie, Event, Screening, ScreeningNullFK, Package, PackageNullFK 4 5 6 # These are tests for #16715. The basic scheme is always the same: 3 models with 7 # 2 relations. The first relation may be null, while the second is non-nullable. 8 # In some cases, Django would pick the wrong join type for the second relation, 9 # resulting in missing objects in the queryset. 10 # 11 # Model A 12 # | (Relation A/B : nullable) 13 # Model B 14 # | (Relation B/C : non-nullable) 15 # Model C 16 # 17 # Because of the possibility of NULL rows resulting from the LEFT OUTER JOIN 18 # between Model A and Model B (i.e. instances of A without reference to B), 19 # the second join must also be LEFT OUTER JOIN, so that we do not ignore 20 # instances of A that do not reference B. 21 # 22 # Relation A/B can either be an explicit foreign key or an implicit reverse 23 # relation such as introduced by one-to-one relations (through multi-table 24 # inheritance). 25 class NestedForeignKeysTests(TestCase): 26 def setUp(self): 27 self.director = Person.objects.create(name=u'Terry Gilliam / Terry Jones') 28 self.movie = Movie.objects.create(title=u'Monty Python and the Holy Grail', director=self.director) 29 30 31 # This test failed in #16715 because in some cases INNER JOIN was selected 32 # for the second foreign key relation instead of LEFT OUTER JOIN. 33 def testInheritance(self): 34 some_event = Event.objects.create() 35 screening = Screening.objects.create(movie=self.movie) 36 37 self.assertEqual(len(Event.objects.all()), 2) 38 self.assertEqual(len(Event.objects.select_related('screening')), 2) 39 # This failed. 40 self.assertEqual(len(Event.objects.select_related('screening__movie')), 2) 41 42 self.assertEqual(len(Event.objects.values()), 2) 43 self.assertEqual(len(Event.objects.values('screening__pk')), 2) 44 self.assertEqual(len(Event.objects.values('screening__movie__pk')), 2) 45 self.assertEqual(len(Event.objects.values('screening__movie__title')), 2) 46 # This failed. 47 self.assertEqual(len(Event.objects.values('screening__movie__pk', 'screening__movie__title')), 2) 48 49 # Simple filter/exclude queries for good measure. 50 self.assertEqual(Event.objects.filter(screening__movie=self.movie).count(), 1) 51 self.assertEqual(Event.objects.exclude(screening__movie=self.movie).count(), 1) 52 53 54 # These all work because the second foreign key in the chain has null=True. 55 def testInheritanceNullFK(self): 56 some_event = Event.objects.create() 57 screening = ScreeningNullFK.objects.create(movie=None) 58 screening_with_movie = ScreeningNullFK.objects.create(movie=self.movie) 59 60 self.assertEqual(len(Event.objects.all()), 3) 61 self.assertEqual(len(Event.objects.select_related('screeningnullfk')), 3) 62 self.assertEqual(len(Event.objects.select_related('screeningnullfk__movie')), 3) 63 64 self.assertEqual(len(Event.objects.values()), 3) 65 self.assertEqual(len(Event.objects.values('screeningnullfk__pk')), 3) 66 self.assertEqual(len(Event.objects.values('screeningnullfk__movie__pk')), 3) 67 self.assertEqual(len(Event.objects.values('screeningnullfk__movie__title')), 3) 68 self.assertEqual(len(Event.objects.values('screeningnullfk__movie__pk', 'screeningnullfk__movie__title')), 3) 69 70 self.assertEqual(Event.objects.filter(screeningnullfk__movie=self.movie).count(), 1) 71 self.assertEqual(Event.objects.exclude(screeningnullfk__movie=self.movie).count(), 2) 72 73 74 # This test failed in #16715 because in some cases INNER JOIN was selected 75 # for the second foreign key relation instead of LEFT OUTER JOIN. 76 def testExplicitForeignKey(self): 77 package = Package.objects.create() 78 screening = Screening.objects.create(movie=self.movie) 79 package_with_screening = Package.objects.create(screening=screening) 80 81 self.assertEqual(len(Package.objects.all()), 2) 82 self.assertEqual(len(Package.objects.select_related('screening')), 2) 83 self.assertEqual(len(Package.objects.select_related('screening__movie')), 2) 84 85 self.assertEqual(len(Package.objects.values()), 2) 86 self.assertEqual(len(Package.objects.values('screening__pk')), 2) 87 self.assertEqual(len(Package.objects.values('screening__movie__pk')), 2) 88 self.assertEqual(len(Package.objects.values('screening__movie__title')), 2) 89 # This failed. 90 self.assertEqual(len(Package.objects.values('screening__movie__pk', 'screening__movie__title')), 2) 91 92 self.assertEqual(Package.objects.filter(screening__movie=self.movie).count(), 1) 93 self.assertEqual(Package.objects.exclude(screening__movie=self.movie).count(), 1) 94 95 96 # These all work because the second foreign key in the chain has null=True. 97 def testExplicitForeignKeyNullFK(self): 98 package = PackageNullFK.objects.create() 99 screening = ScreeningNullFK.objects.create(movie=None) 100 screening_with_movie = ScreeningNullFK.objects.create(movie=self.movie) 101 package_with_screening = PackageNullFK.objects.create(screening=screening) 102 package_with_screening_with_movie = PackageNullFK.objects.create(screening=screening_with_movie) 103 104 self.assertEqual(len(PackageNullFK.objects.all()), 3) 105 self.assertEqual(len(PackageNullFK.objects.select_related('screening')), 3) 106 self.assertEqual(len(PackageNullFK.objects.select_related('screening__movie')), 3) 107 108 self.assertEqual(len(PackageNullFK.objects.values()), 3) 109 self.assertEqual(len(PackageNullFK.objects.values('screening__pk')), 3) 110 self.assertEqual(len(PackageNullFK.objects.values('screening__movie__pk')), 3) 111 self.assertEqual(len(PackageNullFK.objects.values('screening__movie__title')), 3) 112 self.assertEqual(len(PackageNullFK.objects.values('screening__movie__pk', 'screening__movie__title')), 3) 113 114 self.assertEqual(PackageNullFK.objects.filter(screening__movie=self.movie).count(), 1) 115 self.assertEqual(PackageNullFK.objects.exclude(screening__movie=self.movie).count(), 2) 116 117 118 # Some additional tests for #16715. The only difference is the depth of the 119 # nesting as we now use 4 models instead of 3 (and thus 3 relations). This 120 # checks if promotion of join types works for deeper nesting too. 121 class DeeplyNestedForeignKeysTests(TestCase): 122 def setUp(self): 123 self.director = Person.objects.create(name=u'Terry Gilliam / Terry Jones') 124 self.movie = Movie.objects.create(title=u'Monty Python and the Holy Grail', director=self.director) 125 126 127 def testInheritance(self): 128 some_event = Event.objects.create() 129 screening = Screening.objects.create(movie=self.movie) 130 131 self.assertEqual(len(Event.objects.all()), 2) 132 self.assertEqual(len(Event.objects.select_related('screening__movie__director')), 2) 133 134 self.assertEqual(len(Event.objects.values()), 2) 135 self.assertEqual(len(Event.objects.values('screening__movie__director__pk')), 2) 136 self.assertEqual(len(Event.objects.values('screening__movie__director__name')), 2) 137 self.assertEqual(len(Event.objects.values('screening__movie__director__pk', 'screening__movie__director__name')), 2) 138 self.assertEqual(len(Event.objects.values('screening__movie__pk', 'screening__movie__director__pk')), 2) 139 self.assertEqual(len(Event.objects.values('screening__movie__pk', 'screening__movie__director__name')), 2) 140 self.assertEqual(len(Event.objects.values('screening__movie__title', 'screening__movie__director__pk')), 2) 141 self.assertEqual(len(Event.objects.values('screening__movie__title', 'screening__movie__director__name')), 2) 142 143 self.assertEqual(Event.objects.filter(screening__movie__director=self.director).count(), 1) 144 self.assertEqual(Event.objects.exclude(screening__movie__director=self.director).count(), 1) 145 146 147 def testExplicitForeignKey(self): 148 package = Package.objects.create() 149 screening = Screening.objects.create(movie=self.movie) 150 package_with_screening = Package.objects.create(screening=screening) 151 152 self.assertEqual(len(Package.objects.all()), 2) 153 self.assertEqual(len(Package.objects.select_related('screening__movie__director')), 2) 154 155 self.assertEqual(len(Package.objects.values()), 2) 156 self.assertEqual(len(Package.objects.values('screening__movie__director__pk')), 2) 157 self.assertEqual(len(Package.objects.values('screening__movie__director__name')), 2) 158 self.assertEqual(len(Package.objects.values('screening__movie__director__pk', 'screening__movie__director__name')), 2) 159 self.assertEqual(len(Package.objects.values('screening__movie__pk', 'screening__movie__director__pk')), 2) 160 self.assertEqual(len(Package.objects.values('screening__movie__pk', 'screening__movie__director__name')), 2) 161 self.assertEqual(len(Package.objects.values('screening__movie__title', 'screening__movie__director__pk')), 2) 162 self.assertEqual(len(Package.objects.values('screening__movie__title', 'screening__movie__director__name')), 2) 163 164 self.assertEqual(Package.objects.filter(screening__movie__director=self.director).count(), 1) 165 self.assertEqual(Package.objects.exclude(screening__movie__director=self.director).count(), 1) -
tests/regressiontests/nested_foreign_keys/models.py
1 from django.db import models 2 3 4 class Person(models.Model): 5 name = models.CharField(max_length=200) 6 7 8 class Movie(models.Model): 9 title = models.CharField(max_length=200) 10 director = models.ForeignKey(Person) 11 12 13 class Event(models.Model): 14 pass 15 16 17 class Screening(Event): 18 movie = models.ForeignKey(Movie) 19 20 class ScreeningNullFK(Event): 21 movie = models.ForeignKey(Movie, null=True) 22 23 24 class Package(models.Model): 25 screening = models.ForeignKey(Screening, null=True) 26 27 class PackageNullFK(models.Model): 28 screening = models.ForeignKey(ScreeningNullFK, null=True) -
django/db/models/sql/compiler.py
397 397 self.query.ref_alias(alias) 398 398 399 399 # Must use left outer joins for nullable fields and their relations. 400 self.query.promote_alias_chain(joins, 401 self.query.alias_map[joins[0]][JOIN_TYPE] == self.query.LOUTER) 400 self.query.promote_joins(joins) 402 401 403 402 # If we get to this point and the field is a relation to another model, 404 403 # append the default ordering for that model. … … 579 578 alias_chain.append(alias) 580 579 for (dupe_opts, dupe_col) in dupe_set: 581 580 self.query.update_dupe_avoidance(dupe_opts, dupe_col, alias) 582 if self.query.alias_map[root_alias][JOIN_TYPE] == self.query.LOUTER: 583 self.query.promote_alias_chain(alias_chain, True) 581 self.query.promote_joins(alias_chain) 584 582 else: 585 583 alias = root_alias 586 584 … … 597 595 columns, aliases = self.get_default_columns(start_alias=alias, 598 596 opts=f.rel.to._meta, as_pairs=True) 599 597 self.query.related_select_cols.extend(columns) 600 if self.query.alias_map[alias][JOIN_TYPE] == self.query.LOUTER: 601 self.query.promote_alias_chain(aliases, True) 598 self.query.promote_joins(aliases) 602 599 self.query.related_select_fields.extend(f.rel.to._meta.fields) 603 600 if restricted: 604 601 next = requested.get(f.name, {}) … … 671 668 self.query.related_select_fields.extend(model._meta.fields) 672 669 673 670 next = requested.get(f.related_query_name(), {}) 674 new_nullable = f.null or None 671 # Use True here because we are looking at the _reverse_ side of 672 # the relation, which is always nullable. 673 new_nullable = True 675 674 676 675 self.fill_related_selections(model._meta, table, cur_depth+1, 677 676 used, next, restricted, new_nullable) -
django/db/models/sql/query.py
497 497 # Again, some of the tables won't have aliases due to 498 498 # the trimming of unnecessary tables. 499 499 if self.alias_refcount.get(alias) or rhs.alias_refcount.get(alias): 500 self.promote_ alias(alias, True)500 self.promote_joins([alias], True) 501 501 502 502 # Now relabel a copy of the rhs where-clause and add it to the current 503 503 # one. … … 677 677 """ Decreases the reference count for this alias. """ 678 678 self.alias_refcount[alias] -= 1 679 679 680 def promote_ alias(self, alias, unconditional=False):680 def promote_joins(self, aliases, unconditional=False): 681 681 """ 682 Promotes the join type of an alias to an outer join if it's possible 683 for the join to contain NULL values on the left. If 'unconditional' is 684 False, the join is only promoted if it is nullable, otherwise it is 685 always promoted. 686 687 Returns True if the join was promoted by this call. 682 Promotes recursively the join type of given aliases and its children to 683 an outer join if it's possible for the join to contain NULL values on 684 the left. If 'unconditional' is False, the join is only promoted if it 685 is nullable or the parent join is an outer join, otherwise it is always 686 promoted. 688 687 """ 689 if ((unconditional or self.alias_map[alias][NULLABLE]) and 690 self.alias_map[alias][JOIN_TYPE] != self.LOUTER): 691 data = list(self.alias_map[alias]) 692 data[JOIN_TYPE] = self.LOUTER 693 self.alias_map[alias] = tuple(data) 694 return True 695 return False 688 aliases = list(aliases) 689 while aliases: 690 alias = aliases.pop(0) 691 parent_alias = self.alias_map[alias][LHS_ALIAS] 692 parent_louter = parent_alias and self.alias_map[parent_alias][JOIN_TYPE] == self.LOUTER or False 693 if ((unconditional or self.alias_map[alias][NULLABLE] or parent_louter) and 694 self.alias_map[alias][JOIN_TYPE] != self.LOUTER): 695 data = list(self.alias_map[alias]) 696 data[JOIN_TYPE] = self.LOUTER 697 self.alias_map[alias] = tuple(data) 698 # Join type of 'alias' changed, so re-examine all aliases that 699 # refer to this one (and, in turn, their children). 700 aliases.extend(child for child in self.alias_map.iterkeys() 701 if self.alias_map[child][LHS_ALIAS] == alias and child not in aliases) 696 702 697 def promote_alias_chain(self, chain, must_promote=False):698 """699 Walks along a chain of aliases, promoting the first nullable join and700 any joins following that. If 'must_promote' is True, all the aliases in701 the chain are promoted.702 """703 for alias in chain:704 if self.promote_alias(alias, must_promote):705 must_promote = True706 707 703 def promote_unused_aliases(self, initial_refcounts, used_aliases): 708 704 """ 709 705 Given a "before" copy of the alias_refcounts dictionary (as … … 712 708 then and which ones haven't been used and promotes all of those 713 709 aliases, plus any children of theirs in the alias tree, to outer joins. 714 710 """ 715 # FIXME: There's some (a lot of!) overlap with the similar OR promotion716 # in add_filter(). It's not quite identical, but is very similar. So717 # pulling out the common bits is something for later.718 considered = {}719 711 for alias in self.tables: 720 if alias not in used_aliases: 721 continue 722 if (alias not in initial_refcounts or 712 if alias in used_aliases and (alias not in initial_refcounts or 723 713 self.alias_refcount[alias] == initial_refcounts[alias]): 724 parent = self.alias_map[alias][LHS_ALIAS] 725 must_promote = considered.get(parent, False) 726 promoted = self.promote_alias(alias, must_promote) 727 considered[alias] = must_promote or promoted 714 self.promote_joins([alias]) 728 715 729 716 def change_aliases(self, change_map): 730 717 """ … … 891 878 continue 892 879 self.ref_alias(alias) 893 880 if promote: 894 self.promote_ alias(alias)881 self.promote_joins([alias]) 895 882 return alias 896 883 897 884 # No reuse is possible, so we need a new alias. … … 1000 987 # If the aggregate references a model or field that requires a join, 1001 988 # those joins must be LEFT OUTER - empty join rows must be returned 1002 989 # in order for zeros to be returned for those aggregates. 1003 for column_alias in join_list: 1004 self.promote_alias(column_alias, unconditional=True) 990 self.promote_joins(join_list, True) 1005 991 1006 992 col = (join_list[-1], col) 1007 993 else: … … 1100 1086 # If the comparison is against NULL, we may need to use some left 1101 1087 # outer joins when creating the join chain. This is only done when 1102 1088 # needed, as it's less efficient at the database level. 1103 self.promote_ alias_chain(join_list)1089 self.promote_joins(join_list) 1104 1090 join_promote = True 1105 1091 1106 1092 # Process the join list to see if we can remove any inner joins from … … 1131 1117 # This means that we are dealing with two different query 1132 1118 # subtrees, so we don't need to do any join promotion. 1133 1119 continue 1134 join_promote = join_promote or self.promote_ alias(join, unconditional)1120 join_promote = join_promote or self.promote_joins([join], unconditional) 1135 1121 if table != join: 1136 table_promote = self.promote_ alias(table)1122 table_promote = self.promote_joins([table]) 1137 1123 # We only get here if we have found a table that exists 1138 1124 # in the join list, but isn't on the original tables list. 1139 1125 # This means we've reached the point where we only have 1140 1126 # new tables, so we can break out of this promotion loop. 1141 1127 break 1142 self.promote_ alias_chain(join_it, join_promote)1143 self.promote_ alias_chain(table_it, table_promote or join_promote)1128 self.promote_joins(join_it, join_promote) 1129 self.promote_joins(table_it, table_promote or join_promote) 1144 1130 1145 1131 if having_clause or force_having: 1146 1132 if (alias, col) not in self.group_by: … … 1152 1138 connector) 1153 1139 1154 1140 if negate: 1155 self.promote_ alias_chain(join_list)1141 self.promote_joins(join_list) 1156 1142 if lookup_type != 'isnull': 1157 1143 if len(join_list) > 1: 1158 1144 for alias in join_list: … … 1617 1603 final_alias = join[LHS_ALIAS] 1618 1604 col = join[LHS_JOIN_COL] 1619 1605 joins = joins[:-1] 1620 self.promote_ alias_chain(joins[1:])1606 self.promote_joins(joins[1:]) 1621 1607 self.select.append((final_alias, col)) 1622 1608 self.select_fields.append(field) 1623 1609 except MultiJoin: