summaryrefslogtreecommitdiff
path: root/django/db/models/sql
diff options
context:
space:
mode:
Diffstat (limited to 'django/db/models/sql')
-rw-r--r--django/db/models/sql/query.py149
1 files changed, 113 insertions, 36 deletions
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 8914bce15c..35c6cc28dc 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -8,6 +8,7 @@ all about the internals of models in order to get the information it needs.
"""
import copy
+import re
from django.utils import tree
from django.db.models.sql.where import WhereNode, AND, OR
@@ -35,7 +36,7 @@ GET_ITERATOR_CHUNK_SIZE = 100
# Separator used to split filter strings apart.
LOOKUP_SEP = '__'
-# Constants to make looking up tuple values clearerer.
+# Constants to make looking up tuple values clearer.
# Join lists
TABLE_NAME = 0
RHS_ALIAS = 1
@@ -43,7 +44,7 @@ JOIN_TYPE = 2
LHS_ALIAS = 3
LHS_JOIN_COL = 4
RHS_JOIN_COL = 5
-# Alias maps lists
+# Alias map lists
ALIAS_TABLE = 0
ALIAS_REFCOUNT = 1
ALIAS_JOIN = 2
@@ -54,6 +55,8 @@ MULTI = 'multi'
SINGLE = 'single'
NONE = None
+ORDER_PATTERN = re.compile(r'\?|[-+]?\w+$')
+
class Query(object):
"""
A single SQL query.
@@ -91,6 +94,7 @@ class Query(object):
self.extra_tables = []
self.extra_where = []
self.extra_params = []
+ self.extra_order_by = []
def __str__(self):
"""
@@ -140,6 +144,7 @@ class Query(object):
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
@@ -189,19 +194,22 @@ class Query(object):
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()
+
result = ['SELECT']
if self.distinct:
result.append('DISTINCT')
- out_cols = self.get_columns()
result.append(', '.join(out_cols))
result.append('FROM')
- from_, f_params = self.get_from_clause()
result.extend(from_)
params = list(f_params)
- where, w_params = self.where.as_sql()
- params.extend(w_params)
if where:
result.append('WHERE %s' % where)
if self.extra_where:
@@ -210,12 +218,12 @@ class Query(object):
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))
- ordering = self.get_ordering()
if ordering:
result.append('ORDER BY %s' % ', '.join(ordering))
@@ -312,11 +320,14 @@ class Query(object):
# 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 prior to producing SQL. This is for
- things that can't necessarily be done in __init__.
+ 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))
@@ -356,9 +367,14 @@ class Query(object):
"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
@@ -370,12 +386,16 @@ class Query(object):
% (join_type, qn(name), alias_str, qn(lhs),
qn(lhs_col), qn(alias), qn(col)))
else:
- result.append('%s%s' % (qn(name), alias_str))
+ 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:
- result.append(', %s' % alias)
+ connector = not first and ', ' or ''
+ result.append('%s%s' % (connector, alias))
+ first = False
return result, []
def get_grouping(self):
@@ -396,13 +416,20 @@ class Query(object):
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.order_by is None:
+ 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
- opts = self.model._meta
result = []
for field in ordering:
if field == '?':
@@ -416,24 +443,62 @@ class Query(object):
order = 'ASC'
result.append('%s %s' % (field, order))
continue
- if field[0] == '-':
- col = field[1:]
- order = 'DESC'
- else:
- col = field
- order = 'ASC'
- if '.' in col:
+ 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)
- table = '%s.' % qn(self.table_alias(table)[0])
- elif col not in self.extra_select:
- # Use the root model's database table as the referenced table.
- table = '%s.' % qn(self.tables[0])
+ result.append('%s.%s %s' % (qn(self.table_alias(table)[0]), col,
+ 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):
+ result.append('%s.%s %s' % (qn(table), qn(col), order))
else:
- table = ''
- result.append('%s%s %s' % (table,
- qn(orderfield_to_column(col, opts)), order))
+ col, order = get_order_dir(field)
+ result.append('%s %s' % (qn(col), order))
return result
+ def find_ordering_name(self, name, opts, alias=None, default_order='ASC'):
+ """
+ Returns the table alias (the name might not be unambiguous, the alias
+ will 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))
+ for elt in pieces:
+ joins, opts, unused1, field, col, unused2 = \
+ self.get_next_join(elt, opts, alias, False)
+ if joins:
+ alias = joins[-1]
+ col = col or field.column
+
+ # If we get to this point and the field is a relation to another model,
+ # append the default ordering for that model.
+ if joins 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
@@ -557,7 +622,7 @@ class Query(object):
alias = self.join((root_alias, table, f.column,
f.rel.get_related_field().column), exclusions=used)
used.append(alias)
- self.select.extend([(table, f2.column)
+ 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)
@@ -802,6 +867,12 @@ class Query(object):
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):
@@ -813,6 +884,7 @@ class Query(object):
self.order_by = None
else:
self.order_by = []
+ self.extra_order_by = []
def add_count_column(self):
"""
@@ -1043,6 +1115,9 @@ class CountQuery(Query):
result, params = self._query.as_sql()
return ['(%s) AS A1' % result], params
+ def get_ordering(self):
+ return ()
+
def find_field(name, field_list, related_query):
"""
Finds a field with a specific name in a list of field instances.
@@ -1077,14 +1152,16 @@ def get_legal_fields(opts):
+ field_choices(opts.get_all_related_objects(), True)
+ field_choices(opts.fields, False))
-def orderfield_to_column(name, opts):
+def get_order_dir(field, default='ASC'):
"""
- 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.
+ 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.
"""
- try:
- return opts.get_field(name, False).column
- except FieldDoesNotExist:
- return name
+ dirn = {'ASC': ('ASC', 'DESC'), 'DESC': ('DESC', 'ASC')}[default]
+ if field[0] == '-':
+ return field[1:], dirn[1]
+ return field, dirn[0]