summaryrefslogtreecommitdiff
path: root/django/db/models/fields
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2025-01-27 23:10:13 -0500
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-02-11 09:08:35 +0100
commit41239fe34d64e801212dccaa4585e4802d0fac68 (patch)
tree5a7375f6945d53932f682fa894dd856d22129327 /django/db/models/fields
parent0597e8ad1e55b565292ead732916aa0e39bdf37b (diff)
Fixed #36149 -- Allowed subquery values against tuple exact and in lookups.
Non-tuple exact and in lookups have specialized logic for subqueries that can be adapted to properly assign select mask if unspecified and ensure the number of involved members are matching on both side of the operator.
Diffstat (limited to 'django/db/models/fields')
-rw-r--r--django/db/models/fields/related_lookups.py11
-rw-r--r--django/db/models/fields/tuple_lookups.py51
2 files changed, 28 insertions, 34 deletions
diff --git a/django/db/models/fields/related_lookups.py b/django/db/models/fields/related_lookups.py
index a6e28b11fb..38d6308f53 100644
--- a/django/db/models/fields/related_lookups.py
+++ b/django/db/models/fields/related_lookups.py
@@ -40,7 +40,16 @@ def get_normalized_value(value, lhs):
class RelatedIn(In):
def get_prep_lookup(self):
- if not isinstance(self.lhs, ColPairs):
+ from django.db.models.sql.query import Query # avoid circular import
+
+ if isinstance(self.lhs, ColPairs):
+ if (
+ isinstance(self.rhs, Query)
+ and not self.rhs.has_select_fields
+ and self.lhs.output_field.related_model is self.rhs.model
+ ):
+ self.rhs.set_values([f.name for f in self.lhs.sources])
+ else:
if self.rhs_is_direct_value():
# If we get here, we are dealing with single-column relations.
self.rhs = [get_normalized_value(val, self.lhs)[0] for val in self.rhs]
diff --git a/django/db/models/fields/tuple_lookups.py b/django/db/models/fields/tuple_lookups.py
index b45bcaf2cd..f62a49bd60 100644
--- a/django/db/models/fields/tuple_lookups.py
+++ b/django/db/models/fields/tuple_lookups.py
@@ -47,7 +47,8 @@ class TupleLookupMixin:
self.check_rhs_is_tuple_or_list()
self.check_rhs_length_equals_lhs_length()
else:
- self.check_rhs_is_outer_ref()
+ self.check_rhs_is_supported_expression()
+ super().get_prep_lookup()
return self.rhs
def check_rhs_is_tuple_or_list(self):
@@ -65,13 +66,13 @@ class TupleLookupMixin:
f"{self.lookup_name!r} lookup of {lhs_str} must have {len_lhs} elements"
)
- def check_rhs_is_outer_ref(self):
- if not isinstance(self.rhs, ResolvedOuterRef):
+ def check_rhs_is_supported_expression(self):
+ if not isinstance(self.rhs, (ResolvedOuterRef, Query)):
lhs_str = self.get_lhs_str()
rhs_cls = self.rhs.__class__.__name__
raise ValueError(
f"{self.lookup_name!r} subquery lookup of {lhs_str} "
- f"only supports OuterRef objects (received {rhs_cls!r})"
+ f"only supports OuterRef and QuerySet objects (received {rhs_cls!r})"
)
def get_lhs_str(self):
@@ -101,11 +102,14 @@ class TupleLookupMixin:
return compiler.compile(Tuple(*args))
else:
sql, params = compiler.compile(self.rhs)
- if not isinstance(self.rhs, ColPairs):
+ if isinstance(self.rhs, ColPairs):
+ return "(%s)" % sql, params
+ elif isinstance(self.rhs, Query):
+ return super().process_rhs(compiler, connection)
+ else:
raise ValueError(
"Composite field lookups only work with composite expressions."
)
- return "(%s)" % sql, params
def get_fallback_sql(self, compiler, connection):
raise NotImplementedError(
@@ -121,6 +125,8 @@ class TupleLookupMixin:
class TupleExact(TupleLookupMixin, Exact):
def get_fallback_sql(self, compiler, connection):
+ if isinstance(self.rhs, Query):
+ return super(TupleLookupMixin, self).as_sql(compiler, connection)
# Process right-hand-side to trigger sanitization.
self.process_rhs(compiler, connection)
# e.g.: (a, b, c) == (x, y, z) as SQL:
@@ -273,7 +279,7 @@ class TupleIn(TupleLookupMixin, In):
self.check_rhs_elements_length_equals_lhs_length()
else:
self.check_rhs_is_query()
- self.check_rhs_select_length_equals_lhs_length()
+ super(TupleLookupMixin, self).get_prep_lookup()
return self.rhs # skip checks from mixin
@@ -303,19 +309,10 @@ class TupleIn(TupleLookupMixin, In):
f"must be a Query object (received {rhs_cls!r})"
)
- def check_rhs_select_length_equals_lhs_length(self):
- len_rhs = len(self.rhs.select)
- if len_rhs == 1 and isinstance(self.rhs.select[0], ColPairs):
- len_rhs = len(self.rhs.select[0])
- len_lhs = len(self.lhs)
- if len_rhs != len_lhs:
- lhs_str = self.get_lhs_str()
- raise ValueError(
- f"{self.lookup_name!r} subquery lookup of {lhs_str} "
- f"must have {len_lhs} fields (received {len_rhs})"
- )
-
def process_rhs(self, compiler, connection):
+ if not self.rhs_is_direct_value():
+ return super(TupleLookupMixin, self).process_rhs(compiler, connection)
+
rhs = self.rhs
if not rhs:
raise EmptyResultSet
@@ -337,19 +334,12 @@ class TupleIn(TupleLookupMixin, In):
return compiler.compile(Tuple(*result))
- def as_subquery_sql(self, compiler, connection):
- lhs = self.lhs
- rhs = self.rhs
- if isinstance(lhs, ColPairs):
- rhs = rhs.clone()
- rhs.set_values([source.name for source in lhs.sources])
- lhs = Tuple(lhs)
- return compiler.compile(In(lhs, rhs))
-
def get_fallback_sql(self, compiler, connection):
rhs = self.rhs
if not rhs:
raise EmptyResultSet
+ if 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:
# WHERE (a = x1 AND b = y1 AND c = z1) OR (a = x2 AND b = y2 AND c = z2)
@@ -362,11 +352,6 @@ class TupleIn(TupleLookupMixin, In):
return root.as_sql(compiler, connection)
- def as_sql(self, compiler, connection):
- if not self.rhs_is_direct_value():
- return self.as_subquery_sql(compiler, connection)
- return super().as_sql(compiler, connection)
-
tuple_lookups = {
"exact": TupleExact,