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('POWER', 2, none_guard(operator.pow))
conn.create_function('RADIANS', 1, none_guard(math.radians)) conn.create_function('RADIANS', 1, none_guard(math.radians))
conn.create_function('REPEAT', 2, none_guard(operator.mul)) 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('RPAD', 3, _sqlite_rpad)
conn.create_function('SIN', 1, none_guard(math.sin)) conn.create_function('SIN', 1, none_guard(math.sin))
conn.create_function('SQRT', 1, none_guard(math.sqrt)) conn.create_function('SQRT', 1, none_guard(math.sqrt))

View File

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

View File

@ -185,6 +185,25 @@ class Replace(Func):
super().__init__(expression, text, replacement, **extra) 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): class Right(Left):
function = 'RIGHT' function = 'RIGHT'

View File

@ -1377,6 +1377,27 @@ Usage example::
>>> Author.objects.values('name') >>> Author.objects.values('name')
<QuerySet [{'name': 'Margareth Johnson'}, {'name': 'Margareth Smith'}]> <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`` ``Right``
--------- ---------

View File

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