1
0
mirror of https://github.com/django/django.git synced 2025-03-06 07:22:32 +00:00

[5.2.x] Fixed #36025 -- Fixed re-aliasing of iterable (in/range) lookups rhs.

In order for Expression.relabeled_clone to work appropriately its
get_source_expressions method must return all resolvable which wasn't the case
for Lookup when its right-hand-side is "direct" (not a compilable).

While refs #22288 added support for non-literals iterable right-hand-side
lookups it predated the subclassing of Lookup(Expression) refs #27021 which
could have been an opportunity to ensure right-hand-sides are always resolvable
(ValueList and ExpressionList).

Addressing all edge case with non-resolvable right-hand-sides would require
a significant refactor and deprecation of some parts of the Lookup interface so
this patch only focuses on FieldGetDbPrepValueIterableMixin (In and Range
lookups) by making sure that a right-hand-side containing resolvables are dealt
with appropriately during the resolving phase.

Thanks Aashay Amballi for the report.

Backport of 089deb82b9ac2d002af36fd36f288368cdac4b53 from main.
This commit is contained in:
Simon Charette 2024-12-23 05:00:04 +01:00 committed by Sarah Boyce
parent b96e4c04b6
commit d99985bbc1
3 changed files with 49 additions and 3 deletions

View File

@ -4,7 +4,15 @@ import warnings
from django.core.exceptions import EmptyResultSet, FullResultSet
from django.db.backends.base.operations import BaseDatabaseOperations
from django.db.models.expressions import Case, ColPairs, Expression, Func, Value, When
from django.db.models.expressions import (
Case,
ColPairs,
Expression,
ExpressionList,
Func,
Value,
When,
)
from django.db.models.fields import (
BooleanField,
CharField,
@ -298,12 +306,13 @@ class FieldGetDbPrepValueIterableMixin(FieldGetDbPrepValueMixin):
def get_prep_lookup(self):
if hasattr(self.rhs, "resolve_expression"):
return self.rhs
contains_expr = False
prepared_values = []
for rhs_value in self.rhs:
if hasattr(rhs_value, "resolve_expression"):
# An expression will be handled by the database but can coexist
# alongside real values.
pass
contains_expr = True
elif (
self.prepare_rhs
and hasattr(self.lhs, "output_field")
@ -311,6 +320,19 @@ class FieldGetDbPrepValueIterableMixin(FieldGetDbPrepValueMixin):
):
rhs_value = self.lhs.output_field.get_prep_value(rhs_value)
prepared_values.append(rhs_value)
if contains_expr:
return ExpressionList(
*[
# Expression defaults `str` to field references while
# lookups default them to literal values.
(
Value(prep_value, self.lhs.output_field)
if isinstance(prep_value, str)
else prep_value
)
for prep_value in prepared_values
]
)
return prepared_values
def process_rhs(self, compiler, connection):
@ -318,6 +340,12 @@ class FieldGetDbPrepValueIterableMixin(FieldGetDbPrepValueMixin):
# rhs should be an iterable of values. Use batch_process_rhs()
# to prepare/transform those values.
return self.batch_process_rhs(compiler, connection)
elif isinstance(self.rhs, ExpressionList):
# rhs contains at least one expression. Unwrap them and delegate
# to batch_process_rhs() to prepare/transform those values.
copy = self.copy()
copy.rhs = self.rhs.get_source_expressions()
return copy.process_rhs(compiler, connection)
else:
return super().process_rhs(compiler, connection)

View File

@ -1276,6 +1276,23 @@ class IterableLookupInnerExpressionsTests(TestCase):
queryset = Result.objects.filter(**{lookup: within_experiment_time})
self.assertSequenceEqual(queryset, [r1])
def test_relabeled_clone_rhs(self):
Number.objects.bulk_create([Number(integer=1), Number(integer=2)])
self.assertIs(
Number.objects.filter(
# Ensure iterable of expressions are properly re-labelled on
# subquery pushdown. If the inner query __range right-hand-side
# members are not relabelled they will point at the outer query
# alias and this test will fail.
Exists(
Number.objects.exclude(pk=OuterRef("pk")).filter(
integer__range=(F("integer"), F("integer"))
)
)
).exists(),
True,
)
class FTests(SimpleTestCase):
def test_deepcopy(self):

View File

@ -13,6 +13,7 @@ from django.db import (
OperationalError,
connection,
models,
transaction,
)
from django.db.models import (
Count,
@ -974,7 +975,7 @@ class TestQuerying(TestCase):
("value__i__in", [False, "foo"], [self.objs[4]]),
]
for lookup, value, expected in tests:
with self.subTest(lookup=lookup, value=value):
with self.subTest(lookup=lookup, value=value), transaction.atomic():
self.assertCountEqual(
NullableJSONModel.objects.filter(**{lookup: value}),
expected,