diff options
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. |
