mirror of
https://github.com/django/django.git
synced 2025-08-03 18:38:50 +00:00
Improved Query join promotion logic
There were multiple cases where join promotion was a bit too aggressive. This resulted in using outer joins where not necessary. Refs #21150.
This commit is contained in:
parent
ed0d720b78
commit
ecaba36028
4 changed files with 136 additions and 52 deletions
|
@ -2689,6 +2689,15 @@ class NullJoinPromotionOrTest(TestCase):
|
|||
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
|
||||
self.assertEqual(list(qs), [self.a2])
|
||||
|
||||
def test_null_join_demotion(self):
|
||||
qs = ModelA.objects.filter(Q(b__name__isnull=False) & Q(b__name__isnull=True))
|
||||
self.assertTrue(' INNER JOIN ' in str(qs.query))
|
||||
qs = ModelA.objects.filter(Q(b__name__isnull=True) & Q(b__name__isnull=False))
|
||||
self.assertTrue(' INNER JOIN ' in str(qs.query))
|
||||
qs = ModelA.objects.filter(Q(b__name__isnull=False) | Q(b__name__isnull=True))
|
||||
self.assertTrue(' LEFT OUTER JOIN ' in str(qs.query))
|
||||
qs = ModelA.objects.filter(Q(b__name__isnull=True) | Q(b__name__isnull=False))
|
||||
self.assertTrue(' LEFT OUTER JOIN ' in str(qs.query))
|
||||
|
||||
class ReverseJoinTrimmingTest(TestCase):
|
||||
def test_reverse_trimming(self):
|
||||
|
@ -2785,22 +2794,19 @@ class DisjunctionPromotionTests(TestCase):
|
|||
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
|
||||
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_disjunction_promotion3_failing(self):
|
||||
# Now the ORed filter creates LOUTER join, but we do not have
|
||||
# logic to unpromote it for the AND filter after it. The query
|
||||
# results will be correct, but we have one LOUTER JOIN too much
|
||||
# currently.
|
||||
def test_disjunction_promotion3_demote(self):
|
||||
# This one needs demotion logic: the first filter causes a to be
|
||||
# outer joined, the second filter makes it inner join again.
|
||||
qs = BaseA.objects.filter(
|
||||
Q(a__f1='foo') | Q(b__f2='foo')).filter(a__f2='bar')
|
||||
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
|
||||
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_disjunction_promotion4_failing(self):
|
||||
# Failure because no join repromotion
|
||||
def test_disjunction_promotion4_demote(self):
|
||||
qs = BaseA.objects.filter(Q(a=1) | Q(a=2))
|
||||
self.assertEqual(str(qs.query).count('JOIN'), 0)
|
||||
# Demote needed for the "a" join. It is marked as outer join by
|
||||
# above filter (even if it is trimmed away).
|
||||
qs = qs.filter(a__f1='foo')
|
||||
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
|
||||
|
||||
|
@ -2810,9 +2816,8 @@ class DisjunctionPromotionTests(TestCase):
|
|||
qs = qs.filter(Q(a=1) | Q(a=2))
|
||||
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_disjunction_promotion5_failing(self):
|
||||
# Failure because no join repromotion logic.
|
||||
def test_disjunction_promotion5_demote(self):
|
||||
# Failure because no join demotion logic for this case.
|
||||
qs = BaseA.objects.filter(Q(a=1) | Q(a=2))
|
||||
# Note that the above filters on a force the join to an
|
||||
# inner join even if it is trimmed.
|
||||
|
@ -2823,8 +2828,8 @@ class DisjunctionPromotionTests(TestCase):
|
|||
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1)
|
||||
qs = BaseA.objects.filter(Q(a__f1='foo') | Q(b__f1='foo'))
|
||||
# Now the join to a is created as LOUTER
|
||||
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 0)
|
||||
qs = qs.objects.filter(Q(a=1) | Q(a=2))
|
||||
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 2)
|
||||
qs = qs.filter(Q(a=1) | Q(a=2))
|
||||
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
|
||||
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1)
|
||||
|
||||
|
@ -3079,3 +3084,17 @@ class Ticket21203Tests(TestCase):
|
|||
qs = Ticket21203Child.objects.select_related('parent').defer('parent__created')
|
||||
self.assertQuerysetEqual(qs, [c], lambda x: x)
|
||||
self.assertIs(qs[0].parent.parent_bool, True)
|
||||
|
||||
class ValuesJoinPromotionTests(TestCase):
|
||||
def test_values_no_promotion_for_existing(self):
|
||||
qs = Node.objects.filter(parent__parent__isnull=False)
|
||||
self.assertTrue(' INNER JOIN ' in str(qs.query))
|
||||
qs = qs.values('parent__parent__id')
|
||||
self.assertTrue(' INNER JOIN ' in str(qs.query))
|
||||
# Make sure there is a left outer join without the filter.
|
||||
qs = Node.objects.values('parent__parent__id')
|
||||
self.assertTrue(' LEFT OUTER JOIN ' in str(qs.query))
|
||||
|
||||
def test_non_nullable_fk_not_promoted(self):
|
||||
qs = ObjectB.objects.values('objecta__name')
|
||||
self.assertTrue(' INNER JOIN ' in str(qs.query))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue