From b625907a79bb1336cbc54231bdf7cace25fecaf7 Mon Sep 17 00:00:00 2001 From: Brad Melin Date: Tue, 21 Feb 2017 08:43:38 +0200 Subject: [PATCH] Fixed #27834 -- Added StrIndex database function. --- django/db/models/functions/__init__.py | 4 +- django/db/models/functions/base.py | 23 +++++++++ docs/ref/models/database-functions.txt | 32 +++++++++++++ docs/releases/2.0.txt | 3 +- tests/db_functions/test_strindex.py | 65 ++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 tests/db_functions/test_strindex.py diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py index eeafc1dc48..b8bb89b171 100644 --- a/django/db/models/functions/__init__.py +++ b/django/db/models/functions/__init__.py @@ -1,6 +1,6 @@ from .base import ( Cast, Coalesce, Concat, ConcatPair, Greatest, Least, Length, Lower, Now, - Substr, Upper, + StrIndex, Substr, Upper, ) from .datetime import ( Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth, @@ -12,7 +12,7 @@ from .datetime import ( __all__ = [ # base 'Cast', 'Coalesce', 'Concat', 'ConcatPair', 'Greatest', 'Least', 'Length', - 'Lower', 'Now', 'Substr', 'Upper', + 'Lower', 'Now', 'StrIndex', 'Substr', 'Upper', # datetime 'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay', 'ExtractYear', diff --git a/django/db/models/functions/base.py b/django/db/models/functions/base.py index beac2e2d72..bb4354ac87 100644 --- a/django/db/models/functions/base.py +++ b/django/db/models/functions/base.py @@ -187,6 +187,29 @@ class Now(Func): return self.as_sql(compiler, connection, template='STATEMENT_TIMESTAMP()') +class StrIndex(Func): + """ + Return a positive integer corresponding to the 1-indexed position of the + first occurrence of a substring inside another string, or 0 if the + substring is not found. + """ + function = 'INSTR' + arity = 2 + + def __init__(self, expression, substring, **extra): + """ + expression: the name of a field, or an expression returning a string + substring: a string to find inside expression + """ + if not hasattr(substring, 'resolve_expression'): + substring = Value(substring) + expressions = [expression, substring] + super().__init__(*expressions, output_field=fields.IntegerField(), **extra) + + def as_postgresql(self, compiler, connection): + return super().as_sql(compiler, connection, function='STRPOS') + + class Substr(Func): function = 'SUBSTRING' diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index 18d547d906..21b6f897ac 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -237,6 +237,38 @@ Usage example:: ``Now()`` uses ``STATEMENT_TIMESTAMP`` instead. If you need the transaction timestamp, use :class:`django.contrib.postgres.functions.TransactionNow`. +``StrIndex`` +============ + +.. class:: StrIndex(expression, substring, **extra) + +.. versionadded:: 2.0 + +Returns a positive integer corresponding to the 1-indexed position of the +first occurrence of ``substring`` inside another string, or 0 if the substring +is not found. + +Usage example:: + + >>> from django.db.models.functions import StrIndex + >>> Author.objects.create(name='Margaret Smith') + >>> Author.objects.create(name='Smith, Margaret') + >>> Author.objects.create(name='Margaret Jackson') + >>> authors = Author.objects.annotate( + ... smith_index=StrIndex('name', 'Smith')).order_by('smith_index') + >>> authors.first().smith_index + 0 + >>> authors = Author.objects.annotate( + ... smith_index=StrIndex('name', 'Smith')).filter(smith_index__gt=0) + , ]> + +.. warning:: + + In MySQL, a database table's :ref:`collation` determines + whether string comparisons (such as the ``expression`` and ``substring`` of + this function) are case-sensitive. Comparisons are case-insensitive by + default. + ``Substr`` ========== diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 4592d8b76d..369a5893ba 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -170,7 +170,8 @@ Migrations Models ~~~~~~ -* ... +* The new :class:`~django.db.models.functions.StrIndex` database function + finds the starting index of a string inside another string. Requests and Responses ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/db_functions/test_strindex.py b/tests/db_functions/test_strindex.py new file mode 100644 index 0000000000..c516d2d2ab --- /dev/null +++ b/tests/db_functions/test_strindex.py @@ -0,0 +1,65 @@ +from django.db.models.functions import StrIndex +from django.test import TestCase +from django.utils import timezone + +from .models import Article, Author + + +class StrIndexTests(TestCase): + def test_annotate_charfield(self): + Author.objects.create(name='George. R. R. Martin') + Author.objects.create(name='J. R. R. Tolkien') + Author.objects.create(name='Terry Pratchett') + authors = Author.objects.annotate(fullstop=StrIndex('name', 'R.')) + self.assertQuerysetEqual(authors.order_by('name'), [9, 4, 0], lambda a: a.fullstop) + + def test_annotate_textfield(self): + Article.objects.create( + title='How to Django', + text='Lorem ipsum dolor sit amet.', + written=timezone.now(), + ) + Article.objects.create( + title='How to Tango', + text="Won't find anything here.", + written=timezone.now(), + ) + articles = Article.objects.annotate(ipsum_index=StrIndex('text', 'ipsum')) + self.assertQuerysetEqual(articles.order_by('title'), [7, 0], lambda a: a.ipsum_index) + + def test_order_by(self): + Author.objects.create(name='Terry Pratchett') + Author.objects.create(name='J. R. R. Tolkien') + Author.objects.create(name='George. R. R. Martin') + self.assertQuerysetEqual( + Author.objects.order_by(StrIndex('name', 'R.').asc()), [ + 'Terry Pratchett', + 'J. R. R. Tolkien', + 'George. R. R. Martin', + ], + lambda a: a.name + ) + self.assertQuerysetEqual( + Author.objects.order_by(StrIndex('name', 'R.').desc()), [ + 'George. R. R. Martin', + 'J. R. R. Tolkien', + 'Terry Pratchett', + ], + lambda a: a.name + ) + + def test_unicode_values(self): + Author.objects.create(name='ツリー') + Author.objects.create(name='皇帝') + Author.objects.create(name='皇帝 ツリー') + authors = Author.objects.annotate(sb=StrIndex('name', 'リ')) + self.assertQuerysetEqual(authors.order_by('name'), [2, 0, 5], lambda a: a.sb) + + def test_filtering(self): + Author.objects.create(name='George. R. R. Martin') + Author.objects.create(name='Terry Pratchett') + self.assertQuerysetEqual( + Author.objects.annotate(middle_name=StrIndex('name', 'R.')).filter(middle_name__gt=0), + ['George. R. R. Martin'], + lambda a: a.name + )