1
0
mirror of https://github.com/django/django.git synced 2025-07-06 02:39:12 +00:00

Initial portion of queryset rewrite. This breaks a lot more than it fixes in

the current state. Committing so that Adrian and I can work on it together.

Do not try to use this yet!


git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@6116 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2007-09-13 02:50:14 +00:00
parent b791612779
commit a7a517f329
8 changed files with 1370 additions and 777 deletions

View File

@ -117,6 +117,13 @@ class Field(object):
# This is needed because bisect does not take a comparison function.
return cmp(self.creation_counter, other.creation_counter)
def __deepcopy__(self, memodict):
# Slight hack; deepcopy() is difficult to do on classes with
# dynamically created methods. Fortunately, we can get away with doing
# a shallow copy in this particular case.
import copy
return copy.copy(self)
def to_python(self, value):
"""
Converts the input value into the expected Python data type, raising

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
"""
Various data structures used in query construction.
Factored out from django.db.models.query so that they can also be used by other
modules without getting into circular import difficulties.
"""
from django.utils import tree
class EmptyResultSet(Exception):
"""
Raised when a QuerySet cannot contain any data.
"""
pass
class Q(tree.Node):
"""
Encapsulates filters as objects that can then be combined logically (using
& and |).
"""
# Connection types
AND = 'AND'
OR = 'OR'
default = AND
def __init__(self, *args, **kwargs):
if args and kwargs:
raise TypeError('Use positional *or* kwargs; not both!')
nodes = list(args) + kwargs.items()
super(Q, self).__init__(children=nodes)
def _combine(self, other, conn):
if not isinstance(other, Q):
raise TypeError(other)
self.add(other, conn)
return self
def __or__(self, other):
return self._combine(other, self.OR)
def __and__(self, other):
return self._combine(other, self.AND)
class QNot(Q):
"""
Encapsulates the negation of a Q object.
"""
def __init__(self, q):
"""Creates the negation of the Q object passed in."""
super(QNot, self).__init__()
self.add(q, self.AND)
self.negate()

View File

@ -0,0 +1,5 @@
from query import *
from where import AND, OR
__all__ = ['Query', 'AND', 'OR']

View File

@ -0,0 +1,59 @@
"""
Useful auxilliary data structures for query construction. Not useful outside
the SQL domain.
"""
class EmptyResultSet(Exception):
pass
class Aggregate(object):
"""
Base class for all aggregate-related classes (min, max, avg, count, sum).
"""
def relabel_aliases(self, change_map):
"""
Relabel the column alias, if necessary. Must be implemented by
subclasses.
"""
raise NotImplementedError
def as_sql(self, quote_func=None):
"""
Returns the SQL string fragment for this object.
The quote_func function is used to quote the column components. If
None, it defaults to doing nothing.
Must be implemented by subclasses.
"""
raise NotImplementedError
class Count(Aggregate):
"""
Perform a count on the given column.
"""
def __init__(self, col=None, distinct=False):
"""
Set the column to count on (defaults to '*') and set whether the count
should be distinct or not.
"""
self.col = col and col or '*'
self.distinct = distinct
def relabel_aliases(self, change_map):
c = self.col
if isinstance(c, (list, tuple)):
self.col = (change_map.get(c[0], c[0]), c[1])
def as_sql(self, quote_func=None):
if not quote_func:
quote_func = lambda x: x
if isinstance(self.col, (list, tuple)):
col = '%s.%s' % tuple([quote_func(c) for c in self.col])
else:
col = self.col
if self.distinct:
return 'COUNT(DISTINCT(%s))' % col
else:
return 'COUNT(%s)' % col

View File

@ -0,0 +1,880 @@
"""
Create SQL statements for QuerySets.
The code in here encapsulates all of the SQL construction so that QuerySets
themselves do not have to (and could be backed by things other than SQL
databases). The abstraction barrier only works one way: this module has to know
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.datastructures import Count
from django.db.models.fields import FieldDoesNotExist
from django.contrib.contenttypes import generic
from datastructures import EmptyResultSet
from utils import handle_legacy_orderlist
try:
reversed
except NameError:
from django.utils.itercompat import reversed # For python 2.3.
# Valid query types (a dictionary is used for speedy lookups).
QUERY_TERMS = dict([(x, None) for x in (
'exact', 'iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', 'lte', 'in',
'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'year',
'month', 'day', 'isnull', 'search', 'regex', 'iregex',
)])
# Size of each "chunk" for get_iterator calls.
# Larger values are slightly faster at the expense of more storage space.
GET_ITERATOR_CHUNK_SIZE = 100
# Separator used to split filter strings apart.
LOOKUP_SEP = '__'
# Constants to make looking up tuple values clearerer.
# Join lists
TABLE_NAME = 0
RHS_ALIAS = 1
JOIN_TYPE = 2
LHS_ALIAS = 3
LHS_JOIN_COL = 4
RHS_JOIN_COL = 5
# Alias maps lists
ALIAS_TABLE = 0
ALIAS_REFCOUNT = 1
ALIAS_JOIN = 2
# How many results to expect from a cursor.execute call
MULTI = 'multi'
SINGLE = 'single'
NONE = None
class Query(object):
"""
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'
def __init__(self, model, connection):
self.model = model
self.connection = connection
self.alias_map = {} # Maps alias to table name
self.table_map = {} # Maps table names to list of aliases.
self.join_map = {} # Maps join_tuple to list of aliases.
self.rev_join_map = {} # Reverse of join_map.
# SQL-related attributes
self.select = []
self.tables = [] # Aliases in the order they are created.
self.where = WhereNode(self)
self.having = []
self.group_by = []
self.order_by = []
self.low_mark, self.high_mark = 0, None # Used for offset/limit
self.distinct = False
self.select_related = False
self.max_depth = 0
# These are for extensions. The contents are more or less appended
# verbatim to the appropriate clause.
self.extra_select = {} # Maps col_alias -> col_sql.
self.extra_tables = []
self.extra_where = []
self.extra_params = []
def __str__(self):
"""
Returns the query as a string of SQL with the parameter values
substituted in.
Parameter values won't necessarily be quoted correctly, since that is
done by the database interface at execution time.
"""
sql, params = self.as_sql()
return sql % params
def clone(self, **kwargs):
"""
Creates a copy of the current instance. The 'kwargs' parameter can be
used by clients to update attributes after copying has taken place.
"""
obj = self.__class__(self.model, self.connection)
obj.table_map = self.table_map.copy()
obj.alias_map = copy.deepcopy(self.alias_map)
obj.join_map = copy.deepcopy(self.join_map)
obj.rev_join_map = copy.deepcopy(self.rev_join_map)
obj.select = self.select[:]
obj.tables = self.tables[:]
obj.where = copy.deepcopy(self.where)
obj.having = self.having[:]
obj.group_by = self.group_by[:]
obj.order_by = self.order_by[:]
obj.low_mark, obj.high_mark = self.low_mark, self.high_mark
obj.distinct = self.distinct
obj.select_related = self.select_related
obj.max_depth = self.max_depth
obj.extra_select = self.extra_select.copy()
obj.extra_tables = self.extra_tables[:]
obj.extra_where = self.extra_where[:]
obj.extra_params = self.extra_params[:]
obj.__dict__.update(kwargs)
return obj
def results_iter(self):
"""
Returns an iterator over the results from executing this query.
"""
fields = self.model._meta.fields
resolve_columns = hasattr(self, 'resolve_columns')
for rows in self.execute_sql(MULTI):
for row in rows:
if resolve_columns:
row = self.resolve_columns(row, fields)
yield row
def get_count(self):
"""
Performs a COUNT() or COUNT(DISTINCT()) query, as appropriate, using
the current filter constraints.
"""
counter = self.clone()
counter.clear_ordering()
counter.clear_limits()
counter.select_related = False
counter.add_count_column()
data = counter.execute_sql(SINGLE)
if not data:
return 0
number = data[0]
# Apply offset and limit constraints manually, since using LIMIT/OFFSET
# in SQL doesn't change the COUNT output.
number = max(0, number - self.low_mark)
if self.high_mark:
number = min(number, self.high_mark - self.low_mark)
return number
def as_sql(self, with_limits=True):
"""
Creates the SQL for this query. Returns the SQL string and list of
parameters.
If 'with_limits' is False, any limit/offset information is not included
in the query.
"""
self.pre_sql_setup()
result = ['SELECT']
if self.distinct:
result.append('DISTINCT')
out_cols = self.get_columns()
result.append(', '.join(out_cols))
result.append('FROM')
for alias in self.tables:
if not self.alias_map[alias][ALIAS_REFCOUNT]:
continue
name, alias, join_type, lhs, lhs_col, col = \
self.alias_map[alias][ALIAS_JOIN]
alias_str = (alias != name and ' AS %s' % alias or '')
if join_type:
result.append('%s %s%s ON (%s.%s = %s.%s)'
% (join_type, name, alias_str, lhs, lhs_col, alias,
col))
else:
result.append('%s%s' % (name, alias_str))
result.extend(self.extra_tables)
where, params = self.where.as_sql()
if where:
result.append('WHERE %s' % where)
result.append(' AND'.join(self.extra_where))
ordering = self.get_ordering()
if ordering:
result.append('ORDER BY %s' % ', '.join(ordering))
if with_limits:
if self.high_mark:
result.append('LIMIT %d' % (self.high_mark - self.low_mark))
if self.low_mark:
assert self.high_mark, "OFFSET not allowed without LIMIT."
result.append('OFFSET %d' % self.low_mark)
params.extend(self.extra_params)
return ' '.join(result), tuple(params)
def combine(self, rhs, connection):
"""
Merge the 'rhs' query into the current one (with any 'rhs' effects
being applied *after* (that is, "to the right of") anything in the
current query. 'rhs' is not modified during a call to this function.
The 'connection' parameter describes how to connect filters from the
'rhs' query.
"""
assert self.model == rhs.model, \
"Cannot combine queries on two different base models."
assert self.can_filter(), \
"Cannot combine queries once a slice has been taken."
assert self.distinct == rhs.distinct, \
"Cannot combine a unique query with a non-unique query."
# Work out how to relabel the rhs aliases, if necessary.
change_map = {}
used = {}
for alias in rhs.tables:
promote = (rhs.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] ==
self.LOUTER)
new_alias = self.join(rhs.rev_join_map[alias], exclusions=used,
promote=promote)
used[new_alias] = None
change_map[alias] = new_alias
# Now relabel a copy of the rhs where-clause and add it to the current
# one.
if rhs.where:
w = copy.deepcopy(rhs.where)
w.relabel_aliases(change_map)
if not self.where:
# Since 'self' matches everything, add an explicit "include
# everything" (pk is not NULL) where-constraint so that
# connections between the where clauses won't exclude valid
# results.
alias = self.join((None, self.model._meta.db_table, None, None))
pk = self.model._meta.pk
self.where.add((alias, pk.column, pk, 'isnull', False), AND)
elif self.where:
# rhs has an empty where clause. Make it match everything (see
# above for reasoning).
w = WhereNode()
alias = self.join((None, self.model._meta.db_table, None, None))
pk = self.model._meta.pk
w.add((alias, pk.column, pk, 'isnull', False), AND)
else:
w = WhereNode()
self.where.add(w, connection)
# Selection columns and extra extensions are those provided by 'rhs'.
self.select = []
for col in rhs.select:
if isinstance(col, (list, tuple)):
self.select.append((change_map.get(col[0], col[0]), col[1]))
else:
item = copy.deepcopy(col)
item.relabel_aliases(change_map)
self.select.append(item)
self.extra_select = rhs.extra_select.copy()
self.extra_tables = rhs.extra_tables[:]
self.extra_where = rhs.extra_where[:]
self.extra_params = rhs.extra_params[:]
# Ordering uses the 'rhs' ordering, unless it has none, in which case
# the current ordering is used.
self.order_by = rhs.order_by and rhs.order_by[:] or self.order_by
def pre_sql_setup(self):
"""
Does any necessary class setup prior to producing SQL. This is for
things that can't necessarily be done in __init__.
"""
if not self.tables:
self.join((None, self.model._meta.db_table, None, None))
def get_columns(self):
"""
Return the list of columns to use in the select statement. If no
columns have been specified, returns all columns relating to fields in
the model.
"""
qn = self.connection.ops.quote_name
result = []
if self.select:
for col in self.select:
if isinstance(col, (list, tuple)):
result.append('%s.%s' % (qn(col[0]), qn(col[1])))
else:
result.append(col.as_sql())
else:
table_alias = self.tables[0]
result = ['%s.%s' % (table_alias, qn(f.column))
for f in self.model._meta.fields]
# We sort extra_select so that the result columns are in a well-defined
# order (and thus QuerySet.iterator can extract them correctly).
extra_select = self.extra_select.items()
extra_select.sort()
result.extend(['(%s) AS %s' % (col, alias)
for alias, col in extra_select])
return result
def get_ordering(self):
"""
Returns a tuple representing the SQL elements in the "order by" clause.
"""
ordering = self.order_by or self.model._meta.ordering
qn = self.connection.ops.quote_name
opts = self.model._meta
result = []
for field in handle_legacy_orderlist(ordering):
if field == '?':
result.append(self.connection.ops.random_function_sql())
continue
if field[0] == '-':
col = field[1:]
order = 'DESC'
else:
col = field
order = 'ASC'
if '.' in col:
table, col = col.split('.', 1)
table = '%s.' % self.table_alias[table]
elif col not in self.extra_select:
# Use the root model's database table as the referenced table.
table = '%s.' % self.tables[0]
else:
table = ''
result.append('%s%s %s' % (table,
qn(orderfield_to_column(col, opts)), order))
return result
def table_alias(self, table_name, create=False):
"""
Returns a table alias for the given table_name and whether this is a
new alias or not.
If 'create' is true, a new alias is always created. Otherwise, the
most recently created alias for the table (if one exists) is reused.
"""
if not create and table_name in self.table_map:
alias = self.table_map[table_name][-1]
self.alias_map[alias][ALIAS_REFCOUNT] += 1
return alias, False
# Create a new alias for this table.
if table_name not in self.table_map:
# The first occurence of a table uses the table name directly.
alias = table_name
else:
alias = '%s%d' % (self.alias_prefix, len(self.alias_map) + 1)
self.alias_map[alias] = [table_name, 1, None]
self.table_map.setdefault(table_name, []).append(alias)
self.tables.append(alias)
return alias, True
def ref_alias(self, alias):
""" Increases the reference count for this alias. """
self.alias_map[alias][ALIAS_REFCOUNT] += 1
def unref_alias(self, alias):
""" Decreases the reference count for this alias. """
self.alias_map[alias][ALIAS_REFCOUNT] -= 1
def join(self, (lhs, table, lhs_col, col), always_create=False,
exclusions=(), promote=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
new one.
'lhs' is either an existing table alias or a table name. If
'always_create' is True, a new alias is always created, regardless of
whether one already exists or not.
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
the alias previously existed, the join type will be promoted from INNER
to LOUTER, if necessary).
"""
if lhs not in self.alias_map:
lhs_table = lhs
is_table = (lhs is not None)
else:
lhs_table = self.alias_map[lhs][ALIAS_TABLE]
is_table = False
t_ident = (lhs_table, table, lhs_col, col)
aliases = self.join_map.get(t_ident)
if aliases and not always_create:
for alias in aliases:
if alias not in exclusions:
self.ref_alias(alias)
if promote:
self.alias_map[alias][ALIAS_JOIN][JOIN_TYPE] = \
self.LOUTER
return alias
# If we get to here (no non-excluded alias exists), we'll fall
# through to creating a new alias.
# No reuse is possible, so we need a new alias.
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 = [table, alias, join_type, lhs, lhs_col, col]
if not lhs:
# Not all tables need to be joined to anything. No join type
# means the later columns are ignored.
join[JOIN_TYPE] = None
self.alias_map[alias][ALIAS_JOIN] = join
self.join_map.setdefault(t_ident, []).append(alias)
self.rev_join_map[alias] = t_ident
return alias
def fill_table_cache(self, opts=None, root_alias=None, cur_depth=0,
used=None):
"""
Fill in the information needed for a select_related query.
"""
if self.max_depth and cur_depth > self.max_depth:
# We've recursed too deeply; bail out.
return
if not opts:
opts = self.model._meta
root_alias = self.tables[0]
self.select.extend([(root_alias, f) for f in opts.fields])
if not used:
used = []
for f in opts.fields:
if not f.rel or f.null:
continue
table = f.rel.to._meta.db_table
alias = self.join((root_alias, table, f.column,
f.rel.get_related_field().column), exclusion=used)
used.append(alias)
self.select.extend([(table, f2.column)
for f2 in f.rel.to._meta.fields])
self.fill_table_cache(f.rel.to._meta, alias, cur_depth + 1, used)
def add_filter(self, filter_expr, connection=AND, negate=False):
"""
Add a single filter to the query.
"""
arg, value = filter_expr
parts = arg.split(LOOKUP_SEP)
if not parts:
raise TypeError("Cannot parse keyword query %r" % arg)
# Work out the lookup type and remove it from 'parts', if necessary.
if len(parts) == 1 or parts[-1] not in QUERY_TERMS:
lookup_type = 'exact'
else:
lookup_type = parts.pop()
# Interpret '__exact=None' as the sql '= NULL'; otherwise, reject all
# uses of None as a query value.
# FIXME: Weren't we going to change this so that '__exact=None' was the
# same as '__isnull=True'? Need to check the conclusion of the mailing
# list thread.
if value is None and lookup_type != 'exact':
raise ValueError("Cannot use None as a query value")
elif callable(value):
value = value()
opts = self.model._meta
alias = self.join((None, opts.db_table, None, None))
dupe_multis = (connection == AND)
last = None
# FIXME: Using enumerate() here is expensive. We only need 'i' to
# check we aren't joining against a non-joinable field. Find a
# better way to do this!
for i, name in enumerate(parts):
joins, opts, orig_field, target_field, target_col = \
self.get_next_join(name, opts, alias, dupe_multis)
if name == 'pk':
name = target_field.name
if joins is not None:
last = joins
alias = joins[-1]
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."
% name)
name = target_col or name
if target_field is opts.pk and last:
# 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]
alias = join[LHS_ALIAS]
name = join[LHS_JOIN_COL]
if (lookup_type == 'isnull' and value is True):
# If the comparison is against NULL, we need to use a left outer
# 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.where.add([alias, name, orig_field, lookup_type, value],
connection)
if negate:
self.alias_map[last[0]][ALIAS_JOIN][JOIN_TYPE] = self.LOUTER
self.where.negate()
def add_q(self, q_object):
"""
Adds a Q-object to the current filter.
Can also be used to add anything that has an 'add_to_query()' method.
"""
if hasattr(q_object, 'add_to_query'):
# Complex custom objects are responsible for adding themselves.
q_object.add_to_query(self)
return
for child in q_object.children:
if isinstance(child, tree.Node):
self.where.start_subtree(q_object.connection)
self.add_q(child)
self.where.end_subtree()
else:
self.add_filter(child, q_object.connection, q_object.negated)
def get_next_join(self, name, opts, root_alias, dupe_multis):
"""
Compute the necessary table joins for the field called 'name'. 'opts'
is the Options class for the current model (which gives the table we
are joining to), root_alias is the alias for the table we are joining
to. If dupe_multis is True, any many-to-many or many-to-one joins will
always create a new alias (necessary for disjunctive filters).
Returns a list of aliases involved in the join, the next value for
'opts' and the field class that was matched. For a non-joining field,
the first value (join alias) is None.
"""
if name == 'pk':
name = opts.pk.name
field = find_field(name, opts.many_to_many, False)
if field:
# Many-to-many field defined on the current model.
remote_opts = field.rel.to._meta
int_alias = self.join((root_alias, field.m2m_db_table(),
opts.pk.column, field.m2m_column_name()), dupe_multis)
far_alias = self.join((int_alias, remote_opts.db_table,
field.m2m_reverse_name(), remote_opts.pk.column),
dupe_multis)
return ([int_alias, far_alias], remote_opts, field, remote_opts.pk,
None)
field = find_field(name, opts.get_all_related_many_to_many_objects(),
True)
if field:
# Many-to-many field defined on the target model.
remote_opts = field.opts
field = field.field
int_alias = self.join((root_alias, field.m2m_db_table(),
opts.pk.column, field.m2m_reverse_name()), dupe_multis)
far_alias = self.join((int_alias, remote_opts.db_table,
field.m2m_column_name(), remote_opts.pk.column),
dupe_multis)
# XXX: Why is the final component able to be None here?
return ([int_alias, far_alias], remote_opts, field, remote_opts.pk,
None)
field = find_field(name, opts.get_all_related_objects(), True)
if field:
# One-to-many field (ForeignKey defined on the target model)
remote_opts = field.opts
field = field.field
local_field = opts.get_field(field.rel.field_name)
alias = self.join((root_alias, remote_opts.db_table,
local_field.column, field.column), dupe_multis)
return ([alias], remote_opts, field, field, remote_opts.pk.column)
field = find_field(name, opts.fields, False)
if not field:
raise TypeError, \
("Cannot resolve keyword '%s' into field. Choices are: %s"
% (name, ", ".join(get_legal_fields(opts))))
if field.rel:
# One-to-one or many-to-one field
remote_opts = field.rel.to._meta
alias = self.join((root_alias, remote_opts.db_table, field.column,
field.rel.field_name))
target = remote_opts.get_field(field.rel.field_name)
return [alias], remote_opts, field, target, target.column
# Only remaining possibility is a normal (direct lookup) field. No
# join is required.
return None, opts, field, field, None
def set_limits(self, low=None, high=None):
"""
Adjusts the limits on the rows retrieved. We use low/high to set these,
as it makes it more Pythonic to read and write. When the SQL query is
created, they are converted to the appropriate offset and limit values.
Any limits passed in here are applied relative to the existing
constraints. So low is added to the current low value and both will be
clamped to any existing high value.
"""
if high:
# None (high_mark's default) is less than any number, so this works.
self.high_mark = max(self.high_mark, high)
if low:
self.low_mark = max(self.high_mark, self.low_mark + low)
def clear_limits(self):
"""
Clears any existing limits.
"""
self.low_mark, self.high_mark = 0, None
def add_ordering(self, *ordering):
"""
Adds items from the 'ordering' sequence to the query's "order by"
clause.
"""
self.order_by.extend(ordering)
def clear_ordering(self):
"""
Removes any ordering settings.
"""
self.order_by = []
def can_filter(self):
"""
Returns True if adding filters to this instance is still possible.
Typically, this means no limits or offsets have been put on the results.
"""
return not (self.low_mark or self.high_mark)
def add_count_column(self):
"""
Converts the query to do count(*) or count(distinct(pk)) in order to
get its size.
"""
# TODO: When group_by support is added, this needs to be adjusted so
# that it doesn't totally overwrite the select list.
if not self.distinct:
select = Count()
# Distinct handling is now done in Count(), so don't do it at this
# level.
self.distinct = False
else:
select = Count((self.table_map[self.model._meta.db_table][0],
self.model._meta.pk.column), True)
self.select = [select]
self.extra_select = {}
def execute_sql(self, result_type=MULTI):
"""
Run the query against the database and returns the result(s). The
return value is a single data item if result_type is SINGLE, or an
iterator over the results if the result_type is MULTI.
result_type is either MULTI (use fetchmany() to retrieve all rows),
SINGLE (only retrieve a single row), or NONE (no results expected).
"""
try:
sql, params = self.as_sql()
except EmptyResultSet:
raise StopIteration
cursor = self.connection.cursor()
cursor.execute(sql, params)
if result_type == NONE:
return
if result_type == SINGLE:
return cursor.fetchone()
# The MULTI case.
def it():
while 1:
rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)
if not rows:
raise StopIteration
yield rows
return it()
class DeleteQuery(Query):
"""
Delete queries are done through this class, since they are more constrained
than general queries.
"""
def as_sql(self):
"""
Creates the SQL for this query. Returns the SQL string and list of
parameters.
"""
assert len(self.tables) == 1, \
"Can only delete from one table at a time."
result = ['DELETE FROM %s' % self.tables[0]]
where, params = self.where.as_sql()
result.append('WHERE %s' % where)
return ' '.join(result), tuple(params)
def do_query(self, table, where):
self.tables = [table]
self.where = where
self.execute_sql(NONE)
def delete_batch_related(self, pk_list):
"""
Set up and execute delete queries for all the objects related to the
primary key values in pk_list. To delete the objects themselves, use
the delete_batch() method.
More than one physical query may be executed if there are a
lot of values in pk_list.
"""
cls = self.model
for related in cls._meta.get_all_related_many_to_many_objects():
if not isinstance(related.field, generic.GenericRelation):
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
where = WhereNode(self)
where.add((None, related.field.m2m_reverse_name(), None,
'in',
pk_list[offset : offset+GET_ITERATOR_CHUNK_SIZE]),
AND)
self.do_query(related.field.m2m_db_table(), where)
for f in cls._meta.many_to_many:
w1 = WhereNode(self)
if isinstance(f, generic.GenericRelation):
from django.contrib.contenttypes.models import ContentType
field = f.rel.to._meta.get_field(f.content_type_field_name)
w1.add((None, field.column, field, 'exact',
ContentType.objects.get_for_model(cls).id), AND)
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
where = WhereNode(self)
where.add((None, f.m2m_column_name(), None, 'in',
pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
AND)
if w1:
where.add(w1, AND)
self.do_query(f.m2m_db_table(), where)
def delete_batch(self, pk_list):
"""
Set up and execute delete queries for all the objects in pk_list. This
should be called after delete_batch_related(), if necessary.
More than one physical query may be executed if there are a
lot of values in pk_list.
"""
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
where = WhereNode(self)
field = self.model._meta.pk
where.add((None, field.column, field, 'in',
pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND)
self.do_query(self.model._meta.db_table, where)
class UpdateQuery(Query):
"""
Represents an "update" SQL query.
"""
def __init__(self, *args, **kwargs):
super(UpdateQuery, self).__init__(*args, **kwargs)
self.values = []
def as_sql(self):
"""
Creates the SQL for this query. Returns the SQL string and list of
parameters.
"""
assert len(self.tables) == 1, \
"Can only update one table at a time."
result = ['UPDATE %s' % self.tables[0]]
result.append('SET')
qn = self.connection.ops.quote_name
values = ['%s = %s' % (qn(v[0]), v[1]) for v in self.values]
result.append(', '.join(values))
where, params = self.where.as_sql()
result.append('WHERE %s' % where)
return ' '.join(result), tuple(params)
def do_query(self, table, values, where):
self.tables = [table]
self.values = values
self.where = where
self.execute_sql(NONE)
def clear_related(self, related_field, pk_list):
"""
Set up and execute an update query that clears related entries for the
keys in pk_list.
This is used by the QuerySet.delete_objects() method.
"""
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
where = WhereNode()
f = self.model._meta.pk
where.add((None, f, f.db_type(), 'in',
pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
AND)
values = [(related_field.column, 'NULL')]
self.do_query(self.model._meta.db_table, values, where)
def find_field(name, field_list, related_query):
"""
Finds a field with a specific name in a list of field instances.
Returns None if there are no matches, or several matches.
"""
if related_query:
matches = [f for f in field_list
if f.field.related_query_name() == name]
else:
matches = [f for f in field_list if f.name == name]
if len(matches) != 1:
return None
return matches[0]
def field_choices(field_list, related_query):
"""
Returns the names of the field objects in field_list. Used to construct
readable error messages.
"""
if related_query:
return [f.field.related_query_name() for f in field_list]
else:
return [f.name for f in field_list]
def get_legal_fields(opts):
"""
Returns a list of fields that are valid at this point in the query. Used in
error reporting.
"""
return (field_choices(opts.many_to_many, False)
+ field_choices( opts.get_all_related_many_to_many_objects(), True)
+ field_choices(opts.get_all_related_objects(), True)
+ field_choices(opts.fields, False))
def orderfield_to_column(name, opts):
"""
For a field name specified in an "order by" clause, returns the database
column name. If 'name' is not a field in the current model, it is returned
unchanged.
"""
try:
return opts.get_field(name, False).column
except FieldDoesNotExist:
return name

View File

@ -0,0 +1,27 @@
"""
Miscellaneous helper functions.
"""
import warnings
from django.utils.encoding import smart_unicode
# Django currently supports two forms of ordering.
# Form 1 (deprecated) example:
# order_by=(('pub_date', 'DESC'), ('headline', 'ASC'), (None, 'RANDOM'))
# Form 2 (new-style) example:
# order_by=('-pub_date', 'headline', '?')
# Form 1 is deprecated and will no longer be supported for Django's first
# official release. The following code converts from Form 1 to Form 2.
LEGACY_ORDERING_MAPPING = {'ASC': '_', 'DESC': '-_', 'RANDOM': '?'}
def handle_legacy_orderlist(order_list):
if not order_list or isinstance(order_list[0], basestring):
return order_list
else:
new_order_list = [LEGACY_ORDERING_MAPPING[j.upper()].replace('_', smart_unicode(i)) for i, j in order_list]
warnings.warn("%r ordering syntax is deprecated. Use %r instead."
% (order_list, new_order_list), DeprecationWarning)
return new_order_list

View File

@ -0,0 +1,151 @@
"""
Code to manage the creation and SQL rendering of 'where' constraints.
"""
import datetime
from django.utils import tree
from datastructures import EmptyResultSet
# Connection types
AND = 'AND'
OR = 'OR'
class WhereNode(tree.Node):
"""
Used to represent the SQL where-clause.
The class is tied to the Query class that created it (in order to create
the corret SQL).
The children in this tree are usually either Q-like objects or lists of
[table_alias, field_name, field_class, lookup_type, value]. However, a
child could also be any class with as_sql() and relabel_aliases() methods.
"""
default = AND
def __init__(self, query=None, children=None, connection=None):
super(WhereNode, self).__init__(children, connection)
if query:
# XXX: Would be nice to use a weakref here, but it seems tricky to
# make it work.
self.query = query
def __deepcopy__(self, memodict):
"""
Used by copy.deepcopy().
"""
obj = super(WhereNode, self).__deepcopy__(memodict)
obj.query = self.query
return obj
def as_sql(self, node=None):
"""
Returns the SQL version of the where clause and the value to be
substituted in. Returns None, None if this node is empty.
If 'node' is provided, that is the root of the SQL generation
(generally not needed except by the internal implementation for
recursion).
"""
if node is None:
node = self
if not node.children:
return None, []
result = []
result_params = []
for child in node.children:
if hasattr(child, 'as_sql'):
sql, params = child.as_sql()
format = '(%s)'
elif isinstance(child, tree.Node):
sql, params = self.as_sql(child)
if child.negated:
format = 'NOT (%s)'
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)
conn = ' %s ' % node.connection
return conn.join(result), result_params
def make_atom(self, child):
"""
Turn a tuple (table_alias, field_name, field_class, lookup_type, value)
into valid SQL.
Returns the string for the SQL fragment. The caller is responsible for
converting the child's value into an appropriate for for the parameters
list.
"""
table_alias, name, field, lookup_type, value = child
conn = self.query.connection
if table_alias:
lhs = '%s.%s' % (table_alias, conn.ops.quote_name(name))
else:
lhs = conn.ops.quote_name(name)
field_sql = conn.ops.field_cast_sql(field.db_type()) % lhs
if isinstance(value, datetime.datetime):
# FIXME datetime_cast_sql() should return '%s' by default.
cast_sql = conn.ops.datetime_cast_sql() or '%s'
else:
cast_sql = '%s'
# FIXME: This is out of place. Move to a function like
# datetime_cast_sql()
if (lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith')
and conn.features.needs_upper_for_iops):
format = 'UPPER(%s) %s'
else:
format = '%s %s'
if lookup_type in conn.operators:
return format % (field_sql, conn.operators[lookup_type] % cast_sql)
if lookup_type == 'in':
if not value:
raise EmptyResultSet
return '%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(value)))
elif lookup_type in ('range', 'year'):
return '%s BETWEEN %%s and %%s' % field_sql
elif lookup_type in ('month', 'day'):
return '%s = %%s' % conn.ops.date_extract_sql(lookup_type,
field_sql)
elif lookup_type == 'isnull':
return '%s IS %sNULL' % (field_sql, (not value and 'NOT ' or ''))
elif lookup_type in 'search':
return conn.op.fulltest_search_sql(field_sql)
elif lookup_type in ('regex', 'iregex'):
# FIXME: Factor this out in to conn.ops
if settings.DATABASE_ENGINE == 'oracle':
if lookup_type == 'regex':
match_option = 'c'
else:
match_option = 'i'
return "REGEXP_LIKE(%s, %s, '%s')" % (field_sql, cast_sql,
match_option)
else:
raise NotImplementedError
raise TypeError('Invalid lookup_type: %r' % lookup_type)
def relabel_aliases(self, change_map, node=None):
"""
Relabels the alias values of any children. 'change_map' is a dictionary
mapping old (current) alias values to the new values.
"""
if not node:
node = self
for child in node.children:
if hasattr(child, 'relabel_aliases'):
child.relabel_aliases(change_map)
elif isinstance(child, tree.Node):
self.relabel_aliases(change_map, child)
else:
val = child[0]
child[0] = change_map.get(val, val)