diff options
Diffstat (limited to 'django/db/models/sql')
| -rw-r--r-- | django/db/models/sql/query.py | 106 |
1 files changed, 50 insertions, 56 deletions
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index a030112e75..c71bc634aa 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -772,17 +772,37 @@ class Query(object): unref_amount = cur_refcount - to_counts.get(alias, 0) self.unref_alias(alias, unref_amount) - def promote_unused_aliases(self, initial_refcounts, used_aliases): + def promote_disjunction(self, aliases_before, alias_usage_counts, + num_childs): """ - Given a "before" copy of the alias_refcounts dictionary (as - 'initial_refcounts') and a collection of aliases that may have been - changed or created, works out which aliases have been created since - then and which ones haven't been used and promotes all of those - aliases, plus any children of theirs in the alias tree, to outer joins. + This method is to be used for promoting joins in ORed filters. + + The principle for promotion is: any alias which is used (it is in + alias_usage_counts), is not used by every child of the ORed filter, + and isn't pre-existing needs to be promoted to LOUTER join. + + Some examples (assume all joins used are nullable): + - existing filter: a__f1=foo + - add filter: b__f1=foo|b__f2=foo + In this case we should not promote either of the joins (using INNER + doesn't remove results). We correctly avoid join promotion, because + a is not used in this branch, and b is used two times. + + - add filter a__f1=foo|b__f2=foo + In this case we should promote both a and b, otherwise they will + remove results. We will also correctly do that as both aliases are + used, and in addition both are used only once while there are two + filters. + + - existing: a__f1=bar + - add filter: a__f2=foo|b__f2=foo + We will not promote a as it is previously used. If the join results + in null, the existing filter can't succeed. + + The above (and some more) are tested in queries.DisjunctionPromotionTests """ - for alias in self.tables: - if alias in used_aliases and (alias not in initial_refcounts or - self.alias_refcount[alias] == initial_refcounts[alias]): + for alias, use_count in alias_usage_counts.items(): + if use_count < num_childs and alias not in aliases_before: self.promote_joins([alias]) def change_aliases(self, change_map): @@ -1150,16 +1170,12 @@ class Query(object): can_reuse) return - table_promote = False - join_promote = False - if (lookup_type == 'isnull' and value is True and not negate and len(join_list) > 1): # If the comparison is against NULL, we may need to use some left # outer joins when creating the join chain. This is only done when # needed, as it's less efficient at the database level. self.promote_joins(join_list) - 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). Note that join @@ -1167,39 +1183,6 @@ class Query(object): # 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 - # disjunction. We walk the list of new joins and where it diverges - # from any previous joins (ref count is 1 in the table list), we - # make the new additions (and any existing ones not used in the new - # join list) an outer join. - join_it = iter(join_list) - table_it = iter(self.tables) - next(join_it), next(table_it) - unconditional = False - for join in join_it: - table = next(table_it) - # Once we hit an outer join, all subsequent joins must - # also be promoted, regardless of whether they have been - # promoted as a result of this pass through the tables. - unconditional = (unconditional or - self.alias_map[join].join_type == self.LOUTER) - if join == table and self.alias_refcount[join] > 1: - # We have more than one reference to this join table. - # This means that we are dealing with two different query - # subtrees, so we don't need to do any join promotion. - continue - join_promote = join_promote or self.promote_joins([join], unconditional) - if table != join: - table_promote = self.promote_joins([table]) - # We only get here if we have found a table that exists - # in the join list, but isn't on the original tables list. - # This means we've reached the point where we only have - # new tables, so we can break out of this promotion loop. - break - self.promote_joins(join_it, join_promote) - self.promote_joins(table_it, table_promote or join_promote) - if having_clause or force_having: if (alias, col) not in self.group_by: self.group_by.append((alias, col)) @@ -1256,33 +1239,36 @@ class Query(object): subtree = True else: subtree = False - connector = AND + connector = q_object.connector + if connector == OR: + alias_usage_counts = dict() + aliases_before = set(self.tables) if q_object.connector == OR and not force_having: force_having = self.need_force_having(q_object) for child in q_object.children: - if connector == OR: - refcounts_before = self.alias_refcount.copy() if force_having: self.having.start_subtree(connector) else: self.where.start_subtree(connector) + if connector == OR: + refcounts_before = self.alias_refcount.copy() if isinstance(child, Node): self.add_q(child, used_aliases, force_having=force_having) else: self.add_filter(child, connector, q_object.negated, can_reuse=used_aliases, force_having=force_having) + if connector == OR: + used = alias_diff(refcounts_before, self.alias_refcount) + for alias in used: + alias_usage_counts[alias] = alias_usage_counts.get(alias, 0) + 1 if force_having: self.having.end_subtree() else: self.where.end_subtree() - if connector == OR: - # Aliases that were newly added or not used at all need to - # be promoted to outer joins if they are nullable relations. - # (they shouldn't turn the whole conditional into the empty - # set just because they don't match anything). - self.promote_unused_aliases(refcounts_before, used_aliases) - connector = q_object.connector + if connector == OR: + self.promote_disjunction(aliases_before, alias_usage_counts, + len(q_object.children)) if q_object.negated: self.where.negate() if subtree: @@ -2005,3 +1991,11 @@ def is_reverse_o2o(field): expected to be some sort of relation field or related object. """ return not hasattr(field, 'rel') and field.field.unique + +def alias_diff(refcounts_before, refcounts_after): + """ + Given the before and after copies of refcounts works out which aliases + have been added to the after copy. + """ + return set(t for t in refcounts_after + if refcounts_after[t] > refcounts_before.get(t, 0)) |
