summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrad Melin <melinbrad@gmail.com>2017-02-21 08:43:38 +0200
committerTim Graham <timograham@gmail.com>2017-03-14 19:58:56 -0400
commitb625907a79bb1336cbc54231bdf7cace25fecaf7 (patch)
tree824ee84d7123c31c87860e137dc48806be5b6cb8
parent3f64fd2f75342fa061338d75916c08b98f0c7797 (diff)
Fixed #27834 -- Added StrIndex database function.
-rw-r--r--django/db/models/functions/__init__.py4
-rw-r--r--django/db/models/functions/base.py23
-rw-r--r--docs/ref/models/database-functions.txt32
-rw-r--r--docs/releases/2.0.txt3
-rw-r--r--tests/db_functions/test_strindex.py65
5 files changed, 124 insertions, 3 deletions
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)
+ <QuerySet [<Author: Margaret Smith>, <Author: Smith, Margaret>]>
+
+.. warning::
+
+ In MySQL, a database table's :ref:`collation<mysql-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
+ )