Fixed #23853 -- Added Join class to replace JoinInfo

Also removed Query.join_map. This structure was used to speed up join
reuse calculation. Initial benchmarking shows that this isn't actually
needed. If there are use cases where the removal has real-world
performance implications, it should be relatively straightforward to
reintroduce it as map {alias: [Join-like objects]}.
This commit is contained in:
Anssi Kääriäinen 2014-11-17 10:26:10 +02:00 committed by Tim Graham
parent c7175fcdfe
commit ab89414f40
5 changed files with 189 additions and 131 deletions

View File

@ -37,7 +37,7 @@ class SQLCompiler(object):
# cleaned. We are not using a clone() of the query here. # cleaned. We are not using a clone() of the query here.
""" """
if not self.query.tables: if not self.query.tables:
self.query.join((None, self.query.get_meta().db_table, None)) self.query.get_initial_alias()
if (not self.query.select and self.query.default_cols and not if (not self.query.select and self.query.default_cols and not
self.query.included_inherited_models): self.query.included_inherited_models):
self.query.setup_inherited_models() self.query.setup_inherited_models()
@ -171,7 +171,6 @@ class SQLCompiler(object):
# Finally do cleanup - get rid of the joins we created above. # Finally do cleanup - get rid of the joins we created above.
self.query.reset_refcounts(refcounts_before) self.query.reset_refcounts(refcounts_before)
return ' '.join(result), tuple(params) return ' '.join(result), tuple(params)
def as_nested_sql(self): def as_nested_sql(self):
@ -511,51 +510,27 @@ class SQLCompiler(object):
ordering and distinct must be done first. ordering and distinct must be done first.
""" """
result = [] result = []
qn = self.quote_name_unless_alias params = []
qn2 = self.connection.ops.quote_name
first = True
from_params = []
for alias in self.query.tables: for alias in self.query.tables:
if not self.query.alias_refcount[alias]: if not self.query.alias_refcount[alias]:
continue continue
try: try:
name, alias, join_type, lhs, join_cols, _, join_field = self.query.alias_map[alias] from_clause = self.query.alias_map[alias]
except KeyError: except KeyError:
# Extra tables can end up in self.tables, but not in the # Extra tables can end up in self.tables, but not in the
# alias_map if they aren't in a join. That's OK. We skip them. # alias_map if they aren't in a join. That's OK. We skip them.
continue continue
alias_str = '' if alias == name else (' %s' % alias) clause_sql, clause_params = self.compile(from_clause)
if join_type and not first: result.append(clause_sql)
extra_cond = join_field.get_extra_restriction( params.extend(clause_params)
self.query.where_class, alias, lhs)
if extra_cond:
extra_sql, extra_params = self.compile(extra_cond)
extra_sql = 'AND (%s)' % extra_sql
from_params.extend(extra_params)
else:
extra_sql = ""
result.append('%s %s%s ON ('
% (join_type, qn(name), alias_str))
for index, (lhs_col, rhs_col) in enumerate(join_cols):
if index != 0:
result.append(' AND ')
result.append('%s.%s = %s.%s' %
(qn(lhs), qn2(lhs_col), qn(alias), qn2(rhs_col)))
result.append('%s)' % extra_sql)
else:
connector = '' if first else ', '
result.append('%s%s%s' % (connector, qn(name), alias_str))
first = False
for t in self.query.extra_tables: for t in self.query.extra_tables:
alias, _ = self.query.table_alias(t) alias, _ = self.query.table_alias(t)
# Only add the alias if it's not already present (the table_alias() # Only add the alias if it's not already present (the table_alias()
# calls increments the refcount, so an alias refcount of one means # call increments the refcount, so an alias refcount of one means
# this is the only reference. # this is the only reference).
if alias not in self.query.alias_map or self.query.alias_refcount[alias] == 1: if alias not in self.query.alias_map or self.query.alias_refcount[alias] == 1:
connector = '' if first else ', ' result.append(', %s' % self.quote_name_unless_alias(alias))
result.append('%s%s' % (connector, qn(alias))) return result, params
first = False
return result, from_params
def get_grouping(self, having_group_by, ordering_group_by): def get_grouping(self, having_group_by, ordering_group_by):
""" """

View File

@ -21,12 +21,6 @@ GET_ITERATOR_CHUNK_SIZE = 100
# Namedtuples for sql.* internal use. # Namedtuples for sql.* internal use.
# Join lists (indexes into the tuples that are values in the alias_map
# dictionary in the Query class).
JoinInfo = namedtuple('JoinInfo',
'table_name rhs_alias join_type lhs_alias '
'join_cols nullable join_field')
# Pairs of column clauses to select, and (possibly None) field for the clause. # Pairs of column clauses to select, and (possibly None) field for the clause.
SelectInfo = namedtuple('SelectInfo', 'col field') SelectInfo = namedtuple('SelectInfo', 'col field')
@ -41,3 +35,7 @@ ORDER_DIR = {
'ASC': ('ASC', 'DESC'), 'ASC': ('ASC', 'DESC'),
'DESC': ('DESC', 'ASC'), 'DESC': ('DESC', 'ASC'),
} }
# SQL join types.
INNER = 'INNER JOIN'
LOUTER = 'LEFT OUTER JOIN'

View File

@ -2,6 +2,7 @@
Useful auxiliary data structures for query construction. Not useful outside Useful auxiliary data structures for query construction. Not useful outside
the SQL domain. the SQL domain.
""" """
from django.db.models.sql.constants import INNER, LOUTER
class EmptyResultSet(Exception): class EmptyResultSet(Exception):
@ -22,3 +23,119 @@ class MultiJoin(Exception):
class Empty(object): class Empty(object):
pass pass
class Join(object):
"""
Used by sql.Query and sql.SQLCompiler to generate JOIN clauses into the
FROM entry. For example, the SQL generated could be
LEFT OUTER JOIN "sometable" T1 ON ("othertable"."sometable_id" = "sometable"."id")
This class is primarily used in Query.alias_map. All entries in alias_map
must be Join compatible by providing the following attributes and methods:
- table_name (string)
- table_alias (possible alias for the table, can be None)
- join_type (can be None for those entries that aren't joined from
anything)
- parent_alias (which table is this join's parent, can be None similarly
to join_type)
- as_sql()
- relabeled_clone()
"""
def __init__(self, table_name, parent_alias, table_alias, join_type,
join_field, nullable):
# Join table
self.table_name = table_name
self.parent_alias = parent_alias
# Note: table_alias is not necessarily known at instantiation time.
self.table_alias = table_alias
# LOUTER or INNER
self.join_type = join_type
# A list of 2-tuples to use in the ON clause of the JOIN.
# Each 2-tuple will create one join condition in the ON clause.
self.join_cols = join_field.get_joining_columns()
# Along which field (or RelatedObject in the reverse join case)
self.join_field = join_field
# Is this join nullabled?
self.nullable = nullable
def as_sql(self, compiler, connection):
"""
Generates the full
LEFT OUTER JOIN sometable ON sometable.somecol = othertable.othercol, params
clause for this join.
"""
params = []
sql = []
alias_str = '' if self.table_alias == self.table_name else (' %s' % self.table_alias)
qn = compiler.quote_name_unless_alias
qn2 = connection.ops.quote_name
sql.append('%s %s%s ON (' % (self.join_type, qn(self.table_name), alias_str))
for index, (lhs_col, rhs_col) in enumerate(self.join_cols):
if index != 0:
sql.append(' AND ')
sql.append('%s.%s = %s.%s' % (
qn(self.parent_alias),
qn2(lhs_col),
qn(self.table_alias),
qn2(rhs_col),
))
extra_cond = self.join_field.get_extra_restriction(
compiler.query.where_class, self.table_alias, self.parent_alias)
if extra_cond:
extra_sql, extra_params = compiler.compile(extra_cond)
extra_sql = 'AND (%s)' % extra_sql
params.extend(extra_params)
sql.append('%s' % extra_sql)
sql.append(')')
return ' '.join(sql), params
def relabeled_clone(self, change_map):
new_parent_alias = change_map.get(self.parent_alias, self.parent_alias)
new_table_alias = change_map.get(self.table_alias, self.table_alias)
return self.__class__(
self.table_name, new_parent_alias, new_table_alias, self.join_type,
self.join_field, self.nullable)
def __eq__(self, other):
if isinstance(other, self.__class__):
return (
self.table_name == other.table_name and
self.parent_alias == other.parent_alias and
self.join_field == other.join_field
)
return False
def demote(self):
new = self.relabeled_clone({})
new.join_type = INNER
return new
def promote(self):
new = self.relabeled_clone({})
new.join_type = LOUTER
return new
class BaseTable(object):
"""
The BaseTable class is used for base table references in FROM clause. For
example, the SQL "foo" in
SELECT * FROM "foo" WHERE somecond
could be generated by this class.
"""
join_type = None
parent_alias = None
def __init__(self, table_name, alias):
self.table_name = table_name
self.table_alias = alias
def as_sql(self, compiler, connection):
alias_str = '' if self.table_alias == self.table_name else (' %s' % self.table_alias)
base_sql = compiler.quote_name_unless_alias(self.table_name)
return base_sql + alias_str, []
def relabeled_clone(self, change_map):
return self.__class__(self.table_name, change_map.get(self.table_alias, self.table_alias))

View File

@ -20,8 +20,9 @@ from django.db.models.query_utils import Q, refs_aggregate
from django.db.models.related import PathInfo from django.db.models.related import PathInfo
from django.db.models.aggregates import Count from django.db.models.aggregates import Count
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, SelectInfo, INNER, LOUTER)
from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin from django.db.models.sql.datastructures import (
EmptyResultSet, Empty, MultiJoin, Join, BaseTable)
from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode, from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode,
ExtraWhere, AND, OR, EmptyWhere) ExtraWhere, AND, OR, EmptyWhere)
from django.utils import six from django.utils import six
@ -87,10 +88,6 @@ class Query(object):
""" """
A single SQL query. A single SQL query.
""" """
# SQL join types. These are part of the class because their string forms
# vary from database to database and can be customised by a subclass.
INNER = 'INNER JOIN'
LOUTER = 'LEFT OUTER JOIN'
alias_prefix = 'T' alias_prefix = 'T'
subq_aliases = frozenset([alias_prefix]) subq_aliases = frozenset([alias_prefix])
@ -103,15 +100,15 @@ class Query(object):
self.alias_refcount = {} self.alias_refcount = {}
# alias_map is the most important data structure regarding joins. # alias_map is the most important data structure regarding joins.
# It's used for recording which joins exist in the query and what # It's used for recording which joins exist in the query and what
# type they are. The key is the alias of the joined table (possibly # types they are. The key is the alias of the joined table (possibly
# the table name) and the value is JoinInfo from constants.py. # the table name) and the value is a Join-like object (see
# sql.datastructures.Join for more information).
self.alias_map = {} self.alias_map = {}
# Sometimes the query contains references to aliases in outer queries (as # Sometimes the query contains references to aliases in outer queries (as
# a result of split_exclude). Correct alias quoting needs to know these # a result of split_exclude). Correct alias quoting needs to know these
# aliases too. # aliases too.
self.external_aliases = set() self.external_aliases = set()
self.table_map = {} # Maps table names to list of aliases. self.table_map = {} # Maps table names to list of aliases.
self.join_map = {}
self.default_cols = True self.default_cols = True
self.default_ordering = True self.default_ordering = True
self.standard_ordering = True self.standard_ordering = True
@ -246,7 +243,6 @@ class Query(object):
obj.alias_map = self.alias_map.copy() obj.alias_map = self.alias_map.copy()
obj.external_aliases = self.external_aliases.copy() obj.external_aliases = self.external_aliases.copy()
obj.table_map = self.table_map.copy() obj.table_map = self.table_map.copy()
obj.join_map = self.join_map.copy()
obj.default_cols = self.default_cols obj.default_cols = self.default_cols
obj.default_ordering = self.default_ordering obj.default_ordering = self.default_ordering
obj.standard_ordering = self.standard_ordering obj.standard_ordering = self.standard_ordering
@ -495,19 +491,17 @@ class Query(object):
self.get_initial_alias() self.get_initial_alias()
joinpromoter = JoinPromoter(connector, 2, False) joinpromoter = JoinPromoter(connector, 2, False)
joinpromoter.add_votes( joinpromoter.add_votes(
j for j in self.alias_map if self.alias_map[j].join_type == self.INNER) j for j in self.alias_map if self.alias_map[j].join_type == INNER)
rhs_votes = set() rhs_votes = set()
# Now, add the joins from rhs query into the new query (skipping base # Now, add the joins from rhs query into the new query (skipping base
# table). # table).
for alias in rhs.tables[1:]: for alias in rhs.tables[1:]:
table, _, join_type, lhs, join_cols, nullable, join_field = rhs.alias_map[alias] join = rhs.alias_map[alias]
# 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) join = join.relabeled_clone(change_map)
new_alias = self.join( new_alias = self.join(join, reuse=reuse)
(lhs, table, join_cols), reuse=reuse, if join.join_type == INNER:
nullable=nullable, join_field=join_field)
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
# distinct joins for the same connection in rhs query, then the # distinct joins for the same connection in rhs query, then the
@ -714,27 +708,26 @@ class Query(object):
aliases = list(aliases) aliases = list(aliases)
while aliases: while aliases:
alias = aliases.pop(0) alias = aliases.pop(0)
if self.alias_map[alias].join_cols[0][1] is None: if self.alias_map[alias].join_type is None:
# This is the base table (first FROM entry) - this table # This is the base table (first FROM entry) - this table
# isn't really joined at all in the query, so we should not # isn't really joined at all in the query, so we should not
# alter its join type. # alter its join type.
continue continue
# Only the first alias (skipped above) should have None join_type # Only the first alias (skipped above) should have None join_type
assert self.alias_map[alias].join_type is not None assert self.alias_map[alias].join_type is not None
parent_alias = self.alias_map[alias].lhs_alias parent_alias = self.alias_map[alias].parent_alias
parent_louter = ( parent_louter = (
parent_alias parent_alias
and self.alias_map[parent_alias].join_type == self.LOUTER) and self.alias_map[parent_alias].join_type == LOUTER)
already_louter = self.alias_map[alias].join_type == self.LOUTER already_louter = self.alias_map[alias].join_type == LOUTER
if ((self.alias_map[alias].nullable or parent_louter) and if ((self.alias_map[alias].nullable or parent_louter) and
not already_louter): not already_louter):
data = self.alias_map[alias]._replace(join_type=self.LOUTER) self.alias_map[alias] = self.alias_map[alias].promote()
self.alias_map[alias] = data
# Join type of 'alias' changed, so re-examine all aliases that # Join type of 'alias' changed, so re-examine all aliases that
# refer to this one. # refer to this one.
aliases.extend( aliases.extend(
join for join in self.alias_map.keys() join for join in self.alias_map.keys()
if (self.alias_map[join].lhs_alias == alias if (self.alias_map[join].parent_alias == alias
and join not in aliases)) and join not in aliases))
def demote_joins(self, aliases): def demote_joins(self, aliases):
@ -750,10 +743,10 @@ class Query(object):
aliases = list(aliases) aliases = list(aliases)
while aliases: while aliases:
alias = aliases.pop(0) alias = aliases.pop(0)
if self.alias_map[alias].join_type == self.LOUTER: if self.alias_map[alias].join_type == LOUTER:
self.alias_map[alias] = self.alias_map[alias]._replace(join_type=self.INNER) self.alias_map[alias] = self.alias_map[alias].demote()
parent_alias = self.alias_map[alias].lhs_alias parent_alias = self.alias_map[alias].parent_alias
if self.alias_map[parent_alias].join_type == self.INNER: if self.alias_map[parent_alias].join_type == INNER:
aliases.append(parent_alias) aliases.append(parent_alias)
def reset_refcounts(self, to_counts): def reset_refcounts(self, to_counts):
@ -792,19 +785,13 @@ class Query(object):
(key, relabel_column(col)) for key, col in self._annotations.items()) (key, relabel_column(col)) for key, col in self._annotations.items())
# 2. Rename the alias in the internal table/alias datastructures. # 2. Rename the alias in the internal table/alias datastructures.
for ident, aliases in self.join_map.items():
del self.join_map[ident]
aliases = tuple(change_map.get(a, a) for a in 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.get(old_alias) if old_alias not in self.alias_map:
if alias_data is None:
continue continue
alias_data = alias_data._replace(rhs_alias=new_alias) alias_data = self.alias_map[old_alias].relabeled_clone(change_map)
self.alias_map[new_alias] = alias_data
self.alias_refcount[new_alias] = self.alias_refcount[old_alias] self.alias_refcount[new_alias] = self.alias_refcount[old_alias]
del self.alias_refcount[old_alias] del self.alias_refcount[old_alias]
self.alias_map[new_alias] = alias_data
del self.alias_map[old_alias] del self.alias_map[old_alias]
table_aliases = self.table_map[alias_data.table_name] table_aliases = self.table_map[alias_data.table_name]
@ -819,14 +806,6 @@ class Query(object):
for key, alias in self.included_inherited_models.items(): for key, alias in self.included_inherited_models.items():
if alias in change_map: if alias in change_map:
self.included_inherited_models[key] = change_map[alias] self.included_inherited_models[key] = change_map[alias]
# 3. Update any joins that refer to the old alias.
for alias, data in six.iteritems(self.alias_map):
lhs = data.lhs_alias
if lhs in change_map:
data = data._replace(lhs_alias=change_map[lhs])
self.alias_map[alias] = data
self.external_aliases = {change_map.get(alias, alias) self.external_aliases = {change_map.get(alias, alias)
for alias in self.external_aliases} for alias in self.external_aliases}
@ -862,7 +841,7 @@ class Query(object):
alias = self.tables[0] alias = self.tables[0]
self.ref_alias(alias) self.ref_alias(alias)
else: else:
alias = self.join((None, self.get_meta().db_table, None)) alias = self.join(BaseTable(self.get_meta().db_table, None))
return alias return alias
def count_active_tables(self): def count_active_tables(self):
@ -873,7 +852,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, nullable=False, join_field=None): def join(self, join, reuse=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
@ -897,40 +876,22 @@ class Query(object):
The 'join_field' is the field we are joining along (if any). The 'join_field' is the field we are joining along (if any).
""" """
lhs, table, join_cols = connection reuse = [a for a, j in self.alias_map.items()
assert lhs is None or join_field is not None if (reuse is None or a in reuse) and j == join]
existing = self.join_map.get(connection, ()) if reuse:
if reuse is None: self.ref_alias(reuse[0])
reuse = existing return reuse[0]
else:
reuse = [a for a in existing if a in reuse]
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)
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, create=True) alias, _ = self.table_alias(join.table_name, create=True)
if not lhs: if join.join_type:
# Not all tables need to be joined to anything. No join type if self.alias_map[join.parent_alias].join_type == LOUTER or join.nullable:
# means the later columns are ignored. join_type = LOUTER
join_type = None else:
elif self.alias_map[lhs].join_type == self.LOUTER or nullable: join_type = INNER
join_type = self.LOUTER join.join_type = join_type
else: join.table_alias = alias
join_type = self.INNER
join = JoinInfo(table, alias, join_type, lhs, join_cols or ((None, None),), nullable,
join_field)
self.alias_map[alias] = join self.alias_map[alias] = join
if connection in self.join_map:
self.join_map[connection] += (alias,)
else:
self.join_map[connection] = (alias,)
return alias return alias
def setup_inherited_models(self): def setup_inherited_models(self):
@ -1249,7 +1210,7 @@ class Query(object):
require_outer = True require_outer = True
if (lookup_type != 'isnull' and ( if (lookup_type != 'isnull' and (
self.is_nullable(targets[0]) or self.is_nullable(targets[0]) or
self.alias_map[join_list[-1]].join_type == self.LOUTER)): self.alias_map[join_list[-1]].join_type == LOUTER)):
# The condition added here will be SQL like this: # The condition added here will be SQL like this:
# NOT (col IS NOT NULL), where the first NOT is added in # NOT (col IS NOT NULL), where the first NOT is added in
# upper layers of code. The reason for addition is that if col # upper layers of code. The reason for addition is that if col
@ -1326,7 +1287,7 @@ class Query(object):
# rel_a doesn't produce any rows, then the whole condition must fail. # rel_a doesn't produce any rows, then the whole condition must fail.
# So, demotion is OK. # So, demotion is OK.
existing_inner = set( existing_inner = set(
(a for a in self.alias_map if self.alias_map[a].join_type == self.INNER)) (a for a in self.alias_map if self.alias_map[a].join_type == INNER))
clause, require_inner = self._add_q(where_part, self.used_aliases) clause, require_inner = self._add_q(where_part, self.used_aliases)
self.where.add(clause, AND) self.where.add(clause, AND)
for hp in having_parts: for hp in having_parts:
@ -1490,10 +1451,9 @@ class Query(object):
nullable = self.is_nullable(join.join_field) nullable = self.is_nullable(join.join_field)
else: else:
nullable = True nullable = True
connection = alias, opts.db_table, join.join_field.get_joining_columns() connection = Join(opts.db_table, alias, None, INNER, join.join_field, nullable)
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)
connection, reuse=reuse, nullable=nullable, join_field=join.join_field)
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
@ -1991,9 +1951,10 @@ class Query(object):
for trimmed_paths, path in enumerate(all_paths): for trimmed_paths, path in enumerate(all_paths):
if path.m2m: if path.m2m:
break break
if self.alias_map[lookup_tables[trimmed_paths + 1]].join_type == self.LOUTER: if self.alias_map[lookup_tables[trimmed_paths + 1]].join_type == LOUTER:
contains_louter = True contains_louter = True
self.unref_alias(lookup_tables[trimmed_paths]) alias = lookup_tables[trimmed_paths]
self.unref_alias(alias)
# The path.join_field is a Rel, lets get the other side's field # The path.join_field is a Rel, lets get the other side's field
join_field = path.join_field.field join_field = path.join_field.field
# Build the filter prefix. # Build the filter prefix.
@ -2010,7 +1971,7 @@ class Query(object):
# Lets still see if we can trim the first join from the inner query # Lets still see if we can trim the first join from the inner query
# (that is, self). We can't do this for LEFT JOINs because we would # (that is, self). We can't do this for LEFT JOINs because we would
# miss those rows that have nothing on the outer side. # miss those rows that have nothing on the outer side.
if self.alias_map[lookup_tables[trimmed_paths + 1]].join_type != self.LOUTER: if self.alias_map[lookup_tables[trimmed_paths + 1]].join_type != LOUTER:
select_fields = [r[0] for r in join_field.related_fields] select_fields = [r[0] for r in join_field.related_fields]
select_alias = lookup_tables[trimmed_paths + 1] select_alias = lookup_tables[trimmed_paths + 1]
self.unref_alias(lookup_tables[trimmed_paths]) self.unref_alias(lookup_tables[trimmed_paths])
@ -2024,6 +1985,12 @@ class Query(object):
# values in select_fields. Lets punt this one for now. # values in select_fields. Lets punt this one for now.
select_fields = [r[1] for r in join_field.related_fields] select_fields = [r[1] for r in join_field.related_fields]
select_alias = lookup_tables[trimmed_paths] select_alias = lookup_tables[trimmed_paths]
# The found starting point is likely a Join instead of a BaseTable reference.
# But the first entry in the query's FROM clause must not be a JOIN.
for table in self.tables:
if self.alias_refcount[table] > 0:
self.alias_map[table] = BaseTable(self.alias_map[table].table_name, table)
break
self.select = [SelectInfo((select_alias, f.column), f) for f in select_fields] self.select = [SelectInfo((select_alias, f.column), f) for f in select_fields]
return trimmed_prefix, contains_louter return trimmed_prefix, contains_louter

View File

@ -11,6 +11,7 @@ from django.core.exceptions import FieldError
from django.db import connection, DEFAULT_DB_ALIAS from django.db import connection, DEFAULT_DB_ALIAS
from django.db.models import Count, F, Q from django.db.models import Count, F, Q
from django.db.models.sql.where import WhereNode, EverythingNode, NothingNode from django.db.models.sql.where import WhereNode, EverythingNode, NothingNode
from django.db.models.sql.constants import LOUTER
from django.db.models.sql.datastructures import EmptyResultSet from django.db.models.sql.datastructures import EmptyResultSet
from django.test import TestCase, skipUnlessDBFeature from django.test import TestCase, skipUnlessDBFeature
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
@ -128,7 +129,7 @@ class Queries1Tests(BaseQuerysetTest):
def test_ticket2306(self): def test_ticket2306(self):
# Checking that no join types are "left outer" joins. # Checking that no join types are "left outer" joins.
query = Item.objects.filter(tags=self.t2).query query = Item.objects.filter(tags=self.t2).query
self.assertNotIn(query.LOUTER, [x[2] for x in query.alias_map.values()]) self.assertNotIn(LOUTER, [x.join_type for x in query.alias_map.values()])
self.assertQuerysetEqual( self.assertQuerysetEqual(
Item.objects.filter(Q(tags=self.t1)).order_by('name'), Item.objects.filter(Q(tags=self.t1)).order_by('name'),
@ -336,7 +337,7 @@ class Queries1Tests(BaseQuerysetTest):
# Excluding from a relation that cannot be NULL should not use outer joins. # Excluding from a relation that cannot be NULL should not use outer joins.
query = Item.objects.exclude(creator__in=[self.a1, self.a2]).query query = Item.objects.exclude(creator__in=[self.a1, self.a2]).query
self.assertNotIn(query.LOUTER, [x[2] for x in query.alias_map.values()]) self.assertNotIn(LOUTER, [x.join_type for x in query.alias_map.values()])
# Similarly, when one of the joins cannot possibly, ever, involve NULL # Similarly, when one of the joins cannot possibly, ever, involve NULL
# values (Author -> ExtraInfo, in the following), it should never be # values (Author -> ExtraInfo, in the following), it should never be
@ -344,7 +345,7 @@ class Queries1Tests(BaseQuerysetTest):
# involve one "left outer" join (Author -> Item is 0-to-many). # involve one "left outer" join (Author -> Item is 0-to-many).
qs = Author.objects.filter(id=self.a1.id).filter(Q(extra__note=self.n1) | Q(item__note=self.n3)) qs = Author.objects.filter(id=self.a1.id).filter(Q(extra__note=self.n1) | Q(item__note=self.n3))
self.assertEqual( self.assertEqual(
len([x[2] for x in qs.query.alias_map.values() if x[2] == query.LOUTER and qs.query.alias_refcount[x[1]]]), len([x for x in qs.query.alias_map.values() if x.join_type == LOUTER and qs.query.alias_refcount[x.table_alias]]),
1 1
) )
@ -855,7 +856,7 @@ class Queries1Tests(BaseQuerysetTest):
) )
q = Note.objects.filter(Q(extrainfo__author=self.a1) | Q(extrainfo=xx)).query q = Note.objects.filter(Q(extrainfo__author=self.a1) | Q(extrainfo=xx)).query
self.assertEqual( self.assertEqual(
len([x[2] for x in q.alias_map.values() if x[2] == q.LOUTER and q.alias_refcount[x[1]]]), len([x for x in q.alias_map.values() if x.join_type == LOUTER and q.alias_refcount[x.table_alias]]),
1 1
) )