From 2d3cc94284674638c334670903d49565039d77ae Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Sun, 12 Nov 2017 14:28:11 +0100 Subject: [PATCH] Fixed #28781 -- Added QuerySet.values()/values_list() support for union(), difference(), and intersection(). Thanks Tim Graham for the review. --- django/db/models/sql/compiler.py | 5 +++++ docs/ref/models/querysets.txt | 13 +++++++++--- docs/releases/1.11.8.txt | 4 ++++ tests/queries/test_qs_combinators.py | 30 ++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index a3a37b52be..59f5ac6491 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -407,6 +407,11 @@ class SQLCompiler: parts = () for compiler in compilers: 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(),) except EmptyResultSet: # Omit the empty queryset with UNION and with DIFFERENCE if the diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 4903d61132..2003e1bf17 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -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 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 -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. -slicing, :meth:`count`, and :meth:`order_by`) are allowed on the resulting + >>> qs1 = Author.objects.values_list('name') + >>> 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 allowed in the combined queries. For example, most databases don't allow ``LIMIT`` or ``OFFSET`` in the combined queries. diff --git a/docs/releases/1.11.8.txt b/docs/releases/1.11.8.txt index dd9fb0c3ce..7e4963f713 100644 --- a/docs/releases/1.11.8.txt +++ b/docs/releases/1.11.8.txt @@ -11,3 +11,7 @@ Bugfixes * Reallowed, following a regression in Django 1.10, ``AuthenticationForm`` to 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`). diff --git a/tests/queries/test_qs_combinators.py b/tests/queries/test_qs_combinators.py index 84fb0fb81c..8026bf227d 100644 --- a/tests/queries/test_qs_combinators.py +++ b/tests/queries/test_qs_combinators.py @@ -30,6 +30,16 @@ class QuerySetSetOperationTests(TestCase): qs3 = Number.objects.filter(num__gte=4, num__lte=6) 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') def test_simple_difference(self): qs1 = Number.objects.filter(num__lte=5) @@ -66,6 +76,17 @@ class QuerySetSetOperationTests(TestCase): self.assertEqual(len(qs2.difference(qs2)), 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): qs1 = Number.objects.all() qs2 = Number.objects.none() @@ -89,6 +110,15 @@ class QuerySetSetOperationTests(TestCase): qs2 = Number.objects.filter(num__gte=2, num__lte=3) 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): qs1 = Number.objects.filter(num__lte=1).values('num') qs2 = Number.objects.filter(num__gte=2, num__lte=3).values('num')