Fixed #27834 -- Added StrIndex database function.
This commit is contained in:
parent
3f64fd2f75
commit
b625907a79
|
@ -1,6 +1,6 @@
|
||||||
from .base import (
|
from .base import (
|
||||||
Cast, Coalesce, Concat, ConcatPair, Greatest, Least, Length, Lower, Now,
|
Cast, Coalesce, Concat, ConcatPair, Greatest, Least, Length, Lower, Now,
|
||||||
Substr, Upper,
|
StrIndex, Substr, Upper,
|
||||||
)
|
)
|
||||||
from .datetime import (
|
from .datetime import (
|
||||||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||||
|
@ -12,7 +12,7 @@ from .datetime import (
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# base
|
# base
|
||||||
'Cast', 'Coalesce', 'Concat', 'ConcatPair', 'Greatest', 'Least', 'Length',
|
'Cast', 'Coalesce', 'Concat', 'ConcatPair', 'Greatest', 'Least', 'Length',
|
||||||
'Lower', 'Now', 'Substr', 'Upper',
|
'Lower', 'Now', 'StrIndex', 'Substr', 'Upper',
|
||||||
# datetime
|
# datetime
|
||||||
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
||||||
'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay', 'ExtractYear',
|
'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay', 'ExtractYear',
|
||||||
|
|
|
@ -187,6 +187,29 @@ class Now(Func):
|
||||||
return self.as_sql(compiler, connection, template='STATEMENT_TIMESTAMP()')
|
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):
|
class Substr(Func):
|
||||||
function = 'SUBSTRING'
|
function = 'SUBSTRING'
|
||||||
|
|
||||||
|
|
|
@ -237,6 +237,38 @@ Usage example::
|
||||||
``Now()`` uses ``STATEMENT_TIMESTAMP`` instead. If you need the transaction
|
``Now()`` uses ``STATEMENT_TIMESTAMP`` instead. If you need the transaction
|
||||||
timestamp, use :class:`django.contrib.postgres.functions.TransactionNow`.
|
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``
|
``Substr``
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
|
@ -170,7 +170,8 @@ Migrations
|
||||||
Models
|
Models
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
* ...
|
* The new :class:`~django.db.models.functions.StrIndex` database function
|
||||||
|
finds the starting index of a string inside another string.
|
||||||
|
|
||||||
Requests and Responses
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
Loading…
Reference in New Issue