Kaydet (Commit) f82de6bf authored tarafından bobort's avatar bobort Kaydeden (comit) Tim Graham

Refs #28643 -- Added Ord, Chr, Left, and Right database functions.

üst c412926a
......@@ -6,7 +6,8 @@ from .datetime import (
TruncQuarter, TruncSecond, TruncTime, TruncWeek, TruncYear,
)
from .text import (
Concat, ConcatPair, Length, Lower, Replace, StrIndex, Substr, Upper,
Chr, Concat, ConcatPair, Left, Length, Lower, Ord, Replace, Right,
StrIndex, Substr, Upper,
)
from .window import (
CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile,
......@@ -23,8 +24,8 @@ __all__ = [
'TruncMinute', 'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime',
'TruncWeek', 'TruncYear',
# text
'Concat', 'ConcatPair', 'Length', 'Lower', 'Replace', 'StrIndex', 'Substr',
'Upper',
'Chr', 'Concat', 'ConcatPair', 'Left', 'Length', 'Lower', 'Ord', 'Replace',
'Right', 'StrIndex', 'Substr', 'Upper',
# window
'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead',
'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber',
......
from django.db.models import Func, Transform, Value, fields
from django.db.models import Func, IntegerField, Transform, Value, fields
from django.db.models.functions import Coalesce
class Chr(Transform):
function = 'CHR'
lookup_name = 'chr'
def as_mysql(self, compiler, connection):
return super().as_sql(
compiler, connection, function='CHAR', template='%(function)s(%(expressions)s USING utf16)'
)
def as_oracle(self, compiler, connection):
return super().as_sql(compiler, connection, template='%(function)s(%(expressions)s USING NCHAR_CS)')
def as_sqlite(self, compiler, connection, **extra_context):
return super().as_sql(compiler, connection, function='CHAR', **extra_context)
class ConcatPair(Func):
"""
Concatenate two arguments together. This is used by `Concat` because not
......@@ -55,6 +71,30 @@ class Concat(Func):
return ConcatPair(expressions[0], self._paired(expressions[1:]))
class Left(Func):
function = 'LEFT'
arity = 2
def __init__(self, expression, length, **extra):
"""
expression: the name of a field, or an expression returning a string
length: the number of characters to return from the start of the string
"""
if not hasattr(length, 'resolve_expression'):
if length < 1:
raise ValueError("'length' must be greater than 0.")
super().__init__(expression, length, **extra)
def get_substr(self):
return Substr(self.source_expressions[0], Value(1), self.source_expressions[1])
def use_substr(self, compiler, connection, **extra_context):
return self.get_substr().as_oracle(compiler, connection, **extra_context)
as_oracle = use_substr
as_sqlite = use_substr
class Length(Transform):
"""Return the number of characters in the expression."""
function = 'LENGTH'
......@@ -70,6 +110,18 @@ class Lower(Transform):
lookup_name = 'lower'
class Ord(Transform):
function = 'ASCII'
lookup_name = 'ord'
output_field = IntegerField()
def as_mysql(self, compiler, connection, **extra_context):
return super().as_sql(compiler, connection, function='ORD', **extra_context)
def as_sqlite(self, compiler, connection, **extra_context):
return super().as_sql(compiler, connection, function='UNICODE', **extra_context)
class Replace(Func):
function = 'REPLACE'
......@@ -77,6 +129,13 @@ class Replace(Func):
super().__init__(expression, text, replacement, **extra)
class Right(Left):
function = 'RIGHT'
def get_substr(self):
return Substr(self.source_expressions[0], self.source_expressions[1] * Value(-1))
class StrIndex(Func):
"""
Return a positive integer corresponding to the 1-indexed position of the
......
......@@ -685,6 +685,28 @@ that deal with time-parts can be used with ``TimeField``::
Text functions
==============
``Chr``
-------
.. class:: Chr(expression, **extra)
.. versionadded:: 2.1
Accepts a numeric field or expression and returns the text representation of
the expression as a single character. It works the same as Python's :func:`chr`
function.
Like :class:`Length`, it can be registered as a transform on ``IntegerField``.
The default lookup name is ``chr``.
Usage example::
>>> from django.db.models.functions import Chr
>>> Author.objects.create(name='Margaret Smith')
>>> author = Author.objects.filter(name__startswith=Chr(ord('M'))).get()
>>> print(author.name)
Margaret Smith
``Concat``
----------
......@@ -716,6 +738,23 @@ Usage example::
>>> print(author.screen_name)
Margaret Smith (Maggie)
``Left``
--------
.. class:: Left(expression, length, **extra)
.. versionadded:: 2.1
Returns the first ``length`` characters of the given text field or expression.
Usage example::
>>> from django.db.models.functions import Left
>>> Author.objects.create(name='Margaret Smith')
>>> author = Author.objects.annotate(first_initial=Left('name', 1)).get()
>>> print(author.first_initial)
M
``Length``
----------
......@@ -761,6 +800,29 @@ Usage example::
>>> print(author.name_lower)
margaret smith
``Ord``
-------
.. class:: Ord(expression, **extra)
.. versionadded:: 2.1
Accepts a single text field or expression and returns the Unicode code point
value for the first character of that expression. It works similar to Python's
:func:`ord` function, but an exception isn't raised if the expression is more
than one character long.
It can also be registered as a transform as described in :class:`Length`.
The default lookup name is ``ord``.
Usage example::
>>> from django.db.models.functions import Ord
>>> Author.objects.create(name='Margaret Smith')
>>> author = Author.objects.annotate(name_code_point=Ord('name')).get()
>>> print(author.name_code_point)
77
``Replace``
-----------
......@@ -783,6 +845,23 @@ Usage example::
>>> Author.objects.values('name')
<QuerySet [{'name': 'Margareth Johnson'}, {'name': 'Margareth Smith'}]>
``Right``
---------
.. class:: Right(expression, length, **extra)
.. versionadded:: 2.1
Returns the last ``length`` characters of the given text field or expression.
Usage example::
>>> from django.db.models.functions import Right
>>> Author.objects.create(name='Margaret Smith')
>>> author = Author.objects.annotate(last_letter=Right('name', 1)).get()
>>> print(author.last_letter)
h
``StrIndex``
------------
......
......@@ -183,8 +183,12 @@ Models
* A ``BinaryField`` may now be set to ``editable=True`` if you wish to include
it in model forms.
* The new :class:`~django.db.models.functions.Replace` database function
replaces strings in an expression.
* A number of new text database functions are added:
:class:`~django.db.models.functions.Chr`,
:class:`~django.db.models.functions.Ord`,
:class:`~django.db.models.functions.Left`,
:class:`~django.db.models.functions.Right`, and
:class:`~django.db.models.functions.Replace`.
* The new :class:`~django.db.models.functions.TruncWeek` function truncates
:class:`~django.db.models.DateField` and
......
from django.db.models import IntegerField
from django.db.models.functions import Chr, Left, Ord
from django.test import TestCase
from .models import Author
class ChrTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.john = Author.objects.create(name='John Smith', alias='smithj')
cls.elena = Author.objects.create(name='Élena Jordan', alias='elena')
cls.rhonda = Author.objects.create(name='Rhonda')
def test_basic(self):
authors = Author.objects.annotate(first_initial=Left('name', 1))
self.assertCountEqual(authors.filter(first_initial=Chr(ord('J'))), [self.john])
self.assertCountEqual(authors.exclude(first_initial=Chr(ord('J'))), [self.elena, self.rhonda])
def test_non_ascii(self):
authors = Author.objects.annotate(first_initial=Left('name', 1))
self.assertCountEqual(authors.filter(first_initial=Chr(ord('É'))), [self.elena])
self.assertCountEqual(authors.exclude(first_initial=Chr(ord('É'))), [self.john, self.rhonda])
def test_transform(self):
try:
IntegerField.register_lookup(Chr)
authors = Author.objects.annotate(name_code_point=Ord('name'))
self.assertCountEqual(authors.filter(name_code_point__chr=Chr(ord('J'))), [self.john])
self.assertCountEqual(authors.exclude(name_code_point__chr=Chr(ord('J'))), [self.elena, self.rhonda])
finally:
IntegerField._unregister_lookup(Chr)
from django.db.models import CharField, Value
from django.db.models.functions import Left, Lower
from django.test import TestCase
from .models import Author
class LeftTests(TestCase):
@classmethod
def setUpTestData(cls):
Author.objects.create(name='John Smith', alias='smithj')
Author.objects.create(name='Rhonda')
def test_basic(self):
authors = Author.objects.annotate(name_part=Left('name', 5))
self.assertQuerysetEqual(authors.order_by('name'), ['John ', 'Rhond'], lambda a: a.name_part)
# If alias is null, set it to the first 2 lower characters of the name.
Author.objects.filter(alias__isnull=True).update(alias=Lower(Left('name', 2)))
self.assertQuerysetEqual(authors.order_by('name'), ['smithj', 'rh'], lambda a: a.alias)
def test_invalid_length(self):
with self.assertRaisesMessage(ValueError, "'length' must be greater than 0"):
Author.objects.annotate(raises=Left('name', 0))
def test_expressions(self):
authors = Author.objects.annotate(name_part=Left('name', Value(3), output_field=CharField()))
self.assertQuerysetEqual(authors.order_by('name'), ['Joh', 'Rho'], lambda a: a.name_part)
from django.db.models import CharField, Value
from django.db.models.functions import Left, Ord
from django.test import TestCase
from .models import Author
class OrdTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.john = Author.objects.create(name='John Smith', alias='smithj')
cls.elena = Author.objects.create(name='Élena Jordan', alias='elena')
cls.rhonda = Author.objects.create(name='Rhonda')
def test_basic(self):
authors = Author.objects.annotate(name_part=Ord('name'))
self.assertCountEqual(authors.filter(name_part__gt=Ord(Value('John'))), [self.elena, self.rhonda])
self.assertCountEqual(authors.exclude(name_part__gt=Ord(Value('John'))), [self.john])
def test_transform(self):
try:
CharField.register_lookup(Ord)
authors = Author.objects.annotate(first_initial=Left('name', 1))
self.assertCountEqual(authors.filter(first_initial__ord=ord('J')), [self.john])
self.assertCountEqual(authors.exclude(first_initial__ord=ord('J')), [self.elena, self.rhonda])
finally:
CharField._unregister_lookup(Ord)
from django.db.models import CharField, Value
from django.db.models.functions import Lower, Right
from django.test import TestCase
from .models import Author
class RightTests(TestCase):
@classmethod
def setUpTestData(cls):
Author.objects.create(name='John Smith', alias='smithj')
Author.objects.create(name='Rhonda')
def test_basic(self):
authors = Author.objects.annotate(name_part=Right('name', 5))
self.assertQuerysetEqual(authors.order_by('name'), ['Smith', 'honda'], lambda a: a.name_part)
# If alias is null, set it to the first 2 lower characters of the name.
Author.objects.filter(alias__isnull=True).update(alias=Lower(Right('name', 2)))
self.assertQuerysetEqual(authors.order_by('name'), ['smithj', 'da'], lambda a: a.alias)
def test_invalid_length(self):
with self.assertRaisesMessage(ValueError, "'length' must be greater than 0"):
Author.objects.annotate(raises=Right('name', 0))
def test_expressions(self):
authors = Author.objects.annotate(name_part=Right('name', Value(3), output_field=CharField()))
self.assertQuerysetEqual(authors.order_by('name'), ['ith', 'nda'], lambda a: a.name_part)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment