diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 6c728d17bf..be08c55c7c 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -631,6 +631,10 @@ class Query(BaseExpression): joinpromoter.add_votes(rhs_votes) joinpromoter.update_join_types(self) + # Combine subqueries aliases to ensure aliases relabelling properly + # handle subqueries when combining where and select clauses. + self.subq_aliases |= rhs.subq_aliases + # Now relabel a copy of the rhs where-clause and add it to the current # one. w = rhs.where.clone() diff --git a/docs/releases/3.2.1.txt b/docs/releases/3.2.1.txt index 4aece451e1..07d449ae8d 100644 --- a/docs/releases/3.2.1.txt +++ b/docs/releases/3.2.1.txt @@ -51,3 +51,8 @@ Bugfixes * Fixed a bug in Django 3.2 where a system check would crash on the :setting:`STATICFILES_DIRS` setting with a list of 2-tuples of ``(prefix, path)`` (:ticket:`32665`). + +* Fixed a long standing bug involving queryset bitwise combination when used + with subqueries that began manifesting in Django 3.2, due to a separate fix + using ``Exists`` to ``exclude()`` multi-valued relationships + (:ticket:`32650`). diff --git a/tests/queries/tests.py b/tests/queries/tests.py index b9b0a72686..a7176240a0 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -2063,36 +2063,50 @@ class SubqueryTests(TestCase): ) -@skipUnlessDBFeature('allow_sliced_subqueries_with_in') class QuerySetBitwiseOperationTests(TestCase): @classmethod def setUpTestData(cls): - school = School.objects.create() - cls.room_1 = Classroom.objects.create(school=school, has_blackboard=False, name='Room 1') - cls.room_2 = Classroom.objects.create(school=school, has_blackboard=True, name='Room 2') - cls.room_3 = Classroom.objects.create(school=school, has_blackboard=True, name='Room 3') - cls.room_4 = Classroom.objects.create(school=school, has_blackboard=False, name='Room 4') + cls.school = School.objects.create() + cls.room_1 = Classroom.objects.create(school=cls.school, has_blackboard=False, name='Room 1') + cls.room_2 = Classroom.objects.create(school=cls.school, has_blackboard=True, name='Room 2') + cls.room_3 = Classroom.objects.create(school=cls.school, has_blackboard=True, name='Room 3') + cls.room_4 = Classroom.objects.create(school=cls.school, has_blackboard=False, name='Room 4') + @skipUnlessDBFeature('allow_sliced_subqueries_with_in') def test_or_with_rhs_slice(self): qs1 = Classroom.objects.filter(has_blackboard=True) qs2 = Classroom.objects.filter(has_blackboard=False)[:1] self.assertCountEqual(qs1 | qs2, [self.room_1, self.room_2, self.room_3]) + @skipUnlessDBFeature('allow_sliced_subqueries_with_in') def test_or_with_lhs_slice(self): qs1 = Classroom.objects.filter(has_blackboard=True)[:1] qs2 = Classroom.objects.filter(has_blackboard=False) self.assertCountEqual(qs1 | qs2, [self.room_1, self.room_2, self.room_4]) + @skipUnlessDBFeature('allow_sliced_subqueries_with_in') def test_or_with_both_slice(self): qs1 = Classroom.objects.filter(has_blackboard=False)[:1] qs2 = Classroom.objects.filter(has_blackboard=True)[:1] self.assertCountEqual(qs1 | qs2, [self.room_1, self.room_2]) + @skipUnlessDBFeature('allow_sliced_subqueries_with_in') def test_or_with_both_slice_and_ordering(self): qs1 = Classroom.objects.filter(has_blackboard=False).order_by('-pk')[:1] qs2 = Classroom.objects.filter(has_blackboard=True).order_by('-name')[:1] self.assertCountEqual(qs1 | qs2, [self.room_3, self.room_4]) + def test_subquery_aliases(self): + combined = School.objects.filter(pk__isnull=False) & School.objects.filter( + Exists(Classroom.objects.filter( + has_blackboard=True, + school=OuterRef('pk'), + )), + ) + self.assertSequenceEqual(combined, [self.school]) + nested_combined = School.objects.filter(pk__in=combined.values('pk')) + self.assertSequenceEqual(nested_combined, [self.school]) + class CloneTests(TestCase): @@ -2790,6 +2804,21 @@ class ExcludeTests(TestCase): ) self.assertIn('exists', captured_queries[0]['sql'].lower()) + def test_exclude_subquery(self): + subquery = JobResponsibilities.objects.filter( + responsibility__description='bar', + ) | JobResponsibilities.objects.exclude( + job__responsibilities__description='foo', + ) + self.assertSequenceEqual( + Job.objects.annotate( + responsibility=subquery.filter( + job=OuterRef('name'), + ).values('id')[:1] + ), + [self.j1, self.j2], + ) + class ExcludeTest17600(TestCase): """