From a150160c9fc6ba6220f1e63863b03e7f7978b747 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sat, 14 Jun 2025 11:04:17 -0400 Subject: [PATCH] [5.2.x] Fixed #36464 -- Fixed "__in" tuple lookup on backends lacking native support. When native support for tuple lookups is missing in a DB backend, it can be emulated with an EXISTS clause. This is controlled by the backend feature flag "supports_tuple_lookups". The mishandling of subquery right-hand side in `TupleIn` (added to support `CompositePrimaryKey` in Refs #373) was likely missed because the only core backend we test with the feature flag disabled (Oracle < 23.4) supports it natively. Thanks to Nandana Raol for the report, and to Sarah Boyce, Jacob Walls, and Natalia Bidart for reviews. Backport of 192bc7a7be92e20cc250907fb4083df689715679 from main. --- django/db/models/fields/tuple_lookups.py | 21 ++++++++++++++++++--- docs/releases/5.2.4.txt | 4 ++++ tests/composite_pk/test_filter.py | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/django/db/models/fields/tuple_lookups.py b/django/db/models/fields/tuple_lookups.py index 4523a19a98..2af2e193d5 100644 --- a/django/db/models/fields/tuple_lookups.py +++ b/django/db/models/fields/tuple_lookups.py @@ -1,9 +1,10 @@ import itertools from django.core.exceptions import EmptyResultSet -from django.db.models import Field +from django.db import models from django.db.models.expressions import ( ColPairs, + Exists, Func, ResolvedOuterRef, Subquery, @@ -25,7 +26,7 @@ from django.db.models.sql.where import AND, OR, WhereNode class Tuple(Func): allows_composite_expressions = True function = "" - output_field = Field() + output_field = models.Field() def __len__(self): return len(self.source_expressions) @@ -340,7 +341,21 @@ class TupleIn(TupleLookupMixin, In): rhs = self.rhs if not rhs: raise EmptyResultSet - if not self.rhs_is_direct_value(): + if isinstance(rhs, Query): + rhs_exprs = itertools.chain.from_iterable( + ( + select_expr + if isinstance((select_expr := select[0]), ColPairs) + else [select_expr] + ) + for select in rhs.get_compiler(connection=connection).get_select()[0] + ) + rhs = rhs.clone() + rhs.add_q( + models.Q(*[Exact(col, val) for col, val in zip(self.lhs, rhs_exprs)]) + ) + return compiler.compile(Exists(rhs)) + elif not self.rhs_is_direct_value(): return super(TupleLookupMixin, self).as_sql(compiler, connection) # e.g.: (a, b, c) in [(x1, y1, z1), (x2, y2, z2)] as SQL: diff --git a/docs/releases/5.2.4.txt b/docs/releases/5.2.4.txt index 521ed94d6c..31f4b8984f 100644 --- a/docs/releases/5.2.4.txt +++ b/docs/releases/5.2.4.txt @@ -16,3 +16,7 @@ Bugfixes * Fixed a regression in Django 5.2.3 where ``Value(None, JSONField())`` used in a :class:`~django.db.models.expressions.When` condition was incorrectly serialized as SQL ``NULL`` instead of JSON ``null`` (:ticket:`36453`). + +* Fixed a crash in Django 5.2 when performing an ``__in`` lookup involving a + composite primary key and a subquery on backends that lack native support for + tuple lookups (:ticket:`36464`). diff --git a/tests/composite_pk/test_filter.py b/tests/composite_pk/test_filter.py index c00b6660d8..03037d4d82 100644 --- a/tests/composite_pk/test_filter.py +++ b/tests/composite_pk/test_filter.py @@ -1,3 +1,6 @@ +from unittest.mock import patch + +from django.db import connection from django.db.models import ( Case, F, @@ -246,6 +249,10 @@ class CompositePKFilterTests(TestCase): Comment.objects.filter(user=self.user_1).contains(self.comment_1), True ) + def test_filter_query_does_not_mutate(self): + queryset = User.objects.filter(comments__in=Comment.objects.all()) + self.assertEqual(str(queryset.query), str(queryset.query)) + def test_filter_users_by_comments_in(self): c1, c2, c3, c4, c5 = ( self.comment_1, @@ -541,3 +548,12 @@ class CompositePKFilterTests(TestCase): ).filter(filtered_tokens=(1, 1)), [self.tenant_1], ) + + +@skipUnlessDBFeature("supports_tuple_lookups") +class CompositePKFilterTupleLookupFallbackTests(CompositePKFilterTests): + def setUp(self): + feature_patch = patch.object( + connection.features, "supports_tuple_lookups", False + ) + self.enterContext(feature_patch)