summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorAnssi Kääriäinen <akaariai@gmail.com>2013-09-24 19:37:55 +0300
committerAnssi Kääriäinen <akaariai@gmail.com>2013-10-05 14:52:17 +0300
commitecaba3602837d1e02fe1e961f7d3bf9086453259 (patch)
tree3915e8d7b57c55d69b18b89c5d40f26382f9d769 /tests
parented0d720b78c8f1c655ead0057d767a0712f1a6a8 (diff)
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.
Diffstat (limited to 'tests')
-rw-r--r--tests/aggregation_regress/models.py2
-rw-r--r--tests/aggregation_regress/tests.py23
-rw-r--r--tests/queries/tests.py47
3 files changed, 54 insertions, 18 deletions
diff --git a/tests/aggregation_regress/models.py b/tests/aggregation_regress/models.py
index 24859f4ce4..a2dc060640 100644
--- a/tests/aggregation_regress/models.py
+++ b/tests/aggregation_regress/models.py
@@ -90,7 +90,7 @@ class HardbackBook(Book):
# Models for ticket #21150
class Alfa(models.Model):
- pass
+ name = models.CharField(max_length=10, null=True)
class Bravo(models.Model):
pass
diff --git a/tests/aggregation_regress/tests.py b/tests/aggregation_regress/tests.py
index 82be5afab2..9fcaefe6f5 100644
--- a/tests/aggregation_regress/tests.py
+++ b/tests/aggregation_regress/tests.py
@@ -12,9 +12,8 @@ from django.test import TestCase, skipUnlessDBFeature
from django.test.utils import Approximate
from django.utils import six
-from .models import (
- Author, Book, Publisher, Clues, Entries, HardbackBook, ItemTag,
- WithManualPK, Alfa, Bravo, Charlie)
+from .models import (Author, Book, Publisher, Clues, Entries, HardbackBook,
+ ItemTag, WithManualPK, Alfa, Bravo, Charlie)
class AggregationTests(TestCase):
@@ -1137,6 +1136,8 @@ class AggregationTests(TestCase):
'select__avg': Approximate(1.666, places=2),
})
+
+class JoinPromotionTests(TestCase):
def test_ticket_21150(self):
b = Bravo.objects.create()
c = Charlie.objects.create(bravo=b)
@@ -1152,3 +1153,19 @@ class AggregationTests(TestCase):
self.assertQuerysetEqual(
qs, [c], lambda x: x)
self.assertEqual(qs[0].alfa, a)
+
+ def test_existing_join_not_promoted(self):
+ # No promotion for existing joins
+ qs = Charlie.objects.filter(alfa__name__isnull=False).annotate(Count('alfa__name'))
+ self.assertTrue(' INNER JOIN ' in str(qs.query))
+ # Also, the existing join is unpromoted when doing filtering for already
+ # promoted join.
+ qs = Charlie.objects.annotate(Count('alfa__name')).filter(alfa__name__isnull=False)
+ self.assertTrue(' INNER JOIN ' in str(qs.query))
+ # But, as the join is nullable first use by annotate will be LOUTER
+ qs = Charlie.objects.annotate(Count('alfa__name'))
+ self.assertTrue(' LEFT OUTER JOIN ' in str(qs.query))
+
+ def test_non_nullable_fk_not_promoted(self):
+ qs = Book.objects.annotate(Count('contact__name'))
+ self.assertTrue(' INNER JOIN ' in str(qs.query))
diff --git a/tests/queries/tests.py b/tests/queries/tests.py
index 7bb7a150a2..a7b0af55a3 100644
--- a/tests/queries/tests.py
+++ b/tests/queries/tests.py
@@ -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))