Fixed #13312 -- Allowed specifying the order of null fields in queries.
Thanks Mariusz Felisiak for finishing the patch.
This commit is contained in:
parent
82fd779af5
commit
47ef8f31f3
|
@ -319,11 +319,11 @@ class BaseExpression(object):
|
||||||
"""
|
"""
|
||||||
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):
|
def asc(self, **kwargs):
|
||||||
return OrderBy(self)
|
return OrderBy(self, **kwargs)
|
||||||
|
|
||||||
def desc(self):
|
def desc(self, **kwargs):
|
||||||
return OrderBy(self, descending=True)
|
return OrderBy(self, descending=True, **kwargs)
|
||||||
|
|
||||||
def reverse_ordering(self):
|
def reverse_ordering(self):
|
||||||
return self
|
return self
|
||||||
|
@ -462,11 +462,11 @@ class F(Combinable):
|
||||||
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
|
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
|
||||||
return query.resolve_ref(self.name, allow_joins, reuse, summarize)
|
return query.resolve_ref(self.name, allow_joins, reuse, summarize)
|
||||||
|
|
||||||
def asc(self):
|
def asc(self, **kwargs):
|
||||||
return OrderBy(self)
|
return OrderBy(self, **kwargs)
|
||||||
|
|
||||||
def desc(self):
|
def desc(self, **kwargs):
|
||||||
return OrderBy(self, descending=True)
|
return OrderBy(self, descending=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Func(Expression):
|
class Func(Expression):
|
||||||
|
@ -869,7 +869,11 @@ class Case(Expression):
|
||||||
class OrderBy(BaseExpression):
|
class OrderBy(BaseExpression):
|
||||||
template = '%(expression)s %(ordering)s'
|
template = '%(expression)s %(ordering)s'
|
||||||
|
|
||||||
def __init__(self, expression, descending=False):
|
def __init__(self, expression, descending=False, nulls_first=False, nulls_last=False):
|
||||||
|
if nulls_first and nulls_last:
|
||||||
|
raise ValueError('nulls_first and nulls_last are mutually exclusive')
|
||||||
|
self.nulls_first = nulls_first
|
||||||
|
self.nulls_last = nulls_last
|
||||||
self.descending = descending
|
self.descending = descending
|
||||||
if not hasattr(expression, 'resolve_expression'):
|
if not hasattr(expression, 'resolve_expression'):
|
||||||
raise ValueError('expression must be an expression type')
|
raise ValueError('expression must be an expression type')
|
||||||
|
@ -886,6 +890,11 @@ class OrderBy(BaseExpression):
|
||||||
return [self.expression]
|
return [self.expression]
|
||||||
|
|
||||||
def as_sql(self, compiler, connection, template=None, **extra_context):
|
def as_sql(self, compiler, connection, template=None, **extra_context):
|
||||||
|
if not template:
|
||||||
|
if self.nulls_last:
|
||||||
|
template = '%s NULLS LAST' % self.template
|
||||||
|
elif self.nulls_first:
|
||||||
|
template = '%s NULLS FIRST' % self.template
|
||||||
connection.ops.check_expression_support(self)
|
connection.ops.check_expression_support(self)
|
||||||
expression_sql, params = compiler.compile(self.expression)
|
expression_sql, params = compiler.compile(self.expression)
|
||||||
placeholders = {
|
placeholders = {
|
||||||
|
@ -896,6 +905,22 @@ class OrderBy(BaseExpression):
|
||||||
template = template or self.template
|
template = template or self.template
|
||||||
return (template % placeholders).rstrip(), params
|
return (template % placeholders).rstrip(), params
|
||||||
|
|
||||||
|
def as_sqlite(self, compiler, connection):
|
||||||
|
template = None
|
||||||
|
if self.nulls_last:
|
||||||
|
template = '%(expression)s IS NULL, %(expression)s %(ordering)s'
|
||||||
|
elif self.nulls_first:
|
||||||
|
template = '%(expression)s IS NOT NULL, %(expression)s %(ordering)s'
|
||||||
|
return self.as_sql(compiler, connection, template=template)
|
||||||
|
|
||||||
|
def as_mysql(self, compiler, connection):
|
||||||
|
template = None
|
||||||
|
if self.nulls_last:
|
||||||
|
template = 'IF(ISNULL(%(expression)s),1,0), %(expression)s %(ordering)s '
|
||||||
|
elif self.nulls_first:
|
||||||
|
template = 'IF(ISNULL(%(expression)s),0,1), %(expression)s %(ordering)s '
|
||||||
|
return self.as_sql(compiler, connection, template=template)
|
||||||
|
|
||||||
def get_group_by_cols(self):
|
def get_group_by_cols(self):
|
||||||
cols = []
|
cols = []
|
||||||
for source in self.get_source_expressions():
|
for source in self.get_source_expressions():
|
||||||
|
|
|
@ -560,14 +560,26 @@ 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()
|
.. method:: asc(nulls_first=False, nulls_last=False)
|
||||||
|
|
||||||
Returns the expression ready to be sorted in ascending order.
|
Returns the expression ready to be sorted in ascending order.
|
||||||
|
|
||||||
.. method:: desc()
|
``nulls_first`` and ``nulls_last`` define how null values are sorted.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.11
|
||||||
|
|
||||||
|
The ``nulls_last`` and ``nulls_first`` parameters were added.
|
||||||
|
|
||||||
|
.. method:: desc(nulls_first=False, nulls_last=False)
|
||||||
|
|
||||||
Returns the expression ready to be sorted in descending order.
|
Returns the expression ready to be sorted in descending order.
|
||||||
|
|
||||||
|
``nulls_first`` and ``nulls_last`` define how null values are sorted.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.11
|
||||||
|
|
||||||
|
The ``nulls_first`` and ``nulls_last`` parameters were added.
|
||||||
|
|
||||||
.. method:: reverse_ordering()
|
.. method:: reverse_ordering()
|
||||||
|
|
||||||
Returns ``self`` with any modifications required to reverse the sort
|
Returns ``self`` with any modifications required to reverse the sort
|
||||||
|
|
|
@ -337,6 +337,11 @@ Models
|
||||||
* You can now use the ``unique=True`` option with
|
* You can now use the ``unique=True`` option with
|
||||||
:class:`~django.db.models.FileField`.
|
:class:`~django.db.models.FileField`.
|
||||||
|
|
||||||
|
* Added the ``nulls_first`` and ``nulls_last`` parameters to
|
||||||
|
:class:`Expression.asc() <django.db.models.Expression.asc>` and
|
||||||
|
:meth:`~django.db.models.Expression.desc` to control
|
||||||
|
the ordering of null values.
|
||||||
|
|
||||||
Requests and Responses
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
|
||||||
|
|
||||||
class Author(models.Model):
|
class Author(models.Model):
|
||||||
|
name = models.CharField(max_length=63, null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('-pk',)
|
ordering = ('-pk',)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from datetime import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
|
from django.db.models.functions import Upper
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .models import Article, Author, Reference
|
from .models import Article, Author, Reference
|
||||||
|
@ -17,8 +18,8 @@ class OrderingTests(TestCase):
|
||||||
cls.a2 = Article.objects.create(headline="Article 2", pub_date=datetime(2005, 7, 27))
|
cls.a2 = Article.objects.create(headline="Article 2", pub_date=datetime(2005, 7, 27))
|
||||||
cls.a3 = Article.objects.create(headline="Article 3", pub_date=datetime(2005, 7, 27))
|
cls.a3 = Article.objects.create(headline="Article 3", pub_date=datetime(2005, 7, 27))
|
||||||
cls.a4 = Article.objects.create(headline="Article 4", pub_date=datetime(2005, 7, 28))
|
cls.a4 = Article.objects.create(headline="Article 4", pub_date=datetime(2005, 7, 28))
|
||||||
cls.author_1 = Author.objects.create()
|
cls.author_1 = Author.objects.create(name="Name 1")
|
||||||
cls.author_2 = Author.objects.create()
|
cls.author_2 = Author.objects.create(name="Name 2")
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
Author.objects.create()
|
Author.objects.create()
|
||||||
|
|
||||||
|
@ -88,6 +89,53 @@ class OrderingTests(TestCase):
|
||||||
attrgetter("headline")
|
attrgetter("headline")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_order_by_nulls_first_and_last(self):
|
||||||
|
msg = "nulls_first and nulls_last are mutually exclusive"
|
||||||
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
Article.objects.order_by(F("author").desc(nulls_last=True, nulls_first=True))
|
||||||
|
|
||||||
|
def test_order_by_nulls_last(self):
|
||||||
|
Article.objects.filter(headline="Article 3").update(author=self.author_1)
|
||||||
|
Article.objects.filter(headline="Article 4").update(author=self.author_2)
|
||||||
|
# asc and desc are chainable with nulls_last.
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Article.objects.order_by(F("author").desc(nulls_last=True)),
|
||||||
|
[self.a4, self.a3, self.a1, self.a2],
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Article.objects.order_by(F("author").asc(nulls_last=True)),
|
||||||
|
[self.a3, self.a4, self.a1, self.a2],
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Article.objects.order_by(Upper("author__name").desc(nulls_last=True)),
|
||||||
|
[self.a4, self.a3, self.a1, self.a2],
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Article.objects.order_by(Upper("author__name").asc(nulls_last=True)),
|
||||||
|
[self.a3, self.a4, self.a1, self.a2],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_order_by_nulls_first(self):
|
||||||
|
Article.objects.filter(headline="Article 3").update(author=self.author_1)
|
||||||
|
Article.objects.filter(headline="Article 4").update(author=self.author_2)
|
||||||
|
# asc and desc are chainable with nulls_first.
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Article.objects.order_by(F("author").asc(nulls_first=True)),
|
||||||
|
[self.a1, self.a2, self.a3, self.a4],
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Article.objects.order_by(F("author").desc(nulls_first=True)),
|
||||||
|
[self.a1, self.a2, self.a4, self.a3],
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Article.objects.order_by(Upper("author__name").asc(nulls_first=True)),
|
||||||
|
[self.a1, self.a2, self.a3, self.a4],
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(
|
||||||
|
Article.objects.order_by(Upper("author__name").desc(nulls_first=True)),
|
||||||
|
[self.a1, self.a2, self.a4, self.a3],
|
||||||
|
)
|
||||||
|
|
||||||
def test_stop_slicing(self):
|
def test_stop_slicing(self):
|
||||||
"""
|
"""
|
||||||
Use the 'stop' part of slicing notation to limit the results.
|
Use the 'stop' part of slicing notation to limit the results.
|
||||||
|
|
Loading…
Reference in New Issue