1
0
mirror of https://github.com/django/django.git synced 2025-10-24 06:06:09 +00:00

Fixed #27149 -- Added Subquery and Exists database expressions.

Thanks Josh Smeaton for Oracle fixes.
This commit is contained in:
Matthew Schinckel
2016-04-20 16:26:51 +09:30
committed by Tim Graham
parent 84c1826ded
commit 236ebe94bf
6 changed files with 479 additions and 11 deletions

View File

@@ -12,8 +12,8 @@ from django.db.models.aggregates import (
Avg, Count, Max, Min, StdDev, Sum, Variance,
)
from django.db.models.expressions import (
Case, Col, ExpressionWrapper, F, Func, OrderBy, Random, RawSQL, Ref, Value,
When,
Case, Col, Exists, ExpressionWrapper, F, Func, OrderBy, OuterRef, Random,
RawSQL, Ref, Subquery, Value, When,
)
from django.db.models.functions import (
Coalesce, Concat, Length, Lower, Substr, Upper,
@@ -32,15 +32,15 @@ from .models import (
class BasicExpressionsTests(TestCase):
@classmethod
def setUpTestData(cls):
Company.objects.create(
cls.example_inc = Company.objects.create(
name="Example Inc.", num_employees=2300, num_chairs=5,
ceo=Employee.objects.create(firstname="Joe", lastname="Smith", salary=10)
)
Company.objects.create(
cls.foobar_ltd = Company.objects.create(
name="Foobar Ltd.", num_employees=3, num_chairs=4,
ceo=Employee.objects.create(firstname="Frank", lastname="Meyer", salary=20)
)
Company.objects.create(
cls.gmbh = Company.objects.create(
name="Test GmbH", num_employees=32, num_chairs=1,
ceo=Employee.objects.create(firstname="Max", lastname="Mustermann", salary=30)
)
@@ -387,6 +387,136 @@ class BasicExpressionsTests(TestCase):
)
self.assertEqual(str(qs.query).count('JOIN'), 2)
def test_outerref(self):
inner = Company.objects.filter(point_of_contact=OuterRef('pk'))
msg = (
'This queryset contains a reference to an outer query and may only '
'be used in a subquery.'
)
with self.assertRaisesMessage(ValueError, msg):
inner.exists()
outer = Employee.objects.annotate(is_point_of_contact=Exists(inner))
self.assertIs(outer.exists(), True)
def test_subquery(self):
Company.objects.filter(name='Example Inc.').update(
point_of_contact=Employee.objects.get(firstname='Joe', lastname='Smith'),
ceo=Employee.objects.get(firstname='Max', lastname='Mustermann'),
)
Employee.objects.create(firstname='Bob', lastname='Brown', salary=40)
qs = Employee.objects.annotate(
is_point_of_contact=Exists(Company.objects.filter(point_of_contact=OuterRef('pk'))),
is_not_point_of_contact=~Exists(Company.objects.filter(point_of_contact=OuterRef('pk'))),
is_ceo_of_small_company=Exists(Company.objects.filter(num_employees__lt=200, ceo=OuterRef('pk'))),
is_ceo_small_2=~~Exists(Company.objects.filter(num_employees__lt=200, ceo=OuterRef('pk'))),
largest_company=Subquery(Company.objects.order_by('-num_employees').filter(
models.Q(ceo=OuterRef('pk')) | models.Q(point_of_contact=OuterRef('pk'))
).values('name')[:1], output_field=models.CharField())
).values(
'firstname',
'is_point_of_contact',
'is_not_point_of_contact',
'is_ceo_of_small_company',
'is_ceo_small_2',
'largest_company',
).order_by('firstname')
results = list(qs)
# Could use Coalesce(subq, Value('')) instead except for the bug in
# cx_Oracle mentioned in #23843.
bob = results[0]
if bob['largest_company'] == '' and connection.features.interprets_empty_strings_as_nulls:
bob['largest_company'] = None
self.assertEqual(results, [
{
'firstname': 'Bob',
'is_point_of_contact': False,
'is_not_point_of_contact': True,
'is_ceo_of_small_company': False,
'is_ceo_small_2': False,
'largest_company': None,
},
{
'firstname': 'Frank',
'is_point_of_contact': False,
'is_not_point_of_contact': True,
'is_ceo_of_small_company': True,
'is_ceo_small_2': True,
'largest_company': 'Foobar Ltd.',
},
{
'firstname': 'Joe',
'is_point_of_contact': True,
'is_not_point_of_contact': False,
'is_ceo_of_small_company': False,
'is_ceo_small_2': False,
'largest_company': 'Example Inc.',
},
{
'firstname': 'Max',
'is_point_of_contact': False,
'is_not_point_of_contact': True,
'is_ceo_of_small_company': True,
'is_ceo_small_2': True,
'largest_company': 'Example Inc.'
}
])
# A less elegant way to write the same query: this uses a LEFT OUTER
# JOIN and an IS NULL, inside a WHERE NOT IN which is probably less
# efficient than EXISTS.
self.assertCountEqual(
qs.filter(is_point_of_contact=True).values('pk'),
Employee.objects.exclude(company_point_of_contact_set=None).values('pk')
)
def test_in_subquery(self):
# This is a contrived test (and you really wouldn't write this query),
# but it is a succinct way to test the __in=Subquery() construct.
small_companies = Company.objects.filter(num_employees__lt=200).values('pk')
subquery_test = Company.objects.filter(pk__in=Subquery(small_companies))
self.assertCountEqual(subquery_test, [self.foobar_ltd, self.gmbh])
subquery_test2 = Company.objects.filter(pk=Subquery(small_companies.filter(num_employees=3)))
self.assertCountEqual(subquery_test2, [self.foobar_ltd])
def test_nested_subquery(self):
inner = Company.objects.filter(point_of_contact=OuterRef('pk'))
outer = Employee.objects.annotate(is_point_of_contact=Exists(inner))
contrived = Employee.objects.annotate(
is_point_of_contact=Subquery(
outer.filter(pk=OuterRef('pk')).values('is_point_of_contact'),
output_field=models.BooleanField(),
),
)
self.assertCountEqual(contrived.values_list(), outer.values_list())
def test_nested_subquery_outer_ref_2(self):
first = Time.objects.create(time='09:00')
second = Time.objects.create(time='17:00')
third = Time.objects.create(time='21:00')
SimulationRun.objects.bulk_create([
SimulationRun(start=first, end=second, midpoint='12:00'),
SimulationRun(start=first, end=third, midpoint='15:00'),
SimulationRun(start=second, end=first, midpoint='00:00'),
])
inner = Time.objects.filter(time=OuterRef(OuterRef('time')), pk=OuterRef('start')).values('time')
middle = SimulationRun.objects.annotate(other=Subquery(inner)).values('other')[:1]
outer = Time.objects.annotate(other=Subquery(middle, output_field=models.TimeField()))
# This is a contrived example. It exercises the double OuterRef form.
self.assertCountEqual(outer, [first, second, third])
def test_annotations_within_subquery(self):
Company.objects.filter(num_employees__lt=50).update(ceo=Employee.objects.get(firstname='Frank'))
inner = Company.objects.filter(
ceo=OuterRef('pk')
).values('ceo').annotate(total_employees=models.Sum('num_employees')).values('total_employees')
outer = Employee.objects.annotate(total_employees=Subquery(inner)).filter(salary__lte=Subquery(inner))
self.assertSequenceEqual(
outer.order_by('-total_employees').values('salary', 'total_employees'),
[{'salary': 10, 'total_employees': 2300}, {'salary': 20, 'total_employees': 35}],
)
class IterableLookupInnerExpressionsTests(TestCase):
@classmethod