summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorJosh Smeaton <josh.smeaton@gmail.com>2016-03-05 23:05:47 +1100
committerJosh Smeaton <josh.smeaton@gmail.com>2016-05-18 20:14:58 +1000
commit2a4af0ea43512370764303d35bc5309f8abce666 (patch)
treeea0c9ba8051ae30df2f09e9a57e564e88d156489 /django
parent77b73e79a4750dcbfabc528bf00cad81ff5bb4d9 (diff)
Fixed #25774 -- Refactor datetime expressions into public API
Diffstat (limited to 'django')
-rw-r--r--django/contrib/postgres/functions.py3
-rw-r--r--django/db/models/expressions.py108
-rw-r--r--django/db/models/functions/__init__.py21
-rw-r--r--django/db/models/functions/base.py (renamed from django/db/models/functions.py)0
-rw-r--r--django/db/models/functions/datetime.py250
-rw-r--r--django/db/models/lookups.py128
-rw-r--r--django/db/models/query.py9
7 files changed, 300 insertions, 219 deletions
diff --git a/django/contrib/postgres/functions.py b/django/contrib/postgres/functions.py
index 3184c492fc..fc0dae8ece 100644
--- a/django/contrib/postgres/functions.py
+++ b/django/contrib/postgres/functions.py
@@ -1,5 +1,4 @@
-from django.db.models import DateTimeField
-from django.db.models.functions import Func
+from django.db.models import DateTimeField, Func
class TransactionNow(Func):
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index df82d2ec58..f79865d153 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -1,12 +1,11 @@
import copy
import datetime
-from django.conf import settings
from django.core.exceptions import FieldError
from django.db.backends import utils as backend_utils
from django.db.models import fields
from django.db.models.query_utils import Q
-from django.utils import six, timezone
+from django.utils import six
from django.utils.functional import cached_property
@@ -860,111 +859,6 @@ class Case(Expression):
return sql, sql_params
-class Date(Expression):
- """
- Add a date selection column.
- """
- def __init__(self, lookup, lookup_type):
- super(Date, self).__init__(output_field=fields.DateField())
- self.lookup = lookup
- self.col = None
- self.lookup_type = lookup_type
-
- def __repr__(self):
- return "{}({}, {})".format(self.__class__.__name__, self.lookup, self.lookup_type)
-
- def get_source_expressions(self):
- return [self.col]
-
- def set_source_expressions(self, exprs):
- self.col, = exprs
-
- def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
- copy = self.copy()
- copy.col = query.resolve_ref(self.lookup, allow_joins, reuse, summarize)
- field = copy.col.output_field
- assert isinstance(field, fields.DateField), "%r isn't a DateField." % field.name
- if settings.USE_TZ:
- assert not isinstance(field, fields.DateTimeField), (
- "%r is a DateTimeField, not a DateField." % field.name
- )
- return copy
-
- def as_sql(self, compiler, connection):
- sql, params = self.col.as_sql(compiler, connection)
- assert not(params)
- return connection.ops.date_trunc_sql(self.lookup_type, sql), []
-
- def copy(self):
- copy = super(Date, self).copy()
- copy.lookup = self.lookup
- copy.lookup_type = self.lookup_type
- return copy
-
- def convert_value(self, value, expression, connection, context):
- if isinstance(value, datetime.datetime):
- value = value.date()
- return value
-
-
-class DateTime(Expression):
- """
- Add a datetime selection column.
- """
- def __init__(self, lookup, lookup_type, tzinfo):
- super(DateTime, self).__init__(output_field=fields.DateTimeField())
- self.lookup = lookup
- self.col = None
- self.lookup_type = lookup_type
- if tzinfo is None:
- self.tzname = None
- else:
- self.tzname = timezone._get_timezone_name(tzinfo)
- self.tzinfo = tzinfo
-
- def __repr__(self):
- return "{}({}, {}, {})".format(
- self.__class__.__name__, self.lookup, self.lookup_type, self.tzinfo)
-
- def get_source_expressions(self):
- return [self.col]
-
- def set_source_expressions(self, exprs):
- self.col, = exprs
-
- def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
- copy = self.copy()
- copy.col = query.resolve_ref(self.lookup, allow_joins, reuse, summarize)
- field = copy.col.output_field
- assert isinstance(field, fields.DateTimeField), (
- "%r isn't a DateTimeField." % field.name
- )
- return copy
-
- def as_sql(self, compiler, connection):
- sql, params = self.col.as_sql(compiler, connection)
- assert not(params)
- return connection.ops.datetime_trunc_sql(self.lookup_type, sql, self.tzname)
-
- def copy(self):
- copy = super(DateTime, self).copy()
- copy.lookup = self.lookup
- copy.lookup_type = self.lookup_type
- copy.tzname = self.tzname
- return copy
-
- def convert_value(self, value, expression, connection, context):
- if settings.USE_TZ:
- if value is None:
- raise ValueError(
- "Database returned an invalid value in QuerySet.datetimes(). "
- "Are time zone definitions for your database and pytz installed?"
- )
- value = value.replace(tzinfo=None)
- value = timezone.make_aware(value, self.tzinfo)
- return value
-
-
class OrderBy(BaseExpression):
template = '%(expression)s %(ordering)s'
diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py
new file mode 100644
index 0000000000..01e1c5f6d7
--- /dev/null
+++ b/django/db/models/functions/__init__.py
@@ -0,0 +1,21 @@
+from .base import (
+ Cast, Coalesce, Concat, ConcatPair, Greatest, Least, Length, Lower, Now,
+ Substr, Upper,
+)
+
+from .datetime import (
+ Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
+ ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
+ TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncYear,
+)
+
+__all__ = [
+ # base
+ 'Cast', 'Coalesce', 'Concat', 'ConcatPair', 'Greatest', 'Least', 'Length',
+ 'Lower', 'Now', 'Substr', 'Upper',
+ # datetime
+ 'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
+ 'ExtractSecond', 'ExtractWeekDay', 'ExtractYear',
+ 'Trunc', 'TruncDate', 'TruncDay', 'TruncHour', 'TruncMinute', 'TruncMonth',
+ 'TruncSecond', 'TruncYear',
+]
diff --git a/django/db/models/functions.py b/django/db/models/functions/base.py
index ca73340b85..ca73340b85 100644
--- a/django/db/models/functions.py
+++ b/django/db/models/functions/base.py
diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py
new file mode 100644
index 0000000000..e6815fbb03
--- /dev/null
+++ b/django/db/models/functions/datetime.py
@@ -0,0 +1,250 @@
+from __future__ import absolute_import
+from datetime import datetime
+
+from django.conf import settings
+from django.db.models import (
+ DateField, DateTimeField, IntegerField, TimeField, Transform,
+)
+from django.db.models.lookups import (
+ YearExact, YearGt, YearGte, YearLt, YearLte,
+)
+from django.utils import timezone
+from django.utils.functional import cached_property
+
+
+class TimezoneMixin(object):
+ tzinfo = None
+
+ def get_tzname(self):
+ # Timezone conversions must happen to the input datetime *before*
+ # applying a function. 2015-12-31 23:00:00 -02:00 is stored in the
+ # database as 2016-01-01 01:00:00 +00:00. Any results should be
+ # based on the input datetime not the stored datetime.
+ tzname = None
+ if settings.USE_TZ:
+ if self.tzinfo is None:
+ tzname = timezone.get_current_timezone_name()
+ else:
+ tzname = timezone._get_timezone_name(self.tzinfo)
+ return tzname
+
+
+class Extract(TimezoneMixin, Transform):
+ lookup_name = None
+
+ def __init__(self, expression, lookup_name=None, tzinfo=None, **extra):
+ if self.lookup_name is None:
+ self.lookup_name = lookup_name
+ if self.lookup_name is None:
+ raise ValueError('lookup_name must be provided')
+ self.tzinfo = tzinfo
+ super(Extract, self).__init__(expression, **extra)
+
+ def as_sql(self, compiler, connection):
+ sql, params = compiler.compile(self.lhs)
+ lhs_output_field = self.lhs.output_field
+ if isinstance(lhs_output_field, DateTimeField):
+ tzname = self.get_tzname()
+ sql, tz_params = connection.ops.datetime_extract_sql(self.lookup_name, sql, tzname)
+ params.extend(tz_params)
+ elif isinstance(lhs_output_field, DateField):
+ sql = connection.ops.date_extract_sql(self.lookup_name, sql)
+ elif isinstance(lhs_output_field, TimeField):
+ sql = connection.ops.time_extract_sql(self.lookup_name, sql)
+ else:
+ # resolve_expression has already validated the output_field so this
+ # assert should never be hit.
+ assert False, "Tried to Extract from an invalid type."
+ return sql, params
+
+ def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
+ copy = super(Extract, self).resolve_expression(query, allow_joins, reuse, summarize, for_save)
+ field = copy.lhs.output_field
+ if not isinstance(field, (DateField, DateTimeField, TimeField)):
+ raise ValueError('Extract input expression must be DateField, DateTimeField, or TimeField.')
+ # Passing dates to functions expecting datetimes is most likely a mistake.
+ if type(field) == DateField and copy.lookup_name in ('hour', 'minute', 'second'):
+ raise ValueError(
+ "Cannot extract time component '%s' from DateField '%s'. " % (copy.lookup_name, field.name)
+ )
+ return copy
+
+ @cached_property
+ def output_field(self):
+ return IntegerField()
+
+
+class ExtractYear(Extract):
+ lookup_name = 'year'
+
+
+class ExtractMonth(Extract):
+ lookup_name = 'month'
+
+
+class ExtractDay(Extract):
+ lookup_name = 'day'
+
+
+class ExtractWeekDay(Extract):
+ """
+ Return Sunday=1 through Saturday=7.
+
+ To replicate this in Python: (mydatetime.isoweekday() % 7) + 1
+ """
+ lookup_name = 'week_day'
+
+
+class ExtractHour(Extract):
+ lookup_name = 'hour'
+
+
+class ExtractMinute(Extract):
+ lookup_name = 'minute'
+
+
+class ExtractSecond(Extract):
+ lookup_name = 'second'
+
+
+DateField.register_lookup(ExtractYear)
+DateField.register_lookup(ExtractMonth)
+DateField.register_lookup(ExtractDay)
+DateField.register_lookup(ExtractWeekDay)
+
+TimeField.register_lookup(ExtractHour)
+TimeField.register_lookup(ExtractMinute)
+TimeField.register_lookup(ExtractSecond)
+
+DateTimeField.register_lookup(ExtractYear)
+DateTimeField.register_lookup(ExtractMonth)
+DateTimeField.register_lookup(ExtractDay)
+DateTimeField.register_lookup(ExtractWeekDay)
+DateTimeField.register_lookup(ExtractHour)
+DateTimeField.register_lookup(ExtractMinute)
+DateTimeField.register_lookup(ExtractSecond)
+
+ExtractYear.register_lookup(YearExact)
+ExtractYear.register_lookup(YearGt)
+ExtractYear.register_lookup(YearGte)
+ExtractYear.register_lookup(YearLt)
+ExtractYear.register_lookup(YearLte)
+
+
+class TruncBase(TimezoneMixin, Transform):
+ arity = 1
+ kind = None
+ tzinfo = None
+
+ def __init__(self, expression, output_field=None, tzinfo=None, **extra):
+ self.tzinfo = tzinfo
+ super(TruncBase, self).__init__(expression, output_field=output_field, **extra)
+
+ def as_sql(self, compiler, connection):
+ inner_sql, inner_params = compiler.compile(self.lhs)
+ # Escape any params because trunc_sql will format the string.
+ inner_sql = inner_sql.replace('%s', '%%s')
+ if isinstance(self.output_field, DateTimeField):
+ tzname = self.get_tzname()
+ sql, params = connection.ops.datetime_trunc_sql(self.kind, inner_sql, tzname)
+ elif isinstance(self.output_field, DateField):
+ sql = connection.ops.date_trunc_sql(self.kind, inner_sql)
+ params = []
+ else:
+ raise ValueError('Trunc only valid on DateField or DateTimeField.')
+ return sql, inner_params + params
+
+ def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
+ copy = super(TruncBase, self).resolve_expression(query, allow_joins, reuse, summarize, for_save)
+ field = copy.lhs.output_field
+ # DateTimeField is a subclass of DateField so this works for both.
+ assert isinstance(field, DateField), (
+ "%r isn't a DateField or DateTimeField." % field.name
+ )
+ # If self.output_field was None, then accessing the field will trigger
+ # the resolver to assign it to self.lhs.output_field.
+ if not isinstance(copy.output_field, (DateField, DateTimeField)):
+ raise ValueError('output_field must be either DateField or DateTimeField')
+ # Passing dates to functions expecting datetimes is most likely a
+ # mistake.
+ if type(field) == DateField and (
+ isinstance(copy.output_field, DateTimeField) or copy.kind in ('hour', 'minute', 'second')):
+ raise ValueError("Cannot truncate DateField '%s' to DateTimeField. " % field.name)
+ return copy
+
+ def convert_value(self, value, expression, connection, context):
+ if isinstance(self.output_field, DateTimeField):
+ if settings.USE_TZ:
+ if value is None:
+ raise ValueError(
+ "Database returned an invalid datetime value. "
+ "Are time zone definitions for your database and pytz installed?"
+ )
+ value = value.replace(tzinfo=None)
+ value = timezone.make_aware(value, self.tzinfo)
+ elif isinstance(value, datetime):
+ # self.output_field is definitely a DateField here.
+ value = value.date()
+ return value
+
+
+class Trunc(TruncBase):
+
+ def __init__(self, expression, kind, output_field=None, tzinfo=None, **extra):
+ self.kind = kind
+ super(Trunc, self).__init__(expression, output_field=output_field, tzinfo=tzinfo, **extra)
+
+
+class TruncYear(TruncBase):
+ kind = 'year'
+
+
+class TruncMonth(TruncBase):
+ kind = 'month'
+
+
+class TruncDay(TruncBase):
+ kind = 'day'
+
+
+class TruncDate(TruncBase):
+ lookup_name = 'date'
+
+ @cached_property
+ def output_field(self):
+ return DateField()
+
+ def as_sql(self, compiler, connection):
+ # Cast to date rather than truncate to date.
+ lhs, lhs_params = compiler.compile(self.lhs)
+ tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None
+ sql, tz_params = connection.ops.datetime_cast_date_sql(lhs, tzname)
+ lhs_params.extend(tz_params)
+ return sql, lhs_params
+
+
+class TruncHour(TruncBase):
+ kind = 'hour'
+
+ @cached_property
+ def output_field(self):
+ return DateTimeField()
+
+
+class TruncMinute(TruncBase):
+ kind = 'minute'
+
+ @cached_property
+ def output_field(self):
+ return DateTimeField()
+
+
+class TruncSecond(TruncBase):
+ kind = 'second'
+
+ @cached_property
+ def output_field(self):
+ return DateTimeField()
+
+
+DateTimeField.register_lookup(TruncDate)
diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py
index 9f398b2b1a..cc02e2f6ff 100644
--- a/django/db/models/lookups.py
+++ b/django/db/models/lookups.py
@@ -2,13 +2,9 @@ import math
import warnings
from copy import copy
-from django.conf import settings
from django.db.models.expressions import Func, Value
-from django.db.models.fields import (
- DateField, DateTimeField, Field, IntegerField, TimeField,
-)
+from django.db.models.fields import DateTimeField, Field, IntegerField
from django.db.models.query_utils import RegisterLookupMixin
-from django.utils import timezone
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.functional import cached_property
from django.utils.six.moves import range
@@ -480,46 +476,6 @@ class IRegex(Regex):
Field.register_lookup(IRegex)
-class DateTimeDateTransform(Transform):
- lookup_name = 'date'
-
- @cached_property
- def output_field(self):
- return DateField()
-
- def as_sql(self, compiler, connection):
- lhs, lhs_params = compiler.compile(self.lhs)
- tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None
- sql, tz_params = connection.ops.datetime_cast_date_sql(lhs, tzname)
- lhs_params.extend(tz_params)
- return sql, lhs_params
-
-
-class DateTransform(Transform):
- def as_sql(self, compiler, connection):
- sql, params = compiler.compile(self.lhs)
- lhs_output_field = self.lhs.output_field
- if isinstance(lhs_output_field, DateTimeField):
- tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None
- sql, tz_params = connection.ops.datetime_extract_sql(self.lookup_name, sql, tzname)
- params.extend(tz_params)
- elif isinstance(lhs_output_field, DateField):
- sql = connection.ops.date_extract_sql(self.lookup_name, sql)
- elif isinstance(lhs_output_field, TimeField):
- sql = connection.ops.time_extract_sql(self.lookup_name, sql)
- else:
- raise ValueError('DateTransform only valid on Date/Time/DateTimeFields')
- return sql, params
-
- @cached_property
- def output_field(self):
- return IntegerField()
-
-
-class YearTransform(DateTransform):
- lookup_name = 'year'
-
-
class YearLookup(Lookup):
def year_lookup_bounds(self, connection, year):
output_field = self.lhs.lhs.output_field
@@ -530,20 +486,6 @@ class YearLookup(Lookup):
return bounds
-@YearTransform.register_lookup
-class YearExact(YearLookup):
- lookup_name = 'exact'
-
- def as_sql(self, compiler, connection):
- # We will need to skip the extract part and instead go
- # directly with the originating field, that is self.lhs.lhs.
- lhs_sql, params = self.process_lhs(compiler, connection, self.lhs.lhs)
- rhs_sql, rhs_params = self.process_rhs(compiler, connection)
- bounds = self.year_lookup_bounds(connection, rhs_params[0])
- params.extend(bounds)
- return '%s BETWEEN %%s AND %%s' % lhs_sql, params
-
-
class YearComparisonLookup(YearLookup):
def as_sql(self, compiler, connection):
# We will need to skip the extract part and instead go
@@ -564,7 +506,27 @@ class YearComparisonLookup(YearLookup):
)
-@YearTransform.register_lookup
+class YearExact(YearLookup, Exact):
+ lookup_name = 'exact'
+
+ def as_sql(self, compiler, connection):
+ # We will need to skip the extract part and instead go
+ # directly with the originating field, that is self.lhs.lhs.
+ lhs_sql, params = self.process_lhs(compiler, connection, self.lhs.lhs)
+ rhs_sql, rhs_params = self.process_rhs(compiler, connection)
+ try:
+ # Check that rhs_params[0] exists (IndexError),
+ # it isn't None (TypeError), and is a number (ValueError)
+ int(rhs_params[0])
+ except (IndexError, TypeError, ValueError):
+ # Can't determine the bounds before executing the query, so skip
+ # optimizations by falling back to a standard exact comparison.
+ return super(Exact, self).as_sql(compiler, connection)
+ bounds = self.year_lookup_bounds(connection, rhs_params[0])
+ params.extend(bounds)
+ return '%s BETWEEN %%s AND %%s' % lhs_sql, params
+
+
class YearGt(YearComparisonLookup):
lookup_name = 'gt'
@@ -572,7 +534,6 @@ class YearGt(YearComparisonLookup):
return finish
-@YearTransform.register_lookup
class YearGte(YearComparisonLookup):
lookup_name = 'gte'
@@ -580,7 +541,6 @@ class YearGte(YearComparisonLookup):
return start
-@YearTransform.register_lookup
class YearLt(YearComparisonLookup):
lookup_name = 'lt'
@@ -588,52 +548,8 @@ class YearLt(YearComparisonLookup):
return start
-@YearTransform.register_lookup
class YearLte(YearComparisonLookup):
lookup_name = 'lte'
def get_bound(self, start, finish):
return finish
-
-
-class MonthTransform(DateTransform):
- lookup_name = 'month'
-
-
-class DayTransform(DateTransform):
- lookup_name = 'day'
-
-
-class WeekDayTransform(DateTransform):
- lookup_name = 'week_day'
-
-
-class HourTransform(DateTransform):
- lookup_name = 'hour'
-
-
-class MinuteTransform(DateTransform):
- lookup_name = 'minute'
-
-
-class SecondTransform(DateTransform):
- lookup_name = 'second'
-
-
-DateField.register_lookup(YearTransform)
-DateField.register_lookup(MonthTransform)
-DateField.register_lookup(DayTransform)
-DateField.register_lookup(WeekDayTransform)
-
-TimeField.register_lookup(HourTransform)
-TimeField.register_lookup(MinuteTransform)
-TimeField.register_lookup(SecondTransform)
-
-DateTimeField.register_lookup(DateTimeDateTransform)
-DateTimeField.register_lookup(YearTransform)
-DateTimeField.register_lookup(MonthTransform)
-DateTimeField.register_lookup(DayTransform)
-DateTimeField.register_lookup(WeekDayTransform)
-DateTimeField.register_lookup(HourTransform)
-DateTimeField.register_lookup(MinuteTransform)
-DateTimeField.register_lookup(SecondTransform)
diff --git a/django/db/models/query.py b/django/db/models/query.py
index dce643869e..8bd981db51 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -13,11 +13,12 @@ from django.db import (
DJANGO_VERSION_PICKLE_KEY, IntegrityError, connections, router,
transaction,
)
-from django.db.models import sql
+from django.db.models import DateField, DateTimeField, sql
from django.db.models.constants import LOOKUP_SEP
from django.db.models.deletion import Collector
-from django.db.models.expressions import Date, DateTime, F
+from django.db.models.expressions import F
from django.db.models.fields import AutoField
+from django.db.models.functions import Trunc
from django.db.models.query_utils import (
InvalidQuery, Q, check_rel_lookup_compatibility,
)
@@ -739,7 +740,7 @@ class QuerySet(object):
assert order in ('ASC', 'DESC'), \
"'order' must be either 'ASC' or 'DESC'."
return self.annotate(
- datefield=Date(field_name, kind),
+ datefield=Trunc(field_name, kind, output_field=DateField()),
plain_field=F(field_name)
).values_list(
'datefield', flat=True
@@ -760,7 +761,7 @@ class QuerySet(object):
else:
tzinfo = None
return self.annotate(
- datetimefield=DateTime(field_name, kind, tzinfo),
+ datetimefield=Trunc(field_name, kind, output_field=DateTimeField(), tzinfo=tzinfo),
plain_field=F(field_name)
).values_list(
'datetimefield', flat=True