Skip to content

Commit 192bc7a

Browse files
charettesnessita
authored andcommitted
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.
1 parent ff0ff98 commit 192bc7a

File tree

3 files changed

+38
-3
lines changed

3 files changed

+38
-3
lines changed

django/db/models/fields/tuple_lookups.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import itertools
22

33
from django.core.exceptions import EmptyResultSet
4-
from django.db.models import Field
4+
from django.db import models
55
from django.db.models.expressions import (
66
ColPairs,
7+
Exists,
78
Func,
89
ResolvedOuterRef,
910
Subquery,
@@ -25,7 +26,7 @@
2526
class Tuple(Func):
2627
allows_composite_expressions = True
2728
function = ""
28-
output_field = Field()
29+
output_field = models.Field()
2930

3031
def __len__(self):
3132
return len(self.source_expressions)
@@ -351,7 +352,21 @@ def get_fallback_sql(self, compiler, connection):
351352
rhs = self.rhs
352353
if not rhs:
353354
raise EmptyResultSet
354-
if not self.rhs_is_direct_value():
355+
if isinstance(rhs, Query):
356+
rhs_exprs = itertools.chain.from_iterable(
357+
(
358+
select_expr
359+
if isinstance((select_expr := select[0]), ColPairs)
360+
else [select_expr]
361+
)
362+
for select in rhs.get_compiler(connection=connection).get_select()[0]
363+
)
364+
rhs = rhs.clone()
365+
rhs.add_q(
366+
models.Q(*[Exact(col, val) for col, val in zip(self.lhs, rhs_exprs)])
367+
)
368+
return compiler.compile(Exists(rhs))
369+
elif not self.rhs_is_direct_value():
355370
return super(TupleLookupMixin, self).as_sql(compiler, connection)
356371

357372
# e.g.: (a, b, c) in [(x1, y1, z1), (x2, y2, z2)] as SQL:

docs/releases/5.2.4.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ Bugfixes
1616
* Fixed a regression in Django 5.2.3 where ``Value(None, JSONField())`` used in
1717
a :class:`~django.db.models.expressions.When` condition was incorrectly
1818
serialized as SQL ``NULL`` instead of JSON ``null`` (:ticket:`36453`).
19+
20+
* Fixed a crash in Django 5.2 when performing an ``__in`` lookup involving a
21+
composite primary key and a subquery on backends that lack native support for
22+
tuple lookups (:ticket:`36464`).

tests/composite_pk/test_filter.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from unittest.mock import patch
2+
3+
from django.db import connection
14
from django.db.models import (
25
Case,
36
F,
@@ -246,6 +249,10 @@ def test_filter_comments_by_user_and_contains(self):
246249
Comment.objects.filter(user=self.user_1).contains(self.comment_1), True
247250
)
248251

252+
def test_filter_query_does_not_mutate(self):
253+
queryset = User.objects.filter(comments__in=Comment.objects.all())
254+
self.assertEqual(str(queryset.query), str(queryset.query))
255+
249256
def test_filter_users_by_comments_in(self):
250257
c1, c2, c3, c4, c5 = (
251258
self.comment_1,
@@ -541,3 +548,12 @@ def test_outer_ref_in_filtered_relation(self):
541548
).filter(filtered_tokens=(1, 1)),
542549
[self.tenant_1],
543550
)
551+
552+
553+
@skipUnlessDBFeature("supports_tuple_lookups")
554+
class CompositePKFilterTupleLookupFallbackTests(CompositePKFilterTests):
555+
def setUp(self):
556+
feature_patch = patch.object(
557+
connection.features, "supports_tuple_lookups", False
558+
)
559+
self.enterContext(feature_patch)

0 commit comments

Comments
 (0)