mirror of
https://github.com/django/django.git
synced 2025-07-03 17:29:12 +00:00
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.
This commit is contained in:
parent
ff0ff98d42
commit
192bc7a7be
@ -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)
|
||||||
@ -351,7 +352,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:
|
||||||
|
@ -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`).
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user