Refs #28643 -- Added Reverse database function.
Thanks Mariusz Felisiak for Oracle advice and review.
This commit is contained in:
parent
b69f8eb04c
commit
abf8e390a4
|
@ -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))
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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``
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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])
|
Loading…
Reference in New Issue