Browse Source

Refs #28643 -- Added Reverse database function.

Thanks Mariusz Felisiak for Oracle advice and review.
Nick Pope 6 years ago
parent
commit
abf8e390a4

+ 1 - 0
django/db/backends/sqlite3/base.py

@@ -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))

+ 3 - 3
django/db/models/functions/__init__.py

@@ -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',

+ 19 - 0
django/db/models/functions/text.py

@@ -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'
 

+ 21 - 0
docs/ref/models/database-functions.txt

@@ -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``
 ---------
 

+ 3 - 4
docs/releases/2.2.txt

@@ -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

+ 46 - 0
tests/db_functions/text/test_reverse.py

@@ -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])