Refs #28643 -- Added Reverse database function.

Thanks Mariusz Felisiak for Oracle advice and review.
This commit is contained in:
Nick Pope 2019-01-09 00:12:10 +00:00 committed by Tim Graham
parent b69f8eb04c
commit abf8e390a4
6 changed files with 93 additions and 7 deletions

View File

@ -215,6 +215,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
conn.create_function('POWER', 2, none_guard(operator.pow))
conn.create_function('RADIANS', 1, none_guard(math.radians))
conn.create_function('REPEAT', 2, none_guard(operator.mul))
conn.create_function('REVERSE', 1, none_guard(lambda x: x[::-1]))
conn.create_function('RPAD', 3, _sqlite_rpad)
conn.create_function('SIN', 1, none_guard(math.sin))
conn.create_function('SQRT', 1, none_guard(math.sqrt))

View File

@ -11,7 +11,7 @@ from .math import (
)
from .text import (
Chr, Concat, ConcatPair, Left, Length, Lower, LPad, LTrim, Ord, Repeat,
Replace, Right, RPad, RTrim, StrIndex, Substr, Trim, Upper,
Replace, Reverse, Right, RPad, RTrim, StrIndex, Substr, Trim, Upper,
)
from .window import (
CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile,
@ -34,8 +34,8 @@ __all__ = [
'Sin', 'Sqrt', 'Tan',
# text
'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'LPad', 'LTrim',
'Ord', 'Repeat', 'Replace', 'Right', 'RPad', 'RTrim', 'StrIndex', 'Substr',
'Trim', 'Upper',
'Ord', 'Repeat', 'Replace', 'Reverse', 'Right', 'RPad', 'RTrim',
'StrIndex', 'Substr', 'Trim', 'Upper',
# window
'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead',
'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber',

View File

@ -185,6 +185,25 @@ class Replace(Func):
super().__init__(expression, text, replacement, **extra)
class Reverse(Transform):
function = 'REVERSE'
lookup_name = 'reverse'
def as_oracle(self, compiler, connection, **extra_context):
# REVERSE in Oracle is undocumented and doesn't support multi-byte
# strings. Use a special subquery instead.
return super().as_sql(
compiler, connection,
template=(
'(SELECT LISTAGG(s) WITHIN GROUP (ORDER BY n DESC) FROM '
'(SELECT LEVEL n, SUBSTR(%(expressions)s, LEVEL, 1) s '
'FROM DUAL CONNECT BY LEVEL <= LENGTH(%(expressions)s)) '
'GROUP BY %(expressions)s)'
),
**extra_context
)
class Right(Left):
function = 'RIGHT'

View File

@ -1377,6 +1377,27 @@ Usage example::
>>> Author.objects.values('name')
<QuerySet [{'name': 'Margareth Johnson'}, {'name': 'Margareth Smith'}]>
``Reverse``
-----------
.. class:: Reverse(expression, **extra)
.. versionadded:: 2.2
Accepts a single text field or expression and returns the characters of that
expression in reverse order.
It can also be registered as a transform as described in :class:`Length`. The
default lookup name is ``reverse``.
Usage example::
>>> from django.db.models.functions import Reverse
>>> Author.objects.create(name='Margaret Smith')
>>> author = Author.objects.annotate(backward=Reverse('name')).get()
>>> print(author.backward)
htimS teragraM
``Right``
---------

View File

@ -221,10 +221,9 @@ Models
* Added support for partial indexes (:attr:`.Index.condition`).
* Added many :ref:`math database functions <math-functions>`.
* The new :class:`~django.db.models.functions.NullIf` database function
returns ``None`` if the two expressions are equal.
* Added the :class:`~django.db.models.functions.NullIf` and
:class:`~django.db.models.functions.Reverse` database functions, as well as
many :ref:`math database functions <math-functions>`.
* Setting the new ``ignore_conflicts`` parameter of
:meth:`.QuerySet.bulk_create` to ``True`` tells the database to ignore

View File

@ -0,0 +1,46 @@
from django.db import connection
from django.db.models import CharField
from django.db.models.functions import Length, Reverse, Trim
from django.test import TestCase
from django.test.utils import register_lookup
from ..models import Author
class ReverseTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.john = Author.objects.create(name='John Smith', alias='smithj')
cls.elena = Author.objects.create(name='Élena Jordan', alias='elena')
cls.python = Author.objects.create(name='パイソン')
def test_null(self):
author = Author.objects.annotate(backward=Reverse('alias')).get(pk=self.python.pk)
self.assertEqual(author.backward, '' if connection.features.interprets_empty_strings_as_nulls else None)
def test_basic(self):
authors = Author.objects.annotate(backward=Reverse('name'))
self.assertQuerysetEqual(
authors,
[
('John Smith', 'htimS nhoJ'),
('Élena Jordan', 'nadroJ anelÉ'),
('パイソン', 'ンソイパ'),
],
lambda a: (a.name, a.backward),
ordered=False,
)
def test_transform(self):
with register_lookup(CharField, Reverse):
authors = Author.objects.all()
self.assertCountEqual(authors.filter(name__reverse=self.john.name[::-1]), [self.john])
self.assertCountEqual(authors.exclude(name__reverse=self.john.name[::-1]), [self.elena, self.python])
def test_expressions(self):
author = Author.objects.annotate(backward=Reverse(Trim('name'))).get(pk=self.john.pk)
self.assertEqual(author.backward, self.john.name[::-1])
with register_lookup(CharField, Reverse), register_lookup(CharField, Length):
authors = Author.objects.all()
self.assertCountEqual(authors.filter(name__reverse__length__gt=7), [self.john, self.elena])
self.assertCountEqual(authors.exclude(name__reverse__length__gt=7), [self.python])