From 4f27e475b30d0cf91be24f3116a54b17789ac403 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Fri, 13 Oct 2017 21:23:00 +0200 Subject: [PATCH] Refs #28643 -- Reorganized database functions. Thanks Tim Graham for the review. --- django/db/models/functions/__init__.py | 20 ++-- django/db/models/functions/comparison.py | 84 ++++++++++++++++ django/db/models/functions/datetime.py | 15 ++- .../db/models/functions/{base.py => text.py} | 96 +------------------ 4 files changed, 108 insertions(+), 107 deletions(-) create mode 100644 django/db/models/functions/comparison.py rename django/db/models/functions/{base.py => text.py} (51%) diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py index aab74b232a..dd11dd1772 100644 --- a/django/db/models/functions/__init__.py +++ b/django/db/models/functions/__init__.py @@ -1,27 +1,27 @@ -from .base import ( - Cast, Coalesce, Concat, ConcatPair, Greatest, Least, Length, Lower, Now, - StrIndex, Substr, Upper, -) +from .comparison import Cast, Coalesce, Greatest, Least from .datetime import ( Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth, ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear, - Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth, + Now, 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', + # comparison and conversion + 'Cast', 'Coalesce', 'Greatest', 'Least', # datetime 'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth', 'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractWeekDay', - 'ExtractYear', 'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', - 'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime', 'TruncYear', + 'ExtractYear', 'Now', 'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', + 'TruncMinute', 'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime', + 'TruncYear', + # text + 'Concat', 'ConcatPair', 'Length', 'Lower', 'StrIndex', 'Substr', 'Upper', # window 'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead', 'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber', diff --git a/django/db/models/functions/comparison.py b/django/db/models/functions/comparison.py new file mode 100644 index 0000000000..dba30979a8 --- /dev/null +++ b/django/db/models/functions/comparison.py @@ -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') diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py index c6614a14c2..44bcb731e6 100644 --- a/django/db/models/functions/datetime.py +++ b/django/db/models/functions/datetime.py @@ -2,8 +2,8 @@ from datetime import datetime from django.conf import settings from django.db.models import ( - DateField, DateTimeField, DurationField, Field, IntegerField, TimeField, - Transform, + DateField, DateTimeField, DurationField, Field, Func, IntegerField, + TimeField, Transform, fields, ) from django.db.models.lookups import ( YearExact, YearGt, YearGte, YearLt, YearLte, @@ -143,6 +143,17 @@ ExtractYear.register_lookup(YearLt) 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): kind = None tzinfo = None diff --git a/django/db/models/functions/base.py b/django/db/models/functions/text.py similarity index 51% rename from django/db/models/functions/base.py rename to django/db/models/functions/text.py index 4b4d4f4ea5..4ec07be2df 100644 --- a/django/db/models/functions/base.py +++ b/django/db/models/functions/text.py @@ -1,48 +1,5 @@ -""" -Classes that represent database functions. -""" from django.db.models import Func, Transform, Value, fields - - -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) +from django.db.models.functions import Coalesce class ConcatPair(Func): @@ -98,46 +55,6 @@ class Concat(Func): 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): """Return the number of characters in the expression.""" function = 'LENGTH' @@ -153,17 +70,6 @@ class Lower(Transform): 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): """ Return a positive integer corresponding to the 1-indexed position of the