Refs #28643 -- Reorganized database functions.
Thanks Tim Graham for the review.
This commit is contained in:
parent
8c538871bd
commit
4f27e475b3
|
@ -1,27 +1,27 @@
|
||||||
from .base import (
|
from .comparison import Cast, Coalesce, Greatest, Least
|
||||||
Cast, Coalesce, Concat, ConcatPair, Greatest, Least, Length, Lower, Now,
|
|
||||||
StrIndex, Substr, Upper,
|
|
||||||
)
|
|
||||||
from .datetime import (
|
from .datetime import (
|
||||||
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
|
||||||
ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear,
|
ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear,
|
||||||
Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth,
|
Now, Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth,
|
||||||
TruncQuarter, TruncSecond, TruncTime, TruncYear,
|
TruncQuarter, TruncSecond, TruncTime, TruncYear,
|
||||||
)
|
)
|
||||||
|
from .text import Concat, ConcatPair, Length, Lower, StrIndex, Substr, Upper
|
||||||
from .window import (
|
from .window import (
|
||||||
CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile,
|
CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile,
|
||||||
PercentRank, Rank, RowNumber,
|
PercentRank, Rank, RowNumber,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# base
|
# comparison and conversion
|
||||||
'Cast', 'Coalesce', 'Concat', 'ConcatPair', 'Greatest', 'Least', 'Length',
|
'Cast', 'Coalesce', 'Greatest', 'Least',
|
||||||
'Lower', 'Now', 'StrIndex', 'Substr', 'Upper',
|
|
||||||
# datetime
|
# datetime
|
||||||
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
||||||
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay',
|
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay',
|
||||||
'ExtractYear', 'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute',
|
'ExtractYear', 'Now', 'Trunc', 'TruncDate', 'TruncDay', 'TruncHour',
|
||||||
'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime', 'TruncYear',
|
'TruncMinute', 'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime',
|
||||||
|
'TruncYear',
|
||||||
|
# text
|
||||||
|
'Concat', 'ConcatPair', 'Length', 'Lower', 'StrIndex', 'Substr', 'Upper',
|
||||||
# window
|
# window
|
||||||
'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead',
|
'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead',
|
||||||
'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber',
|
'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber',
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
"""Database functions that do comparisons or type conversions."""
|
||||||
|
from django.db.models import Func
|
||||||
|
|
||||||
|
|
||||||
|
class Cast(Func):
|
||||||
|
"""Coerce an expression to a new field type."""
|
||||||
|
function = 'CAST'
|
||||||
|
template = '%(function)s(%(expressions)s AS %(db_type)s)'
|
||||||
|
|
||||||
|
def __init__(self, expression, output_field):
|
||||||
|
super().__init__(expression, output_field=output_field)
|
||||||
|
|
||||||
|
def as_sql(self, compiler, connection, **extra_context):
|
||||||
|
extra_context['db_type'] = self.output_field.cast_db_type(connection)
|
||||||
|
return super().as_sql(compiler, connection, **extra_context)
|
||||||
|
|
||||||
|
def as_postgresql(self, compiler, connection):
|
||||||
|
# CAST would be valid too, but the :: shortcut syntax is more readable.
|
||||||
|
return self.as_sql(compiler, connection, template='%(expressions)s::%(db_type)s')
|
||||||
|
|
||||||
|
|
||||||
|
class Coalesce(Func):
|
||||||
|
"""Return, from left to right, the first non-null expression."""
|
||||||
|
function = 'COALESCE'
|
||||||
|
|
||||||
|
def __init__(self, *expressions, **extra):
|
||||||
|
if len(expressions) < 2:
|
||||||
|
raise ValueError('Coalesce must take at least two expressions')
|
||||||
|
super().__init__(*expressions, **extra)
|
||||||
|
|
||||||
|
def as_oracle(self, compiler, connection):
|
||||||
|
# Oracle prohibits mixing TextField (NCLOB) and CharField (NVARCHAR2),
|
||||||
|
# so convert all fields to NCLOB when that type is expected.
|
||||||
|
if self.output_field.get_internal_type() == 'TextField':
|
||||||
|
class ToNCLOB(Func):
|
||||||
|
function = 'TO_NCLOB'
|
||||||
|
|
||||||
|
expressions = [
|
||||||
|
ToNCLOB(expression) for expression in self.get_source_expressions()
|
||||||
|
]
|
||||||
|
clone = self.copy()
|
||||||
|
clone.set_source_expressions(expressions)
|
||||||
|
return super(Coalesce, clone).as_sql(compiler, connection)
|
||||||
|
return self.as_sql(compiler, connection)
|
||||||
|
|
||||||
|
|
||||||
|
class Greatest(Func):
|
||||||
|
"""
|
||||||
|
Return the maximum expression.
|
||||||
|
|
||||||
|
If any expression is null the return value is database-specific:
|
||||||
|
On PostgreSQL, the maximum not-null expression is returned.
|
||||||
|
On MySQL, Oracle, and SQLite, if any expression is null, null is returned.
|
||||||
|
"""
|
||||||
|
function = 'GREATEST'
|
||||||
|
|
||||||
|
def __init__(self, *expressions, **extra):
|
||||||
|
if len(expressions) < 2:
|
||||||
|
raise ValueError('Greatest must take at least two expressions')
|
||||||
|
super().__init__(*expressions, **extra)
|
||||||
|
|
||||||
|
def as_sqlite(self, compiler, connection):
|
||||||
|
"""Use the MAX function on SQLite."""
|
||||||
|
return super().as_sqlite(compiler, connection, function='MAX')
|
||||||
|
|
||||||
|
|
||||||
|
class Least(Func):
|
||||||
|
"""
|
||||||
|
Return the minimum expression.
|
||||||
|
|
||||||
|
If any expression is null the return value is database-specific:
|
||||||
|
On PostgreSQL, return the minimum not-null expression.
|
||||||
|
On MySQL, Oracle, and SQLite, if any expression is null, return null.
|
||||||
|
"""
|
||||||
|
function = 'LEAST'
|
||||||
|
|
||||||
|
def __init__(self, *expressions, **extra):
|
||||||
|
if len(expressions) < 2:
|
||||||
|
raise ValueError('Least must take at least two expressions')
|
||||||
|
super().__init__(*expressions, **extra)
|
||||||
|
|
||||||
|
def as_sqlite(self, compiler, connection):
|
||||||
|
"""Use the MIN function on SQLite."""
|
||||||
|
return super().as_sqlite(compiler, connection, function='MIN')
|
|
@ -2,8 +2,8 @@ from datetime import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
DateField, DateTimeField, DurationField, Field, IntegerField, TimeField,
|
DateField, DateTimeField, DurationField, Field, Func, IntegerField,
|
||||||
Transform,
|
TimeField, Transform, fields,
|
||||||
)
|
)
|
||||||
from django.db.models.lookups import (
|
from django.db.models.lookups import (
|
||||||
YearExact, YearGt, YearGte, YearLt, YearLte,
|
YearExact, YearGt, YearGte, YearLt, YearLte,
|
||||||
|
@ -143,6 +143,17 @@ ExtractYear.register_lookup(YearLt)
|
||||||
ExtractYear.register_lookup(YearLte)
|
ExtractYear.register_lookup(YearLte)
|
||||||
|
|
||||||
|
|
||||||
|
class Now(Func):
|
||||||
|
template = 'CURRENT_TIMESTAMP'
|
||||||
|
output_field = fields.DateTimeField()
|
||||||
|
|
||||||
|
def as_postgresql(self, compiler, connection):
|
||||||
|
# PostgreSQL's CURRENT_TIMESTAMP means "the time at the start of the
|
||||||
|
# transaction". Use STATEMENT_TIMESTAMP to be cross-compatible with
|
||||||
|
# other databases.
|
||||||
|
return self.as_sql(compiler, connection, template='STATEMENT_TIMESTAMP()')
|
||||||
|
|
||||||
|
|
||||||
class TruncBase(TimezoneMixin, Transform):
|
class TruncBase(TimezoneMixin, Transform):
|
||||||
kind = None
|
kind = None
|
||||||
tzinfo = None
|
tzinfo = None
|
||||||
|
|
|
@ -1,48 +1,5 @@
|
||||||
"""
|
|
||||||
Classes that represent database functions.
|
|
||||||
"""
|
|
||||||
from django.db.models import Func, Transform, Value, fields
|
from django.db.models import Func, Transform, Value, fields
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
class Cast(Func):
|
|
||||||
"""Coerce an expression to a new field type."""
|
|
||||||
function = 'CAST'
|
|
||||||
template = '%(function)s(%(expressions)s AS %(db_type)s)'
|
|
||||||
|
|
||||||
def __init__(self, expression, output_field):
|
|
||||||
super().__init__(expression, output_field=output_field)
|
|
||||||
|
|
||||||
def as_sql(self, compiler, connection, **extra_context):
|
|
||||||
extra_context['db_type'] = self.output_field.cast_db_type(connection)
|
|
||||||
return super().as_sql(compiler, connection, **extra_context)
|
|
||||||
|
|
||||||
def as_postgresql(self, compiler, connection):
|
|
||||||
# CAST would be valid too, but the :: shortcut syntax is more readable.
|
|
||||||
return self.as_sql(compiler, connection, template='%(expressions)s::%(db_type)s')
|
|
||||||
|
|
||||||
|
|
||||||
class Coalesce(Func):
|
|
||||||
"""Return, from left to right, the first non-null expression."""
|
|
||||||
function = 'COALESCE'
|
|
||||||
|
|
||||||
def __init__(self, *expressions, **extra):
|
|
||||||
if len(expressions) < 2:
|
|
||||||
raise ValueError('Coalesce must take at least two expressions')
|
|
||||||
super().__init__(*expressions, **extra)
|
|
||||||
|
|
||||||
def as_oracle(self, compiler, connection):
|
|
||||||
# we can't mix TextField (NCLOB) and CharField (NVARCHAR), so convert
|
|
||||||
# all fields to NCLOB when we expect NCLOB
|
|
||||||
if self.output_field.get_internal_type() == 'TextField':
|
|
||||||
class ToNCLOB(Func):
|
|
||||||
function = 'TO_NCLOB'
|
|
||||||
|
|
||||||
expressions = [
|
|
||||||
ToNCLOB(expression) for expression in self.get_source_expressions()]
|
|
||||||
clone = self.copy()
|
|
||||||
clone.set_source_expressions(expressions)
|
|
||||||
return super(Coalesce, clone).as_sql(compiler, connection)
|
|
||||||
return self.as_sql(compiler, connection)
|
|
||||||
|
|
||||||
|
|
||||||
class ConcatPair(Func):
|
class ConcatPair(Func):
|
||||||
|
@ -98,46 +55,6 @@ class Concat(Func):
|
||||||
return ConcatPair(expressions[0], self._paired(expressions[1:]))
|
return ConcatPair(expressions[0], self._paired(expressions[1:]))
|
||||||
|
|
||||||
|
|
||||||
class Greatest(Func):
|
|
||||||
"""
|
|
||||||
Return the maximum expression.
|
|
||||||
|
|
||||||
If any expression is null the return value is database-specific:
|
|
||||||
On Postgres, the maximum not-null expression is returned.
|
|
||||||
On MySQL, Oracle, and SQLite, if any expression is null, null is returned.
|
|
||||||
"""
|
|
||||||
function = 'GREATEST'
|
|
||||||
|
|
||||||
def __init__(self, *expressions, **extra):
|
|
||||||
if len(expressions) < 2:
|
|
||||||
raise ValueError('Greatest must take at least two expressions')
|
|
||||||
super().__init__(*expressions, **extra)
|
|
||||||
|
|
||||||
def as_sqlite(self, compiler, connection):
|
|
||||||
"""Use the MAX function on SQLite."""
|
|
||||||
return super().as_sqlite(compiler, connection, function='MAX')
|
|
||||||
|
|
||||||
|
|
||||||
class Least(Func):
|
|
||||||
"""
|
|
||||||
Return the minimum expression.
|
|
||||||
|
|
||||||
If any expression is null the return value is database-specific:
|
|
||||||
On Postgres, return the minimum not-null expression.
|
|
||||||
On MySQL, Oracle, and SQLite, if any expression is null, return null.
|
|
||||||
"""
|
|
||||||
function = 'LEAST'
|
|
||||||
|
|
||||||
def __init__(self, *expressions, **extra):
|
|
||||||
if len(expressions) < 2:
|
|
||||||
raise ValueError('Least must take at least two expressions')
|
|
||||||
super().__init__(*expressions, **extra)
|
|
||||||
|
|
||||||
def as_sqlite(self, compiler, connection):
|
|
||||||
"""Use the MIN function on SQLite."""
|
|
||||||
return super().as_sqlite(compiler, connection, function='MIN')
|
|
||||||
|
|
||||||
|
|
||||||
class Length(Transform):
|
class Length(Transform):
|
||||||
"""Return the number of characters in the expression."""
|
"""Return the number of characters in the expression."""
|
||||||
function = 'LENGTH'
|
function = 'LENGTH'
|
||||||
|
@ -153,17 +70,6 @@ class Lower(Transform):
|
||||||
lookup_name = 'lower'
|
lookup_name = 'lower'
|
||||||
|
|
||||||
|
|
||||||
class Now(Func):
|
|
||||||
template = 'CURRENT_TIMESTAMP'
|
|
||||||
output_field = fields.DateTimeField()
|
|
||||||
|
|
||||||
def as_postgresql(self, compiler, connection):
|
|
||||||
# Postgres' CURRENT_TIMESTAMP means "the time at the start of the
|
|
||||||
# transaction". We use STATEMENT_TIMESTAMP to be cross-compatible with
|
|
||||||
# other databases.
|
|
||||||
return self.as_sql(compiler, connection, template='STATEMENT_TIMESTAMP()')
|
|
||||||
|
|
||||||
|
|
||||||
class StrIndex(Func):
|
class StrIndex(Func):
|
||||||
"""
|
"""
|
||||||
Return a positive integer corresponding to the 1-indexed position of the
|
Return a positive integer corresponding to the 1-indexed position of the
|
Loading…
Reference in New Issue