summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2025-06-14 11:04:17 -0400
committerNatalia <124304+nessita@users.noreply.github.com>2025-06-30 20:16:08 -0300
commita150160c9fc6ba6220f1e63863b03e7f7978b747 (patch)
tree0aea5b13e19b4d06442376f94c406e00838bffda
parentdb5da3c91c3122300680c4e7200a463273a5351e (diff)
[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.
-rw-r--r--django/db/models/fields/tuple_lookups.py21
-rw-r--r--docs/releases/5.2.4.txt4
-rw-r--r--tests/composite_pk/test_filter.py16
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)