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

Fixed some more join and lookup tests.

git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@6121 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2007-09-13 06:33:41 +00:00
parent 184a6435cd
commit ff3f6df54c
6 changed files with 70 additions and 25 deletions

View File

@ -334,8 +334,8 @@ class Model(object):
qn(self._meta.db_table), qn(self._meta.pk.column), op)
param = smart_str(getattr(self, field.attname))
q = self.__class__._default_manager.filter(**kwargs).order_by((not is_next and '-' or '') + field.name, (not is_next and '-' or '') + self._meta.pk.name)
q._where.append(where)
q._params.extend([param, param, getattr(self, self._meta.pk.attname)])
q.extra(where=where, params=[param, param,
getattr(self, self._meta.pk.attname)])
try:
return q[0]
except IndexError:

View File

@ -10,7 +10,7 @@ all about the internals of models in order to get the information it needs.
import copy
from django.utils import tree
from django.db.models.sql.where import WhereNode, AND
from django.db.models.sql.where import WhereNode, AND, OR
from django.db.models.sql.datastructures import Count
from django.db.models.fields import FieldDoesNotExist
from django.contrib.contenttypes import generic
@ -233,14 +233,27 @@ class Query(object):
# Work out how to relabel the rhs aliases, if necessary.
change_map = {}
used = {}
first_new_join = True
for alias in rhs.tables:
if not rhs.alias_map[alias][ALIAS_REFCOUNT]:
# An unused alias.
continue
promote = (rhs.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] ==
self.LOUTER)
new_alias = self.join(rhs.rev_join_map[alias], exclusions=used,
promote=promote)
promote=promote, outer_if_first=True)
if self.alias_map[alias][ALIAS_REFCOUNT] == 1:
first_new_join = False
used[new_alias] = None
change_map[alias] = new_alias
# So that we don't exclude valid results, the first join that is
# exclusive to the lhs (self) must be converted to an outer join.
for alias in self.tables[1:]:
if self.alias_map[alias][ALIAS_REFCOUNT] == 1:
self.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] = self.LOUTER
break
# Now relabel a copy of the rhs where-clause and add it to the current
# one.
if rhs.where:
@ -380,8 +393,12 @@ class Query(object):
""" Decreases the reference count for this alias. """
self.alias_map[alias][ALIAS_REFCOUNT] -= 1
def promote_alias(self, alias):
""" Promotes the join type of an alias to an outer join. """
self.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] = self.LOUTER
def join(self, (lhs, table, lhs_col, col), always_create=False,
exclusions=(), promote=False):
exclusions=(), promote=False, outer_if_first=False):
"""
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
@ -398,6 +415,10 @@ class Query(object):
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
to LOUTER, if necessary).
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
and Q-objects together.
"""
if lhs not in self.alias_map:
lhs_table = lhs
@ -422,7 +443,7 @@ class Query(object):
assert not is_table, \
"Must pass in lhs alias when creating a new join."
alias, _ = self.table_alias(table, True)
join_type = promote and self.LOUTER or self.INNER
join_type = (promote or outer_if_first) and self.LOUTER or self.INNER
join = [table, alias, join_type, lhs, lhs_col, col]
if not lhs:
# Not all tables need to be joined to anything. No join type
@ -487,7 +508,8 @@ class Query(object):
opts = self.model._meta
alias = self.join((None, opts.db_table, None, None))
dupe_multis = (connection == AND)
last = None
seen_aliases = []
done_split = not self.where
# FIXME: Using enumerate() here is expensive. We only need 'i' to
# check we aren't joining against a non-joinable field. Find a
@ -498,23 +520,35 @@ class Query(object):
if name == 'pk':
name = target_field.name
if joins is not None:
seen_aliases.extend(joins)
last = joins
alias = joins[-1]
if connection == OR and not done_split:
if self.alias_map[joins[0]][ALIAS_REFCOUNT] == 1:
done_split = True
self.promote_alias(joins[0])
for t in self.tables[1:]:
if t in seen_aliases:
continue
self.promote_alias(t)
break
else:
seen_aliases.extend(joins)
else:
# Normal field lookup must be the last field in the filter.
if i != len(parts) - 1:
raise TypeError("Joins on field %r not permitted."
raise TypeError("Join on field %r not permitted."
% name)
col = target_col or target_field.column
if target_field is opts.pk and last:
if target_field is opts.pk and seen_aliases:
# An optimization: if the final join is against a primary key,
# we can go back one step in the join chain and compare against
# the lhs of the join instead. The result (potentially) involves
# one less table join.
self.unref_alias(alias)
join = self.alias_map[last[-1]][ALIAS_JOIN]
join = self.alias_map[seen_aliases[-1]][ALIAS_JOIN]
alias = join[LHS_ALIAS]
col = join[LHS_JOIN_COL]
@ -523,13 +557,13 @@ class Query(object):
# join when connecting to the previous model. We make that
# adjustment here. We don't do this unless needed because it's less
# efficient at the database level.
self.alias_map[joins[0]][ALIAS_JOIN][JOIN_TYPE] = self.LOUTER
self.promote_alias(joins[0])
self.where.add([alias, col, orig_field, lookup_type, value],
connection)
if negate:
if last:
self.alias_map[last[0]][ALIAS_JOIN][JOIN_TYPE] = self.LOUTER
if seen_aliases:
self.promote_alias(last[0])
self.where.negate()
def add_q(self, q_object):

View File

@ -64,11 +64,19 @@ class WhereNode(tree.Node):
else:
format = '(%s)'
else:
sql = self.make_atom(child)
params = child[2].get_db_prep_lookup(child[3], child[4])
format = '%s'
result.append(format % sql)
result_params.extend(params)
try:
sql = self.make_atom(child)
params = child[2].get_db_prep_lookup(child[3], child[4])
format = '%s'
except EmptyResultSet:
if node.negated:
# If this is a "not" atom, being empty means it has no
# effect on the result, so we can ignore it.
continue
raise
if sql:
result.append(format % sql)
result_params.extend(params)
conn = ' %s ' % node.connection
return conn.join(result), result_params

View File

@ -21,6 +21,10 @@ class Node(object):
self.subtree_parents = []
self.negated = False
def __str__(self):
return '(%s: %s)' % (self.connection, ', '.join([str(c) for c in
self.children]))
def __deepcopy__(self, memodict):
"""
Utility method used by copy.deepcopy().
@ -59,7 +63,8 @@ class Node(object):
if len(self.children) < 2:
self.connection = conn_type
if self.connection == conn_type:
if isinstance(node, Node) and node.connection == conn_type:
if isinstance(node, Node) and (node.connection == conn_type
or len(node) == 1):
self.children.extend(node.children)
else:
self.children.append(node)

View File

@ -258,7 +258,7 @@ TypeError: Cannot resolve keyword 'pub_date_year' into field. Choices are: id, h
>>> Article.objects.filter(headline__starts='Article')
Traceback (most recent call last):
...
TypeError: Cannot resolve keyword 'headline__starts' into field. Choices are: id, headline, pub_date
TypeError: Join on field 'headline' not permitted.
# Create some articles with a bit more interesting headlines for testing field lookups:
>>> now = datetime.now()

View File

@ -111,11 +111,9 @@ Bug #4464
>>> Item.objects.filter(tags__in=[t1, t2]).filter(tags=t3)
[<Item: two>]
Bug #2080
# FIXME: Still problematic: the join needs to be "left outer" on the reverse
# fk, but the individual joins only need to be inner.
# >>> Author.objects.filter(Q(name='a3') | Q(item__name='one'))
# [<Author: a3>]
Bug #2080, #3592
>>> Author.objects.filter(Q(name='a3') | Q(item__name='one'))
[<Author: a1>, <Author: a3>]
Bug #2939
# FIXME: ValueQuerySets don't work yet.