From 55cc26941a1eb6093bf9602e67a2b5fdf7e68683 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 3 Apr 2018 19:36:12 +0200 Subject: [PATCH] Refs #28643 -- Added Repeat database function. Thanks Tim Graham and Nick Pope for reviews. --- django/db/backends/sqlite3/base.py | 2 ++ django/db/models/functions/__init__.py | 8 ++++---- django/db/models/functions/text.py | 14 ++++++++++++++ docs/ref/models/database-functions.txt | 19 +++++++++++++++++++ docs/releases/2.1.txt | 1 + tests/db_functions/test_repeat.py | 24 ++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 tests/db_functions/test_repeat.py 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)