diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index 129a592b31..e14816c965 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -15,7 +15,8 @@ GET_ITERATOR_CHUNK_SIZE = 100 LOOKUP_SEP = '__' # Constants to make looking up tuple values clearer. -# Join lists +# Join lists (indexes into the tuples that are values in the alias_map +# dictionary in the Query class). TABLE_NAME = 0 RHS_ALIAS = 1 JOIN_TYPE = 2 diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index f15e4ac2f2..73584a65fa 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -716,7 +716,6 @@ class Query(object): alias = table_name self.table_map[alias] = [alias] self.alias_refcount[alias] = 1 - #self.alias_map[alias] = None self.tables.append(alias) return alias, True @@ -1188,6 +1187,8 @@ class Query(object): subtree = False connector = AND for child in q_object.children: + if connector == OR: + refcounts_before = self.alias_refcount.copy() if isinstance(child, Node): self.where.start_subtree(connector) self.add_q(child, used_aliases) @@ -1195,6 +1196,27 @@ class Query(object): else: self.add_filter(child, connector, q_object.negated, can_reuse=used_aliases) + if connector == OR: + # Aliases that were newly added or not used at all need to + # be promoted to outer joins if they are nullable relations. + # (they shouldn't turn the whole conditional into the empty + # set just because they don't match anything). + # FIXME: There's some (a lot of!) overlap with the similar + # OR promotion in add_filter(). It's not quite identical, + # but is very similar. So pulling out the common bits is + # something for later (code smell: too much indentation + # here) + considered = {} + for alias in self.tables: + if alias not in used_aliases: + continue + if (alias not in refcounts_before or + self.alias_refcount[alias] == + refcounts_before[alias]): + parent = self.alias_map[alias][LHS_ALIAS] + must_promote = considered.get(parent, False) + promoted = self.promote_alias(alias, must_promote) + considered[alias] = must_promote or promoted connector = q_object.connector if q_object.negated: self.where.negate() diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index 31c6b8d52b..aa1fa1edfc 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -223,10 +223,10 @@ class Join(models.Model): class ReservedName(models.Model): name = models.CharField(max_length=20) order = models.IntegerField() - + def __unicode__(self): return self.name - + __test__ = {'API_TESTS':""" >>> t1 = Tag.objects.create(name='t1') >>> t2 = Tag.objects.create(name='t2', parent=t1) @@ -238,6 +238,11 @@ __test__ = {'API_TESTS':""" >>> n2 = Note.objects.create(note='n2', misc='bar') >>> n3 = Note.objects.create(note='n3', misc='foo') +>>> ann1 = Annotation.objects.create(name='a1', tag=t1) +>>> ann1.notes.add(n1) +>>> ann2 = Annotation.objects.create(name='a2', tag=t4) +>>> ann2.notes.add(n2, n3) + Create these out of order so that sorting by 'id' will be different to sorting by 'info'. Helps detect some problems later. >>> e2 = ExtraInfo.objects.create(info='e2', note=n2) @@ -842,8 +847,6 @@ unpickling. True Bug #7277 ->>> ann1 = Annotation.objects.create(name='a1', tag=t1) ->>> ann1.notes.add(n1) >>> n1.annotation_set.filter(Q(tag=t5) | Q(tag__children=t5) | Q(tag__children__children=t5)) [] @@ -931,10 +934,25 @@ Bug #7302: reserved names are appropriately escaped >>> _ = ReservedName.objects.create(name='b',order=37) >>> ReservedName.objects.all().order_by('order') [, ] - >>> ReservedName.objects.extra(select={'stuff':'name'}, order_by=('order','stuff')) [, ] - + +Bug #8439 -- complex combinations of conjunctions, disjunctions and nullable +relations. +>>> Author.objects.filter(Q(item__note__extrainfo=e2)|Q(report=r1, name='xyz')) +[] +>>> Author.objects.filter(Q(report=r1, name='xyz')|Q(item__note__extrainfo=e2)) +[] +>>> Annotation.objects.filter(Q(tag__parent=t1)|Q(notes__note='n1', name='a1')) +[] +>>> xx = ExtraInfo.objects.create(info='xx', note=n3) +>>> Note.objects.filter(Q(extrainfo__author=a1)|Q(extrainfo=xx)) +[, ] +>>> xx.delete() +>>> q = Note.objects.filter(Q(extrainfo__author=a1)|Q(extrainfo=xx)).query +>>> len([x[2] for x in q.alias_map.values() if x[2] == q.LOUTER and q.alias_refcount[x[1]]]) +1 + """} # In Python 2.3 and the Python 2.6 beta releases, exceptions raised in __len__