diff options
Diffstat (limited to 'django/db/models/sql/query.py')
| -rw-r--r-- | django/db/models/sql/query.py | 459 |
1 files changed, 231 insertions, 228 deletions
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 0e7bd92aa2..f6b812c54d 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -14,13 +14,13 @@ from django.utils.encoding import force_text from django.utils.tree import Node from django.utils import six from django.db import connections, DEFAULT_DB_ALIAS -from django.db.models import signals from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import ExpressionNode from django.db.models.fields import FieldDoesNotExist +from django.db.models.loading import get_model from django.db.models.sql import aggregates as base_aggregates_module from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE, - ORDER_PATTERN, REUSE_ALL, JoinInfo, SelectInfo) + ORDER_PATTERN, REUSE_ALL, JoinInfo, SelectInfo, PathInfo) from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode, @@ -119,7 +119,7 @@ class Query(object): self.filter_is_sticky = False self.included_inherited_models = {} - # SQL-related attributes + # SQL-related attributes # Select and related select clauses as SelectInfo instances. # The select is used for cases where we want to set up the select # clause to contain other than default fields (values(), annotate(), @@ -201,6 +201,16 @@ class Query(object): (s.col, s.field is not None and s.field.name or None) for s in obj_dict['select'] ] + # alias_map can also contain references to fields. + new_alias_map = {} + for alias, join_info in obj_dict['alias_map'].items(): + if join_info.join_field is None: + new_alias_map[alias] = join_info + else: + model = join_info.join_field.model._meta + field_id = (model.app_label, model.object_name, join_info.join_field.name) + new_alias_map[alias] = join_info._replace(join_field=field_id) + obj_dict['alias_map'] = new_alias_map return obj_dict def __setstate__(self, obj_dict): @@ -213,6 +223,15 @@ class Query(object): SelectInfo(tpl[0], tpl[1] is not None and opts.get_field(tpl[1]) or None) for tpl in obj_dict['select'] ] + new_alias_map = {} + for alias, join_info in obj_dict['alias_map'].items(): + if join_info.join_field is None: + new_alias_map[alias] = join_info + else: + field_id = join_info.join_field + new_alias_map[alias] = join_info._replace( + join_field=get_model(field_id[0], field_id[1])._meta.get_field(field_id[2])) + obj_dict['alias_map'] = new_alias_map self.__dict__.update(obj_dict) @@ -479,21 +498,26 @@ class Query(object): # Now, add the joins from rhs query into the new query (skipping base # table). for alias in rhs.tables[1:]: - if not rhs.alias_refcount[alias]: - continue - table, _, join_type, lhs, lhs_col, col, nullable = rhs.alias_map[alias] + table, _, join_type, lhs, lhs_col, col, nullable, join_field = rhs.alias_map[alias] promote = (join_type == self.LOUTER) # If the left side of the join was already relabeled, use the # updated alias. lhs = change_map.get(lhs, lhs) new_alias = self.join( (lhs, table, lhs_col, col), reuse=reuse, promote=promote, - outer_if_first=not conjunction, nullable=nullable) + outer_if_first=not conjunction, nullable=nullable, + join_field=join_field) # We can't reuse the same join again in the query. If we have two # distinct joins for the same connection in rhs query, then the # combined query must have two joins, too. reuse.discard(new_alias) change_map[alias] = new_alias + if not rhs.alias_refcount[alias]: + # The alias was unused in the rhs query. Unref it so that it + # will be unused in the new query, too. We have to add and + # unref the alias so that join promotion has information of + # the join type for the unused alias. + self.unref_alias(new_alias) # So that we don't exclude valid results in an "or" query combination, # all joins exclusive to either the lhs or the rhs must be converted @@ -868,7 +892,7 @@ class Query(object): return len([1 for count in self.alias_refcount.values() if count]) def join(self, connection, reuse=REUSE_ALL, promote=False, - outer_if_first=False, nullable=False): + outer_if_first=False, nullable=False, join_field=None): """ Returns an alias for the join in 'connection', either reusing an existing alias for that join or creating a new one. 'connection' is a @@ -897,6 +921,8 @@ class Query(object): If 'nullable' is True, the join can potentially involve NULL values and is a candidate for promotion (to "left outer") when combining querysets. + + The 'join_field' is the field we are joining along (if any). """ lhs, table, lhs_col, col = connection existing = self.join_map.get(connection, ()) @@ -906,8 +932,13 @@ class Query(object): reuse = set() else: reuse = [a for a in existing if a in reuse] - if reuse: - alias = reuse[0] + for alias in reuse: + if join_field and self.alias_map[alias].join_field != join_field: + # The join_map doesn't contain join_field (mainly because + # fields in Query structs are problematic in pickling), so + # check that the existing join is created using the same + # join_field used for the under work join. + continue self.ref_alias(alias) if promote or (lhs and self.alias_map[lhs].join_type == self.LOUTER): self.promote_joins([alias]) @@ -926,7 +957,8 @@ class Query(object): join_type = self.LOUTER else: join_type = self.INNER - join = JoinInfo(table, alias, join_type, lhs, lhs_col, col, nullable) + join = JoinInfo(table, alias, join_type, lhs, lhs_col, col, nullable, + join_field) self.alias_map[alias] = join if connection in self.join_map: self.join_map[connection] += (alias,) @@ -1007,11 +1039,11 @@ class Query(object): # - this is an annotation over a model field # then we need to explore the joins that are required. - field, source, opts, join_list, last, _ = self.setup_joins( + field, source, opts, join_list, path = self.setup_joins( field_list, opts, self.get_initial_alias(), REUSE_ALL) # Process the join chain to see if it can be trimmed - col, _, join_list = self.trim_joins(source, join_list, last, False) + col, _, join_list = self.trim_joins(source, join_list, path) # If the aggregate references a model or field that requires a join, # those joins must be LEFT OUTER - empty join rows must be returned @@ -1030,7 +1062,7 @@ class Query(object): aggregate.add_to_query(self, alias, col=col, source=source, is_summary=is_summary) def add_filter(self, filter_expr, connector=AND, negate=False, - can_reuse=None, process_extras=True, force_having=False): + can_reuse=None, force_having=False): """ Add a single filter to the query. The 'filter_expr' is a pair: (filter_string, value). E.g. ('name__contains', 'fred') @@ -1047,10 +1079,6 @@ class Query(object): will be a set of table aliases that can be reused in this filter, even if we would otherwise force the creation of new aliases for a join (needed for nested Q-filters). The set is updated by this method. - - If 'process_extras' is set, any extra filters returned from the table - joining process will be processed. This parameter is set to False - during the processing of extra filters to avoid infinite recursion. """ arg, value = filter_expr parts = arg.split(LOOKUP_SEP) @@ -1115,10 +1143,11 @@ class Query(object): allow_many = not negate try: - field, target, opts, join_list, last, extra_filters = self.setup_joins( + field, target, opts, join_list, path = self.setup_joins( parts, opts, alias, can_reuse, allow_many, - allow_explicit_fk=True, negate=negate, - process_extras=process_extras) + allow_explicit_fk=True) + if can_reuse is not None: + can_reuse.update(join_list) except MultiJoin as e: self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]), can_reuse) @@ -1136,10 +1165,10 @@ class Query(object): join_promote = True # Process the join list to see if we can remove any inner joins from - # the far end (fewer tables in a query is better). - nonnull_comparison = (lookup_type == 'isnull' and value is False) - col, alias, join_list = self.trim_joins(target, join_list, last, - nonnull_comparison) + # the far end (fewer tables in a query is better). Note that join + # promotion must happen before join trimming to have the join type + # information available when reusing joins. + col, alias, join_list = self.trim_joins(target, join_list, path) if connector == OR: # Some joins may need to be promoted when adding a new filter to a @@ -1212,12 +1241,6 @@ class Query(object): # is added in upper layers of the code. self.where.add((Constraint(alias, col, None), 'isnull', False), AND) - if can_reuse is not None: - can_reuse.update(join_list) - if process_extras: - for filter in extra_filters: - self.add_filter(filter, negate=negate, can_reuse=can_reuse, - process_extras=False) def add_q(self, q_object, used_aliases=None, force_having=False): """ @@ -1270,37 +1293,24 @@ class Query(object): if self.filter_is_sticky: self.used_aliases = used_aliases - def setup_joins(self, names, opts, alias, can_reuse, allow_many=True, - allow_explicit_fk=False, negate=False, process_extras=True): + def names_to_path(self, names, opts, allow_many=False, + allow_explicit_fk=True): """ - Compute the necessary table joins for the passage through the fields - given in 'names'. 'opts' is the Options class for the current model - (which gives the table we are joining to), 'alias' is the alias for the - table we are joining to. + Walks the names path and turns them PathInfo tuples. Note that a + single name in 'names' can generate multiple PathInfos (m2m for + example). - The 'can_reuse' defines the reverse foreign key joins we can reuse. It - can be either sql.constants.REUSE_ALL in which case all joins are - reusable or a set of aliases that can be reused. Non-reverse foreign - keys are always reusable. + 'names' is the path of names to travle, 'opts' is the model Options we + start the name resolving from, 'allow_many' and 'allow_explicit_fk' + are as for setup_joins(). - The 'allow_explicit_fk' controls if field.attname is allowed in the - lookups. - - Finally, 'negate' is used in the same sense as for add_filter() - -- it indicates an exclude() filter, or something similar. It is only - passed in here so that it can be passed to a field's extra_filter() for - customized behavior. - - Returns the final field involved in the join, the target database - column (used for any 'where' constraint), the final 'opts' value and the - list of tables joined. + Returns a list of PathInfo tuples. In addition returns the final field + (the last used join field), and target (which is a field guaranteed to + contain the same value as the final field). """ - joins = [alias] - last = [0] - extra_filters = [] - int_alias = None + path = [] + multijoin_pos = None for pos, name in enumerate(names): - last.append(len(joins)) if name == 'pk': name = opts.pk.name try: @@ -1314,14 +1324,12 @@ class Query(object): field, model, direct, m2m = opts.get_field_by_name(f.name) break else: - names = opts.get_all_field_names() + list(self.aggregate_select) + available = opts.get_all_field_names() + list(self.aggregate_select) raise FieldError("Cannot resolve keyword %r into field. " - "Choices are: %s" % (name, ", ".join(names))) - - if not allow_many and (m2m or not direct): - for alias in joins: - self.unref_alias(alias) - raise MultiJoin(pos + 1) + "Choices are: %s" % (name, ", ".join(available))) + # Check if we need any joins for concrete inheritance cases (the + # field lives in parent, but we are currently in one of its + # children) if model: # The field lives on a base class of the current model. # Skip the chain of proxy to the concrete proxied model @@ -1331,172 +1339,179 @@ class Query(object): if int_model is proxied_model: opts = int_model._meta else: - lhs_col = opts.parents[int_model].column + final_field = opts.parents[int_model] + target = final_field.rel.get_related_field() opts = int_model._meta - alias = self.join((alias, opts.db_table, lhs_col, - opts.pk.column)) - joins.append(alias) - cached_data = opts._join_cache.get(name) - orig_opts = opts - - if process_extras and hasattr(field, 'extra_filters'): - extra_filters.extend(field.extra_filters(names, pos, negate)) - if direct: - if m2m: - # Many-to-many field defined on the current model. - if cached_data: - (table1, from_col1, to_col1, table2, from_col2, - to_col2, opts, target) = cached_data - else: - table1 = field.m2m_db_table() - from_col1 = opts.get_field_by_name( - field.m2m_target_field_name())[0].column - to_col1 = field.m2m_column_name() - opts = field.rel.to._meta - table2 = opts.db_table - from_col2 = field.m2m_reverse_name() - to_col2 = opts.get_field_by_name( - field.m2m_reverse_target_field_name())[0].column - target = opts.pk - orig_opts._join_cache[name] = (table1, from_col1, - to_col1, table2, from_col2, to_col2, opts, - target) - - int_alias = self.join((alias, table1, from_col1, to_col1), - reuse=can_reuse, nullable=True) - if int_alias == table2 and from_col2 == to_col2: - joins.append(int_alias) - alias = int_alias - else: - alias = self.join( - (int_alias, table2, from_col2, to_col2), - reuse=can_reuse, nullable=True) - joins.extend([int_alias, alias]) - elif field.rel: - # One-to-one or many-to-one field - if cached_data: - (table, from_col, to_col, opts, target) = cached_data - else: - opts = field.rel.to._meta - target = field.rel.get_related_field() - table = opts.db_table - from_col = field.column - to_col = target.column - orig_opts._join_cache[name] = (table, from_col, to_col, - opts, target) - - alias = self.join((alias, table, from_col, to_col), - nullable=self.is_nullable(field)) - joins.append(alias) + path.append(PathInfo(final_field, target, final_field.model._meta, + opts, final_field)) + # We have five different cases to solve: foreign keys, reverse + # foreign keys, m2m fields (also reverse) and non-relational + # fields. We are mostly just using the related field API to + # fetch the from and to fields. The m2m fields are handled as + # two foreign keys, first one reverse, the second one direct. + if direct and not field.rel and not m2m: + # Local non-relational field. + final_field = target = field + break + elif direct and not m2m: + # Foreign Key + opts = field.rel.to._meta + target = field.rel.get_related_field() + final_field = field + from_opts = field.model._meta + path.append(PathInfo(field, target, from_opts, opts, field)) + elif not direct and not m2m: + # Revere foreign key + final_field = to_field = field.field + opts = to_field.model._meta + from_field = to_field.rel.get_related_field() + from_opts = from_field.model._meta + path.append( + PathInfo(from_field, to_field, from_opts, opts, to_field)) + if from_field.model is to_field.model: + # Recursive foreign key to self. + target = opts.get_field_by_name( + field.field.rel.field_name)[0] else: - # Non-relation fields. - target = field - break - else: - orig_field = field - field = field.field - if m2m: - # Many-to-many field defined on the target model. - if cached_data: - (table1, from_col1, to_col1, table2, from_col2, - to_col2, opts, target) = cached_data - else: - table1 = field.m2m_db_table() - from_col1 = opts.get_field_by_name( - field.m2m_reverse_target_field_name())[0].column - to_col1 = field.m2m_reverse_name() - opts = orig_field.opts - table2 = opts.db_table - from_col2 = field.m2m_column_name() - to_col2 = opts.get_field_by_name( - field.m2m_target_field_name())[0].column - target = opts.pk - orig_opts._join_cache[name] = (table1, from_col1, - to_col1, table2, from_col2, to_col2, opts, - target) - - int_alias = self.join((alias, table1, from_col1, to_col1), - reuse=can_reuse, nullable=True) - alias = self.join((int_alias, table2, from_col2, to_col2), - reuse=can_reuse, nullable=True) - joins.extend([int_alias, alias]) + target = opts.pk + elif direct and m2m: + if not field.rel.through: + # Gotcha! This is just a fake m2m field - a generic relation + # field). + from_field = opts.pk + opts = field.rel.to._meta + target = opts.get_field_by_name(field.object_id_field_name)[0] + final_field = field + # Note that we are using different field for the join_field + # than from_field or to_field. This is a hack, but we need the + # GenericRelation to generate the extra SQL. + path.append(PathInfo(from_field, target, field.model._meta, opts, + field)) else: - # One-to-many field (ForeignKey defined on the target model) - if cached_data: - (table, from_col, to_col, opts, target) = cached_data - else: - local_field = opts.get_field_by_name( - field.rel.field_name)[0] - opts = orig_field.opts - table = opts.db_table - from_col = local_field.column - to_col = field.column - # In case of a recursive FK, use the to_field for - # reverse lookups as well - if orig_field.model is local_field.model: - target = opts.get_field_by_name( - field.rel.field_name)[0] - else: - target = opts.pk - orig_opts._join_cache[name] = (table, from_col, to_col, - opts, target) + # m2m field. We are travelling first to the m2m table along a + # reverse relation, then from m2m table to the target table. + from_field1 = opts.get_field_by_name( + field.m2m_target_field_name())[0] + opts = field.rel.through._meta + to_field1 = opts.get_field_by_name(field.m2m_field_name())[0] + path.append( + PathInfo(from_field1, to_field1, from_field1.model._meta, + opts, to_field1)) + final_field = from_field2 = opts.get_field_by_name( + field.m2m_reverse_field_name())[0] + opts = field.rel.to._meta + target = to_field2 = opts.get_field_by_name( + field.m2m_reverse_target_field_name())[0] + path.append( + PathInfo(from_field2, to_field2, from_field2.model._meta, + opts, from_field2)) + elif not direct and m2m: + # This one is just like above, except we are travelling the + # fields in opposite direction. + field = field.field + from_field1 = opts.get_field_by_name( + field.m2m_reverse_target_field_name())[0] + int_opts = field.rel.through._meta + to_field1 = int_opts.get_field_by_name( + field.m2m_reverse_field_name())[0] + path.append( + PathInfo(from_field1, to_field1, from_field1.model._meta, + int_opts, to_field1)) + final_field = from_field2 = int_opts.get_field_by_name( + field.m2m_field_name())[0] + opts = field.opts + target = to_field2 = opts.get_field_by_name( + field.m2m_target_field_name())[0] + path.append(PathInfo(from_field2, to_field2, from_field2.model._meta, + opts, from_field2)) - alias = self.join((alias, table, from_col, to_col), - reuse=can_reuse, nullable=True) - joins.append(alias) + if m2m and multijoin_pos is None: + multijoin_pos = pos + if not direct and not path[-1].to_field.unique and multijoin_pos is None: + multijoin_pos = pos if pos != len(names) - 1: if pos == len(names) - 2: - raise FieldError("Join on field %r not permitted. Did you misspell %r for the lookup type?" % (name, names[pos + 1])) + raise FieldError( + "Join on field %r not permitted. Did you misspell %r for " + "the lookup type?" % (name, names[pos + 1])) else: raise FieldError("Join on field %r not permitted." % name) + if multijoin_pos is not None and len(path) >= multijoin_pos and not allow_many: + raise MultiJoin(multijoin_pos + 1) + return path, final_field, target - return field, target, opts, joins, last, extra_filters - - def trim_joins(self, target, join_list, last, nonnull_check=False): + def setup_joins(self, names, opts, alias, can_reuse, allow_many=True, + allow_explicit_fk=False): """ - Sometimes joins at the end of a multi-table sequence can be trimmed. If - the final join is against the same column as we are comparing against, - and is an inner join, we can go back one step in a join chain and - compare against the LHS of the join instead (and then repeat the - optimization). The result, potentially, involves fewer table joins. + Compute the necessary table joins for the passage through the fields + given in 'names'. 'opts' is the Options class for the current model + (which gives the table we are starting from), 'alias' is the alias for + the table to start the joining from. - The 'target' parameter is the final field being joined to, 'join_list' - is the full list of join aliases. + The 'can_reuse' defines the reverse foreign key joins we can reuse. It + can be sql.constants.REUSE_ALL in which case all joins are reusable + or a set of aliases that can be reused. Note that Non-reverse foreign + keys are always reusable. - The 'last' list contains offsets into 'join_list', corresponding to - each component of the filter. Many-to-many relations, for example, add - two tables to the join list and we want to deal with both tables the - same way, so 'last' has an entry for the first of the two tables and - then the table immediately after the second table, in that case. + If 'allow_many' is False, then any reverse foreign key seen will + generate a MultiJoin exception. - The 'nonnull_check' parameter is True when we are using inner joins - between tables explicitly to exclude NULL entries. In that case, the - tables shouldn't be trimmed, because the very action of joining to them - alters the result set. + The 'allow_explicit_fk' controls if field.attname is allowed in the + lookups. + + Returns the final field involved in the joins, the target field (used + for any 'where' constraint), the final 'opts' value, the joins and the + field path travelled to generate the joins. + + The target field is the field containing the concrete value. Final + field can be something different, for example foreign key pointing to + that value. Final field is needed for example in some value + conversions (convert 'obj' in fk__id=obj to pk val using the foreign + key field for example). + """ + joins = [alias] + # First, generate the path for the names + path, final_field, target = self.names_to_path( + names, opts, allow_many, allow_explicit_fk) + # Then, add the path to the query's joins. Note that we can't trim + # joins at this stage - we will need the information about join type + # of the trimmed joins. + for pos, join in enumerate(path): + from_field, to_field, from_opts, opts, join_field = join + direct = join_field == from_field + if direct: + nullable = self.is_nullable(from_field) + else: + nullable = True + connection = alias, opts.db_table, from_field.column, to_field.column + alias = self.join(connection, reuse=can_reuse, nullable=nullable, + join_field=join_field) + joins.append(alias) + return final_field, target, opts, joins, path + + def trim_joins(self, target, joins, path): + """ + The 'target' parameter is the final field being joined to, 'joins' + is the full list of join aliases. The 'path' contain the PathInfos + used to create the joins. Returns the final active column and table alias and the new active - join_list. + joins. + + We will always trim any direct join if we have the target column + available already in the previous table. Reverse joins can't be + trimmed as we don't know if there is anything on the other side of + the join. """ - final = len(join_list) - penultimate = last.pop() - if penultimate == final: - penultimate = last.pop() - col = target.column - alias = join_list[-1] - while final > 1: - join = self.alias_map[alias] - if (col != join.rhs_join_col or join.join_type != self.INNER or - nonnull_check): + for info in reversed(path): + direct = info.join_field == info.from_field + if info.to_field == target and direct: + target = info.from_field + self.unref_alias(joins.pop()) + else: break - self.unref_alias(alias) - alias = join.lhs_alias - col = join.lhs_join_col - join_list.pop() - final -= 1 - if final == penultimate: - penultimate = last.pop() - return col, alias, join_list + return target.column, joins[-1], joins def split_exclude(self, filter_expr, prefix, can_reuse): """ @@ -1627,9 +1642,9 @@ class Query(object): try: for name in field_names: - field, target, u2, joins, u3, u4 = self.setup_joins( - name.split(LOOKUP_SEP), opts, alias, REUSE_ALL, - allow_m2m, True) + field, target, u2, joins, u3 = self.setup_joins( + name.split(LOOKUP_SEP), opts, alias, REUSE_ALL, allow_m2m, + True) final_alias = joins[-1] col = target.column if len(joins) > 1: @@ -1918,7 +1933,7 @@ class Query(object): """ opts = self.model._meta alias = self.get_initial_alias() - field, col, opts, joins, last, extra = self.setup_joins( + field, col, opts, joins, extra = self.setup_joins( start.split(LOOKUP_SEP), opts, alias, REUSE_ALL) select_col = self.alias_map[joins[1]].lhs_join_col select_alias = alias @@ -1975,18 +1990,6 @@ def get_order_dir(field, default='ASC'): return field, dirn[0] -def setup_join_cache(sender, **kwargs): - """ - The information needed to join between model fields is something that is - invariant over the life of the model, so we cache it in the model's Options - class, rather than recomputing it all the time. - - This method initialises the (empty) cache when the model is created. - """ - sender._meta._join_cache = {} - -signals.class_prepared.connect(setup_join_cache) - def add_to_dict(data, key, value): """ A helper function to add "value" to the set of values for "key", whether or |
