Fixed qs.order_by() join promotion for already existing joins
When order_by causes new joins to be added to the query, the joins must be LEFT OUTER joins for nullable relations, otherwise the order_by could cause the results to be altered. This commit fixes the logic to only promote new joins, previously all joins in the order_by lookup path were promoted. Thanks to Bruno Desthuilliers for spotting this corner case.
This commit is contained in:
parent
2daf1ae8b9
commit
8c72aa2379
|
@ -455,6 +455,9 @@ class SQLCompiler(object):
|
||||||
alias = self.query.get_initial_alias()
|
alias = self.query.get_initial_alias()
|
||||||
field, target, opts, joins, _, _ = self.query.setup_joins(pieces,
|
field, target, opts, joins, _, _ = self.query.setup_joins(pieces,
|
||||||
opts, alias, False)
|
opts, alias, False)
|
||||||
|
# We will later on need to promote those joins that were added to the
|
||||||
|
# query afresh above.
|
||||||
|
joins_to_promote = [j for j in joins if self.query.alias_refcount[j] < 2]
|
||||||
alias = joins[-1]
|
alias = joins[-1]
|
||||||
col = target.column
|
col = target.column
|
||||||
if not field.rel:
|
if not field.rel:
|
||||||
|
@ -466,8 +469,9 @@ class SQLCompiler(object):
|
||||||
# Must use left outer joins for nullable fields and their relations.
|
# Must use left outer joins for nullable fields and their relations.
|
||||||
# Ordering or distinct must not affect the returned set, and INNER
|
# Ordering or distinct must not affect the returned set, and INNER
|
||||||
# JOINS for nullable fields could do this.
|
# JOINS for nullable fields could do this.
|
||||||
self.query.promote_alias_chain(joins,
|
if joins_to_promote:
|
||||||
self.query.alias_map[joins[0]].join_type == self.query.LOUTER)
|
self.query.promote_alias_chain(joins_to_promote,
|
||||||
|
self.query.alias_map[joins_to_promote[0]].join_type == self.query.LOUTER)
|
||||||
return field, col, alias, joins, opts
|
return field, col, alias, joins, opts
|
||||||
|
|
||||||
def _final_join_removal(self, col, alias):
|
def _final_join_removal(self, col, alias):
|
||||||
|
|
|
@ -264,7 +264,7 @@ class SingleObject(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class RelatedObject(models.Model):
|
class RelatedObject(models.Model):
|
||||||
single = models.ForeignKey(SingleObject)
|
single = models.ForeignKey(SingleObject, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['single']
|
ordering = ['single']
|
||||||
|
|
|
@ -19,7 +19,8 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover,
|
||||||
ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA,
|
ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA,
|
||||||
Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten,
|
Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten,
|
||||||
Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory,
|
Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory,
|
||||||
SpecialCategory, OneToOneCategory, NullableName, ProxyCategory)
|
SpecialCategory, OneToOneCategory, NullableName, ProxyCategory,
|
||||||
|
SingleObject, RelatedObject)
|
||||||
|
|
||||||
|
|
||||||
class BaseQuerysetTest(TestCase):
|
class BaseQuerysetTest(TestCase):
|
||||||
|
@ -1321,12 +1322,33 @@ class NullableRelOrderingTests(TestCase):
|
||||||
def test_ticket10028(self):
|
def test_ticket10028(self):
|
||||||
# Ordering by model related to nullable relations(!) should use outer
|
# Ordering by model related to nullable relations(!) should use outer
|
||||||
# joins, so that all results are included.
|
# joins, so that all results are included.
|
||||||
_ = Plaything.objects.create(name="p1")
|
Plaything.objects.create(name="p1")
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
Plaything.objects.all(),
|
Plaything.objects.all(),
|
||||||
['<Plaything: p1>']
|
['<Plaything: p1>']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_join_already_in_query(self):
|
||||||
|
# Ordering by model related to nullable relations should not change
|
||||||
|
# the join type of already existing joins.
|
||||||
|
Plaything.objects.create(name="p1")
|
||||||
|
s = SingleObject.objects.create(name='s')
|
||||||
|
r = RelatedObject.objects.create(single=s)
|
||||||
|
Plaything.objects.create(name="p2", others=r)
|
||||||
|
qs = Plaything.objects.all().filter(others__isnull=False).order_by('pk')
|
||||||
|
self.assertTrue('INNER' in str(qs.query))
|
||||||
|
qs = qs.order_by('others__single__name')
|
||||||
|
# The ordering by others__single__pk will add one new join (to single)
|
||||||
|
# and that join must be LEFT join. The already existing join to related
|
||||||
|
# objects must be kept INNER. So, we have both a INNER and a LEFT join
|
||||||
|
# in the query.
|
||||||
|
self.assertTrue('LEFT' in str(qs.query))
|
||||||
|
self.assertTrue('INNER' in str(qs.query))
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
qs,
|
||||||
|
['<Plaything: p2>']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DisjunctiveFilterTests(TestCase):
|
class DisjunctiveFilterTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
Loading…
Reference in New Issue