Fixed #28781 -- Added QuerySet.values()/values_list() support for union(), difference(), and intersection().

Thanks Tim Graham for the review.
This commit is contained in:
Mariusz Felisiak 2017-11-12 14:28:11 +01:00 committed by GitHub
parent 8f8a4d10d3
commit 2d3cc94284
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 3 deletions

View File

@ -407,6 +407,11 @@ class SQLCompiler:
parts = () parts = ()
for compiler in compilers: for compiler in compilers:
try: try:
# If the columns list is limited, then all combined queries
# must have the same columns list. Set the selects defined on
# the query on all combined queries, if not already set.
if not compiler.query.values_select and self.query.values_select:
compiler.query.set_values(self.query.values_select)
parts += (compiler.as_sql(),) parts += (compiler.as_sql(),)
except EmptyResultSet: except EmptyResultSet:
# Omit the empty queryset with UNION and with DIFFERENCE if the # Omit the empty queryset with UNION and with DIFFERENCE if the

View File

@ -813,10 +813,17 @@ duplicate values, use the ``all=True`` argument.
of the type of the first ``QuerySet`` even if the arguments are ``QuerySet``\s of the type of the first ``QuerySet`` even if the arguments are ``QuerySet``\s
of other models. Passing different models works as long as the ``SELECT`` list of other models. Passing different models works as long as the ``SELECT`` list
is the same in all ``QuerySet``\s (at least the types, the names don't matter is the same in all ``QuerySet``\s (at least the types, the names don't matter
as long as the types in the same order). as long as the types in the same order). In such cases, you must use the column
names from the first ``QuerySet`` in ``QuerySet`` methods applied to the
resulting ``QuerySet``. For example::
In addition, only ``LIMIT``, ``OFFSET``, ``COUNT(*)``, and ``ORDER BY`` (i.e. >>> qs1 = Author.objects.values_list('name')
slicing, :meth:`count`, and :meth:`order_by`) are allowed on the resulting >>> qs2 = Entry.objects.values_list('headline')
>>> qs1.union(qs2).order_by('name')
In addition, only ``LIMIT``, ``OFFSET``, ``COUNT(*)``, ``ORDER BY``, and
specifying columns (i.e. slicing, :meth:`count`, :meth:`order_by`, and
:meth:`values()`/:meth:`values_list()`) are allowed on the resulting
``QuerySet``. Further, databases place restrictions on what operations are ``QuerySet``. Further, databases place restrictions on what operations are
allowed in the combined queries. For example, most databases don't allow allowed in the combined queries. For example, most databases don't allow
``LIMIT`` or ``OFFSET`` in the combined queries. ``LIMIT`` or ``OFFSET`` in the combined queries.

View File

@ -11,3 +11,7 @@ Bugfixes
* Reallowed, following a regression in Django 1.10, ``AuthenticationForm`` to * Reallowed, following a regression in Django 1.10, ``AuthenticationForm`` to
raise the inactive user error when using ``ModelBackend`` (:ticket:`28645`). raise the inactive user error when using ``ModelBackend`` (:ticket:`28645`).
* Added support for ``QuerySet.values()`` and ``values_list()`` for
``union()``, ``difference()``, and ``intersection()`` queries
(:ticket:`28781`).

View File

@ -30,6 +30,16 @@ class QuerySetSetOperationTests(TestCase):
qs3 = Number.objects.filter(num__gte=4, num__lte=6) qs3 = Number.objects.filter(num__gte=4, num__lte=6)
self.assertNumbersEqual(qs1.intersection(qs2, qs3), [5], ordered=False) self.assertNumbersEqual(qs1.intersection(qs2, qs3), [5], ordered=False)
@skipUnlessDBFeature('supports_select_intersection')
def test_intersection_with_values(self):
ReservedName.objects.create(name='a', order=2)
qs1 = ReservedName.objects.all()
reserved_name = qs1.intersection(qs1).values('name', 'order', 'id').get()
self.assertEqual(reserved_name['name'], 'a')
self.assertEqual(reserved_name['order'], 2)
reserved_name = qs1.intersection(qs1).values_list('name', 'order', 'id').get()
self.assertEqual(reserved_name[:2], ('a', 2))
@skipUnlessDBFeature('supports_select_difference') @skipUnlessDBFeature('supports_select_difference')
def test_simple_difference(self): def test_simple_difference(self):
qs1 = Number.objects.filter(num__lte=5) qs1 = Number.objects.filter(num__lte=5)
@ -66,6 +76,17 @@ class QuerySetSetOperationTests(TestCase):
self.assertEqual(len(qs2.difference(qs2)), 0) self.assertEqual(len(qs2.difference(qs2)), 0)
self.assertEqual(len(qs3.difference(qs3)), 0) self.assertEqual(len(qs3.difference(qs3)), 0)
@skipUnlessDBFeature('supports_select_difference')
def test_difference_with_values(self):
ReservedName.objects.create(name='a', order=2)
qs1 = ReservedName.objects.all()
qs2 = ReservedName.objects.none()
reserved_name = qs1.difference(qs2).values('name', 'order', 'id').get()
self.assertEqual(reserved_name['name'], 'a')
self.assertEqual(reserved_name['order'], 2)
reserved_name = qs1.difference(qs2).values_list('name', 'order', 'id').get()
self.assertEqual(reserved_name[:2], ('a', 2))
def test_union_with_empty_qs(self): def test_union_with_empty_qs(self):
qs1 = Number.objects.all() qs1 = Number.objects.all()
qs2 = Number.objects.none() qs2 = Number.objects.none()
@ -89,6 +110,15 @@ class QuerySetSetOperationTests(TestCase):
qs2 = Number.objects.filter(num__gte=2, num__lte=3) qs2 = Number.objects.filter(num__gte=2, num__lte=3)
self.assertNumbersEqual(qs1.union(qs2).order_by('-num'), [3, 2, 1, 0]) self.assertNumbersEqual(qs1.union(qs2).order_by('-num'), [3, 2, 1, 0])
def test_union_with_values(self):
ReservedName.objects.create(name='a', order=2)
qs1 = ReservedName.objects.all()
reserved_name = qs1.union(qs1).values('name', 'order', 'id').get()
self.assertEqual(reserved_name['name'], 'a')
self.assertEqual(reserved_name['order'], 2)
reserved_name = qs1.union(qs1).values_list('name', 'order', 'id').get()
self.assertEqual(reserved_name[:2], ('a', 2))
def test_count_union(self): def test_count_union(self):
qs1 = Number.objects.filter(num__lte=1).values('num') qs1 = Number.objects.filter(num__lte=1).values('num')
qs2 = Number.objects.filter(num__gte=2, num__lte=3).values('num') qs2 = Number.objects.filter(num__gte=2, num__lte=3).values('num')