Refs #32690 -- Altered lookups Query rhs alterations during initialization.

Having it happen at the lookup creation time ensures entry points
called before the compilation phase (e.g. get_group_by_cols) don't have
to duplicate the logic in charge of altering Query instances used as
rhs.

It also has the nice effect of reducing the amount of time the
alteration logic to once as opposed to multiple times if the queryset
is compiled more than once.
This commit is contained in:
Simon Charette 2021-12-01 00:27:08 -05:00 committed by Mariusz Felisiak
parent 4ce59f602e
commit e3bde71676
2 changed files with 45 additions and 50 deletions

View File

@ -48,18 +48,37 @@ def get_normalized_value(value, lhs):
class RelatedIn(In): class RelatedIn(In):
def get_prep_lookup(self): def get_prep_lookup(self):
if not isinstance(self.lhs, MultiColSource) and self.rhs_is_direct_value(): if not isinstance(self.lhs, MultiColSource):
# If we get here, we are dealing with single-column relations. if self.rhs_is_direct_value():
self.rhs = [get_normalized_value(val, self.lhs)[0] for val in self.rhs] # If we get here, we are dealing with single-column relations.
# We need to run the related field's get_prep_value(). Consider case self.rhs = [get_normalized_value(val, self.lhs)[0] for val in self.rhs]
# ForeignKey to IntegerField given value 'abc'. The ForeignKey itself # We need to run the related field's get_prep_value(). Consider
# doesn't have validation for non-integers, so we must run validation # case ForeignKey to IntegerField given value 'abc'. The
# using the target field. # ForeignKey itself doesn't have validation for non-integers,
if hasattr(self.lhs.output_field, 'path_infos'): # so we must run validation using the target field.
# Run the target field's get_prep_value. We can safely assume there is if hasattr(self.lhs.output_field, 'path_infos'):
# only one as we don't get to the direct value branch otherwise. # Run the target field's get_prep_value. We can safely
target_field = self.lhs.output_field.path_infos[-1].target_fields[-1] # assume there is only one as we don't get to the direct
self.rhs = [target_field.get_prep_value(v) for v in self.rhs] # value branch otherwise.
target_field = self.lhs.output_field.path_infos[-1].target_fields[-1]
self.rhs = [target_field.get_prep_value(v) for v in self.rhs]
elif (
not getattr(self.rhs, 'has_select_fields', True) and
not getattr(self.lhs.field.target_field, 'primary_key', False)
):
self.rhs.clear_select_clause()
if (
getattr(self.lhs.output_field, 'primary_key', False) and
self.lhs.output_field.model == self.rhs.model
):
# A case like
# Restaurant.objects.filter(place__in=restaurant_qs), where
# place is a OneToOneField and the primary key of
# Restaurant.
target_field = self.lhs.field.name
else:
target_field = self.lhs.field.target_field.name
self.rhs.add_fields([target_field], True)
return super().get_prep_lookup() return super().get_prep_lookup()
def as_sql(self, compiler, connection): def as_sql(self, compiler, connection):
@ -88,20 +107,7 @@ class RelatedIn(In):
[source.name for source in self.lhs.sources], self.rhs), [source.name for source in self.lhs.sources], self.rhs),
AND) AND)
return root_constraint.as_sql(compiler, connection) return root_constraint.as_sql(compiler, connection)
else: return super().as_sql(compiler, connection)
if (not getattr(self.rhs, 'has_select_fields', True) and
not getattr(self.lhs.field.target_field, 'primary_key', False)):
self.rhs.clear_select_clause()
if (getattr(self.lhs.output_field, 'primary_key', False) and
self.lhs.output_field.model == self.rhs.model):
# A case like Restaurant.objects.filter(place__in=restaurant_qs),
# where place is a OneToOneField and the primary key of
# Restaurant.
target_field = self.lhs.field.name
else:
target_field = self.lhs.field.target_field.name
self.rhs.add_fields([target_field], True)
return super().as_sql(compiler, connection)
class RelatedLookupMixin: class RelatedLookupMixin:

View File

@ -302,8 +302,8 @@ class PostgresOperatorLookup(FieldGetDbPrepValueMixin, Lookup):
class Exact(FieldGetDbPrepValueMixin, BuiltinLookup): class Exact(FieldGetDbPrepValueMixin, BuiltinLookup):
lookup_name = 'exact' lookup_name = 'exact'
def process_rhs(self, compiler, connection): def get_prep_lookup(self):
from django.db.models.sql.query import Query from django.db.models.sql.query import Query # avoid circular import
if isinstance(self.rhs, Query): if isinstance(self.rhs, Query):
if self.rhs.has_limit_one(): if self.rhs.has_limit_one():
if not self.rhs.has_select_fields: if not self.rhs.has_select_fields:
@ -314,7 +314,7 @@ class Exact(FieldGetDbPrepValueMixin, BuiltinLookup):
'The QuerySet value for an exact lookup must be limited to ' 'The QuerySet value for an exact lookup must be limited to '
'one result using slicing.' 'one result using slicing.'
) )
return super().process_rhs(compiler, connection) return super().get_prep_lookup()
def as_sql(self, compiler, connection): def as_sql(self, compiler, connection):
# Avoid comparison against direct rhs if lhs is a boolean value. That # Avoid comparison against direct rhs if lhs is a boolean value. That
@ -388,6 +388,15 @@ class IntegerLessThan(IntegerFieldFloatRounding, LessThan):
class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup): class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
lookup_name = 'in' lookup_name = 'in'
def get_prep_lookup(self):
from django.db.models.sql.query import Query # avoid circular import
if isinstance(self.rhs, Query):
self.rhs.clear_ordering(clear_default=True)
if not self.rhs.has_select_fields:
self.rhs.clear_select_clause()
self.rhs.add_fields(['pk'])
return super().get_prep_lookup()
def process_rhs(self, compiler, connection): def process_rhs(self, compiler, connection):
db_rhs = getattr(self.rhs, '_db', None) db_rhs = getattr(self.rhs, '_db', None)
if db_rhs is not None and db_rhs != connection.alias: if db_rhs is not None and db_rhs != connection.alias:
@ -412,27 +421,7 @@ class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
sqls, sqls_params = self.batch_process_rhs(compiler, connection, rhs) sqls, sqls_params = self.batch_process_rhs(compiler, connection, rhs)
placeholder = '(' + ', '.join(sqls) + ')' placeholder = '(' + ', '.join(sqls) + ')'
return (placeholder, sqls_params) return (placeholder, sqls_params)
else: return super().process_rhs(compiler, connection)
from django.db.models.sql.query import ( # avoid circular import
Query,
)
if isinstance(self.rhs, Query):
query = self.rhs
query.clear_ordering(clear_default=True)
if not query.has_select_fields:
query.clear_select_clause()
query.add_fields(['pk'])
return super().process_rhs(compiler, connection)
def get_group_by_cols(self, alias=None):
cols = self.lhs.get_group_by_cols()
if hasattr(self.rhs, 'get_group_by_cols'):
if not getattr(self.rhs, 'has_select_fields', True):
self.rhs.clear_select_clause()
self.rhs.add_fields(['pk'])
cols.extend(self.rhs.get_group_by_cols())
return cols
def get_rhs_op(self, connection, rhs): def get_rhs_op(self, connection, rhs):
return 'IN %s' % rhs return 'IN %s' % rhs