1
0
mirror of https://github.com/django/django.git synced 2025-07-06 18:59:13 +00:00

queryset-refactor: Fixed the way join promotions are done when joining queries (particularly the disjunctive -- 'OR' -- case). This fixes a FIXME and produces better queries.

git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@6958 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2007-12-19 10:58:26 +00:00
parent 519178154b
commit 18adbb6363
2 changed files with 42 additions and 31 deletions

View File

@ -52,6 +52,7 @@ RHS_JOIN_COL = 5
ALIAS_TABLE = 0 ALIAS_TABLE = 0
ALIAS_REFCOUNT = 1 ALIAS_REFCOUNT = 1
ALIAS_JOIN = 2 ALIAS_JOIN = 2
ALIAS_NULLABLE=3
# How many results to expect from a cursor.execute call # How many results to expect from a cursor.execute call
MULTI = 'multi' MULTI = 'multi'
@ -510,8 +511,8 @@ class Query(object):
pieces = name.split(LOOKUP_SEP) pieces = name.split(LOOKUP_SEP)
if not alias: if not alias:
alias = self.join((None, opts.db_table, None, None)) alias = self.join((None, opts.db_table, None, None))
field, target, opts, joins, unused2 = self.setup_joins(pieces, opts, field, target, opts, joins = self.setup_joins(pieces, opts, alias,
alias, False) False)
alias = joins[-1][-1] alias = joins[-1][-1]
col = target.column col = target.column
@ -568,11 +569,19 @@ class Query(object):
self.alias_map[alias][ALIAS_REFCOUNT] -= 1 self.alias_map[alias][ALIAS_REFCOUNT] -= 1
def promote_alias(self, alias): def promote_alias(self, alias):
""" Promotes the join type of an alias to an outer join. """ """
Promotes the join type of an alias to an outer join if it's possible
for the join to contain NULL values on the left.
Returns True if the aliased join was promoted.
"""
if self.alias_map[alias][ALIAS_NULLABLE]:
self.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] = self.LOUTER self.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] = self.LOUTER
return True
return False
def join(self, (lhs, table, lhs_col, col), always_create=False, def join(self, (lhs, table, lhs_col, col), always_create=False,
exclusions=(), promote=False, outer_if_first=False): exclusions=(), promote=False, outer_if_first=False, nullable=False):
""" """
Returns an alias for a join between 'table' and 'lhs' on the given Returns an alias for a join between 'table' and 'lhs' on the given
columns, either reusing an existing alias for that join or creating a columns, either reusing an existing alias for that join or creating a
@ -593,6 +602,9 @@ class Query(object):
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. This is used when joining certain types of querysets
and Q-objects together. and Q-objects together.
If 'nullable' is True, the join can potentially involve NULL values and
is a candidate for promotion (to "left outer") when combining querysets.
""" """
if lhs is None: if lhs is None:
lhs_table = None lhs_table = None
@ -610,7 +622,7 @@ class Query(object):
for alias in aliases: for alias in aliases:
if alias not in exclusions: if alias not in exclusions:
self.ref_alias(alias) self.ref_alias(alias)
if promote: if promote and self.alias_map[alias][ALIAS_NULLABLE]:
self.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] = \ self.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] = \
self.LOUTER self.LOUTER
return alias return alias
@ -631,6 +643,7 @@ class Query(object):
# means the later columns are ignored. # means the later columns are ignored.
join[JOIN_TYPE] = None join[JOIN_TYPE] = None
self.alias_map[alias][ALIAS_JOIN] = join self.alias_map[alias][ALIAS_JOIN] = join
self.alias_map[alias][ALIAS_NULLABLE] = nullable
self.join_map.setdefault(t_ident, []).append(alias) self.join_map.setdefault(t_ident, []).append(alias)
self.rev_join_map[alias] = t_ident self.rev_join_map[alias] = t_ident
return alias return alias
@ -708,8 +721,8 @@ class Query(object):
alias = self.join((None, opts.db_table, None, None)) alias = self.join((None, opts.db_table, None, None))
try: try:
field, target, unused, join_list, nullable = self.setup_joins(parts, field, target, unused, join_list = self.setup_joins(parts, opts,
opts, alias, (connector == AND)) alias, (connector == AND))
except TypeError, e: except TypeError, e:
if len(parts) != 1 or parts[0] not in self.extra_select: if len(parts) != 1 or parts[0] not in self.extra_select:
raise e raise e
@ -752,10 +765,6 @@ class Query(object):
table = table_it.next() table = table_it.next()
if join == table and self.alias_map[join][ALIAS_REFCOUNT] > 1: if join == table and self.alias_map[join][ALIAS_REFCOUNT] > 1:
continue continue
# FIXME: Don't have to promote here (and in the other places in
# this block) if the join isn't nullable. So I should be
# checking this before promoting (avoiding left outer joins is
# important).
self.promote_alias(join) self.promote_alias(join)
if table != join: if table != join:
self.promote_alias(table) self.promote_alias(table)
@ -771,12 +780,10 @@ class Query(object):
if negate: if negate:
flag = False flag = False
for pos, null in enumerate(nullable): for seq in join_list:
if not null: for join in seq:
continue if self.promote_alias(join):
flag = True flag = True
for join in join_list[pos]:
self.promote_alias(join)
self.where.negate() self.where.negate()
if flag: if flag:
self.where.add([alias, col, field, 'isnull', True], OR) self.where.add([alias, col, field, 'isnull', True], OR)
@ -797,13 +804,15 @@ class Query(object):
subtree = True subtree = True
else: else:
subtree = False subtree = False
connector = AND
for child in q_object.children: for child in q_object.children:
if isinstance(child, Node): if isinstance(child, Node):
self.where.start_subtree(q_object.connector) self.where.start_subtree(connector)
self.add_q(child) self.add_q(child)
self.where.end_subtree() self.where.end_subtree()
else: else:
self.add_filter(child, q_object.connector, q_object.negated) self.add_filter(child, connector, q_object.negated)
connector = q_object.connector
if subtree: if subtree:
self.where.end_subtree() self.where.end_subtree()
@ -822,7 +831,6 @@ class Query(object):
can be null. can be null.
""" """
joins = [[alias]] joins = [[alias]]
nullable = [False]
for pos, name in enumerate(names): for pos, name in enumerate(names):
if name == 'pk': if name == 'pk':
name = opts.pk.name name = opts.pk.name
@ -856,11 +864,10 @@ 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) dupe_multis, nullable=True)
alias = self.join((int_alias, table2, from_col2, to_col2), alias = self.join((int_alias, table2, from_col2, to_col2),
dupe_multis) dupe_multis, nullable=True)
joins.append([int_alias, alias]) joins.append([int_alias, alias])
nullable.append(field.null)
elif field.rel: elif field.rel:
# One-to-one or many-to-one field # One-to-one or many-to-one field
if cached_data: if cached_data:
@ -874,16 +881,15 @@ class Query(object):
orig_opts._join_cache[name] = (table, from_col, to_col, orig_opts._join_cache[name] = (table, from_col, to_col,
opts, target) opts, target)
alias = self.join((alias, table, from_col, to_col)) alias = self.join((alias, table, from_col, to_col),
nullable=field.null)
joins.append([alias]) joins.append([alias])
nullable.append(field.null)
else: else:
target = field target = field
break break
else: else:
orig_field = field orig_field = field
field = field.field field = field.field
nullable.append(True)
if m2m: if m2m:
# Many-to-many field defined on the target model. # Many-to-many field defined on the target model.
if cached_data: if cached_data:
@ -903,9 +909,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) dupe_multis, nullable=True)
alias = self.join((int_alias, table2, from_col2, to_col2), alias = self.join((int_alias, table2, from_col2, to_col2),
dupe_multis) dupe_multis, nullable=True)
joins.append([int_alias, alias]) joins.append([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)
@ -923,13 +929,13 @@ 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) dupe_multis, nullable=True)
joins.append([alias]) joins.append([alias])
if pos != len(names) - 1: if pos != len(names) - 1:
raise TypeError("Join on field %r not permitted." % name) raise TypeError("Join on field %r not permitted." % name)
return field, target, opts, joins, nullable return field, target, opts, joins
def set_limits(self, low=None, high=None): def set_limits(self, low=None, high=None):
""" """

View File

@ -308,6 +308,11 @@ True
>>> len(qs.query.alias_map) >>> len(qs.query.alias_map)
3 3
Similarly, when one of the joins cannot possibly, ever, involve NULL values (Author -> ExtraInfo, in the following), it should never be promoted to a left outer join. So hte following query should only involve one "left outer" join (Author -> Item is 0-to-many).
>>> qs = Author.objects.filter(id=a1.id).filter(Q(extra__note=n1)|Q(item__note=n3))
>>> len([x[2][2] for x in qs.query.alias_map.values() if x[2][2] == query.LOUTER])
1
Bug #2091 Bug #2091
>>> t = Tag.objects.get(name='t4') >>> t = Tag.objects.get(name='t4')
>>> Item.objects.filter(tags__in=[t]) >>> Item.objects.filter(tags__in=[t])