1
0
mirror of https://github.com/django/django.git synced 2025-07-04 01:39:20 +00:00

[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.
This commit is contained in:
Simon Charette 2025-06-14 11:04:17 -04:00 committed by Natalia
parent db5da3c91c
commit a150160c9f
3 changed files with 38 additions and 3 deletions

View File

@ -1,9 +1,10 @@
import itertools import itertools
from django.core.exceptions import EmptyResultSet from django.core.exceptions import EmptyResultSet
from django.db.models import Field from django.db import models
from django.db.models.expressions import ( from django.db.models.expressions import (
ColPairs, ColPairs,
Exists,
Func, Func,
ResolvedOuterRef, ResolvedOuterRef,
Subquery, Subquery,
@ -25,7 +26,7 @@ from django.db.models.sql.where import AND, OR, WhereNode
class Tuple(Func): class Tuple(Func):
allows_composite_expressions = True allows_composite_expressions = True
function = "" function = ""
output_field = Field() output_field = models.Field()
def __len__(self): def __len__(self):
return len(self.source_expressions) return len(self.source_expressions)
@ -340,7 +341,21 @@ class TupleIn(TupleLookupMixin, In):
rhs = self.rhs rhs = self.rhs
if not rhs: if not rhs:
raise EmptyResultSet 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) return super(TupleLookupMixin, self).as_sql(compiler, connection)
# e.g.: (a, b, c) in [(x1, y1, z1), (x2, y2, z2)] as SQL: # e.g.: (a, b, c) in [(x1, y1, z1), (x2, y2, z2)] as SQL:

View File

@ -16,3 +16,7 @@ Bugfixes
* Fixed a regression in Django 5.2.3 where ``Value(None, JSONField())`` used in * Fixed a regression in Django 5.2.3 where ``Value(None, JSONField())`` used in
a :class:`~django.db.models.expressions.When` condition was incorrectly a :class:`~django.db.models.expressions.When` condition was incorrectly
serialized as SQL ``NULL`` instead of JSON ``null`` (:ticket:`36453`). 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`).

View File

@ -1,3 +1,6 @@
from unittest.mock import patch
from django.db import connection
from django.db.models import ( from django.db.models import (
Case, Case,
F, F,
@ -246,6 +249,10 @@ class CompositePKFilterTests(TestCase):
Comment.objects.filter(user=self.user_1).contains(self.comment_1), True 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): def test_filter_users_by_comments_in(self):
c1, c2, c3, c4, c5 = ( c1, c2, c3, c4, c5 = (
self.comment_1, self.comment_1,
@ -541,3 +548,12 @@ class CompositePKFilterTests(TestCase):
).filter(filtered_tokens=(1, 1)), ).filter(filtered_tokens=(1, 1)),
[self.tenant_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)