Ticket #16715: 16715_alternate.diff
File 16715_alternate.diff, 19.6 KB (added by , 13 years ago) |
---|
-
django/db/models/sql/compiler.py
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 05c19f3..b8e0e88 100644
a b class SQLCompiler(object): 395 395 self.query.ref_alias(alias) 396 396 397 397 # Must use left outer joins for nullable fields and their relations. 398 self.query.promote_alias_chain(joins, 399 self.query.alias_map[joins[0]][JOIN_TYPE] == self.query.LOUTER) 398 self.query.promote_joins(joins) 400 399 401 400 # If we get to this point and the field is a relation to another model, 402 401 # append the default ordering for that model. … … class SQLCompiler(object): 577 576 alias_chain.append(alias) 578 577 for (dupe_opts, dupe_col) in dupe_set: 579 578 self.query.update_dupe_avoidance(dupe_opts, dupe_col, alias) 580 if self.query.alias_map[root_alias][JOIN_TYPE] == self.query.LOUTER: 581 self.query.promote_alias_chain(alias_chain, True) 579 self.query.promote_joins(alias_chain, True) 582 580 else: 583 581 alias = root_alias 584 582 … … class SQLCompiler(object): 595 593 columns, aliases = self.get_default_columns(start_alias=alias, 596 594 opts=f.rel.to._meta, as_pairs=True) 597 595 self.query.related_select_cols.extend(columns) 598 if self.query.alias_map[alias][JOIN_TYPE] == self.query.LOUTER: 599 self.query.promote_alias_chain(aliases, True) 596 self.query.promote_joins(aliases, True) 600 597 self.query.related_select_fields.extend(f.rel.to._meta.fields) 601 598 if restricted: 602 599 next = requested.get(f.name, {}) … … class SQLCompiler(object): 669 666 self.query.related_select_fields.extend(model._meta.fields) 670 667 671 668 next = requested.get(f.related_query_name(), {}) 672 new_nullable = f.null or None 669 # Use True here because we are looking at the _reverse_ side of 670 # the relation, which is always nullable. 671 new_nullable = True 673 672 674 673 self.fill_related_selections(model._meta, table, cur_depth+1, 675 674 used, next, restricted, new_nullable) -
django/db/models/sql/query.py
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 110e317..2d68a9d 100644
a b class Query(object): 479 479 # Find aliases that are exclusive to rhs or lhs. 480 480 # These are promoted to outer joins. 481 481 outer_aliases = (l_tables | r_tables) - (l_tables & r_tables) 482 for alias in outer_aliases: 483 self.promote_alias(alias, True) 482 self.promote_joins(outer_aliases, True) 484 483 485 484 # Now relabel a copy of the rhs where-clause and add it to the current 486 485 # one. … … class Query(object): 661 660 """ Decreases the reference count for this alias. """ 662 661 self.alias_refcount[alias] -= 1 663 662 664 def promote_alias(self, alias, unconditional=False): 665 """ 666 Promotes the join type of an alias to an outer join if it's possible 667 for the join to contain NULL values on the left. If 'unconditional' is 668 False, the join is only promoted if it is nullable, otherwise it is 669 always promoted. 670 671 Returns True if the join was promoted. 672 """ 673 if ((unconditional or self.alias_map[alias][NULLABLE]) and 674 self.alias_map[alias][JOIN_TYPE] != self.LOUTER): 675 data = list(self.alias_map[alias]) 676 data[JOIN_TYPE] = self.LOUTER 677 self.alias_map[alias] = tuple(data) 678 return True 679 return False 680 681 def promote_alias_chain(self, chain, must_promote=False): 682 """ 683 Walks along a chain of aliases, promoting the first nullable join and 684 any joins following that. If 'must_promote' is True, all the aliases in 685 the chain are promoted. 686 """ 687 for alias in chain: 688 if self.promote_alias(alias, must_promote): 689 must_promote = True 663 def promote_joins(self, aliases, unconditional=False): 664 """ 665 Promotes recursively the join type of given aliases and its childs to an outer 666 join if it's possible for the join to contain NULL values on the left. 667 If 'unconditional' is False, the join is only promoted if it is nullable or the 668 parent join is an outer join. otherwise it is always promoted. 669 """ 670 for alias in aliases: 671 if alias not in ('auth_permission', 'django_content_type'): 672 pass 673 # import ipdb; ipdb.set_trace() 674 parent_alias = self.alias_map[alias][LHS_ALIAS] 675 parent_louter = parent_alias and self.alias_map[parent_alias][JOIN_TYPE] == self.LOUTER or False 676 if ((unconditional or self.alias_map[alias][NULLABLE] or parent_louter) and 677 self.alias_map[alias][JOIN_TYPE] != self.LOUTER): 678 data = list(self.alias_map[alias]) 679 data[JOIN_TYPE] = self.LOUTER 680 self.alias_map[alias] = tuple(data) 690 681 691 682 def promote_unused_aliases(self, initial_refcounts, used_aliases): 692 683 """ … … class Query(object): 696 687 then and which ones haven't been used and promotes all of those 697 688 aliases, plus any children of theirs in the alias tree, to outer joins. 698 689 """ 699 # FIXME: There's some (a lot of!) overlap with the similar OR promotion700 # in add_filter(). It's not quite identical, but is very similar. So701 # pulling out the common bits is something for later.702 considered = {}703 690 for alias in self.tables: 704 if alias not in used_aliases: 705 continue 706 if (alias not in initial_refcounts or 691 if alias in used_aliases and (alias not in initial_refcounts or 707 692 self.alias_refcount[alias] == initial_refcounts[alias]): 708 parent = self.alias_map[alias][LHS_ALIAS] 709 must_promote = considered.get(parent, False) 710 promoted = self.promote_alias(alias, must_promote) 711 considered[alias] = must_promote or promoted 693 self.promote_joins([alias]) 712 694 713 695 def change_aliases(self, change_map): 714 696 """ … … class Query(object): 875 857 continue 876 858 self.ref_alias(alias) 877 859 if promote: 878 self.promote_ alias(alias)860 self.promote_joins([alias]) 879 861 return alias 880 862 881 863 # No reuse is possible, so we need a new alias. … … class Query(object): 984 966 # If the aggregate references a model or field that requires a join, 985 967 # those joins must be LEFT OUTER - empty join rows must be returned 986 968 # in order for zeros to be returned for those aggregates. 987 for column_alias in join_list: 988 self.promote_alias(column_alias, unconditional=True) 969 self.promote_joins(join_list, unconditional=True) 989 970 990 971 col = (join_list[-1], col) 991 972 else: … … class Query(object): 1081 1062 # If the comparison is against NULL, we may need to use some left 1082 1063 # outer joins when creating the join chain. This is only done when 1083 1064 # needed, as it's less efficient at the database level. 1084 self.promote_ alias_chain(join_list)1065 self.promote_joins(join_list) 1085 1066 1086 1067 # Process the join list to see if we can remove any inner joins from 1087 1068 # the far end (fewer tables in a query is better). … … class Query(object): 1102 1083 table = table_it.next() 1103 1084 if join == table and self.alias_refcount[join] > 1: 1104 1085 continue 1105 join_promote = self.promote_ alias(join)1086 join_promote = self.promote_joins([join]) 1106 1087 if table != join: 1107 table_promote = self.promote_ alias(table)1088 table_promote = self.promote_joins([table]) 1108 1089 break 1109 self.promote_alias_chain(join_it, join_promote)1110 self.promote_alias_chain(table_it, table_promote)1111 1112 1090 1113 1091 if having_clause or force_having: 1114 1092 if (alias, col) not in self.group_by: … … class Query(object): 1120 1098 connector) 1121 1099 1122 1100 if negate: 1123 self.promote_ alias_chain(join_list)1101 self.promote_joins(join_list) 1124 1102 if lookup_type != 'isnull': 1125 1103 if len(join_list) > 1: 1126 1104 for alias in join_list: … … class Query(object): 1579 1557 final_alias = join[LHS_ALIAS] 1580 1558 col = join[LHS_JOIN_COL] 1581 1559 joins = joins[:-1] 1582 self.promote_ alias_chain(joins[1:])1560 self.promote_joins(joins[1:]) 1583 1561 self.select.append((final_alias, col)) 1584 1562 self.select_fields.append(field) 1585 1563 except MultiJoin: -
new file tests/regressiontests/nested_foreign_keys/models.py
diff --git a/tests/regressiontests/nested_foreign_keys/__init__.py b/tests/regressiontests/nested_foreign_keys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/regressiontests/nested_foreign_keys/models.py b/tests/regressiontests/nested_foreign_keys/models.py new file mode 100644 index 0000000..50d4479
- + 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) -
new file tests/regressiontests/nested_foreign_keys/tests.py
diff --git a/tests/regressiontests/nested_foreign_keys/tests.py b/tests/regressiontests/nested_foreign_keys/tests.py new file mode 100644 index 0000000..65e01f2
- + 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)