Skip to content

Commit

Permalink
Moved text database functions to a separate file.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixxm committed Oct 11, 2017
1 parent 22ff4f8 commit 85ce001
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 184 deletions.
11 changes: 5 additions & 6 deletions django/db/models/functions/__init__.py
@@ -1,22 +1,21 @@
from .base import (
Cast, Coalesce, Concat, ConcatPair, Greatest, Least, Length, Lower, Now,
StrIndex, Substr, Upper,
)
from .base import Cast, Coalesce, Greatest, Least, Now
from .datetime import (
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear,
Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth,
TruncQuarter, TruncSecond, TruncTime, TruncYear,
)
from .text import Concat, ConcatPair, Length, Lower, StrIndex, Substr, Upper
from .window import (
CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile,
PercentRank, Rank, RowNumber,
)

__all__ = [
# base
'Cast', 'Coalesce', 'Concat', 'ConcatPair', 'Greatest', 'Least', 'Length',
'Lower', 'Now', 'StrIndex', 'Substr', 'Upper',
'Cast', 'Coalesce', 'Greatest', 'Least', 'Now',
# text
'Concat', 'ConcatPair', 'Length', 'Lower', 'StrIndex', 'Substr', 'Upper',
# datetime
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay',
Expand Down
113 changes: 1 addition & 112 deletions django/db/models/functions/base.py
@@ -1,7 +1,7 @@
"""
Classes that represent database functions.
"""
from django.db.models import Func, Transform, Value, fields
from django.db.models import Func, fields


class Cast(Func):
Expand Down Expand Up @@ -45,59 +45,6 @@ class ToNCLOB(Func):
return self.as_sql(compiler, connection)


class ConcatPair(Func):
"""
Concatenate two arguments together. This is used by `Concat` because not
all backend databases support more than two arguments.
"""
function = 'CONCAT'

def as_sqlite(self, compiler, connection):
coalesced = self.coalesce()
return super(ConcatPair, coalesced).as_sql(
compiler, connection, template='%(expressions)s', arg_joiner=' || '
)

def as_mysql(self, compiler, connection):
# Use CONCAT_WS with an empty separator so that NULLs are ignored.
return super().as_sql(
compiler, connection, function='CONCAT_WS', template="%(function)s('', %(expressions)s)"
)

def coalesce(self):
# null on either side results in null for expression, wrap with coalesce
c = self.copy()
expressions = [
Coalesce(expression, Value('')) for expression in c.get_source_expressions()
]
c.set_source_expressions(expressions)
return c


class Concat(Func):
"""
Concatenate text fields together. Backends that result in an entire
null expression when any arguments are null will wrap each argument in
coalesce functions to ensure a non-null result.
"""
function = None
template = "%(expressions)s"

def __init__(self, *expressions, **extra):
if len(expressions) < 2:
raise ValueError('Concat must take at least two expressions')
paired = self._paired(expressions)
super().__init__(paired, **extra)

def _paired(self, expressions):
# wrap pairs of expressions in successive concat functions
# exp = [a, b, c, d]
# -> ConcatPair(a, ConcatPair(b, ConcatPair(c, d))))
if len(expressions) == 2:
return ConcatPair(*expressions)
return ConcatPair(expressions[0], self._paired(expressions[1:]))


class Greatest(Func):
"""
Return the maximum expression.
Expand Down Expand Up @@ -138,21 +85,6 @@ def as_sqlite(self, compiler, connection):
return super().as_sqlite(compiler, connection, function='MIN')


class Length(Transform):
"""Return the number of characters in the expression."""
function = 'LENGTH'
lookup_name = 'length'
output_field = fields.IntegerField()

def as_mysql(self, compiler, connection):
return super().as_sql(compiler, connection, function='CHAR_LENGTH')


class Lower(Transform):
function = 'LOWER'
lookup_name = 'lower'


class Now(Func):
template = 'CURRENT_TIMESTAMP'
output_field = fields.DateTimeField()
Expand All @@ -162,46 +94,3 @@ def as_postgresql(self, compiler, connection):
# transaction". We use STATEMENT_TIMESTAMP to be cross-compatible with
# other databases.
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
output_field = fields.IntegerField()

def as_postgresql(self, compiler, connection):
return super().as_sql(compiler, connection, function='STRPOS')


class Substr(Func):
function = 'SUBSTRING'

def __init__(self, expression, pos, length=None, **extra):
"""
expression: the name of a field, or an expression returning a string
pos: an integer > 0, or an expression returning an integer
length: an optional number of characters to return
"""
if not hasattr(pos, 'resolve_expression'):
if pos < 1:
raise ValueError("'pos' must be greater than 0")
expressions = [expression, pos]
if length is not None:
expressions.append(length)
super().__init__(*expressions, **extra)

def as_sqlite(self, compiler, connection):
return super().as_sql(compiler, connection, function='SUBSTR')

def as_oracle(self, compiler, connection):
return super().as_sql(compiler, connection, function='SUBSTR')


class Upper(Transform):
function = 'UPPER'
lookup_name = 'upper'
121 changes: 64 additions & 57 deletions docs/ref/models/database-functions.txt
Expand Up @@ -80,37 +80,6 @@ Usage examples::
>>> now = timezone.now()
>>> Coalesce('updated', Cast(now, DateTimeField()))

``Concat``
==========

.. class:: Concat(*expressions, **extra)

Accepts a list of at least two text fields or expressions and returns the
concatenated text. Each argument must be of a text or char type. If you want
to concatenate a ``TextField()`` with a ``CharField()``, then be sure to tell
Django that the ``output_field`` should be a ``TextField()``. Specifying an
``output_field`` is also required when concatenating a ``Value`` as in the
example below.

This function will never have a null result. On backends where a null argument
results in the entire expression being null, Django will ensure that each null
part is converted to an empty string first.

Usage example::

>>> # Get the display name as "name (goes_by)"
>>> from django.db.models import CharField, Value as V
>>> from django.db.models.functions import Concat
>>> Author.objects.create(name='Margaret Smith', goes_by='Maggie')
>>> author = Author.objects.annotate(
... screen_name=Concat(
... 'name', V(' ('), 'goes_by', V(')'),
... output_field=CharField()
... )
... ).get()
>>> print(author.screen_name)
Margaret Smith (Maggie)

``Greatest``
============

Expand Down Expand Up @@ -175,8 +144,67 @@ will result in a database error.
The PostgreSQL behavior can be emulated using ``Coalesce`` if you know
a sensible maximum value to provide as a default.

``Now``
=======

.. class:: Now()

Returns the database server's current date and time when the query is executed,
typically using the SQL ``CURRENT_TIMESTAMP``.

Usage example::

>>> from django.db.models.functions import Now
>>> Article.objects.filter(published__lte=Now())
<QuerySet [<Article: How to Django>]>

.. admonition:: PostgreSQL considerations

On PostgreSQL, the SQL ``CURRENT_TIMESTAMP`` returns the time that the
current transaction started. Therefore for cross-database compatibility,
``Now()`` uses ``STATEMENT_TIMESTAMP`` instead. If you need the transaction
timestamp, use :class:`django.contrib.postgres.functions.TransactionNow`.

.. _text-functions:

Text Functions
==============

.. module:: django.db.models.functions.text

``Concat``
----------

.. class:: Concat(*expressions, **extra)

Accepts a list of at least two text fields or expressions and returns the
concatenated text. Each argument must be of a text or char type. If you want
to concatenate a ``TextField()`` with a ``CharField()``, then be sure to tell
Django that the ``output_field`` should be a ``TextField()``. Specifying an
``output_field`` is also required when concatenating a ``Value`` as in the
example below.

This function will never have a null result. On backends where a null argument
results in the entire expression being null, Django will ensure that each null
part is converted to an empty string first.

Usage example::

>>> # Get the display name as "name (goes_by)"
>>> from django.db.models import CharField, Value as V
>>> from django.db.models.functions import Concat
>>> Author.objects.create(name='Margaret Smith', goes_by='Maggie')
>>> author = Author.objects.annotate(
... screen_name=Concat(
... 'name', V(' ('), 'goes_by', V(')'),
... output_field=CharField()
... )
... ).get()
>>> print(author.screen_name)
Margaret Smith (Maggie)

``Length``
==========
----------

.. class:: Length(expression, **extra)

Expand All @@ -203,7 +231,7 @@ It can also be registered as a transform. For example::
>>> authors = Author.objects.filter(name__length__gt=7)

``Lower``
=========
---------

.. class:: Lower(expression, **extra)

Expand All @@ -220,29 +248,8 @@ Usage example::
>>> print(author.name_lower)
margaret smith

``Now``
=======

.. class:: Now()

Returns the database server's current date and time when the query is executed,
typically using the SQL ``CURRENT_TIMESTAMP``.

Usage example::

>>> from django.db.models.functions import Now
>>> Article.objects.filter(published__lte=Now())
<QuerySet [<Article: How to Django>]>

.. admonition:: PostgreSQL considerations

On PostgreSQL, the SQL ``CURRENT_TIMESTAMP`` returns the time that the
current transaction started. Therefore for cross-database compatibility,
``Now()`` uses ``STATEMENT_TIMESTAMP`` instead. If you need the transaction
timestamp, use :class:`django.contrib.postgres.functions.TransactionNow`.

``StrIndex``
============
------------

.. class:: StrIndex(string, substring, **extra)

Expand Down Expand Up @@ -276,7 +283,7 @@ Usage example::
default.

``Substr``
==========
----------

.. class:: Substr(expression, pos, length=None, **extra)

Expand All @@ -295,7 +302,7 @@ Usage example::
marga

``Upper``
=========
---------

.. class:: Upper(expression, **extra)

Expand Down
2 changes: 1 addition & 1 deletion docs/ref/models/expressions.txt
Expand Up @@ -1075,7 +1075,7 @@ certain function, you can add support for it by monkey patching a new method
onto the function's class.

Let's say we're writing a backend for Microsoft's SQL Server which uses the SQL
``LEN`` instead of ``LENGTH`` for the :class:`~functions.Length` function.
``LEN`` instead of ``LENGTH`` for the :class:`~functions.text.Length` function.
We'll monkey patch a new method called ``as_sqlserver()`` onto the ``Length``
class::

Expand Down
4 changes: 2 additions & 2 deletions docs/ref/models/querysets.txt
Expand Up @@ -354,8 +354,8 @@ respect to case-sensitivity, Django will order results however your database
backend normally orders them.

You can order by a field converted to lowercase with
:class:`~django.db.models.functions.Lower` which will achieve case-consistent
ordering::
:class:`~django.db.models.functions.text.Lower` which will achieve
case-consistent ordering::

Entry.objects.order_by(Lower('headline').desc())

Expand Down
4 changes: 2 additions & 2 deletions docs/releases/1.8.txt
Expand Up @@ -116,8 +116,8 @@ queries.
A collection of :doc:`database functions </ref/models/database-functions>` is
also included with functionality such as
:class:`~django.db.models.functions.Coalesce`,
:class:`~django.db.models.functions.Concat`, and
:class:`~django.db.models.functions.Substr`.
:class:`~django.db.models.functions.text.Concat`, and
:class:`~django.db.models.functions.text.Substr`.

``TestCase`` data setup
-----------------------
Expand Down
6 changes: 3 additions & 3 deletions docs/releases/1.9.txt
Expand Up @@ -534,9 +534,9 @@ Models
:ref:`Func() <func-expressions>` which allows ``Transform``\s to be used on
the right hand side of an expression, just like regular ``Func``\s. This
allows registering some database functions like
:class:`~django.db.models.functions.Length`,
:class:`~django.db.models.functions.Lower`, and
:class:`~django.db.models.functions.Upper` as transforms.
:class:`~django.db.models.functions.text.Length`,
:class:`~django.db.models.functions.text.Lower`, and
:class:`~django.db.models.functions.text.Upper` as transforms.

* :class:`~django.db.models.SlugField` now accepts an
:attr:`~django.db.models.SlugField.allow_unicode` argument to allow Unicode
Expand Down
2 changes: 1 addition & 1 deletion docs/releases/2.0.txt
Expand Up @@ -228,7 +228,7 @@ Migrations
Models
~~~~~~

* The new :class:`~django.db.models.functions.StrIndex` database function
* The new :class:`~django.db.models.functions.text.StrIndex` database function
finds the starting index of a string inside another string.

* On Oracle, ``AutoField`` and ``BigAutoField`` are now created as `identity
Expand Down

0 comments on commit 85ce001

Please sign in to comment.