mirror of
https://github.com/django/django.git
synced 2025-10-19 11:49:11 +00:00
git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@6730 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1198 lines
46 KiB
Python
1198 lines
46 KiB
Python
"""
|
|
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
|
|
import re
|
|
|
|
from django.utils.tree import Node
|
|
from django.utils.datastructures import SortedDict
|
|
from django.dispatch import dispatcher
|
|
from django.db.models import signals
|
|
from django.db.models.sql.where import WhereNode, AND, OR
|
|
from django.db.models.sql.datastructures import Count, Date
|
|
from django.db.models.fields import FieldDoesNotExist, Field, related
|
|
from django.contrib.contenttypes import generic
|
|
from datastructures import EmptyResultSet
|
|
|
|
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 clearer.
|
|
# Join lists
|
|
TABLE_NAME = 0
|
|
RHS_ALIAS = 1
|
|
JOIN_TYPE = 2
|
|
LHS_ALIAS = 3
|
|
LHS_JOIN_COL = 4
|
|
RHS_JOIN_COL = 5
|
|
# Alias map 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
|
|
|
|
ORDER_PATTERN = re.compile(r'\?|[-+]?\w+$')
|
|
ORDER_DIR = {
|
|
'ASC': ('ASC', 'DESC'),
|
|
'DESC': ('DESC', 'ASC')}
|
|
|
|
class Empty(object):
|
|
pass
|
|
|
|
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.
|
|
self.quote_cache = {}
|
|
self.default_cols = True
|
|
|
|
# SQL-related attributes
|
|
self.select = []
|
|
self.tables = [] # Aliases in the order they are created.
|
|
self.where = WhereNode()
|
|
self.group_by = []
|
|
self.having = []
|
|
self.order_by = []
|
|
self.low_mark, self.high_mark = 0, None # Used for offset/limit
|
|
self.distinct = False
|
|
self.select_related = False
|
|
|
|
# Arbitrary maximum limit for select_related to prevent infinite
|
|
# recursion. Can be changed by the depth parameter to select_related().
|
|
self.max_depth = 5
|
|
|
|
# These are for extensions. The contents are more or less appended
|
|
# verbatim to the appropriate clause.
|
|
self.extra_select = SortedDict() # Maps col_alias -> col_sql.
|
|
self.extra_tables = []
|
|
self.extra_where = []
|
|
self.extra_params = []
|
|
self.extra_order_by = []
|
|
|
|
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 quote_name_unless_alias(self, name):
|
|
"""
|
|
A wrapper around connection.ops.quote_name that doesn't quote aliases
|
|
for table names. This avoids problems with some SQL dialects that treat
|
|
quoted strings specially (e.g. PostgreSQL).
|
|
"""
|
|
if name in self.quote_cache:
|
|
return self.quote_cache[name]
|
|
if name in self.alias_map and name not in self.table_map:
|
|
self.quote_cache[name] = name
|
|
return name
|
|
r = self.connection.ops.quote_name(name)
|
|
self.quote_cache[name] = r
|
|
return r
|
|
|
|
def clone(self, klass=None, **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 = Empty()
|
|
obj.__class__ = klass or self.__class__
|
|
obj.model = self.model
|
|
obj.connection = self.connection
|
|
obj.alias_map = copy.deepcopy(self.alias_map)
|
|
obj.table_map = self.table_map.copy()
|
|
obj.join_map = copy.deepcopy(self.join_map)
|
|
obj.rev_join_map = copy.deepcopy(self.rev_join_map)
|
|
obj.quote_cache = {}
|
|
obj.default_cols = self.default_cols
|
|
obj.select = self.select[:]
|
|
obj.tables = self.tables[:]
|
|
obj.where = copy.deepcopy(self.where)
|
|
obj.group_by = self.group_by[:]
|
|
obj.having = self.having[:]
|
|
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.extra_order_by = self.extra_order_by[:]
|
|
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() query using the current filter constraints.
|
|
"""
|
|
obj = self.clone()
|
|
obj.clear_ordering(True)
|
|
obj.clear_limits()
|
|
obj.select_related = False
|
|
if obj.distinct and len(obj.select) > 1:
|
|
obj = self.clone(CountQuery, _query=obj, where=WhereNode(),
|
|
distinct=False)
|
|
obj.add_count_column()
|
|
data = obj.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()
|
|
out_cols = self.get_columns()
|
|
ordering = self.get_ordering()
|
|
# This must come after 'select' and 'ordering' -- see docstring of
|
|
# get_from_clause() for details.
|
|
from_, f_params = self.get_from_clause()
|
|
where, w_params = self.where.as_sql(qn=self.quote_name_unless_alias)
|
|
|
|
result = ['SELECT']
|
|
if self.distinct:
|
|
result.append('DISTINCT')
|
|
result.append(', '.join(out_cols))
|
|
|
|
result.append('FROM')
|
|
result.extend(from_)
|
|
params = list(f_params)
|
|
|
|
if where:
|
|
result.append('WHERE %s' % where)
|
|
if self.extra_where:
|
|
if not where:
|
|
result.append('WHERE')
|
|
else:
|
|
result.append('AND')
|
|
result.append(' AND'.join(self.extra_where))
|
|
params.extend(w_params)
|
|
|
|
if self.group_by:
|
|
grouping = self.get_grouping()
|
|
result.append('GROUP BY %s' % ', '.join(grouping))
|
|
|
|
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' is 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 = {}
|
|
conjunction = (connection == AND)
|
|
first = 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],
|
|
(conjunction and not first), used, promote, not conjunction)
|
|
used[new_alias] = None
|
|
change_map[alias] = new_alias
|
|
first = False
|
|
|
|
# So that we don't exclude valid results in an "or" query combination,
|
|
# the first join that is exclusive to the lhs (self) must be converted
|
|
# to an outer join.
|
|
if not conjunction:
|
|
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:
|
|
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
|
|
self.extra_order_by = (rhs.extra_order_by and rhs.extra_order_by[:] or
|
|
self.extra_order_by)
|
|
|
|
def pre_sql_setup(self):
|
|
"""
|
|
Does any necessary class setup immediately prior to producing SQL. This
|
|
is for things that can't necessarily be done in __init__ because we
|
|
might not have all the pieces in place at that time.
|
|
"""
|
|
if not self.tables:
|
|
self.join((None, self.model._meta.db_table, None, None))
|
|
if self.select_related:
|
|
self.fill_related_selections()
|
|
|
|
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.quote_name_unless_alias
|
|
result = []
|
|
aliases = []
|
|
if self.select:
|
|
for col in self.select:
|
|
if isinstance(col, (list, tuple)):
|
|
r = '%s.%s' % (qn(col[0]), qn(col[1]))
|
|
result.append(r)
|
|
aliases.append(r)
|
|
else:
|
|
result.append(col.as_sql(quote_func=qn))
|
|
if hasattr(col, 'alias'):
|
|
aliases.append(col.alias)
|
|
elif self.default_cols:
|
|
table_alias = self.tables[0]
|
|
result = ['%s.%s' % (qn(table_alias), qn(f.column))
|
|
for f in self.model._meta.fields]
|
|
aliases = result[:]
|
|
|
|
result.extend(['(%s) AS %s' % (col, alias)
|
|
for alias, col in self.extra_select.items()])
|
|
aliases.extend(self.extra_select.keys())
|
|
|
|
self._select_aliases = dict.fromkeys(aliases)
|
|
return result
|
|
|
|
def get_from_clause(self):
|
|
"""
|
|
Returns a list of strings that are joined together to go after the
|
|
"FROM" part of the query, as well as any extra parameters that need to
|
|
be included. Sub-classes, can override this to create a from-clause via
|
|
a "select", for example (e.g. CountQuery).
|
|
|
|
This should only be called after any SQL construction methods that
|
|
might change the tables we need. This means the select columns and
|
|
ordering must be done first.
|
|
"""
|
|
result = []
|
|
qn = self.quote_name_unless_alias
|
|
first = True
|
|
for alias in self.tables:
|
|
if not self.alias_map[alias][ALIAS_REFCOUNT]:
|
|
continue
|
|
join = self.alias_map[alias][ALIAS_JOIN]
|
|
if join:
|
|
name, alias, join_type, lhs, lhs_col, col = join
|
|
alias_str = (alias != name and ' AS %s' % alias or '')
|
|
else:
|
|
join_type = None
|
|
alias_str = ''
|
|
name = alias
|
|
if join_type:
|
|
result.append('%s %s%s ON (%s.%s = %s.%s)'
|
|
% (join_type, qn(name), alias_str, qn(lhs),
|
|
qn(lhs_col), qn(alias), qn(col)))
|
|
else:
|
|
connector = not first and ', ' or ''
|
|
result.append('%s%s%s' % (connector, qn(name), alias_str))
|
|
first = False
|
|
extra_tables = []
|
|
for t in self.extra_tables:
|
|
alias, created = self.table_alias(t)
|
|
if created:
|
|
connector = not first and ', ' or ''
|
|
result.append('%s%s' % (connector, alias))
|
|
first = False
|
|
return result, []
|
|
|
|
def get_grouping(self):
|
|
"""
|
|
Returns a tuple representing the SQL elements in the "group by" clause.
|
|
"""
|
|
qn = self.quote_name_unless_alias
|
|
result = []
|
|
for col in self.group_by:
|
|
if isinstance(col, (list, tuple)):
|
|
result.append('%s.%s' % (qn(col[0]), qn(col[1])))
|
|
elif hasattr(col, 'as_sql'):
|
|
result.append(col.as_sql(qn))
|
|
else:
|
|
result.append(str(col))
|
|
return result
|
|
|
|
def get_ordering(self):
|
|
"""
|
|
Returns a tuple representing the SQL elements in the "order by" clause.
|
|
|
|
Determining the ordering SQL can change the tables we need to include,
|
|
so this should be run *before* get_from_clause().
|
|
"""
|
|
if self.extra_order_by:
|
|
ordering = self.extra_order_by
|
|
elif self.order_by is None:
|
|
ordering = []
|
|
else:
|
|
# Note that self.order_by can be empty in two ways: [] ("use the
|
|
# default"), which is handled here, and None ("no ordering"), which
|
|
# is handled in the previous test.
|
|
ordering = self.order_by or self.model._meta.ordering
|
|
qn = self.quote_name_unless_alias
|
|
distinct = self.distinct
|
|
select_aliases = self._select_aliases
|
|
result = []
|
|
for field in ordering:
|
|
if field == '?':
|
|
result.append(self.connection.ops.random_function_sql())
|
|
continue
|
|
if isinstance(field, int):
|
|
if field < 0:
|
|
order = 'DESC'
|
|
field = -field
|
|
else:
|
|
order = 'ASC'
|
|
result.append('%s %s' % (field, order))
|
|
continue
|
|
if '.' in field:
|
|
# This came in through an extra(ordering=...) addition. Pass it
|
|
# on verbatim, after mapping the table name to an alias, if
|
|
# necessary.
|
|
col, order = get_order_dir(field)
|
|
table, col = col.split('.', 1)
|
|
elt = '%s.%s' % (qn(self.table_alias(table)[0]), col)
|
|
if not distinct or elt in select_aliases:
|
|
result.append('%s %s' % (elt, order))
|
|
elif get_order_dir(field)[0] not in self.extra_select:
|
|
# 'col' is of the form 'field' or 'field1__field2' or
|
|
# '-field1__field2__field', etc.
|
|
for table, col, order in self.find_ordering_name(field,
|
|
self.model._meta):
|
|
elt = '%s.%s' % (qn(table), qn(col))
|
|
if not distinct or elt in select_aliases:
|
|
result.append('%s %s' % (elt, order))
|
|
else:
|
|
col, order = get_order_dir(field)
|
|
elt = qn(col)
|
|
if not distinct or elt in select_aliases:
|
|
result.append('%s %s' % (elt, order))
|
|
return result
|
|
|
|
def find_ordering_name(self, name, opts, alias=None, default_order='ASC'):
|
|
"""
|
|
Returns the table alias (the name might be ambiguous, the alias will
|
|
not be) and column name for ordering by the given 'name' parameter.
|
|
The 'name' is of the form 'field1__field2__...__fieldN'.
|
|
"""
|
|
name, order = get_order_dir(name, default_order)
|
|
pieces = name.split(LOOKUP_SEP)
|
|
if not alias:
|
|
alias = self.join((None, opts.db_table, None, None))
|
|
field, target, opts, joins, unused2 = self.setup_joins(pieces, opts,
|
|
alias, False)
|
|
alias = joins[-1][-1]
|
|
col = target.column
|
|
|
|
# If we get to this point and the field is a relation to another model,
|
|
# append the default ordering for that model.
|
|
if len(joins) > 1 and opts.ordering:
|
|
results = []
|
|
for item in opts.ordering:
|
|
results.extend(self.find_ordering_name(item, opts, alias,
|
|
order))
|
|
return results
|
|
|
|
if alias:
|
|
# We have to do the same "final join" optimisation as in
|
|
# add_filter, since the final column might not otherwise be part of
|
|
# the select set (so we can't order on it).
|
|
join = self.alias_map[alias][ALIAS_JOIN]
|
|
if col == join[RHS_JOIN_COL]:
|
|
self.unref_alias(alias)
|
|
alias = join[LHS_ALIAS]
|
|
col = join[LHS_JOIN_COL]
|
|
return [(alias, col, order)]
|
|
|
|
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, False]
|
|
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 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, 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
|
|
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 '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 is None:
|
|
lhs_table = None
|
|
is_table = False
|
|
elif lhs not in self.alias_map:
|
|
lhs_table = lhs
|
|
is_table = True
|
|
else:
|
|
lhs_table = self.alias_map[lhs][ALIAS_TABLE]
|
|
is_table = False
|
|
t_ident = (lhs_table, table, lhs_col, col)
|
|
if not always_create:
|
|
aliases = self.join_map.get(t_ident)
|
|
if aliases:
|
|
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)
|
|
if promote or outer_if_first:
|
|
join_type = self.LOUTER
|
|
else:
|
|
join_type = 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_related_selections(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.column) 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), exclusions=used)
|
|
used.append(alias)
|
|
self.select.extend([(alias, f2.column)
|
|
for f2 in f.rel.to._meta.fields])
|
|
self.fill_related_selections(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))
|
|
|
|
field, target, unused, join_list, nullable = self.setup_joins(parts,
|
|
opts, alias, (connection == AND))
|
|
col = target.column
|
|
alias = join_list[-1][-1]
|
|
|
|
if join_list:
|
|
# An optimization: if the final join is against the same column as
|
|
# we are comparing against, 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.
|
|
join = self.alias_map[alias][ALIAS_JOIN]
|
|
if col == join[RHS_JOIN_COL]:
|
|
self.unref_alias(alias)
|
|
alias = join[LHS_ALIAS]
|
|
col = 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.promote_alias(join_list[-1][0])
|
|
|
|
self.where.add([alias, col, field, lookup_type, value], connection)
|
|
if negate:
|
|
flag = False
|
|
for pos, null in enumerate(nullable):
|
|
if not null:
|
|
continue
|
|
flag = True
|
|
for join in join_list[pos]:
|
|
self.promote_alias(join)
|
|
self.where.negate()
|
|
if flag:
|
|
self.where.add([alias, col, field, 'isnull', True], OR)
|
|
|
|
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, 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 setup_joins(self, names, opts, alias, dupe_multis):
|
|
"""
|
|
Compute the necessary table joins for the passage through the fields
|
|
given in 'names'. 'opts' is the Options class for the current model
|
|
(which gives the table we are joining to), '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 the final field involved in the join, the target database
|
|
column (used for any 'where' constraint), the final 'opts' value, the
|
|
list of tables joined and a list indicating whether or not each join
|
|
can be null.
|
|
"""
|
|
joins = [[alias]]
|
|
nullable = [False]
|
|
for pos, name in enumerate(names):
|
|
if name == 'pk':
|
|
name = opts.pk.name
|
|
|
|
try:
|
|
field, direct, m2m = opts.get_field_by_name(name)
|
|
except FieldDoesNotExist:
|
|
names = opts.get_all_field_names()
|
|
raise TypeError("Cannot resolve keyword %r into field. "
|
|
"Choices are: %s" % (name, ", ".join(names)))
|
|
cached_data = opts._join_cache.get(name)
|
|
orig_opts = opts
|
|
|
|
if direct:
|
|
if m2m:
|
|
# Many-to-many field defined on the current model.
|
|
if cached_data:
|
|
(table1, from_col1, to_col1, table2, from_col2,
|
|
to_col2, opts, target) = cached_data
|
|
else:
|
|
table1 = field.m2m_db_table()
|
|
from_col1 = opts.pk.column
|
|
to_col1 = field.m2m_column_name()
|
|
opts = field.rel.to._meta
|
|
table2 = opts.db_table
|
|
from_col2 = field.m2m_reverse_name()
|
|
to_col2 = opts.pk.column
|
|
target = opts.pk
|
|
orig_opts._join_cache[name] = (table1, from_col1,
|
|
to_col1, table2, from_col2, to_col2, opts,
|
|
target)
|
|
|
|
int_alias = self.join((alias, table1, from_col1, to_col1),
|
|
dupe_multis)
|
|
alias = self.join((int_alias, table2, from_col2, to_col2),
|
|
dupe_multis)
|
|
joins.append([int_alias, alias])
|
|
nullable.append(field.null)
|
|
elif field.rel:
|
|
# One-to-one or many-to-one field
|
|
if cached_data:
|
|
(table, from_col, to_col, opts, target) = cached_data
|
|
else:
|
|
opts = field.rel.to._meta
|
|
target = field.rel.get_related_field()
|
|
table = opts.db_table
|
|
from_col = field.column
|
|
to_col = target.column
|
|
orig_opts._join_cache[name] = (table, from_col, to_col,
|
|
opts, target)
|
|
|
|
alias = self.join((alias, table, from_col, to_col))
|
|
joins.append([alias])
|
|
nullable.append(field.null)
|
|
else:
|
|
target = field
|
|
break
|
|
else:
|
|
orig_field = field
|
|
field = field.field
|
|
nullable.append(True)
|
|
if m2m:
|
|
# Many-to-many field defined on the target model.
|
|
if cached_data:
|
|
(table1, from_col1, to_col1, table2, from_col2,
|
|
to_col2, opts, target) = cached_data
|
|
else:
|
|
table1 = field.m2m_db_table()
|
|
from_col1 = opts.pk.column
|
|
to_col1 = field.m2m_reverse_name()
|
|
opts = orig_field.opts
|
|
table2 = opts.db_table
|
|
from_col2 = field.m2m_column_name()
|
|
to_col2 = opts.pk.column
|
|
target = opts.pk
|
|
orig_opts._join_cache[name] = (table1, from_col1,
|
|
to_col1, table2, from_col2, to_col2, opts,
|
|
target)
|
|
|
|
int_alias = self.join((alias, table1, from_col1, to_col1),
|
|
dupe_multis)
|
|
alias = self.join((int_alias, table2, from_col2, to_col2),
|
|
dupe_multis)
|
|
joins.append([int_alias, alias])
|
|
else:
|
|
# One-to-many field (ForeignKey defined on the target model)
|
|
if cached_data:
|
|
(table, from_col, to_col, opts, target) = cached_data
|
|
else:
|
|
local_field = opts.get_field_by_name(
|
|
field.rel.field_name)[0]
|
|
opts = orig_field.opts
|
|
table = opts.db_table
|
|
from_col = local_field.column
|
|
to_col = field.column
|
|
target = opts.pk
|
|
orig_opts._join_cache[name] = (table, from_col, to_col,
|
|
opts, target)
|
|
|
|
alias = self.join((alias, table, from_col, to_col),
|
|
dupe_multis)
|
|
joins.append([alias])
|
|
|
|
if pos != len(names) - 1:
|
|
raise TypeError("Join on field %r not permitted." % name)
|
|
|
|
return field, target, opts, joins, nullable
|
|
|
|
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:
|
|
if self.high_mark:
|
|
self.high_mark = min(self.high_mark, self.low_mark + high)
|
|
else:
|
|
self.high_mark = self.low_mark + high
|
|
if low:
|
|
if self.high_mark:
|
|
self.low_mark = min(self.high_mark, self.low_mark + low)
|
|
else:
|
|
self.low_mark = self.low_mark + low
|
|
|
|
def clear_limits(self):
|
|
"""
|
|
Clears any existing limits.
|
|
"""
|
|
self.low_mark, self.high_mark = 0, None
|
|
|
|
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_local_columns(self, columns):
|
|
"""
|
|
Adds the given column names to the select set, assuming they come from
|
|
the root model (the one given in self.model).
|
|
"""
|
|
table = self.model._meta.db_table
|
|
self.select.extend([(table, col) for col in columns])
|
|
|
|
def add_ordering(self, *ordering):
|
|
"""
|
|
Adds items from the 'ordering' sequence to the query's "order by"
|
|
clause. These items are either field names (not column names) --
|
|
possibly with a direction prefix ('-' or '?') -- or ordinals,
|
|
corresponding to column positions in the 'select' list.
|
|
"""
|
|
errors = []
|
|
for item in ordering:
|
|
if not ORDER_PATTERN.match(item):
|
|
errors.append(item)
|
|
if errors:
|
|
raise TypeError('Invalid order_by arguments: %s' % errors)
|
|
self.order_by.extend(ordering)
|
|
|
|
def clear_ordering(self, force_empty=False):
|
|
"""
|
|
Removes any ordering settings. If 'force_empty' is True, there will be
|
|
no ordering in the resulting query (not even the model's default).
|
|
"""
|
|
if force_empty:
|
|
self.order_by = None
|
|
else:
|
|
self.order_by = []
|
|
self.extra_order_by = []
|
|
|
|
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()
|
|
else:
|
|
opts = self.model._meta
|
|
if not self.select:
|
|
select = Count((self.join((None, opts.db_table, None, None)),
|
|
opts.pk.column), True)
|
|
else:
|
|
# Because of SQL portability issues, multi-column, distinct
|
|
# counts need a sub-query -- see get_count() for details.
|
|
assert len(self.select) == 1, \
|
|
"Cannot add count col with multiple cols in 'select'."
|
|
select = Count(self.select[0], True)
|
|
|
|
# Distinct handling is done in Count(), so don't do it at this
|
|
# level.
|
|
self.distinct = False
|
|
self.select = [select]
|
|
self.extra_select = SortedDict()
|
|
|
|
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.
|
|
return results_iter(cursor)
|
|
|
|
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()
|
|
where.add((None, related.field.m2m_reverse_name(),
|
|
related.field, '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()
|
|
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()
|
|
where.add((None, f.m2m_column_name(), f, '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()
|
|
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.quote_name_unless_alias
|
|
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.column, f, '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)
|
|
|
|
class DateQuery(Query):
|
|
"""
|
|
A DateQuery is a normal query, except that it specifically selects a single
|
|
date field. This requires some special handling when converting the results
|
|
back to Python objects, so we put it in a separate class.
|
|
"""
|
|
def results_iter(self):
|
|
"""
|
|
Returns an iterator over the results from executing this query.
|
|
"""
|
|
resolve_columns = hasattr(self, 'resolve_columns')
|
|
if resolve_columns:
|
|
from django.db.models.fields import DateTimeField
|
|
fields = [DateTimeField()]
|
|
else:
|
|
from django.db.backends.util import typecast_timestamp
|
|
needs_string_cast = self.connection.features.needs_datetime_string_cast
|
|
|
|
for rows in self.execute_sql(MULTI):
|
|
for row in rows:
|
|
date = row[0]
|
|
if resolve_columns:
|
|
date = self.resolve_columns([date], fields)[0]
|
|
elif needs_string_cast:
|
|
date = typecast_timestamp(str(date))
|
|
yield date
|
|
|
|
def add_date_select(self, column, lookup_type, order='ASC'):
|
|
"""
|
|
Converts the query into a date extraction query.
|
|
"""
|
|
alias = self.join((None, self.model._meta.db_table, None, None))
|
|
select = Date((alias, column), lookup_type,
|
|
self.connection.ops.date_trunc_sql)
|
|
self.select = [select]
|
|
self.order_by = order == 'ASC' and [1] or [-1]
|
|
if self.connection.features.allows_group_by_ordinal:
|
|
self.group_by = [1]
|
|
else:
|
|
self.group_by = [select]
|
|
|
|
class CountQuery(Query):
|
|
"""
|
|
A CountQuery knows how to take a normal query which would select over
|
|
multiple distinct columns and turn it into SQL that can be used on a
|
|
variety of backends (it requires a select in the FROM clause).
|
|
"""
|
|
def get_from_clause(self):
|
|
result, params = self._query.as_sql()
|
|
return ['(%s) AS A1' % result], params
|
|
|
|
def get_ordering(self):
|
|
return ()
|
|
|
|
def get_order_dir(field, default='ASC'):
|
|
"""
|
|
Returns the field name and direction for an order specification. For
|
|
example, '-foo' is returned as ('foo', 'DESC').
|
|
|
|
The 'default' param is used to indicate which way no prefix (or a '+'
|
|
prefix) should sort. The '-' prefix always sorts the opposite way.
|
|
"""
|
|
dirn = ORDER_DIR[default]
|
|
if field[0] == '-':
|
|
return field[1:], dirn[1]
|
|
return field, dirn[0]
|
|
|
|
def results_iter(cursor):
|
|
while 1:
|
|
rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)
|
|
if not rows:
|
|
raise StopIteration
|
|
yield rows
|
|
|
|
def setup_join_cache(sender):
|
|
"""
|
|
The information needed to join between model fields is something that is
|
|
invariant over the life of the model, so we cache it in the model's Options
|
|
class, rather than recomputing it all the time.
|
|
|
|
This method initialises the (empty) cache when the model is created.
|
|
"""
|
|
sender._meta._join_cache = {}
|
|
|
|
dispatcher.connect(setup_join_cache, signal=signals.class_prepared)
|
|
|