summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2018-04-03 19:36:12 +0200
committerGitHub <noreply@github.com>2018-04-03 19:36:12 +0200
commit55cc26941a1eb6093bf9602e67a2b5fdf7e68683 (patch)
treea7e74536922b6bc467538966e795434dc03ad40a
parent6141c752fe7091d2197f5ac061300a9e0a36b09b (diff)
Refs #28643 -- Added Repeat database function.
Thanks Tim Graham and Nick Pope for reviews.
-rw-r--r--django/db/backends/sqlite3/base.py2
-rw-r--r--django/db/models/functions/__init__.py8
-rw-r--r--django/db/models/functions/text.py14
-rw-r--r--docs/ref/models/database-functions.txt19
-rw-r--r--docs/releases/2.1.txt1
-rw-r--r--tests/db_functions/test_repeat.py24
6 files changed, 64 insertions, 4 deletions
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 3989028930..b644f4d42a 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -4,6 +4,7 @@ SQLite3 backend for the sqlite3 module in the standard library.
import datetime
import decimal
import math
+import operator
import re
import warnings
from sqlite3 import dbapi2 as Database
@@ -170,6 +171,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
conn.create_function("django_format_dtdelta", 3, _sqlite_format_dtdelta)
conn.create_function("django_power", 2, _sqlite_power)
conn.create_function('LPAD', 3, _sqlite_lpad)
+ conn.create_function('REPEAT', 2, operator.mul)
conn.create_function('RPAD', 3, _sqlite_rpad)
conn.execute('PRAGMA foreign_keys = ON')
return conn
diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py
index a0f5a9e8b2..7515cc67d9 100644
--- a/django/db/models/functions/__init__.py
+++ b/django/db/models/functions/__init__.py
@@ -6,8 +6,8 @@ from .datetime import (
TruncQuarter, TruncSecond, TruncTime, TruncWeek, TruncYear,
)
from .text import (
- Chr, Concat, ConcatPair, Left, Length, Lower, LPad, LTrim, Ord, Replace,
- Right, RPad, RTrim, StrIndex, Substr, Trim, Upper,
+ Chr, Concat, ConcatPair, Left, Length, Lower, LPad, LTrim, Ord, Repeat,
+ Replace, Right, RPad, RTrim, StrIndex, Substr, Trim, Upper,
)
from .window import (
CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile,
@@ -25,8 +25,8 @@ __all__ = [
'TruncWeek', 'TruncYear',
# text
'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'LPad', 'LTrim',
- 'Ord', 'Replace', 'Right', 'RPad', 'RTrim', 'StrIndex', 'Substr', 'Trim',
- 'Upper',
+ 'Ord', 'Repeat', 'Replace', 'Right', 'RPad', 'RTrim', 'StrIndex', 'Substr',
+ 'Trim', 'Upper',
# window
'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead',
'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber',
diff --git a/django/db/models/functions/text.py b/django/db/models/functions/text.py
index aecf64e28e..1f04842153 100644
--- a/django/db/models/functions/text.py
+++ b/django/db/models/functions/text.py
@@ -152,6 +152,20 @@ class Ord(Transform):
return super().as_sql(compiler, connection, function='UNICODE', **extra_context)
+class Repeat(BytesToCharFieldConversionMixin, Func):
+ function = 'REPEAT'
+
+ def __init__(self, expression, number, **extra):
+ if not hasattr(number, 'resolve_expression') and number < 0:
+ raise ValueError("'number' must be greater or equal to 0.")
+ super().__init__(expression, number, **extra)
+
+ def as_oracle(self, compiler, connection, **extra_context):
+ expression, number = self.source_expressions
+ rpad = RPad(expression, Length(expression) * number, expression)
+ return rpad.as_sql(compiler, connection, **extra_context)
+
+
class Replace(Func):
function = 'REPLACE'
diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt
index bbc7683ccf..d86853cc12 100644
--- a/docs/ref/models/database-functions.txt
+++ b/docs/ref/models/database-functions.txt
@@ -855,6 +855,25 @@ Usage example::
>>> print(author.name_code_point)
77
+``Repeat``
+----------
+
+.. class:: Repeat(expression, number, **extra)
+
+.. versionadded:: 2.1
+
+Returns the value of the given text field or expression repeated ``number``
+times.
+
+Usage example::
+
+ >>> from django.db.models.functions import Repeat
+ >>> Author.objects.create(name='John', alias='j')
+ >>> Author.objects.update(name=Repeat('name', 3))
+ 1
+ >>> print(Author.objects.get(alias='j').name)
+ JohnJohnJohn
+
``Replace``
-----------
diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt
index 2b1d6dabdc..46a903ba11 100644
--- a/docs/releases/2.1.txt
+++ b/docs/releases/2.1.txt
@@ -210,6 +210,7 @@ Models
:class:`~django.db.models.functions.LPad`,
:class:`~django.db.models.functions.LTrim`,
:class:`~django.db.models.functions.Ord`,
+ :class:`~django.db.models.functions.Repeat`,
:class:`~django.db.models.functions.Replace`,
:class:`~django.db.models.functions.Right`,
:class:`~django.db.models.functions.RPad`,
diff --git a/tests/db_functions/test_repeat.py b/tests/db_functions/test_repeat.py
new file mode 100644
index 0000000000..d3f294c409
--- /dev/null
+++ b/tests/db_functions/test_repeat.py
@@ -0,0 +1,24 @@
+from django.db.models import CharField, Value
+from django.db.models.functions import Length, Repeat
+from django.test import TestCase
+
+from .models import Author
+
+
+class RepeatTests(TestCase):
+ def test_basic(self):
+ Author.objects.create(name='John', alias='xyz')
+ tests = (
+ (Repeat('name', 0), ''),
+ (Repeat('name', 2), 'JohnJohn'),
+ (Repeat('name', Length('alias'), output_field=CharField()), 'JohnJohnJohn'),
+ (Repeat(Value('x'), 3, output_field=CharField()), 'xxx'),
+ )
+ for function, repeated_text in tests:
+ with self.subTest(function=function):
+ authors = Author.objects.annotate(repeated_text=function)
+ self.assertQuerysetEqual(authors, [repeated_text], lambda a: a.repeated_text, ordered=False)
+
+ def test_negative_number(self):
+ with self.assertRaisesMessage(ValueError, "'number' must be greater or equal to 0."):
+ Repeat('name', -1)