Removed Query.setup_joins() and join() argument outer_if_first.

Instead always create new joins as OUTER.
This commit is contained in:
Anssi Kääriäinen 2013-11-06 21:31:19 +02:00
parent e7b61e5717
commit ba6c9fae45
2 changed files with 16 additions and 37 deletions

View File

@ -453,7 +453,7 @@ class SQLCompiler(object):
def _setup_joins(self, pieces, opts, alias): def _setup_joins(self, pieces, opts, alias):
""" """
A helper method for get_ordering and get_distinct. This method will A helper method for get_ordering and get_distinct. This method will
call query.setup_joins, handle refcounts and then promote the joins. call query.setup_joins and handle refcounts.
Note that get_ordering and get_distinct must produce same target Note that get_ordering and get_distinct must produce same target
columns on same input, as the prefixes of get_ordering and get_distinct columns on same input, as the prefixes of get_ordering and get_distinct
@ -463,20 +463,12 @@ class SQLCompiler(object):
alias = self.query.get_initial_alias() alias = self.query.get_initial_alias()
field, targets, opts, joins, path = self.query.setup_joins( field, targets, opts, joins, path = self.query.setup_joins(
pieces, opts, alias) pieces, opts, alias)
# We will later on need to promote those joins that were added to the
# query afresh above.
joins_to_promote = [j for j in joins if self.query.alias_refcount[j] < 2]
alias = joins[-1] alias = joins[-1]
if not field.rel: if not field.rel:
# To avoid inadvertent trimming of a necessary alias, use the # To avoid inadvertent trimming of a necessary alias, use the
# refcount to show that we are referencing a non-relation field on # refcount to show that we are referencing a non-relation field on
# the model. # the model.
self.query.ref_alias(alias) self.query.ref_alias(alias)
# Must use left outer joins for nullable fields and their relations.
# Ordering or distinct must not affect the returned set, and INNER
# JOINS for nullable fields could do this.
self.query.promote_joins(joins_to_promote)
return field, targets, alias, joins, path, opts return field, targets, alias, joins, path, opts
def get_from_clause(self): def get_from_clause(self):
@ -589,7 +581,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,
requested=None, restricted=None, nullable=None): requested=None, restricted=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
@ -623,9 +615,8 @@ 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
promote = nullable or f.null
_, _, _, joins, _ = self.query.setup_joins( _, _, _, joins, _ = self.query.setup_joins(
[f.name], opts, root_alias, outer_if_first=promote) [f.name], opts, root_alias)
alias = joins[-1] alias = joins[-1]
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)
@ -635,9 +626,8 @@ class SQLCompiler(object):
next = requested.get(f.name, {}) next = requested.get(f.name, {})
else: else:
next = False next = False
new_nullable = f.null or promote
self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1, self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1,
next, restricted, new_nullable) next, restricted)
if restricted: if restricted:
related_fields = [ related_fields = [
@ -651,7 +641,7 @@ class SQLCompiler(object):
continue continue
_, _, _, joins, _ = self.query.setup_joins( _, _, _, joins, _ = self.query.setup_joins(
[f.related_query_name()], opts, root_alias, outer_if_first=True) [f.related_query_name()], opts, root_alias)
alias = joins[-1] alias = joins[-1]
from_parent = (opts.model if issubclass(model, opts.model) from_parent = (opts.model if issubclass(model, opts.model)
else None) else None)
@ -661,11 +651,8 @@ class SQLCompiler(object):
SelectInfo(col, field) for col, field SelectInfo(col, field) for col, field
in zip(columns, model._meta.concrete_fields)) in zip(columns, model._meta.concrete_fields))
next = requested.get(f.related_query_name(), {}) next = requested.get(f.related_query_name(), {})
# Use True here because we are looking at the _reverse_ side of
# the relation, which is always nullable.
new_nullable = True
self.fill_related_selections(model._meta, alias, cur_depth + 1, self.fill_related_selections(model._meta, alias, cur_depth + 1,
next, restricted, new_nullable) next, restricted)
def deferred_to_columns(self): def deferred_to_columns(self):
""" """

View File

@ -491,8 +491,7 @@ class Query(object):
lhs = change_map.get(lhs, lhs) lhs = change_map.get(lhs, lhs)
new_alias = self.join( new_alias = self.join(
(lhs, table, join_cols), reuse=reuse, (lhs, table, join_cols), reuse=reuse,
outer_if_first=True, nullable=nullable, nullable=nullable, join_field=join_field)
join_field=join_field)
if join_type == self.INNER: if join_type == self.INNER:
rhs_votes.add(new_alias) rhs_votes.add(new_alias)
# We can't reuse the same join again in the query. If we have two # We can't reuse the same join again in the query. If we have two
@ -854,8 +853,7 @@ class Query(object):
""" """
return len([1 for count in self.alias_refcount.values() if count]) return len([1 for count in self.alias_refcount.values() if count])
def join(self, connection, reuse=None, outer_if_first=False, def join(self, connection, reuse=None, nullable=False, join_field=None):
nullable=False, join_field=None):
""" """
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
@ -870,11 +868,9 @@ class Query(object):
(matching the connection) are reusable, or it can be a set containing (matching the connection) are reusable, or it can be a set containing
the aliases that can be reused. the aliases that can be reused.
If 'outer_if_first' is True and a new join is created, it will have the
LOUTER join type.
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 t1 LOUTER t2 INNER t3. sure we do not generate chains like t1 LOUTER t2 INNER t3. All new
joins are created as LOUTER if nullable is True.
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.
@ -904,15 +900,13 @@ class Query(object):
# Not all tables need to be joined to anything. No join type # Not all tables need to be joined to anything. No join type
# means the later columns are ignored. # means the later columns are ignored.
join_type = None join_type = None
elif self.alias_map[lhs].join_type == self.LOUTER: elif self.alias_map[lhs].join_type == self.LOUTER or nullable:
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, join_cols or ((None, None),), nullable, join = JoinInfo(table, alias, join_type, lhs, join_cols or ((None, None),), nullable,
join_field) join_field)
self.alias_map[alias] = join self.alias_map[alias] = join
if outer_if_first:
self.promote_joins([alias])
if connection in self.join_map: if connection in self.join_map:
self.join_map[connection] += (alias,) self.join_map[connection] += (alias,)
else: else:
@ -1010,7 +1004,7 @@ class Query(object):
# Join promotion note - we must not remove any rows here, so use # Join promotion note - we must not remove any rows here, so use
# outer join if there isn't any existing join. # outer join if there isn't any existing join.
field, sources, opts, join_list, path = self.setup_joins( field, sources, opts, join_list, path = self.setup_joins(
field_list, opts, self.get_initial_alias(), outer_if_first=True) field_list, opts, self.get_initial_alias())
# Process the join chain to see if it can be trimmed # Process the join chain to see if it can be trimmed
targets, _, join_list = self.trim_joins(sources, join_list, path) targets, _, join_list = self.trim_joins(sources, join_list, path)
@ -1139,7 +1133,7 @@ class Query(object):
try: try:
field, sources, opts, join_list, path = self.setup_joins( field, sources, opts, join_list, path = self.setup_joins(
parts, opts, alias, can_reuse, allow_many, outer_if_first=True) parts, opts, alias, can_reuse, allow_many)
except MultiJoin as e: except MultiJoin as e:
return self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]), return self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]),
can_reuse, e.names_with_path) can_reuse, e.names_with_path)
@ -1343,8 +1337,7 @@ class Query(object):
raise FieldError("Join on field %r not permitted." % name) raise FieldError("Join on field %r not permitted." % name)
return path, final_field, targets return path, final_field, targets
def setup_joins(self, names, opts, alias, can_reuse=None, allow_many=True, def setup_joins(self, names, opts, alias, can_reuse=None, allow_many=True):
outer_if_first=False):
""" """
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
@ -1385,8 +1378,7 @@ class Query(object):
connection = alias, opts.db_table, join.join_field.get_joining_columns() connection = alias, opts.db_table, join.join_field.get_joining_columns()
reuse = can_reuse if join.m2m else None reuse = can_reuse if join.m2m else None
alias = self.join( alias = self.join(
connection, reuse=reuse, nullable=nullable, join_field=join.join_field, connection, reuse=reuse, nullable=nullable, join_field=join.join_field)
outer_if_first=outer_if_first)
joins.append(alias) joins.append(alias)
if hasattr(final_field, 'field'): if hasattr(final_field, 'field'):
final_field = final_field.field final_field = final_field.field
@ -1561,7 +1553,7 @@ class Query(object):
# if there is no existing joins, use outer join. # if there is no existing joins, use outer join.
field, targets, u2, joins, path = self.setup_joins( field, targets, u2, joins, path = self.setup_joins(
name.split(LOOKUP_SEP), opts, alias, can_reuse=None, name.split(LOOKUP_SEP), opts, alias, can_reuse=None,
allow_many=allow_m2m, outer_if_first=True) allow_many=allow_m2m)
targets, final_alias, joins = self.trim_joins(targets, joins, path) targets, final_alias, joins = self.trim_joins(targets, joins, path)
for target in targets: for target in targets:
self.select.append(SelectInfo((final_alias, target.column), target)) self.select.append(SelectInfo((final_alias, target.column), target))