From 5362e08624c6745181944a10979da876934ea136 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 10 Sep 2020 14:34:04 +0200 Subject: [PATCH] Fixed #31943 -- Fixed recreating QuerySet.values()/values_list() when using a pickled Query. --- django/db/models/query.py | 2 ++ docs/ref/models/querysets.txt | 14 ++++++++++++++ tests/queryset_pickle/tests.py | 24 +++++++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index b48d0df9c00..85cd8311a7d 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -210,6 +210,8 @@ class QuerySet: @query.setter def query(self, value): + if value.values_select: + self._iterable_class = ValuesIterable self._query = value def as_manager(cls): diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 228e2cf7362..7f55684e082 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -106,6 +106,20 @@ the query construction and is not part of the public API. However, it is safe (and fully supported) to pickle and unpickle the attribute's contents as described here. +.. admonition:: Restrictions on ``QuerySet.values_list()`` + + If you recreate :meth:`QuerySet.values_list` using the pickled ``query`` + attribute, it will be converted to :meth:`QuerySet.values`:: + + >>> import pickle + >>> qs = Blog.objects.values_list('id', 'name') + >>> qs + + >>> reloaded_qs = Blog.objects.all() + >>> reloaded_qs.query = pickle.loads(pickle.dumps(qs.query)) + >>> reloaded_qs + + .. admonition:: You can't share pickles between versions Pickles of ``QuerySets`` are only valid for the version of Django that diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py index a58e45caed4..8575c6fe811 100644 --- a/tests/queryset_pickle/tests.py +++ b/tests/queryset_pickle/tests.py @@ -11,7 +11,7 @@ from .models import Container, Event, Group, Happening, M2MModel, MyEvent class PickleabilityTestCase(TestCase): @classmethod def setUpTestData(cls): - Happening.objects.create() # make sure the defaults are working (#20158) + cls.happening = Happening.objects.create() # make sure the defaults are working (#20158) def assert_pickles(self, qs): self.assertEqual(list(pickle.loads(pickle.dumps(qs))), list(qs)) @@ -224,6 +224,28 @@ class PickleabilityTestCase(TestCase): qs = Happening.objects.annotate(latest_time=models.Max('when')) self.assert_pickles(qs) + def test_annotation_values(self): + qs = Happening.objects.values('name').annotate(latest_time=models.Max('when')) + reloaded = Happening.objects.all() + reloaded.query = pickle.loads(pickle.dumps(qs.query)) + self.assertEqual( + reloaded.get(), + {'name': 'test', 'latest_time': self.happening.when}, + ) + + def test_annotation_values_list(self): + # values_list() is reloaded to values() when using a pickled query. + tests = [ + Happening.objects.values_list('name'), + Happening.objects.values_list('name', flat=True), + Happening.objects.values_list('name', named=True), + ] + for qs in tests: + with self.subTest(qs._iterable_class.__name__): + reloaded = Happening.objects.all() + reloaded.query = pickle.loads(pickle.dumps(qs.query)) + self.assertEqual(reloaded.get(), {'name': 'test'}) + def test_filter_deferred(self): qs = Happening.objects.all() qs._defer_next_filter = True