diff options
| author | Simon Charette <charette.s@gmail.com> | 2025-01-27 23:10:13 -0500 |
|---|---|---|
| committer | Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> | 2025-02-11 09:08:35 +0100 |
| commit | 41239fe34d64e801212dccaa4585e4802d0fac68 (patch) | |
| tree | 5a7375f6945d53932f682fa894dd856d22129327 /django | |
| parent | 0597e8ad1e55b565292ead732916aa0e39bdf37b (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')
| -rw-r--r-- | django/db/models/expressions.py | 3 | ||||
| -rw-r--r-- | django/db/models/fields/related_lookups.py | 11 | ||||
| -rw-r--r-- | django/db/models/fields/tuple_lookups.py | 51 | ||||
| -rw-r--r-- | django/db/models/lookups.py | 23 | ||||
| -rw-r--r-- | django/db/models/query.py | 4 | ||||
| -rw-r--r-- | django/db/models/sql/query.py | 6 |
6 files changed, 54 insertions, 44 deletions
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index a89acaf5a9..57ceadcec4 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -1367,6 +1367,9 @@ class ColPairs(Expression): def resolve_expression(self, *args, **kwargs): return self + def select_format(self, compiler, sql, params): + return sql, params + class Ref(Expression): """ 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, diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index c95f572e00..51817710e9 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -373,16 +373,21 @@ class Exact(FieldGetDbPrepValueMixin, BuiltinLookup): def get_prep_lookup(self): from django.db.models.sql.query import Query # avoid circular import - if isinstance(self.rhs, Query): - if self.rhs.has_limit_one(): - if not self.rhs.has_select_fields: - self.rhs.clear_select_clause() - self.rhs.add_fields(["pk"]) - else: + if isinstance(query := self.rhs, Query): + if not query.has_limit_one(): raise ValueError( "The QuerySet value for an exact lookup must be limited to " "one result using slicing." ) + lhs_len = len(self.lhs) if isinstance(self.lhs, (ColPairs, tuple)) else 1 + if (rhs_len := query._subquery_fields_len) != lhs_len: + raise ValueError( + f"The QuerySet value for the exact lookup must have {lhs_len} " + f"selected fields (received {rhs_len})" + ) + if not query.has_select_fields: + query.clear_select_clause() + query.add_fields(["pk"]) return super().get_prep_lookup() def as_sql(self, compiler, connection): @@ -499,6 +504,12 @@ class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup): from django.db.models.sql.query import Query # avoid circular import if isinstance(self.rhs, Query): + lhs_len = len(self.lhs) if isinstance(self.lhs, (ColPairs, tuple)) else 1 + if (rhs_len := self.rhs._subquery_fields_len) != lhs_len: + raise ValueError( + f"The QuerySet value for the 'in' lookup must have {lhs_len} " + f"selected fields (received {rhs_len})" + ) self.rhs.clear_ordering(clear_default=True) if not self.rhs.has_select_fields: self.rhs.clear_select_clause() diff --git a/django/db/models/query.py b/django/db/models/query.py index 84806a5f72..175073b961 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1958,10 +1958,6 @@ class QuerySet(AltersData): self._known_related_objects.setdefault(field, {}).update(objects) def resolve_expression(self, *args, **kwargs): - if self._fields and len(self._fields) > 1: - # values() queryset can only be used as nested queries - # if they are set up to select only a single field. - raise TypeError("Cannot use multi-field values as a filter value.") query = self.query.resolve_expression(*args, **kwargs) query._db = self._db return query diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index ec47d9aa24..0d1fe5fb43 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1224,6 +1224,12 @@ class Query(BaseExpression): if self.selected: self.selected[alias] = alias + @property + def _subquery_fields_len(self): + if self.has_select_fields: + return len(self.selected) + return len(self.model._meta.pk_fields) + def resolve_expression(self, query, *args, **kwargs): clone = self.clone() # Subqueries need to use a different set of aliases than the outer query. |
