mirror of https://github.com/django/django.git
Removed dupe_avoidance from sql/query and sql/compiler.py
The dupe avoidance logic was removed as it doesn't seem to do anything, it is complicated, and it has nearly zero documentation. The removal of dupe_avoidance allowed for refactoring of both the implementation and signature of Query.join(). This refactoring cascades again to some other parts. The most significant of them is the changes in qs.combine(), and compiler.select_related_descent().
This commit is contained in:
parent
9bf0eedba5
commit
68847135bc
|
@ -1053,11 +1053,6 @@ class ForeignKey(RelatedField, Field):
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
super(ForeignKey, self).contribute_to_class(cls, name)
|
super(ForeignKey, self).contribute_to_class(cls, name)
|
||||||
setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self))
|
setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self))
|
||||||
if isinstance(self.rel.to, six.string_types):
|
|
||||||
target = self.rel.to
|
|
||||||
else:
|
|
||||||
target = self.rel.to._meta.db_table
|
|
||||||
cls._meta.duplicate_targets[self.column] = (target, "o2m")
|
|
||||||
|
|
||||||
def contribute_to_related_class(self, cls, related):
|
def contribute_to_related_class(self, cls, related):
|
||||||
# Internal FK's - i.e., those with a related name ending with '+' -
|
# Internal FK's - i.e., those with a related name ending with '+' -
|
||||||
|
@ -1293,12 +1288,6 @@ class ManyToManyField(RelatedField, Field):
|
||||||
field.rel.through = model
|
field.rel.through = model
|
||||||
add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
|
add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
|
||||||
|
|
||||||
if isinstance(self.rel.to, six.string_types):
|
|
||||||
target = self.rel.to
|
|
||||||
else:
|
|
||||||
target = self.rel.to._meta.db_table
|
|
||||||
cls._meta.duplicate_targets[self.column] = (target, "m2m")
|
|
||||||
|
|
||||||
def contribute_to_related_class(self, cls, related):
|
def contribute_to_related_class(self, cls, related):
|
||||||
# Internal M2Ms (i.e., those with a related name ending with '+')
|
# Internal M2Ms (i.e., those with a related name ending with '+')
|
||||||
# and swapped models don't get a related descriptor.
|
# and swapped models don't get a related descriptor.
|
||||||
|
|
|
@ -58,7 +58,6 @@ class Options(object):
|
||||||
self.concrete_model = None
|
self.concrete_model = None
|
||||||
self.swappable = None
|
self.swappable = None
|
||||||
self.parents = SortedDict()
|
self.parents = SortedDict()
|
||||||
self.duplicate_targets = {}
|
|
||||||
self.auto_created = False
|
self.auto_created = False
|
||||||
|
|
||||||
# To handle various inheritance situations, we need to track where
|
# To handle various inheritance situations, we need to track where
|
||||||
|
@ -147,24 +146,6 @@ class Options(object):
|
||||||
auto_created=True)
|
auto_created=True)
|
||||||
model.add_to_class('id', auto)
|
model.add_to_class('id', auto)
|
||||||
|
|
||||||
# Determine any sets of fields that are pointing to the same targets
|
|
||||||
# (e.g. two ForeignKeys to the same remote model). The query
|
|
||||||
# construction code needs to know this. At the end of this,
|
|
||||||
# self.duplicate_targets will map each duplicate field column to the
|
|
||||||
# columns it duplicates.
|
|
||||||
collections = {}
|
|
||||||
for column, target in six.iteritems(self.duplicate_targets):
|
|
||||||
try:
|
|
||||||
collections[target].add(column)
|
|
||||||
except KeyError:
|
|
||||||
collections[target] = set([column])
|
|
||||||
self.duplicate_targets = {}
|
|
||||||
for elt in six.itervalues(collections):
|
|
||||||
if len(elt) == 1:
|
|
||||||
continue
|
|
||||||
for column in elt:
|
|
||||||
self.duplicate_targets[column] = elt.difference(set([column]))
|
|
||||||
|
|
||||||
def add_field(self, field):
|
def add_field(self, field):
|
||||||
# Insert the given field in the order in which it was created, using
|
# Insert the given field in the order in which it was created, using
|
||||||
# the "creation_counter" attribute of the field.
|
# the "creation_counter" attribute of the field.
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.db.backends.util import truncate_name
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.query_utils import select_related_descend
|
from django.db.models.query_utils import select_related_descend
|
||||||
from django.db.models.sql.constants import (SINGLE, MULTI, ORDER_DIR,
|
from django.db.models.sql.constants import (SINGLE, MULTI, ORDER_DIR,
|
||||||
GET_ITERATOR_CHUNK_SIZE, SelectInfo)
|
GET_ITERATOR_CHUNK_SIZE, REUSE_ALL, SelectInfo)
|
||||||
from django.db.models.sql.datastructures import EmptyResultSet
|
from django.db.models.sql.datastructures import EmptyResultSet
|
||||||
from django.db.models.sql.expressions import SQLEvaluator
|
from django.db.models.sql.expressions import SQLEvaluator
|
||||||
from django.db.models.sql.query import get_order_dir, Query
|
from django.db.models.sql.query import get_order_dir, Query
|
||||||
|
@ -457,7 +457,7 @@ class SQLCompiler(object):
|
||||||
if not alias:
|
if not alias:
|
||||||
alias = self.query.get_initial_alias()
|
alias = self.query.get_initial_alias()
|
||||||
field, target, opts, joins, _, _ = self.query.setup_joins(pieces,
|
field, target, opts, joins, _, _ = self.query.setup_joins(pieces,
|
||||||
opts, alias, False)
|
opts, alias, REUSE_ALL)
|
||||||
# We will later on need to promote those joins that were added to the
|
# We will later on need to promote those joins that were added to the
|
||||||
# query afresh above.
|
# query afresh above.
|
||||||
joins_to_promote = [j for j in joins if self.query.alias_refcount[j] < 2]
|
joins_to_promote = [j for j in joins if self.query.alias_refcount[j] < 2]
|
||||||
|
@ -574,8 +574,7 @@ class SQLCompiler(object):
|
||||||
return result, params
|
return result, params
|
||||||
|
|
||||||
def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
|
def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
|
||||||
used=None, requested=None, restricted=None, nullable=None,
|
requested=None, restricted=None, nullable=None):
|
||||||
dupe_set=None, avoid_set=None):
|
|
||||||
"""
|
"""
|
||||||
Fill in the information needed for a select_related query. The current
|
Fill in the information needed for a select_related query. The current
|
||||||
depth is measured as the number of connections away from the root model
|
depth is measured as the number of connections away from the root model
|
||||||
|
@ -590,13 +589,6 @@ class SQLCompiler(object):
|
||||||
opts = self.query.get_meta()
|
opts = self.query.get_meta()
|
||||||
root_alias = self.query.get_initial_alias()
|
root_alias = self.query.get_initial_alias()
|
||||||
self.query.related_select_cols = []
|
self.query.related_select_cols = []
|
||||||
if not used:
|
|
||||||
used = set()
|
|
||||||
if dupe_set is None:
|
|
||||||
dupe_set = set()
|
|
||||||
if avoid_set is None:
|
|
||||||
avoid_set = set()
|
|
||||||
orig_dupe_set = dupe_set
|
|
||||||
only_load = self.query.get_loaded_field_names()
|
only_load = self.query.get_loaded_field_names()
|
||||||
|
|
||||||
# Setup for the case when only particular related fields should be
|
# Setup for the case when only particular related fields should be
|
||||||
|
@ -616,12 +608,6 @@ class SQLCompiler(object):
|
||||||
if not select_related_descend(f, restricted, requested,
|
if not select_related_descend(f, restricted, requested,
|
||||||
only_load.get(field_model)):
|
only_load.get(field_model)):
|
||||||
continue
|
continue
|
||||||
# The "avoid" set is aliases we want to avoid just for this
|
|
||||||
# particular branch of the recursion. They aren't permanently
|
|
||||||
# forbidden from reuse in the related selection tables (which is
|
|
||||||
# what "used" specifies).
|
|
||||||
avoid = avoid_set.copy()
|
|
||||||
dupe_set = orig_dupe_set.copy()
|
|
||||||
table = f.rel.to._meta.db_table
|
table = f.rel.to._meta.db_table
|
||||||
promote = nullable or f.null
|
promote = nullable or f.null
|
||||||
if model:
|
if model:
|
||||||
|
@ -637,31 +623,17 @@ class SQLCompiler(object):
|
||||||
int_opts = int_model._meta
|
int_opts = int_model._meta
|
||||||
continue
|
continue
|
||||||
lhs_col = int_opts.parents[int_model].column
|
lhs_col = int_opts.parents[int_model].column
|
||||||
dedupe = lhs_col in opts.duplicate_targets
|
|
||||||
if dedupe:
|
|
||||||
avoid.update(self.query.dupe_avoidance.get((id(opts), lhs_col),
|
|
||||||
()))
|
|
||||||
dupe_set.add((opts, lhs_col))
|
|
||||||
int_opts = int_model._meta
|
int_opts = int_model._meta
|
||||||
alias = self.query.join((alias, int_opts.db_table, lhs_col,
|
alias = self.query.join((alias, int_opts.db_table, lhs_col,
|
||||||
int_opts.pk.column), exclusions=used,
|
int_opts.pk.column),
|
||||||
promote=promote)
|
promote=promote)
|
||||||
alias_chain.append(alias)
|
alias_chain.append(alias)
|
||||||
for (dupe_opts, dupe_col) in dupe_set:
|
|
||||||
self.query.update_dupe_avoidance(dupe_opts, dupe_col, alias)
|
|
||||||
else:
|
else:
|
||||||
alias = root_alias
|
alias = root_alias
|
||||||
|
|
||||||
dedupe = f.column in opts.duplicate_targets
|
|
||||||
if dupe_set or dedupe:
|
|
||||||
avoid.update(self.query.dupe_avoidance.get((id(opts), f.column), ()))
|
|
||||||
if dedupe:
|
|
||||||
dupe_set.add((opts, f.column))
|
|
||||||
|
|
||||||
alias = self.query.join((alias, table, f.column,
|
alias = self.query.join((alias, table, f.column,
|
||||||
f.rel.get_related_field().column),
|
f.rel.get_related_field().column),
|
||||||
exclusions=used.union(avoid), promote=promote)
|
promote=promote)
|
||||||
used.add(alias)
|
|
||||||
columns, aliases = self.get_default_columns(start_alias=alias,
|
columns, aliases = self.get_default_columns(start_alias=alias,
|
||||||
opts=f.rel.to._meta, as_pairs=True)
|
opts=f.rel.to._meta, as_pairs=True)
|
||||||
self.query.related_select_cols.extend(
|
self.query.related_select_cols.extend(
|
||||||
|
@ -671,10 +643,8 @@ class SQLCompiler(object):
|
||||||
else:
|
else:
|
||||||
next = False
|
next = False
|
||||||
new_nullable = f.null or promote
|
new_nullable = f.null or promote
|
||||||
for dupe_opts, dupe_col in dupe_set:
|
|
||||||
self.query.update_dupe_avoidance(dupe_opts, dupe_col, alias)
|
|
||||||
self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1,
|
self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1,
|
||||||
used, next, restricted, new_nullable, dupe_set, avoid)
|
next, restricted, new_nullable)
|
||||||
|
|
||||||
if restricted:
|
if restricted:
|
||||||
related_fields = [
|
related_fields = [
|
||||||
|
@ -686,14 +656,8 @@ class SQLCompiler(object):
|
||||||
if not select_related_descend(f, restricted, requested,
|
if not select_related_descend(f, restricted, requested,
|
||||||
only_load.get(model), reverse=True):
|
only_load.get(model), reverse=True):
|
||||||
continue
|
continue
|
||||||
# The "avoid" set is aliases we want to avoid just for this
|
|
||||||
# particular branch of the recursion. They aren't permanently
|
|
||||||
# forbidden from reuse in the related selection tables (which is
|
|
||||||
# what "used" specifies).
|
|
||||||
avoid = avoid_set.copy()
|
|
||||||
dupe_set = orig_dupe_set.copy()
|
|
||||||
table = model._meta.db_table
|
|
||||||
|
|
||||||
|
table = model._meta.db_table
|
||||||
int_opts = opts
|
int_opts = opts
|
||||||
alias = root_alias
|
alias = root_alias
|
||||||
alias_chain = []
|
alias_chain = []
|
||||||
|
@ -708,30 +672,16 @@ class SQLCompiler(object):
|
||||||
int_opts = int_model._meta
|
int_opts = int_model._meta
|
||||||
continue
|
continue
|
||||||
lhs_col = int_opts.parents[int_model].column
|
lhs_col = int_opts.parents[int_model].column
|
||||||
dedupe = lhs_col in opts.duplicate_targets
|
|
||||||
if dedupe:
|
|
||||||
avoid.update((self.query.dupe_avoidance.get(id(opts), lhs_col),
|
|
||||||
()))
|
|
||||||
dupe_set.add((opts, lhs_col))
|
|
||||||
int_opts = int_model._meta
|
int_opts = int_model._meta
|
||||||
alias = self.query.join(
|
alias = self.query.join(
|
||||||
(alias, int_opts.db_table, lhs_col, int_opts.pk.column),
|
(alias, int_opts.db_table, lhs_col, int_opts.pk.column),
|
||||||
exclusions=used, promote=True, reuse=used
|
promote=True,
|
||||||
)
|
)
|
||||||
alias_chain.append(alias)
|
alias_chain.append(alias)
|
||||||
for dupe_opts, dupe_col in dupe_set:
|
|
||||||
self.query.update_dupe_avoidance(dupe_opts, dupe_col, alias)
|
|
||||||
dedupe = f.column in opts.duplicate_targets
|
|
||||||
if dupe_set or dedupe:
|
|
||||||
avoid.update(self.query.dupe_avoidance.get((id(opts), f.column), ()))
|
|
||||||
if dedupe:
|
|
||||||
dupe_set.add((opts, f.column))
|
|
||||||
alias = self.query.join(
|
alias = self.query.join(
|
||||||
(alias, table, f.rel.get_related_field().column, f.column),
|
(alias, table, f.rel.get_related_field().column, f.column),
|
||||||
exclusions=used.union(avoid),
|
|
||||||
promote=True
|
promote=True
|
||||||
)
|
)
|
||||||
used.add(alias)
|
|
||||||
columns, aliases = self.get_default_columns(start_alias=alias,
|
columns, aliases = self.get_default_columns(start_alias=alias,
|
||||||
opts=model._meta, as_pairs=True, local_only=True)
|
opts=model._meta, as_pairs=True, local_only=True)
|
||||||
self.query.related_select_cols.extend(
|
self.query.related_select_cols.extend(
|
||||||
|
@ -743,7 +693,7 @@ class SQLCompiler(object):
|
||||||
new_nullable = True
|
new_nullable = True
|
||||||
|
|
||||||
self.fill_related_selections(model._meta, table, cur_depth+1,
|
self.fill_related_selections(model._meta, table, cur_depth+1,
|
||||||
used, next, restricted, new_nullable)
|
next, restricted, new_nullable)
|
||||||
|
|
||||||
def deferred_to_columns(self):
|
def deferred_to_columns(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -37,3 +37,6 @@ ORDER_DIR = {
|
||||||
'ASC': ('ASC', 'DESC'),
|
'ASC': ('ASC', 'DESC'),
|
||||||
'DESC': ('DESC', 'ASC'),
|
'DESC': ('DESC', 'ASC'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A marker for join-reusability.
|
||||||
|
REUSE_ALL = object()
|
||||||
|
|
|
@ -20,7 +20,7 @@ from django.db.models.expressions import ExpressionNode
|
||||||
from django.db.models.fields import FieldDoesNotExist
|
from django.db.models.fields import FieldDoesNotExist
|
||||||
from django.db.models.sql import aggregates as base_aggregates_module
|
from django.db.models.sql import aggregates as base_aggregates_module
|
||||||
from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE,
|
from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE,
|
||||||
ORDER_PATTERN, JoinInfo, SelectInfo)
|
ORDER_PATTERN, REUSE_ALL, JoinInfo, SelectInfo)
|
||||||
from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin
|
from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin
|
||||||
from django.db.models.sql.expressions import SQLEvaluator
|
from django.db.models.sql.expressions import SQLEvaluator
|
||||||
from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode,
|
from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode,
|
||||||
|
@ -115,7 +115,6 @@ class Query(object):
|
||||||
self.default_ordering = True
|
self.default_ordering = True
|
||||||
self.standard_ordering = True
|
self.standard_ordering = True
|
||||||
self.ordering_aliases = []
|
self.ordering_aliases = []
|
||||||
self.dupe_avoidance = {}
|
|
||||||
self.used_aliases = set()
|
self.used_aliases = set()
|
||||||
self.filter_is_sticky = False
|
self.filter_is_sticky = False
|
||||||
self.included_inherited_models = {}
|
self.included_inherited_models = {}
|
||||||
|
@ -257,7 +256,6 @@ class Query(object):
|
||||||
obj.standard_ordering = self.standard_ordering
|
obj.standard_ordering = self.standard_ordering
|
||||||
obj.included_inherited_models = self.included_inherited_models.copy()
|
obj.included_inherited_models = self.included_inherited_models.copy()
|
||||||
obj.ordering_aliases = []
|
obj.ordering_aliases = []
|
||||||
obj.dupe_avoidance = self.dupe_avoidance.copy()
|
|
||||||
obj.select = self.select[:]
|
obj.select = self.select[:]
|
||||||
obj.related_select_cols = []
|
obj.related_select_cols = []
|
||||||
obj.tables = self.tables[:]
|
obj.tables = self.tables[:]
|
||||||
|
@ -460,24 +458,42 @@ class Query(object):
|
||||||
self.remove_inherited_models()
|
self.remove_inherited_models()
|
||||||
# Work out how to relabel the rhs aliases, if necessary.
|
# Work out how to relabel the rhs aliases, if necessary.
|
||||||
change_map = {}
|
change_map = {}
|
||||||
used = set()
|
|
||||||
conjunction = (connector == AND)
|
conjunction = (connector == AND)
|
||||||
# Add the joins in the rhs query into the new query.
|
|
||||||
first = True
|
# Determine which existing joins can be reused. When combining the
|
||||||
for alias in rhs.tables:
|
# query with AND we must recreate all joins for m2m filters. When
|
||||||
|
# combining with OR we can reuse joins. The reason is that in AND
|
||||||
|
# case a single row can't fulfill a condition like:
|
||||||
|
# revrel__col=1 & revrel__col=2
|
||||||
|
# But, there might be two different related rows matching this
|
||||||
|
# condition. In OR case a single True is enough, so single row is
|
||||||
|
# enough, too.
|
||||||
|
#
|
||||||
|
# Note that we will be creating duplicate joins for non-m2m joins in
|
||||||
|
# the AND case. The results will be correct but this creates too many
|
||||||
|
# joins. This is something that could be fixed later on.
|
||||||
|
reuse = set() if conjunction else set(self.tables)
|
||||||
|
# Base table must be present in the query - this is the same
|
||||||
|
# table on both sides.
|
||||||
|
self.get_initial_alias()
|
||||||
|
# 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]:
|
if not rhs.alias_refcount[alias]:
|
||||||
# An unused alias.
|
|
||||||
continue
|
continue
|
||||||
table, _, join_type, lhs, lhs_col, col, _ = rhs.alias_map[alias]
|
table, _, join_type, lhs, lhs_col, col, nullable = rhs.alias_map[alias]
|
||||||
promote = join_type == self.LOUTER
|
promote = (join_type == self.LOUTER)
|
||||||
# If the left side of the join was already relabeled, use the
|
# If the left side of the join was already relabeled, use the
|
||||||
# updated alias.
|
# updated alias.
|
||||||
lhs = change_map.get(lhs, lhs)
|
lhs = change_map.get(lhs, lhs)
|
||||||
new_alias = self.join((lhs, table, lhs_col, col),
|
new_alias = self.join(
|
||||||
conjunction and not first, used, promote, not conjunction)
|
(lhs, table, lhs_col, col), reuse=reuse, promote=promote,
|
||||||
used.add(new_alias)
|
outer_if_first=not conjunction, nullable=nullable)
|
||||||
|
# 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
|
change_map[alias] = new_alias
|
||||||
first = False
|
|
||||||
|
|
||||||
# So that we don't exclude valid results in an "or" query combination,
|
# 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
|
# all joins exclusive to either the lhs or the rhs must be converted
|
||||||
|
@ -767,9 +783,11 @@ class Query(object):
|
||||||
(key, relabel_column(col)) for key, col in self.aggregates.items())
|
(key, relabel_column(col)) for key, col in self.aggregates.items())
|
||||||
|
|
||||||
# 2. Rename the alias in the internal table/alias datastructures.
|
# 2. Rename the alias in the internal table/alias datastructures.
|
||||||
for k, aliases in self.join_map.items():
|
for ident, aliases in self.join_map.items():
|
||||||
|
del self.join_map[ident]
|
||||||
aliases = tuple([change_map.get(a, a) for a in aliases])
|
aliases = tuple([change_map.get(a, a) for a in aliases])
|
||||||
self.join_map[k] = aliases
|
ident = (change_map.get(ident[0], ident[0]),) + ident[1:]
|
||||||
|
self.join_map[ident] = aliases
|
||||||
for old_alias, new_alias in six.iteritems(change_map):
|
for old_alias, new_alias in six.iteritems(change_map):
|
||||||
alias_data = self.alias_map[old_alias]
|
alias_data = self.alias_map[old_alias]
|
||||||
alias_data = alias_data._replace(rhs_alias=new_alias)
|
alias_data = alias_data._replace(rhs_alias=new_alias)
|
||||||
|
@ -844,8 +862,8 @@ class Query(object):
|
||||||
"""
|
"""
|
||||||
return len([1 for count in six.itervalues(self.alias_refcount) if count])
|
return len([1 for count in six.itervalues(self.alias_refcount) if count])
|
||||||
|
|
||||||
def join(self, connection, always_create=False, exclusions=(),
|
def join(self, connection, reuse=REUSE_ALL, promote=False,
|
||||||
promote=False, outer_if_first=False, nullable=False, reuse=None):
|
outer_if_first=False, nullable=False):
|
||||||
"""
|
"""
|
||||||
Returns an alias for the join in 'connection', either reusing an
|
Returns an alias for the join in 'connection', either reusing an
|
||||||
existing alias for that join or creating a new one. 'connection' is a
|
existing alias for that join or creating a new one. 'connection' is a
|
||||||
|
@ -855,56 +873,40 @@ class Query(object):
|
||||||
|
|
||||||
lhs.lhs_col = table.col
|
lhs.lhs_col = table.col
|
||||||
|
|
||||||
If 'always_create' is True and 'reuse' is None, a new alias is always
|
The 'reuse' parameter can be used in three ways: it can be REUSE_ALL
|
||||||
created, regardless of whether one already exists or not. If
|
which means all joins (matching the connection) are reusable, it can
|
||||||
'always_create' is True and 'reuse' is a set, an alias in 'reuse' that
|
be a set containing the aliases that can be reused, or it can be None
|
||||||
matches the connection will be returned, if possible. If
|
which means a new join is always created.
|
||||||
'always_create' is False, the first existing alias that matches the
|
|
||||||
'connection' is returned, if any. Otherwise a new join is created.
|
|
||||||
|
|
||||||
If 'exclusions' is specified, it is something satisfying the container
|
|
||||||
protocol ("foo in exclusions" must work) and specifies a list of
|
|
||||||
aliases that should not be returned, even if they satisfy the join.
|
|
||||||
|
|
||||||
If 'promote' is True, the join type for the alias will be LOUTER (if
|
If 'promote' is True, the join type for the alias will be LOUTER (if
|
||||||
the alias previously existed, the join type will be promoted from INNER
|
the alias previously existed, the join type will be promoted from INNER
|
||||||
to LOUTER, if necessary).
|
to LOUTER, if necessary).
|
||||||
|
|
||||||
If 'outer_if_first' is True and a new join is created, it will have the
|
If 'outer_if_first' is True and a new join is created, it will have the
|
||||||
LOUTER join type. This is used when joining certain types of querysets
|
LOUTER join type. Used for example when adding ORed filters, where we
|
||||||
and Q-objects together.
|
want to use LOUTER joins except if some other join already restricts
|
||||||
|
the join to INNER join.
|
||||||
|
|
||||||
A join is always created as LOUTER if the lhs alias is LOUTER to make
|
A join is always created as LOUTER if the lhs alias is LOUTER to make
|
||||||
sure we do not generate chains like a LOUTER b INNER c.
|
sure we do not generate chains like t1 LOUTER t2 INNER t3.
|
||||||
|
|
||||||
If 'nullable' is True, the join can potentially involve NULL values and
|
If 'nullable' is True, the join can potentially involve NULL values and
|
||||||
is a candidate for promotion (to "left outer") when combining querysets.
|
is a candidate for promotion (to "left outer") when combining querysets.
|
||||||
"""
|
"""
|
||||||
lhs, table, lhs_col, col = connection
|
lhs, table, lhs_col, col = connection
|
||||||
if lhs in self.alias_map:
|
existing = self.join_map.get(connection, ())
|
||||||
lhs_table = self.alias_map[lhs].table_name
|
if reuse == REUSE_ALL:
|
||||||
|
reuse = existing
|
||||||
|
elif reuse is None:
|
||||||
|
reuse = set()
|
||||||
else:
|
else:
|
||||||
lhs_table = lhs
|
reuse = [a for a in existing if a in reuse]
|
||||||
|
if reuse:
|
||||||
if reuse and always_create and table in self.table_map:
|
alias = reuse[0]
|
||||||
# Convert the 'reuse' to case to be "exclude everything but the
|
self.ref_alias(alias)
|
||||||
# reusable set, minus exclusions, for this table".
|
if promote or (lhs and self.alias_map[lhs].join_type == self.LOUTER):
|
||||||
exclusions = set(self.table_map[table]).difference(reuse).union(set(exclusions))
|
self.promote_joins([alias])
|
||||||
always_create = False
|
return alias
|
||||||
t_ident = (lhs_table, table, lhs_col, col)
|
|
||||||
if not always_create:
|
|
||||||
for alias in self.join_map.get(t_ident, ()):
|
|
||||||
if alias not in exclusions:
|
|
||||||
if lhs_table and not self.alias_refcount[self.alias_map[alias].lhs_alias]:
|
|
||||||
# The LHS of this join tuple is no longer part of the
|
|
||||||
# query, so skip this possibility.
|
|
||||||
continue
|
|
||||||
if self.alias_map[alias].lhs_alias != lhs:
|
|
||||||
continue
|
|
||||||
self.ref_alias(alias)
|
|
||||||
if promote or (lhs and self.alias_map[lhs].join_type == self.LOUTER):
|
|
||||||
self.promote_joins([alias])
|
|
||||||
return alias
|
|
||||||
|
|
||||||
# No reuse is possible, so we need a new alias.
|
# No reuse is possible, so we need a new alias.
|
||||||
alias, _ = self.table_alias(table, True)
|
alias, _ = self.table_alias(table, True)
|
||||||
|
@ -915,18 +917,16 @@ class Query(object):
|
||||||
elif (promote or outer_if_first
|
elif (promote or outer_if_first
|
||||||
or self.alias_map[lhs].join_type == self.LOUTER):
|
or self.alias_map[lhs].join_type == self.LOUTER):
|
||||||
# We need to use LOUTER join if asked by promote or outer_if_first,
|
# We need to use LOUTER join if asked by promote or outer_if_first,
|
||||||
# or if the LHS table is left-joined in the query. Adding inner join
|
# or if the LHS table is left-joined in the query.
|
||||||
# to an existing outer join effectively cancels the effect of the
|
|
||||||
# outer join.
|
|
||||||
join_type = self.LOUTER
|
join_type = self.LOUTER
|
||||||
else:
|
else:
|
||||||
join_type = self.INNER
|
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)
|
||||||
self.alias_map[alias] = join
|
self.alias_map[alias] = join
|
||||||
if t_ident in self.join_map:
|
if connection in self.join_map:
|
||||||
self.join_map[t_ident] += (alias,)
|
self.join_map[connection] += (alias,)
|
||||||
else:
|
else:
|
||||||
self.join_map[t_ident] = (alias,)
|
self.join_map[connection] = (alias,)
|
||||||
return alias
|
return alias
|
||||||
|
|
||||||
def setup_inherited_models(self):
|
def setup_inherited_models(self):
|
||||||
|
@ -1003,7 +1003,7 @@ class Query(object):
|
||||||
# then we need to explore the joins that are required.
|
# then we need to explore the joins that are required.
|
||||||
|
|
||||||
field, source, opts, join_list, last, _ = self.setup_joins(
|
field, source, opts, join_list, last, _ = self.setup_joins(
|
||||||
field_list, opts, self.get_initial_alias(), False)
|
field_list, opts, self.get_initial_alias(), REUSE_ALL)
|
||||||
|
|
||||||
# Process the join chain to see if it can be trimmed
|
# 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, last, False)
|
||||||
|
@ -1114,8 +1114,8 @@ class Query(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
field, target, opts, join_list, last, extra_filters = self.setup_joins(
|
field, target, opts, join_list, last, extra_filters = self.setup_joins(
|
||||||
parts, opts, alias, True, allow_many, allow_explicit_fk=True,
|
parts, opts, alias, can_reuse, allow_many,
|
||||||
can_reuse=can_reuse, negate=negate,
|
allow_explicit_fk=True, negate=negate,
|
||||||
process_extras=process_extras)
|
process_extras=process_extras)
|
||||||
except MultiJoin as e:
|
except MultiJoin as e:
|
||||||
self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]),
|
self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]),
|
||||||
|
@ -1268,9 +1268,8 @@ class Query(object):
|
||||||
if self.filter_is_sticky:
|
if self.filter_is_sticky:
|
||||||
self.used_aliases = used_aliases
|
self.used_aliases = used_aliases
|
||||||
|
|
||||||
def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
|
def setup_joins(self, names, opts, alias, can_reuse, allow_many=True,
|
||||||
allow_explicit_fk=False, can_reuse=None, negate=False,
|
allow_explicit_fk=False, negate=False, process_extras=True):
|
||||||
process_extras=True):
|
|
||||||
"""
|
"""
|
||||||
Compute the necessary table joins for the passage through the fields
|
Compute the necessary table joins for the passage through the fields
|
||||||
given in 'names'. 'opts' is the Options class for the current model
|
given in 'names'. 'opts' is the Options class for the current model
|
||||||
|
@ -1290,14 +1289,9 @@ class Query(object):
|
||||||
"""
|
"""
|
||||||
joins = [alias]
|
joins = [alias]
|
||||||
last = [0]
|
last = [0]
|
||||||
dupe_set = set()
|
|
||||||
exclusions = set()
|
|
||||||
extra_filters = []
|
extra_filters = []
|
||||||
int_alias = None
|
int_alias = None
|
||||||
for pos, name in enumerate(names):
|
for pos, name in enumerate(names):
|
||||||
if int_alias is not None:
|
|
||||||
exclusions.add(int_alias)
|
|
||||||
exclusions.add(alias)
|
|
||||||
last.append(len(joins))
|
last.append(len(joins))
|
||||||
if name == 'pk':
|
if name == 'pk':
|
||||||
name = opts.pk.name
|
name = opts.pk.name
|
||||||
|
@ -1330,28 +1324,12 @@ class Query(object):
|
||||||
opts = int_model._meta
|
opts = int_model._meta
|
||||||
else:
|
else:
|
||||||
lhs_col = opts.parents[int_model].column
|
lhs_col = opts.parents[int_model].column
|
||||||
dedupe = lhs_col in opts.duplicate_targets
|
|
||||||
if dedupe:
|
|
||||||
exclusions.update(self.dupe_avoidance.get(
|
|
||||||
(id(opts), lhs_col), ()))
|
|
||||||
dupe_set.add((opts, lhs_col))
|
|
||||||
opts = int_model._meta
|
opts = int_model._meta
|
||||||
alias = self.join((alias, opts.db_table, lhs_col,
|
alias = self.join((alias, opts.db_table, lhs_col,
|
||||||
opts.pk.column), exclusions=exclusions)
|
opts.pk.column))
|
||||||
joins.append(alias)
|
joins.append(alias)
|
||||||
exclusions.add(alias)
|
|
||||||
for (dupe_opts, dupe_col) in dupe_set:
|
|
||||||
self.update_dupe_avoidance(dupe_opts, dupe_col,
|
|
||||||
alias)
|
|
||||||
cached_data = opts._join_cache.get(name)
|
cached_data = opts._join_cache.get(name)
|
||||||
orig_opts = opts
|
orig_opts = opts
|
||||||
dupe_col = direct and field.column or field.field.column
|
|
||||||
dedupe = dupe_col in opts.duplicate_targets
|
|
||||||
if dupe_set or dedupe:
|
|
||||||
if dedupe:
|
|
||||||
dupe_set.add((opts, dupe_col))
|
|
||||||
exclusions.update(self.dupe_avoidance.get((id(opts), dupe_col),
|
|
||||||
()))
|
|
||||||
|
|
||||||
if process_extras and hasattr(field, 'extra_filters'):
|
if process_extras and hasattr(field, 'extra_filters'):
|
||||||
extra_filters.extend(field.extra_filters(names, pos, negate))
|
extra_filters.extend(field.extra_filters(names, pos, negate))
|
||||||
|
@ -1377,16 +1355,14 @@ class Query(object):
|
||||||
target)
|
target)
|
||||||
|
|
||||||
int_alias = self.join((alias, table1, from_col1, to_col1),
|
int_alias = self.join((alias, table1, from_col1, to_col1),
|
||||||
dupe_multis, exclusions, nullable=True,
|
reuse=can_reuse, nullable=True)
|
||||||
reuse=can_reuse)
|
|
||||||
if int_alias == table2 and from_col2 == to_col2:
|
if int_alias == table2 and from_col2 == to_col2:
|
||||||
joins.append(int_alias)
|
joins.append(int_alias)
|
||||||
alias = int_alias
|
alias = int_alias
|
||||||
else:
|
else:
|
||||||
alias = self.join(
|
alias = self.join(
|
||||||
(int_alias, table2, from_col2, to_col2),
|
(int_alias, table2, from_col2, to_col2),
|
||||||
dupe_multis, exclusions, nullable=True,
|
reuse=can_reuse, nullable=True)
|
||||||
reuse=can_reuse)
|
|
||||||
joins.extend([int_alias, alias])
|
joins.extend([int_alias, alias])
|
||||||
elif field.rel:
|
elif field.rel:
|
||||||
# One-to-one or many-to-one field
|
# One-to-one or many-to-one field
|
||||||
|
@ -1402,7 +1378,6 @@ class Query(object):
|
||||||
opts, target)
|
opts, target)
|
||||||
|
|
||||||
alias = self.join((alias, table, from_col, to_col),
|
alias = self.join((alias, table, from_col, to_col),
|
||||||
exclusions=exclusions,
|
|
||||||
nullable=self.is_nullable(field))
|
nullable=self.is_nullable(field))
|
||||||
joins.append(alias)
|
joins.append(alias)
|
||||||
else:
|
else:
|
||||||
|
@ -1433,11 +1408,9 @@ class Query(object):
|
||||||
target)
|
target)
|
||||||
|
|
||||||
int_alias = self.join((alias, table1, from_col1, to_col1),
|
int_alias = self.join((alias, table1, from_col1, to_col1),
|
||||||
dupe_multis, exclusions, nullable=True,
|
reuse=can_reuse, nullable=True)
|
||||||
reuse=can_reuse)
|
|
||||||
alias = self.join((int_alias, table2, from_col2, to_col2),
|
alias = self.join((int_alias, table2, from_col2, to_col2),
|
||||||
dupe_multis, exclusions, nullable=True,
|
reuse=can_reuse, nullable=True)
|
||||||
reuse=can_reuse)
|
|
||||||
joins.extend([int_alias, alias])
|
joins.extend([int_alias, alias])
|
||||||
else:
|
else:
|
||||||
# One-to-many field (ForeignKey defined on the target model)
|
# One-to-many field (ForeignKey defined on the target model)
|
||||||
|
@ -1461,17 +1434,9 @@ class Query(object):
|
||||||
opts, target)
|
opts, target)
|
||||||
|
|
||||||
alias = self.join((alias, table, from_col, to_col),
|
alias = self.join((alias, table, from_col, to_col),
|
||||||
dupe_multis, exclusions, nullable=True,
|
reuse=can_reuse, nullable=True)
|
||||||
reuse=can_reuse)
|
|
||||||
joins.append(alias)
|
joins.append(alias)
|
||||||
|
|
||||||
for (dupe_opts, dupe_col) in dupe_set:
|
|
||||||
if int_alias is None:
|
|
||||||
to_avoid = alias
|
|
||||||
else:
|
|
||||||
to_avoid = int_alias
|
|
||||||
self.update_dupe_avoidance(dupe_opts, dupe_col, to_avoid)
|
|
||||||
|
|
||||||
if pos != len(names) - 1:
|
if pos != len(names) - 1:
|
||||||
if pos == len(names) - 2:
|
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]))
|
||||||
|
@ -1538,19 +1503,6 @@ class Query(object):
|
||||||
penultimate = last.pop()
|
penultimate = last.pop()
|
||||||
return col, alias, join_list
|
return col, alias, join_list
|
||||||
|
|
||||||
def update_dupe_avoidance(self, opts, col, alias):
|
|
||||||
"""
|
|
||||||
For a column that is one of multiple pointing to the same table, update
|
|
||||||
the internal data structures to note that this alias shouldn't be used
|
|
||||||
for those other columns.
|
|
||||||
"""
|
|
||||||
ident = id(opts)
|
|
||||||
for name in opts.duplicate_targets[col]:
|
|
||||||
try:
|
|
||||||
self.dupe_avoidance[ident, name].add(alias)
|
|
||||||
except KeyError:
|
|
||||||
self.dupe_avoidance[ident, name] = set([alias])
|
|
||||||
|
|
||||||
def split_exclude(self, filter_expr, prefix, can_reuse):
|
def split_exclude(self, filter_expr, prefix, can_reuse):
|
||||||
"""
|
"""
|
||||||
When doing an exclude against any kind of N-to-many relation, we need
|
When doing an exclude against any kind of N-to-many relation, we need
|
||||||
|
@ -1657,8 +1609,8 @@ class Query(object):
|
||||||
try:
|
try:
|
||||||
for name in field_names:
|
for name in field_names:
|
||||||
field, target, u2, joins, u3, u4 = self.setup_joins(
|
field, target, u2, joins, u3, u4 = self.setup_joins(
|
||||||
name.split(LOOKUP_SEP), opts, alias, False, allow_m2m,
|
name.split(LOOKUP_SEP), opts, alias, REUSE_ALL,
|
||||||
True)
|
allow_m2m, True)
|
||||||
final_alias = joins[-1]
|
final_alias = joins[-1]
|
||||||
col = target.column
|
col = target.column
|
||||||
if len(joins) > 1:
|
if len(joins) > 1:
|
||||||
|
@ -1948,7 +1900,7 @@ class Query(object):
|
||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
alias = self.get_initial_alias()
|
alias = self.get_initial_alias()
|
||||||
field, col, opts, joins, last, extra = self.setup_joins(
|
field, col, opts, joins, last, extra = self.setup_joins(
|
||||||
start.split(LOOKUP_SEP), opts, alias, False)
|
start.split(LOOKUP_SEP), opts, alias, REUSE_ALL)
|
||||||
select_col = self.alias_map[joins[1]].lhs_join_col
|
select_col = self.alias_map[joins[1]].lhs_join_col
|
||||||
select_alias = alias
|
select_alias = alias
|
||||||
|
|
||||||
|
|
|
@ -1046,6 +1046,18 @@ class Queries4Tests(BaseQuerysetTest):
|
||||||
self.assertQuerysetEqual(q1, ["<Item: i1>"])
|
self.assertQuerysetEqual(q1, ["<Item: i1>"])
|
||||||
self.assertEqual(str(q1.query), str(q2.query))
|
self.assertEqual(str(q1.query), str(q2.query))
|
||||||
|
|
||||||
|
def test_combine_join_reuse(self):
|
||||||
|
# Test that we correctly recreate joins having identical connections
|
||||||
|
# in the rhs query, in case the query is ORed together. Related to
|
||||||
|
# ticket #18748
|
||||||
|
Report.objects.create(name='r4', creator=self.a1)
|
||||||
|
q1 = Author.objects.filter(report__name='r5')
|
||||||
|
q2 = Author.objects.filter(report__name='r4').filter(report__name='r1')
|
||||||
|
combined = q1|q2
|
||||||
|
self.assertEquals(str(combined.query).count('JOIN'), 2)
|
||||||
|
self.assertEquals(len(combined), 1)
|
||||||
|
self.assertEquals(combined[0].name, 'a1')
|
||||||
|
|
||||||
def test_ticket7095(self):
|
def test_ticket7095(self):
|
||||||
# Updates that are filtered on the model being updated are somewhat
|
# Updates that are filtered on the model being updated are somewhat
|
||||||
# tricky in MySQL. This exercises that case.
|
# tricky in MySQL. This exercises that case.
|
||||||
|
|
Loading…
Reference in New Issue