Fixed #24060 -- Added OrderBy Expressions

This commit is contained in:
Josh Smeaton 2015-01-10 02:16:16 +11:00
parent f48e2258a9
commit 21b858cb67
9 changed files with 310 additions and 54 deletions

View File

@ -300,7 +300,7 @@ class DatabaseOperations(BaseDatabaseOperations):
columns. If no ordering would otherwise be applied, we don't want any columns. If no ordering would otherwise be applied, we don't want any
implicit sorting going on. implicit sorting going on.
""" """
return [(None, ("NULL", [], 'asc', False))] return [(None, ("NULL", [], False))]
def fulltext_search_sql(self, field_name): def fulltext_search_sql(self, field_name):
return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name

View File

@ -118,7 +118,7 @@ class CombinableMixin(object):
) )
class ExpressionNode(CombinableMixin): class BaseExpression(object):
""" """
Base class for all query expressions. Base class for all query expressions.
""" """
@ -189,6 +189,10 @@ class ExpressionNode(CombinableMixin):
""" """
c = self.copy() c = self.copy()
c.is_summary = summarize c.is_summary = summarize
c.set_source_expressions([
expr.resolve_expression(query, allow_joins, reuse, summarize)
for expr in c.get_source_expressions()
])
return c return c
def _prepare(self): def _prepare(self):
@ -319,6 +323,22 @@ class ExpressionNode(CombinableMixin):
""" """
return [e._output_field_or_none for e in self.get_source_expressions()] return [e._output_field_or_none for e in self.get_source_expressions()]
def asc(self):
return OrderBy(self)
def desc(self):
return OrderBy(self, descending=True)
def reverse_ordering(self):
return self
class ExpressionNode(BaseExpression, CombinableMixin):
"""
An expression that can be combined with other expressions.
"""
pass
class Expression(ExpressionNode): class Expression(ExpressionNode):
@ -412,6 +432,12 @@ class F(CombinableMixin):
def refs_aggregate(self, existing_aggregates): def refs_aggregate(self, existing_aggregates):
return refs_aggregate(self.name.split(LOOKUP_SEP), existing_aggregates) return refs_aggregate(self.name.split(LOOKUP_SEP), existing_aggregates)
def asc(self):
return OrderBy(self)
def desc(self):
return OrderBy(self, descending=True)
class Func(ExpressionNode): class Func(ExpressionNode):
""" """
@ -526,15 +552,6 @@ class Random(ExpressionNode):
return connection.ops.random_function_sql(), [] return connection.ops.random_function_sql(), []
class ColIndexRef(ExpressionNode):
def __init__(self, idx):
self.idx = idx
super(ColIndexRef, self).__init__()
def as_sql(self, compiler, connection):
return str(self.idx), []
class Col(ExpressionNode): class Col(ExpressionNode):
def __init__(self, alias, target, source=None): def __init__(self, alias, target, source=None):
if source is None: if source is None:
@ -678,3 +695,43 @@ class DateTime(ExpressionNode):
value = value.replace(tzinfo=None) value = value.replace(tzinfo=None)
value = timezone.make_aware(value, self.tzinfo) value = timezone.make_aware(value, self.tzinfo)
return value return value
class OrderBy(BaseExpression):
template = '%(expression)s %(ordering)s'
descending_template = 'DESC'
ascending_template = 'ASC'
def __init__(self, expression, descending=False):
self.descending = descending
if not hasattr(expression, 'resolve_expression'):
raise ValueError('expression must be an expression type')
self.expression = expression
def set_source_expressions(self, exprs):
self.expression = exprs[0]
def get_source_expressions(self):
return [self.expression]
def as_sql(self, compiler, connection):
expression_sql, params = compiler.compile(self.expression)
placeholders = {'expression': expression_sql}
placeholders['ordering'] = 'DESC' if self.descending else 'ASC'
return (self.template % placeholders).rstrip(), params
def get_group_by_cols(self):
cols = []
for source in self.get_source_expressions():
cols.extend(source.get_group_by_cols())
return cols
def reverse_ordering(self):
self.descending = not self.descending
return self
def asc(self):
self.descending = False
def desc(self):
self.descending = True

View File

@ -1,9 +1,10 @@
from itertools import chain from itertools import chain
import re
import warnings import warnings
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import RawSQL, Ref, Random, ColIndexRef from django.db.models.expressions import OrderBy, Random, RawSQL, Ref
from django.db.models.query_utils import select_related_descend, QueryWrapper from django.db.models.query_utils import select_related_descend, QueryWrapper
from django.db.models.sql.constants import (CURSOR, SINGLE, MULTI, NO_RESULTS, from django.db.models.sql.constants import (CURSOR, SINGLE, MULTI, NO_RESULTS,
ORDER_DIR, GET_ITERATOR_CHUNK_SIZE) ORDER_DIR, GET_ITERATOR_CHUNK_SIZE)
@ -28,6 +29,7 @@ class SQLCompiler(object):
self.select = None self.select = None
self.annotation_col_map = None self.annotation_col_map = None
self.klass_info = None self.klass_info = None
self.ordering_parts = re.compile(r'(.*)\s(ASC|DESC)(.*)')
def setup_query(self): def setup_query(self):
if all(self.query.alias_refcount[a] == 0 for a in self.query.tables): if all(self.query.alias_refcount[a] == 0 for a in self.query.tables):
@ -105,14 +107,14 @@ class SQLCompiler(object):
cols = expr.get_group_by_cols() cols = expr.get_group_by_cols()
for col in cols: for col in cols:
expressions.append(col) expressions.append(col)
for expr, _ in order_by: for expr, (sql, params, is_ref) in order_by:
if expr.contains_aggregate: if expr.contains_aggregate:
continue continue
# We can skip References to select clause, as all expressions in # We can skip References to select clause, as all expressions in
# the select clause are already part of the group by. # the select clause are already part of the group by.
if isinstance(expr, Ref): if is_ref:
continue continue
expressions.append(expr) expressions.extend(expr.get_source_expressions())
having = self.query.having.get_group_by_cols() having = self.query.having.get_group_by_cols()
for expr in having: for expr in having:
expressions.append(expr) expressions.append(expr)
@ -234,54 +236,75 @@ class SQLCompiler(object):
order_by = [] order_by = []
for pos, field in enumerate(ordering): for pos, field in enumerate(ordering):
if field == '?': if hasattr(field, 'resolve_expression'):
order_by.append((Random(), asc, False)) if not isinstance(field, OrderBy):
field = field.asc()
if not self.query.standard_ordering:
field.reverse_ordering()
order_by.append((field, False))
continue continue
if isinstance(field, int): if field == '?': # random
if field < 0: order_by.append((OrderBy(Random()), False))
field = -field
int_ord = desc
order_by.append((ColIndexRef(field), int_ord, True))
continue continue
col, order = get_order_dir(field, asc) col, order = get_order_dir(field, asc)
descending = True if order == 'DESC' else False
if col in self.query.annotation_select: if col in self.query.annotation_select:
order_by.append((Ref(col, self.query.annotation_select[col]), order, True)) order_by.append((
OrderBy(Ref(col, self.query.annotation_select[col]), descending=descending),
True))
continue continue
if '.' in field: if '.' in field:
# This came in through an extra(order_by=...) addition. Pass it # This came in through an extra(order_by=...) addition. Pass it
# on verbatim. # on verbatim.
table, col = col.split('.', 1) table, col = col.split('.', 1)
expr = RawSQL('%s.%s' % (self.quote_name_unless_alias(table), col), []) order_by.append((
order_by.append((expr, order, False)) OrderBy(RawSQL('%s.%s' % (self.quote_name_unless_alias(table), col), [])),
False))
continue continue
if not self.query._extra or get_order_dir(field)[0] not in self.query._extra:
if not self.query._extra or col not in self.query._extra:
# 'col' is of the form 'field' or 'field1__field2' or # 'col' is of the form 'field' or 'field1__field2' or
# '-field1__field2__field', etc. # '-field1__field2__field', etc.
order_by.extend(self.find_ordering_name(field, self.query.get_meta(), order_by.extend(self.find_ordering_name(
default_order=asc)) field, self.query.get_meta(), default_order=asc))
else: else:
if col not in self.query.extra_select: if col not in self.query.extra_select:
order_by.append((RawSQL(*self.query.extra[col]), order, False)) order_by.append((
OrderBy(RawSQL(*self.query.extra[col]), descending=descending),
False))
else: else:
order_by.append((Ref(col, RawSQL(*self.query.extra[col])), order_by.append((
order, True)) OrderBy(Ref(col, RawSQL(*self.query.extra[col])), descending=descending),
True))
result = [] result = []
seen = set() seen = set()
for expr, order, is_ref in order_by:
sql, params = self.compile(expr) for expr, is_ref in order_by:
if (sql, tuple(params)) in seen: resolved = expr.resolve_expression(
self.query, allow_joins=True, reuse=None)
sql, params = self.compile(resolved)
# Don't add the same column twice, but the order direction is
# not taken into account so we strip it. When this entire method
# is refactored into expressions, then we can check each part as we
# generate it.
without_ordering = self.ordering_parts.search(sql).group(1)
if (without_ordering, tuple(params)) in seen:
continue continue
seen.add((sql, tuple(params))) seen.add((without_ordering, tuple(params)))
result.append((expr, (sql, params, order, is_ref))) result.append((resolved, (sql, params, is_ref)))
return result return result
def get_extra_select(self, order_by, select): def get_extra_select(self, order_by, select):
extra_select = [] extra_select = []
select_sql = [t[1] for t in select] select_sql = [t[1] for t in select]
if self.query.distinct and not self.query.distinct_fields: if self.query.distinct and not self.query.distinct_fields:
for expr, (sql, params, _, is_ref) in order_by: for expr, (sql, params, is_ref) in order_by:
if not is_ref and (sql, params) not in select_sql: without_ordering = self.ordering_parts.search(sql).group(1)
extra_select.append((expr, (sql, params), None)) if not is_ref and (without_ordering, params) not in select_sql:
extra_select.append((expr, (without_ordering, params), None))
return extra_select return extra_select
def __call__(self, name): def __call__(self, name):
@ -392,8 +415,8 @@ class SQLCompiler(object):
if order_by: if order_by:
ordering = [] ordering = []
for _, (o_sql, o_params, order, _) in order_by: for _, (o_sql, o_params, _) in order_by:
ordering.append('%s %s' % (o_sql, order)) ordering.append(o_sql)
params.extend(o_params) params.extend(o_params)
result.append('ORDER BY %s' % ', '.join(ordering)) result.append('ORDER BY %s' % ', '.join(ordering))
@ -514,6 +537,7 @@ class SQLCompiler(object):
The 'name' is of the form 'field1__field2__...__fieldN'. The 'name' is of the form 'field1__field2__...__fieldN'.
""" """
name, order = get_order_dir(name, default_order) name, order = get_order_dir(name, default_order)
descending = True if order == 'DESC' else False
pieces = name.split(LOOKUP_SEP) pieces = name.split(LOOKUP_SEP)
field, targets, alias, joins, path, opts = self._setup_joins(pieces, opts, alias) field, targets, alias, joins, path, opts = self._setup_joins(pieces, opts, alias)
@ -535,7 +559,7 @@ class SQLCompiler(object):
order, already_seen)) order, already_seen))
return results return results
targets, alias, _ = self.query.trim_joins(targets, joins, path) targets, alias, _ = self.query.trim_joins(targets, joins, path)
return [(t.get_col(alias), order, False) for t in targets] return [(OrderBy(t.get_col(alias), descending=descending), False) for t in targets]
def _setup_joins(self, pieces, opts, alias): def _setup_joins(self, pieces, opts, alias):
""" """

View File

@ -1691,14 +1691,14 @@ class Query(object):
""" """
Adds items from the 'ordering' sequence to the query's "order by" Adds items from the 'ordering' sequence to the query's "order by"
clause. These items are either field names (not column names) -- clause. These items are either field names (not column names) --
possibly with a direction prefix ('-' or '?') -- or ordinals, possibly with a direction prefix ('-' or '?') -- or OrderBy
corresponding to column positions in the 'select' list. expressions.
If 'ordering' is empty, all ordering is cleared from the query. If 'ordering' is empty, all ordering is cleared from the query.
""" """
errors = [] errors = []
for item in ordering: for item in ordering:
if not ORDER_PATTERN.match(item): if not hasattr(item, 'resolve_expression') and not ORDER_PATTERN.match(item):
errors.append(item) errors.append(item)
if errors: if errors:
raise FieldError('Invalid order_by arguments: %s' % errors) raise FieldError('Invalid order_by arguments: %s' % errors)

View File

@ -5,7 +5,7 @@ Query Expressions
.. currentmodule:: django.db.models .. currentmodule:: django.db.models
Query expressions describe a value or a computation that can be used as part of Query expressions describe a value or a computation that can be used as part of
a filter, an annotation, or an aggregation. There are a number of built-in a filter, order by, annotation, or aggregate. There are a number of built-in
expressions (documented below) that can be used to help you write queries. expressions (documented below) that can be used to help you write queries.
Expressions can be combined, or in some cases nested, to form more complex Expressions can be combined, or in some cases nested, to form more complex
computations. computations.
@ -58,6 +58,10 @@ Some examples
# Aggregates can contain complex computations also # Aggregates can contain complex computations also
Company.objects.annotate(num_offerings=Count(F('products') + F('services'))) Company.objects.annotate(num_offerings=Count(F('products') + F('services')))
# Expressions can also be used in order_by()
Company.objects.order_by(Length('name').asc())
Company.objects.order_by(Length('name').desc())
Built-in Expressions Built-in Expressions
==================== ====================
@ -428,6 +432,24 @@ calling the appropriate methods on the wrapped expression.
nested expressions. ``F()`` objects, in particular, hold a reference nested expressions. ``F()`` objects, in particular, hold a reference
to a column. to a column.
.. method:: asc()
Returns the expression ready to be sorted in ascending order.
.. method:: desc()
Returns the expression ready to be sorted in descending order.
.. method:: reverse_ordering()
Returns ``self`` with any modifications required to reverse the sort
order within an ``order_by`` call. As an example, an expression
implementing ``NULLS LAST`` would change its value to be
``NULLS FIRST``. Modifications are only required for expressions that
implement sort order like ``OrderBy``. This method is called when
:meth:`~django.db.models.query.QuerySet.reverse()` is called on a
queryset.
Writing your own Query Expressions Writing your own Query Expressions
---------------------------------- ----------------------------------

View File

@ -322,11 +322,8 @@ identical to::
Entry.objects.order_by('blog__name') Entry.objects.order_by('blog__name')
.. versionadded:: 1.7 It is also possible to order a queryset by a related field, without incurring
the cost of a JOIN, by referring to the ``_id`` of the related field::
Note that it is also possible to order a queryset by a related field,
without incurring the cost of a JOIN, by referring to the ``_id`` of the
related field::
# No Join # No Join
Entry.objects.order_by('blog_id') Entry.objects.order_by('blog_id')
@ -334,6 +331,20 @@ identical to::
# Join # Join
Entry.objects.order_by('blog__id') Entry.objects.order_by('blog__id')
.. versionadded:: 1.7
The ability to order a queryset by a related field, without incurring
the cost of a JOIN was added.
You can also order by :doc:`query expressions </ref/models/expressions>` by
calling ``asc()`` or ``desc()`` on the expression::
Entry.objects.order_by(Coalesce('summary', 'headline').desc())
.. versionadded:: 1.8
Ordering by query expressions was added.
Be cautious when ordering by fields in related models if you are also using Be cautious when ordering by fields in related models if you are also using
:meth:`distinct()`. See the note in :meth:`distinct` for an explanation of how :meth:`distinct()`. See the note in :meth:`distinct` for an explanation of how
related model ordering can change the expected results. related model ordering can change the expected results.
@ -367,6 +378,16 @@ There's no way to specify whether ordering should be case sensitive. With
respect to case-sensitivity, Django will order results however your database respect to case-sensitivity, Django will order results however your database
backend normally orders them. backend normally orders them.
You can order by a field converted to lowercase with
:class:`~django.db.models.functions.Lower` which will achieve case-consistent
ordering::
Entry.objects.order_by(Lower('headline').desc())
.. versionadded:: 1.8
The ability to order by expressions like ``Lower`` was added.
If you don't want any ordering to be applied to a query, not even the default If you don't want any ordering to be applied to a query, not even the default
ordering, call :meth:`order_by()` with no parameters. ordering, call :meth:`order_by()` with no parameters.

View File

@ -100,7 +100,8 @@ Query Expressions and Database Functions
customize, and compose complex SQL expressions. This has enabled annotate customize, and compose complex SQL expressions. This has enabled annotate
to accept expressions other than aggregates. Aggregates are now able to to accept expressions other than aggregates. Aggregates are now able to
reference multiple fields, as well as perform arithmetic, similar to ``F()`` reference multiple fields, as well as perform arithmetic, similar to ``F()``
objects. objects. :meth:`~django.db.models.query.QuerySet.order_by` has also gained the
ability to accept expressions.
A collection of :doc:`database functions </ref/models/database-functions>` is A collection of :doc:`database functions </ref/models/database-functions>` is
also included with functionality such as also included with functionality such as

View File

@ -68,6 +68,37 @@ class FunctionTests(TestCase):
lambda a: a.headline lambda a: a.headline
) )
def test_coalesce_ordering(self):
Author.objects.create(name='John Smith', alias='smithj')
Author.objects.create(name='Rhonda')
authors = Author.objects.order_by(Coalesce('alias', 'name'))
self.assertQuerysetEqual(
authors, [
'Rhonda',
'John Smith',
],
lambda a: a.name
)
authors = Author.objects.order_by(Coalesce('alias', 'name').asc())
self.assertQuerysetEqual(
authors, [
'Rhonda',
'John Smith',
],
lambda a: a.name
)
authors = Author.objects.order_by(Coalesce('alias', 'name').desc())
self.assertQuerysetEqual(
authors, [
'John Smith',
'Rhonda',
],
lambda a: a.name
)
def test_concat(self): def test_concat(self):
Author.objects.create(name='Jayden') Author.objects.create(name='Jayden')
Author.objects.create(name='John Smith', alias='smithj', goes_by='John') Author.objects.create(name='John Smith', alias='smithj', goes_by='John')
@ -184,6 +215,22 @@ class FunctionTests(TestCase):
self.assertEqual(authors.filter(alias_length__lte=Length('name')).count(), 1) self.assertEqual(authors.filter(alias_length__lte=Length('name')).count(), 1)
def test_length_ordering(self):
Author.objects.create(name='John Smith', alias='smithj')
Author.objects.create(name='John Smith', alias='smithj1')
Author.objects.create(name='Rhonda', alias='ronny')
authors = Author.objects.order_by(Length('name'), Length('alias'))
self.assertQuerysetEqual(
authors, [
('Rhonda', 'ronny'),
('John Smith', 'smithj'),
('John Smith', 'smithj1'),
],
lambda a: (a.name, a.alias)
)
def test_substr(self): def test_substr(self):
Author.objects.create(name='John Smith', alias='smithj') Author.objects.create(name='John Smith', alias='smithj')
Author.objects.create(name='Rhonda') Author.objects.create(name='Rhonda')
@ -230,3 +277,25 @@ class FunctionTests(TestCase):
with six.assertRaisesRegex(self, ValueError, "'pos' must be greater than 0"): with six.assertRaisesRegex(self, ValueError, "'pos' must be greater than 0"):
Author.objects.annotate(raises=Substr('name', 0)) Author.objects.annotate(raises=Substr('name', 0))
def test_nested_function_ordering(self):
Author.objects.create(name='John Smith')
Author.objects.create(name='Rhonda Simpson', alias='ronny')
authors = Author.objects.order_by(Length(Coalesce('alias', 'name')))
self.assertQuerysetEqual(
authors, [
'Rhonda Simpson',
'John Smith',
],
lambda a: a.name
)
authors = Author.objects.order_by(Length(Coalesce('alias', 'name')).desc())
self.assertQuerysetEqual(
authors, [
'John Smith',
'Rhonda Simpson',
],
lambda a: a.name
)

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from datetime import datetime from datetime import datetime
from operator import attrgetter from operator import attrgetter
from django.db.models import F
from django.test import TestCase from django.test import TestCase
from .models import Article, Author from .models import Article, Author
@ -203,3 +204,64 @@ class OrderingTests(TestCase):
], ],
attrgetter("headline") attrgetter("headline")
) )
def test_order_by_f_expression(self):
self.assertQuerysetEqual(
Article.objects.order_by(F('headline')), [
"Article 1",
"Article 2",
"Article 3",
"Article 4",
],
attrgetter("headline")
)
self.assertQuerysetEqual(
Article.objects.order_by(F('headline').asc()), [
"Article 1",
"Article 2",
"Article 3",
"Article 4",
],
attrgetter("headline")
)
self.assertQuerysetEqual(
Article.objects.order_by(F('headline').desc()), [
"Article 4",
"Article 3",
"Article 2",
"Article 1",
],
attrgetter("headline")
)
def test_order_by_f_expression_duplicates(self):
"""
A column may only be included once (the first occurrence) so we check
to ensure there are no duplicates by inspecting the SQL.
"""
qs = Article.objects.order_by(F('headline').asc(), F('headline').desc())
sql = str(qs.query).upper()
fragment = sql[sql.find('ORDER BY'):]
self.assertEqual(fragment.count('HEADLINE'), 1)
self.assertQuerysetEqual(
qs, [
"Article 1",
"Article 2",
"Article 3",
"Article 4",
],
attrgetter("headline")
)
qs = Article.objects.order_by(F('headline').desc(), F('headline').asc())
sql = str(qs.query).upper()
fragment = sql[sql.find('ORDER BY'):]
self.assertEqual(fragment.count('HEADLINE'), 1)
self.assertQuerysetEqual(
qs, [
"Article 4",
"Article 3",
"Article 2",
"Article 1",
],
attrgetter("headline")
)