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/db/models/fields | |
| 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/db/models/fields')
| -rw-r--r-- | django/db/models/fields/related_lookups.py | 11 | ||||
| -rw-r--r-- | django/db/models/fields/tuple_lookups.py | 51 |
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, |
