浏览代码

Fixed #25774 -- Refactor datetime expressions into public API

Josh Smeaton 9 年之前
父节点
当前提交
2a4af0ea43

+ 1 - 2
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):

+ 1 - 107
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'
 

+ 21 - 0
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',
+]

+ 0 - 0
django/db/models/functions.py → django/db/models/functions/base.py


+ 250 - 0
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)

+ 22 - 106
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)

+ 5 - 4
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

+ 399 - 0
docs/ref/models/database-functions.txt

@@ -293,3 +293,402 @@ Usage example::
 .. versionchanged:: 1.9
 
     The ability to register the function as a transform was added.
+
+Date Functions
+==============
+
+.. module:: django.db.models.functions.datetime
+
+.. versionadded:: 1.10
+
+We'll be using the following model in examples of each function::
+
+    class Experiment(models.Model):
+        start_time = models.DateTimeField()
+        start_date = models.DateField(null=True, blank=True)
+        end_time = models.DateTimeField(null=True, blank=True)
+        end_date = models.DateField(null=True, blank=True)
+
+``Extract``
+-----------
+
+.. class:: Extract(expression, lookup_name=None, tzinfo=None, **extra)
+
+Extracts a component of a date as a number.
+
+Takes an ``expression`` representing a ``DateField`` or ``DateTimeField`` and a
+``lookup_name``, and returns the part of the date referenced by ``lookup_name``
+as an ``IntegerField``. Django usually uses the databases' extract function, so
+you may use any ``lookup_name`` that your database supports. A ``tzinfo``
+subclass, usually provided by ``pytz``, can be passed to extract a value in a
+specific timezone.
+
+Given the datetime ``2015-06-15 23:30:01.000321+00:00``, the built-in
+``lookup_name``\s return:
+
+* "year": 2015
+* "month": 6
+* "day": 15
+* "week_day": 2
+* "hour": 23
+* "minute": 30
+* "second": 1
+
+If a different timezone like ``Australia/Melbourne`` is active in Django, then
+the datetime is converted to the timezone before the value is extracted. The
+timezone offset for Melbourne in the example date above is +10:00. The values
+returned when this timezone is active will be the same as above except for:
+
+* "day": 16
+* "week_day": 3
+* "hour": 9
+
+.. admonition:: ``week_day`` values
+
+    The ``week_day`` ``lookup_type`` is calculated differently from most
+    databases and from Python's standard functions. This function will return
+    ``1`` for Sunday, ``2`` for Monday, through ``7`` for Saturday.
+
+    The equivalent calculation in Python is::
+
+        >>> from datetime import datetime
+        >>> dt = datetime(2015, 6, 15)
+        >>> (dt.isoweekday() % 7) + 1
+        2
+
+Each ``lookup_name`` above has a corresponding ``Extract`` subclass (listed
+below) that should typically be used instead of the more verbose equivalent,
+e.g. use ``ExtractYear(...)`` rather than ``Extract(..., lookup_name='year')``.
+
+Usage example::
+
+    >>> from datetime import datetime
+    >>> from django.db.models.functions import Extract
+    >>> start = datetime(2015, 6, 15)
+    >>> end = datetime(2015, 7, 2)
+    >>> Experiment.objects.create(
+    ...    start_time=start, start_date=start.date(),
+    ...    end_time=end, end_date=end.date())
+    >>> # Add the experiment start year as a field in the QuerySet.
+    >>> experiment = Experiment.objects.annotate(
+    ...    start_year=Extract('start_time', 'year')).get()
+    >>> experiment.start_year
+    2015
+    >>> # How many experiments completed in the same year in which they started?
+    >>> Experiment.objects.filter(
+    ...    start_time__year=Extract('end_time', 'year')).count()
+    1
+
+``DateField`` extracts
+~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: ExtractYear(expression, tzinfo=None, **extra)
+
+    .. attribute:: lookup_name = 'year'
+
+.. class:: ExtractMonth(expression, tzinfo=None, **extra)
+
+    .. attribute:: lookup_name = 'month'
+
+.. class:: ExtractDay(expression, tzinfo=None, **extra)
+
+    .. attribute:: lookup_name = 'day'
+
+.. class:: ExtractWeekDay(expression, tzinfo=None, **extra)
+
+    .. attribute:: lookup_name = 'week_day'
+
+These are logically equivalent to ``Extract('date_field', lookup_name)``. Each
+class is also a ``Transform`` registered on ``DateField`` and ``DateTimeField``
+as ``__(lookup_name)``, e.g. ``__year``.
+
+Since ``DateField``\s don't have a time component, only ``Extract`` subclasses
+that deal with date-parts can be used with ``DateField``::
+
+    >>> from datetime import datetime
+    >>> from django.utils import timezone
+    >>> from django.db.models.functions import (
+    ...    ExtractYear, ExtractMonth, ExtractDay, ExtractWeekDay
+    ... )
+    >>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
+    >>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, tzinfo=timezone.utc)
+    >>> Experiment.objects.create(
+    ...    start_time=start_2015, start_date=start_2015.date(),
+    ...    end_time=end_2015, end_date=end_2015.date())
+    >>> Experiment.objects.annotate(
+    ...     year=ExtractYear('start_date'),
+    ...     month=ExtractMonth('start_date'),
+    ...     day=ExtractDay('start_date'),
+    ...     weekday=ExtractWeekDay('start_date'),
+    ... ).values('year', 'month', 'day', 'weekday').get(
+    ...     end_date__year=ExtractYear('start_date'),
+    ... )
+    {'year': 2015, 'month': 6, 'day': 15, 'weekday': 2}
+
+``DateTimeField`` extracts
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In addition to the following, all extracts for ``DateField`` listed above may
+also be used on ``DateTimeField``\s .
+
+.. class:: ExtractHour(expression, tzinfo=None, **extra)
+
+    .. attribute:: lookup_name = 'hour'
+
+.. class:: ExtractMinute(expression, tzinfo=None, **extra)
+
+    .. attribute:: lookup_name = 'minute'
+
+.. class:: ExtractSecond(expression, tzinfo=None, **extra)
+
+    .. attribute:: lookup_name = 'second'
+
+These are logically equivalent to ``Extract('datetime_field', lookup_name)``.
+Each class is also a ``Transform`` registered on ``DateTimeField`` as
+``__(lookup_name)``, e.g. ``__minute``.
+
+``DateTimeField`` examples::
+
+    >>> from datetime import datetime
+    >>> from django.utils import timezone
+    >>> from django.db.models.functions import (
+    ...    ExtractYear, ExtractMonth, ExtractDay, ExtractWeekDay,
+    ...    ExtractHour, ExtractMinute, ExtractSecond,
+    ... )
+    >>> start_2015 = datetime(2015, 6, 15, 23, 30, 1, tzinfo=timezone.utc)
+    >>> end_2015 = datetime(2015, 6, 16, 13, 11, 27, tzinfo=timezone.utc)
+    >>> Experiment.objects.create(
+    ...    start_time=start_2015, start_date=start_2015.date(),
+    ...    end_time=end_2015, end_date=end_2015.date())
+    >>> Experiment.objects.annotate(
+    ...     year=ExtractYear('start_time'),
+    ...     month=ExtractMonth('start_time'),
+    ...     day=ExtractDay('start_time'),
+    ...     weekday=ExtractWeekDay('start_time'),
+    ...     hour=ExtractHour('start_time'),
+    ...     minute=ExtractMinute('start_time'),
+    ...     second=ExtractSecond('start_time'),
+    ... ).values(
+    ...     'year', 'month', 'day', 'weekday', 'hour', 'minute', 'second',
+    ... ).get(end_time__year=ExtractYear('start_time'))
+    {'year': 2015, 'month': 6, 'day': 15, 'weekday': 2, 'hour': 23, 'minute': 30, 'second': 1}
+
+When :setting:`USE_TZ` is ``True`` then datetimes are stored in the database
+in UTC. If a different timezone is active in Django, the datetime is converted
+to that timezone before the value is extracted. The example below converts to
+the Melbourne timezone (UTC +10:00), which changes the day, weekday, and hour
+values that are returned::
+
+    >>> import pytz
+    >>> tzinfo = pytz.timezone('Australia/Melbourne')  # UTC+10:00
+    >>> with timezone.override(tzinfo):
+    ...    Experiment.objects.annotate(
+    ...        day=ExtractDay('start_time'),
+    ...        weekday=ExtractWeekDay('start_time'),
+    ...        hour=ExtractHour('start_time'),
+    ...    ).values('day', 'weekday', 'hour').get(
+    ...        end_time__year=ExtractYear('start_time'),
+    ...    )
+    {'day': 16, 'weekday': 3, 'hour': 9}
+
+Explicitly passing the timezone to the ``Extract`` function behaves in the same
+way, and takes priority over an active timezone::
+
+    >>> import pytz
+    >>> tzinfo = pytz.timezone('Australia/Melbourne')
+    >>> Experiment.objects.annotate(
+    ...     day=ExtractDay('start_time', tzinfo=melb),
+    ...     weekday=ExtractWeekDay('start_time', tzinfo=melb),
+    ...     hour=ExtractHour('start_time', tzinfo=melb),
+    ... ).values('day', 'weekday', 'hour').get(
+    ...     end_time__year=ExtractYear('start_time'),
+    ... )
+    {'day': 16, 'weekday': 3, 'hour': 9}
+
+
+``Trunc``
+---------
+
+.. class:: Trunc(expression, kind, output_field=None, tzinfo=None, **extra)
+
+Truncates a date up to a significant component.
+
+When you only care if something happened in a particular year, hour, or day,
+but not the exact second, then ``Trunc`` (and its subclasses) can be useful to
+filter or aggregate your data. For example, you can use ``Trunc`` to calculate
+the number of sales per day.
+
+``Trunc`` takes a single ``expression``, representing a ``DateField`` or
+``DateTimeField``, a ``kind`` representing a date part, and an ``output_field``
+that's either ``DateTimeField()`` or ``DateField()``. It returns a datetime or
+date, depending on ``output_field``, with fields up to ``kind`` set to their
+minimum value. If ``output_field`` is omitted, it will default to the
+``output_field`` of ``expression``. A ``tzinfo`` subclass, usually provided by
+``pytz``, can be passed to truncate a value in a specific timezone.
+
+Given the datetime ``2015-06-15 14:30:50.000321+00:00``, the built-in ``kind``\s
+return:
+
+* "year": 2015-01-01 00:00:00+00:00
+* "month": 2015-06-01 00:00:00+00:00
+* "day": 2015-06-15 00:00:00+00:00
+* "hour": 2015-06-15 14:00:00+00:00
+* "minute": 2015-06-15 14:30:00+00:00
+* "second": 2015-06-15 14:30:50+00:00
+
+If a different timezone like ``Australia/Melbourne`` is active in Django, then
+the datetime is converted to the new timezone before the value is truncated.
+The timezone offset for Melbourne in the example date above is +10:00. The
+values returned when this timezone is active will be:
+
+* "year": 2015-01-01 00:00:00+11:00
+* "month": 2015-06-01 00:00:00+10:00
+* "day": 2015-06-16 00:00:00+10:00
+* "hour": 2015-06-16 00:00:00+10:00
+* "minute": 2015-06-16 00:30:00+10:00
+* "second": 2015-06-16 00:30:50+10:00
+
+The year has an offset of +11:00 because the result transitioned into daylight
+saving time.
+
+Each ``kind`` above has a corresponding ``Trunc`` subclass (listed below) that
+should typically be used instead of the more verbose equivalent,
+e.g. use ``TruncYear(...)`` rather than ``Trunc(..., kind='year')``.
+
+The subclasses are all defined as transforms, but they aren't registered with
+any fields, because the obvious lookup names are already reserved by the
+``Extract`` subclasses.
+
+Usage example::
+
+    >>> from datetime import datetime
+    >>> from django.db.models import Count, DateTimeField
+    >>> from django.db.models.functions import Trunc
+    >>> Experiment.objects.create(start_time=datetime(2015, 6, 15, 14, 30, 50, 321))
+    >>> Experiment.objects.create(start_time=datetime(2015, 6, 15, 14, 40, 2, 123))
+    >>> Experiment.objects.create(start_time=datetime(2015, 12, 25, 10, 5, 27, 999))
+    >>> experiments_per_day = Experiment.objects.annotate(
+    ...    start_day=Trunc('start_time', 'day', output_field=DateTimeField())
+    ... ).values('start_day').annotate(experiments=Count('id'))
+    >>> for exp in experiments_per_day:
+    ...     print(exp['start_day'], exp['experiments'])
+    ...
+    2015-06-15 00:00:00 2
+    2015-12-25 00:00:00 1
+    >>> experiments = Experiment.objects.annotate(
+    ...    start_day=Trunc('start_time', 'day', output_field=DateTimeField())
+    ... ).filter(start_day=datetime(2015, 6, 15))
+    >>> for exp in experiments:
+    ...     print(exp.start_time)
+    ...
+    2015-06-15 14:30:50.000321
+    2015-06-15 14:40:02.000123
+
+``DateField`` truncation
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: TruncYear(expression, output_field=None, tzinfo=None, **extra)
+
+    .. attribute:: kind = 'year'
+
+.. class:: TruncMonth(expression, output_field=None, tzinfo=None, **extra)
+
+    .. attribute:: kind = 'month'
+
+These are logically equivalent to ``Trunc('date_field', kind)``. They truncate
+all parts of the date up to ``kind`` which allows grouping or filtering dates
+with less precision. ``expression`` can have an ``output_field`` of either
+``DateField`` or ``DateTimeField``.
+
+Since ``DateField``\s don't have a time component, only ``Trunc`` subclasses
+that deal with date-parts can be used with ``DateField``::
+
+    >>> from datetime import datetime
+    >>> from django.db.models import Count
+    >>> from django.db.models.functions import TruncMonth, TruncYear
+    >>> from django.utils import timezone
+    >>> start1 = datetime(2014, 6, 15, 14, 30, 50, 321, tzinfo=timezone.utc)
+    >>> start2 = datetime(2015, 6, 15, 14, 40, 2, 123, tzinfo=timezone.utc)
+    >>> start3 = datetime(2015, 12, 31, 17, 5, 27, 999, tzinfo=timezone.utc)
+    >>> Experiment.objects.create(start_time=start1, start_date=start1.date())
+    >>> Experiment.objects.create(start_time=start2, start_date=start2.date())
+    >>> Experiment.objects.create(start_time=start3, start_date=start3.date())
+    >>> experiments_per_year = Experiment.objects.annotate(
+    ...    year=TruncYear('start_date')).values('year').annotate(
+    ...    experiments=Count('id'))
+    >>> for exp in experiments_per_year:
+    ...     print(exp['year'], exp['experiments'])
+    ...
+    2014-01-01 1
+    2015-01-01 2
+
+    >>> import pytz
+    >>> melb = pytz.timezone('Australia/Melbourne')
+    >>> experiments_per_month = Experiment.objects.annotate(
+    ...    month=TruncMonth('start_time', tzinfo=melb)).values('month').annotate(
+    ...    experiments=Count('id'))
+    >>> for exp in experiments_per_month:
+    ...     print(exp['month'], exp['experiments'])
+    ...
+    2015-06-01 00:00:00+10:00 1
+    2016-01-01 00:00:00+11:00 1
+    2014-06-01 00:00:00+10:00 1
+
+``DateTimeField`` truncation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: TruncDate(expression, **extra)
+
+    .. attribute:: lookup_name = 'date'
+    .. attribute:: output_field = DateField()
+
+``TruncDate`` casts ``expression`` to a date rather than using the built-in SQL
+truncate function. It's also registered as a transform on  ``DateTimeField`` as
+``__date``.
+
+.. class:: TruncDay(expression, output_field=None, tzinfo=None, **extra)
+
+    .. attribute:: kind = 'day'
+
+.. class:: TruncHour(expression, output_field=None, tzinfo=None, **extra)
+
+    .. attribute:: kind = 'hour'
+
+.. class:: TruncMinute(expression, output_field=None, tzinfo=None, **extra)
+
+    .. attribute:: kind = 'minute'
+
+.. class:: TruncSecond(expression, output_field=None, tzinfo=None, **extra)
+
+    .. attribute:: kind = 'second'
+
+These are logically equivalent to ``Trunc('datetime_field', kind)``. They
+truncate all parts of the date up to ``kind`` and allow grouping or filtering
+datetimes with less precision. ``expression`` must have an ``output_field`` of
+``DateTimeField``.
+
+Usage example::
+
+    >>> from datetime import date, datetime
+    >>> from django.db.models import Count
+    >>> from django.db.models.functions import (
+    ...     TruncDate, TruncDay, TruncHour, TruncMinute, TruncSecond,
+    ... )
+    >>> from django.utils import timezone
+    >>> import pytz
+    >>> start1 = datetime(2014, 6, 15, 14, 30, 50, 321, tzinfo=timezone.utc)
+    >>> Experiment.objects.create(start_time=start1, start_date=start1.date())
+    >>> melb = pytz.timezone('Australia/Melbourne')
+    >>> Experiment.objects.annotate(
+    ...     date=TruncDate('start_time'),
+    ...     day=TruncDay('start_time', tzinfo=melb),
+    ...     hour=TruncHour('start_time', tzinfo=melb),
+    ...     minute=TruncMinute('start_time'),
+    ...     second=TruncSecond('start_time'),
+    ... ).values('date', 'day', 'hour', 'minute', 'second').get()
+    {'date': datetime.date(2014, 6, 15),
+     'day': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>),
+     'hour': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>),
+     'minute': 'minute': datetime.datetime(2014, 6, 15, 14, 30, tzinfo=<UTC>),
+     'second': datetime.datetime(2014, 6, 15, 14, 30, 50, tzinfo=<UTC>)
+    }

+ 11 - 0
docs/releases/1.10.txt

@@ -443,6 +443,13 @@ Models
 * A proxy model may now inherit multiple proxy models that share a common
   non-abstract parent class.
 
+* Added :class:`~django.db.models.functions.datetime.Extract` functions
+  to extract datetime components as integers, such as year and hour.
+
+* Added :class:`~django.db.models.functions.datetime.Trunc` functions to
+  truncate a date or datetime to a significant component. They enable queries
+  like sales-per-day or sales-per-hour.
+
 * ``Model.__init__()`` now sets values of virtual fields from its keyword
   arguments.
 
@@ -900,6 +907,10 @@ Miscellaneous
   989 characters. If you were counting on a limited length, truncate the subject
   yourself.
 
+* Private expressions ``django.db.models.expressions.Date`` and ``DateTime``
+  are removed. The new :class:`~django.db.models.functions.datetime.Trunc`
+  expressions provide the same functionality.
+
 .. _deprecated-features-1.10:
 
 Features deprecated in 1.10

+ 1 - 0
tests/datetimes/models.py

@@ -8,6 +8,7 @@ from django.utils.encoding import python_2_unicode_compatible
 class Article(models.Model):
     title = models.CharField(max_length=100)
     pub_date = models.DateTimeField()
+    published_on = models.DateField(null=True)
 
     categories = models.ManyToManyField("Category", related_name="articles")
 

+ 6 - 0
tests/datetimes/tests.py

@@ -153,3 +153,9 @@ class DateTimesTests(TestCase):
             datetime.datetime(2005, 7, 30, 0, 0),
             datetime.datetime(2005, 7, 29, 0, 0),
             datetime.datetime(2005, 7, 28, 0, 0)])
+
+    def test_datetimes_disallows_date_fields(self):
+        dt = datetime.datetime(2005, 7, 28, 12, 15)
+        Article.objects.create(pub_date=dt, published_on=dt.date(), title="Don't put dates into datetime functions!")
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'published_on' to DateTimeField"):
+            list(Article.objects.datetimes('published_on', 'second'))

+ 15 - 0
tests/db_functions/models.py

@@ -41,3 +41,18 @@ class Fan(models.Model):
 
     def __str__(self):
         return self.name
+
+
+@python_2_unicode_compatible
+class DTModel(models.Model):
+    name = models.CharField(max_length=32)
+    start_datetime = models.DateTimeField(null=True, blank=True)
+    end_datetime = models.DateTimeField(null=True, blank=True)
+    start_date = models.DateField(null=True, blank=True)
+    end_date = models.DateField(null=True, blank=True)
+    start_time = models.TimeField(null=True, blank=True)
+    end_time = models.TimeField(null=True, blank=True)
+    duration = models.DurationField(null=True, blank=True)
+
+    def __str__(self):
+        return 'DTModel({0})'.format(self.name)

+ 694 - 0
tests/db_functions/test_datetime.py

@@ -0,0 +1,694 @@
+from __future__ import unicode_literals
+
+from datetime import datetime
+from unittest import skipIf
+
+from django.conf import settings
+from django.db import connection
+from django.db.models import DateField, DateTimeField, IntegerField
+from django.db.models.functions import (
+    Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth,
+    ExtractSecond, ExtractWeekDay, ExtractYear, Trunc, TruncDate, TruncDay,
+    TruncHour, TruncMinute, TruncMonth, TruncSecond, TruncYear,
+)
+from django.test import TestCase, override_settings
+from django.utils import timezone
+
+from .models import DTModel
+
+try:
+    import pytz
+except ImportError:
+    pytz = None
+
+
+def microsecond_support(value):
+    return value if connection.features.supports_microsecond_precision else value.replace(microsecond=0)
+
+
+def truncate_to(value, kind, tzinfo=None):
+    # Convert to target timezone before truncation
+    if tzinfo is not None:
+        value = value.astimezone(tzinfo)
+
+    def truncate(value, kind):
+        if kind == 'second':
+            return value.replace(microsecond=0)
+        if kind == 'minute':
+            return value.replace(second=0, microsecond=0)
+        if kind == 'hour':
+            return value.replace(minute=0, second=0, microsecond=0)
+        if kind == 'day':
+            if isinstance(value, datetime):
+                return value.replace(hour=0, minute=0, second=0, microsecond=0)
+            return value
+        if kind == 'month':
+            if isinstance(value, datetime):
+                return value.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
+            return value.replace(day=1)
+        # otherwise, truncate to year
+        if isinstance(value, datetime):
+            return value.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
+        return value.replace(month=1, day=1)
+
+    value = truncate(value, kind)
+    if tzinfo is not None:
+        # If there was a daylight saving transition, then reset the timezone.
+        value = timezone.make_aware(value.replace(tzinfo=None), tzinfo)
+    return value
+
+
+@override_settings(USE_TZ=False)
+class DateFunctionTests(TestCase):
+
+    def create_model(self, start_datetime, end_datetime):
+        return DTModel.objects.create(
+            name=start_datetime.isoformat(),
+            start_datetime=start_datetime, end_datetime=end_datetime,
+            start_date=start_datetime.date(), end_date=end_datetime.date(),
+            start_time=start_datetime.time(), end_time=end_datetime.time(),
+            duration=(end_datetime - start_datetime),
+        )
+
+    def test_extract_year_exact_lookup(self):
+        """
+        Extract year uses a BETWEEN filter to compare the year to allow indexes
+        to be used.
+        """
+        start_datetime = datetime(2015, 6, 15, 14, 10)
+        end_datetime = datetime(2016, 6, 15, 14, 10)
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+
+        qs = DTModel.objects.filter(start_datetime__year__exact=2015)
+        self.assertEqual(qs.count(), 1)
+        query_string = str(qs.query).lower()
+        self.assertEqual(query_string.count(' between '), 1)
+        self.assertEqual(query_string.count('extract'), 0)
+
+        # exact is implied and should be the same
+        qs = DTModel.objects.filter(start_datetime__year=2015)
+        self.assertEqual(qs.count(), 1)
+        query_string = str(qs.query).lower()
+        self.assertEqual(query_string.count(' between '), 1)
+        self.assertEqual(query_string.count('extract'), 0)
+
+        # date and datetime fields should behave the same
+        qs = DTModel.objects.filter(start_date__year=2015)
+        self.assertEqual(qs.count(), 1)
+        query_string = str(qs.query).lower()
+        self.assertEqual(query_string.count(' between '), 1)
+        self.assertEqual(query_string.count('extract'), 0)
+
+    def test_extract_year_greaterthan_lookup(self):
+        start_datetime = datetime(2015, 6, 15, 14, 10)
+        end_datetime = datetime(2016, 6, 15, 14, 10)
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+
+        qs = DTModel.objects.filter(start_datetime__year__gt=2015)
+        self.assertEqual(qs.count(), 1)
+        self.assertEqual(str(qs.query).lower().count('extract'), 0)
+        qs = DTModel.objects.filter(start_datetime__year__gte=2015)
+        self.assertEqual(qs.count(), 2)
+        self.assertEqual(str(qs.query).lower().count('extract'), 0)
+
+    def test_extract_year_lessthan_lookup(self):
+        start_datetime = datetime(2015, 6, 15, 14, 10)
+        end_datetime = datetime(2016, 6, 15, 14, 10)
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+
+        qs = DTModel.objects.filter(start_datetime__year__lt=2016)
+        self.assertEqual(qs.count(), 1)
+        self.assertEqual(str(qs.query).count('extract'), 0)
+        qs = DTModel.objects.filter(start_datetime__year__lte=2016)
+        self.assertEqual(qs.count(), 2)
+        self.assertEqual(str(qs.query).count('extract'), 0)
+
+    def test_extract_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+
+        with self.assertRaisesMessage(ValueError, 'lookup_name must be provided'):
+            Extract('start_datetime')
+
+        msg = 'Extract input expression must be DateField, DateTimeField, or TimeField.'
+        with self.assertRaisesMessage(ValueError, msg):
+            list(DTModel.objects.annotate(extracted=Extract('name', 'hour')))
+
+        with self.assertRaisesMessage(
+                ValueError, "Cannot extract time component 'second' from DateField 'start_date'."):
+            list(DTModel.objects.annotate(extracted=Extract('start_date', 'second')))
+
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=Extract('start_datetime', 'year')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=Extract('start_datetime', 'month')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=Extract('start_datetime', 'day')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=Extract('start_datetime', 'week_day')).order_by('start_datetime'),
+            [
+                (start_datetime, (start_datetime.isoweekday() % 7) + 1),
+                (end_datetime, (end_datetime.isoweekday() % 7) + 1)
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=Extract('start_datetime', 'hour')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.hour), (end_datetime, end_datetime.hour)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=Extract('start_datetime', 'minute')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.minute), (end_datetime, end_datetime.minute)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=Extract('start_datetime', 'second')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.second), (end_datetime, end_datetime.second)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__year=Extract('start_datetime', 'year')).count(), 2)
+        self.assertEqual(DTModel.objects.filter(start_datetime__hour=Extract('start_datetime', 'hour')).count(), 2)
+        self.assertEqual(DTModel.objects.filter(start_date__month=Extract('start_date', 'month')).count(), 2)
+        self.assertEqual(DTModel.objects.filter(start_time__hour=Extract('start_time', 'hour')).count(), 2)
+
+    def test_extract_year_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractYear('start_datetime')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractYear('start_date')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.year), (end_datetime, end_datetime.year)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__year=ExtractYear('start_datetime')).count(), 2)
+
+    def test_extract_month_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractMonth('start_datetime')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractMonth('start_date')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.month), (end_datetime, end_datetime.month)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__month=ExtractMonth('start_datetime')).count(), 2)
+
+    def test_extract_day_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractDay('start_datetime')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractDay('start_date')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.day), (end_datetime, end_datetime.day)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__day=ExtractDay('start_datetime')).count(), 2)
+
+    def test_extract_weekday_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractWeekDay('start_datetime')).order_by('start_datetime'),
+            [
+                (start_datetime, (start_datetime.isoweekday() % 7) + 1),
+                (end_datetime, (end_datetime.isoweekday() % 7) + 1),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractWeekDay('start_date')).order_by('start_datetime'),
+            [
+                (start_datetime, (start_datetime.isoweekday() % 7) + 1),
+                (end_datetime, (end_datetime.isoweekday() % 7) + 1),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__week_day=ExtractWeekDay('start_datetime')).count(), 2)
+
+    def test_extract_hour_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractHour('start_datetime')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.hour), (end_datetime, end_datetime.hour)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractHour('start_time')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.hour), (end_datetime, end_datetime.hour)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__hour=ExtractHour('start_datetime')).count(), 2)
+
+    def test_extract_minute_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractMinute('start_datetime')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.minute), (end_datetime, end_datetime.minute)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractMinute('start_time')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.minute), (end_datetime, end_datetime.minute)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__minute=ExtractMinute('start_datetime')).count(), 2)
+
+    def test_extract_second_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractSecond('start_datetime')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.second), (end_datetime, end_datetime.second)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=ExtractSecond('start_time')).order_by('start_datetime'),
+            [(start_datetime, start_datetime.second), (end_datetime, end_datetime.second)],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__second=ExtractSecond('start_datetime')).count(), 2)
+
+    def test_trunc_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+
+        with self.assertRaisesMessage(ValueError, 'output_field must be either DateField or DateTimeField'):
+            list(DTModel.objects.annotate(truncated=Trunc('start_datetime', 'year', output_field=IntegerField())))
+
+        with self.assertRaisesMessage(AssertionError, "'name' isn't a DateField or DateTimeField."):
+            list(DTModel.objects.annotate(truncated=Trunc('name', 'year', output_field=DateTimeField())))
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
+            list(DTModel.objects.annotate(truncated=Trunc('start_date', 'second')))
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
+            list(DTModel.objects.annotate(truncated=Trunc('start_date', 'month', output_field=DateTimeField())))
+
+        def test_datetime_kind(kind):
+            self.assertQuerysetEqual(
+                DTModel.objects.annotate(
+                    truncated=Trunc('start_datetime', kind, output_field=DateTimeField())
+                ).order_by('start_datetime'),
+                [
+                    (start_datetime, truncate_to(start_datetime, kind)),
+                    (end_datetime, truncate_to(end_datetime, kind))
+                ],
+                lambda m: (m.start_datetime, m.truncated)
+            )
+
+        def test_date_kind(kind):
+            self.assertQuerysetEqual(
+                DTModel.objects.annotate(
+                    truncated=Trunc('start_date', kind, output_field=DateField())
+                ).order_by('start_datetime'),
+                [
+                    (start_datetime, truncate_to(start_datetime.date(), kind)),
+                    (end_datetime, truncate_to(end_datetime.date(), kind))
+                ],
+                lambda m: (m.start_datetime, m.truncated)
+            )
+
+        test_date_kind('year')
+        test_date_kind('month')
+        test_date_kind('day')
+        test_datetime_kind('year')
+        test_datetime_kind('month')
+        test_datetime_kind('day')
+        test_datetime_kind('hour')
+        test_datetime_kind('minute')
+        test_datetime_kind('second')
+
+        qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField()))
+        self.assertEqual(qs.count(), 2)
+
+    def test_trunc_year_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'year')
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncYear('start_datetime')).order_by('start_datetime'),
+            [
+                (start_datetime, truncate_to(start_datetime, 'year')),
+                (end_datetime, truncate_to(end_datetime, 'year')),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncYear('start_date')).order_by('start_datetime'),
+            [
+                (start_datetime, truncate_to(start_datetime.date(), 'year')),
+                (end_datetime, truncate_to(end_datetime.date(), 'year')),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime=TruncYear('start_datetime')).count(), 1)
+
+    def test_trunc_month_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'month')
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncMonth('start_datetime')).order_by('start_datetime'),
+            [
+                (start_datetime, truncate_to(start_datetime, 'month')),
+                (end_datetime, truncate_to(end_datetime, 'month')),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncMonth('start_date')).order_by('start_datetime'),
+            [
+                (start_datetime, truncate_to(start_datetime.date(), 'month')),
+                (end_datetime, truncate_to(end_datetime.date(), 'month')),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime=TruncMonth('start_datetime')).count(), 1)
+
+    def test_trunc_date_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncDate('start_datetime')).order_by('start_datetime'),
+            [
+                (start_datetime, start_datetime.date()),
+                (end_datetime, end_datetime.date()),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime__date=TruncDate('start_datetime')).count(), 2)
+
+    def test_trunc_day_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'day')
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncDay('start_datetime')).order_by('start_datetime'),
+            [
+                (start_datetime, truncate_to(start_datetime, 'day')),
+                (end_datetime, truncate_to(end_datetime, 'day')),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime=TruncDay('start_datetime')).count(), 1)
+
+    def test_trunc_hour_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'hour')
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncHour('start_datetime')).order_by('start_datetime'),
+            [
+                (start_datetime, truncate_to(start_datetime, 'hour')),
+                (end_datetime, truncate_to(end_datetime, 'hour')),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime=TruncHour('start_datetime')).count(), 1)
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
+            list(DTModel.objects.annotate(truncated=TruncHour('start_date')))
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
+            list(DTModel.objects.annotate(truncated=TruncHour('start_date', output_field=DateField())))
+
+    def test_trunc_minute_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'minute')
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncMinute('start_datetime')).order_by('start_datetime'),
+            [
+                (start_datetime, truncate_to(start_datetime, 'minute')),
+                (end_datetime, truncate_to(end_datetime, 'minute')),
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+        self.assertEqual(DTModel.objects.filter(start_datetime=TruncMinute('start_datetime')).count(), 1)
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
+            list(DTModel.objects.annotate(truncated=TruncMinute('start_date')))
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
+            list(DTModel.objects.annotate(truncated=TruncMinute('start_date', output_field=DateField())))
+
+    def test_trunc_second_func(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = truncate_to(microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123)), 'second')
+        if settings.USE_TZ:
+            start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+            end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+        self.assertQuerysetEqual(
+            DTModel.objects.annotate(extracted=TruncSecond('start_datetime')).order_by('start_datetime'),
+            [
+                (start_datetime, truncate_to(start_datetime, 'second')),
+                (end_datetime, truncate_to(end_datetime, 'second'))
+            ],
+            lambda m: (m.start_datetime, m.extracted)
+        )
+
+        result = 1 if connection.features.supports_microsecond_precision else 2
+        self.assertEqual(DTModel.objects.filter(start_datetime=TruncSecond('start_datetime')).count(), result)
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
+            list(DTModel.objects.annotate(truncated=TruncSecond('start_date')))
+
+        with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"):
+            list(DTModel.objects.annotate(truncated=TruncSecond('start_date', output_field=DateField())))
+
+
+@skipIf(pytz is None, "this test requires pytz")
+@override_settings(USE_TZ=True, TIME_ZONE='UTC')
+class DateFunctionWithTimeZoneTests(DateFunctionTests):
+
+    def test_extract_func_with_timezone(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 23, 30, 1, 321))
+        end_datetime = microsecond_support(datetime(2015, 6, 16, 13, 11, 27, 123))
+        start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+        end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        melb = pytz.timezone('Australia/Melbourne')
+
+        qs = DTModel.objects.annotate(
+            day=Extract('start_datetime', 'day'),
+            day_melb=Extract('start_datetime', 'day', tzinfo=melb),
+            weekday=ExtractWeekDay('start_datetime'),
+            weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
+            hour=ExtractHour('start_datetime'),
+            hour_melb=ExtractHour('start_datetime', tzinfo=melb),
+        ).order_by('start_datetime')
+
+        utc_model = qs.get()
+        self.assertEqual(utc_model.day, 15)
+        self.assertEqual(utc_model.day_melb, 16)
+        self.assertEqual(utc_model.weekday, 2)
+        self.assertEqual(utc_model.weekday_melb, 3)
+        self.assertEqual(utc_model.hour, 23)
+        self.assertEqual(utc_model.hour_melb, 9)
+
+        with timezone.override(melb):
+            melb_model = qs.get()
+
+        self.assertEqual(melb_model.day, 16)
+        self.assertEqual(melb_model.day_melb, 16)
+        self.assertEqual(melb_model.weekday, 3)
+        self.assertEqual(melb_model.weekday_melb, 3)
+        self.assertEqual(melb_model.hour, 9)
+        self.assertEqual(melb_model.hour_melb, 9)
+
+    def test_extract_func_explicit_timezone_priority(self):
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 23, 30, 1, 321))
+        end_datetime = microsecond_support(datetime(2015, 6, 16, 13, 11, 27, 123))
+        start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+        end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        melb = pytz.timezone('Australia/Melbourne')
+
+        with timezone.override(melb):
+            model = DTModel.objects.annotate(
+                day_melb=Extract('start_datetime', 'day'),
+                day_utc=Extract('start_datetime', 'day', tzinfo=timezone.utc),
+            ).order_by('start_datetime').get()
+            self.assertEqual(model.day_melb, 16)
+            self.assertEqual(model.day_utc, 15)
+
+    def test_trunc_timezone_applied_before_truncation(self):
+        start_datetime = microsecond_support(datetime(2016, 1, 1, 1, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+        end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+
+        melb = pytz.timezone('Australia/Melbourne')
+        pacific = pytz.timezone('US/Pacific')
+
+        model = DTModel.objects.annotate(
+            melb_year=TruncYear('start_datetime', tzinfo=melb),
+            pacific_year=TruncYear('start_datetime', tzinfo=pacific),
+        ).order_by('start_datetime').get()
+
+        self.assertEqual(model.start_datetime, start_datetime)
+        self.assertEqual(model.melb_year, truncate_to(start_datetime, 'year', melb))
+        self.assertEqual(model.pacific_year, truncate_to(start_datetime, 'year', pacific))
+        self.assertEqual(model.start_datetime.year, 2016)
+        self.assertEqual(model.melb_year.year, 2016)
+        self.assertEqual(model.pacific_year.year, 2015)
+
+    def test_trunc_func_with_timezone(self):
+        """
+        If the truncated datetime transitions to a different offset (daylight
+        saving) then the returned value will have that new timezone/offset.
+        """
+        start_datetime = microsecond_support(datetime(2015, 6, 15, 14, 30, 50, 321))
+        end_datetime = microsecond_support(datetime(2016, 6, 15, 14, 10, 50, 123))
+        start_datetime = timezone.make_aware(start_datetime, is_dst=False)
+        end_datetime = timezone.make_aware(end_datetime, is_dst=False)
+        self.create_model(start_datetime, end_datetime)
+        self.create_model(end_datetime, start_datetime)
+
+        melb = pytz.timezone('Australia/Melbourne')
+
+        def test_datetime_kind(kind, tzinfo=melb):
+            self.assertQuerysetEqual(
+                DTModel.objects.annotate(
+                    truncated=Trunc('start_datetime', kind, output_field=DateTimeField(), tzinfo=melb)
+                ).order_by('start_datetime'),
+                [
+                    (start_datetime, truncate_to(start_datetime.astimezone(melb), kind, melb)),
+                    (end_datetime, truncate_to(end_datetime.astimezone(melb), kind, melb))
+                ],
+                lambda m: (m.start_datetime, m.truncated)
+            )
+
+        def test_date_kind(kind, tzinfo=melb):
+            self.assertQuerysetEqual(
+                DTModel.objects.annotate(
+                    truncated=Trunc('start_date', kind, output_field=DateField(), tzinfo=melb)
+                ).order_by('start_datetime'),
+                [
+                    (start_datetime, truncate_to(start_datetime.date(), kind)),
+                    (end_datetime, truncate_to(end_datetime.date(), kind))
+                ],
+                lambda m: (m.start_datetime, m.truncated)
+            )
+
+        test_date_kind('year')
+        test_date_kind('month')
+        test_date_kind('day')
+        test_datetime_kind('year')
+        test_datetime_kind('month')
+        test_datetime_kind('day')
+        test_datetime_kind('hour')
+        test_datetime_kind('minute')
+        test_datetime_kind('second')
+
+        qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField()))
+        self.assertEqual(qs.count(), 2)

+ 2 - 5
tests/expressions/tests.py

@@ -11,8 +11,8 @@ from django.db.models.aggregates import (
     Avg, Count, Max, Min, StdDev, Sum, Variance,
 )
 from django.db.models.expressions import (
-    Case, Col, Date, DateTime, ExpressionWrapper, F, Func, OrderBy, Random,
-    RawSQL, Ref, Value, When,
+    Case, Col, ExpressionWrapper, F, Func, OrderBy, Random, RawSQL, Ref, Value,
+    When,
 )
 from django.db.models.functions import (
     Coalesce, Concat, Length, Lower, Substr, Upper,
@@ -20,7 +20,6 @@ from django.db.models.functions import (
 from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
 from django.test.utils import Approximate
 from django.utils import six
-from django.utils.timezone import utc
 
 from .models import UUID, Company, Employee, Experiment, Number, Time
 
@@ -930,8 +929,6 @@ class ReprTests(TestCase):
             "<Case: CASE WHEN <Q: (AND: ('a', 1))> THEN Value(None), ELSE Value(None)>"
         )
         self.assertEqual(repr(Col('alias', 'field')), "Col(alias, field)")
-        self.assertEqual(repr(Date('published', 'exact')), "Date(published, exact)")
-        self.assertEqual(repr(DateTime('published', 'exact', utc)), "DateTime(published, exact, %s)" % utc)
         self.assertEqual(repr(F('published')), "F(published)")
         self.assertEqual(repr(F('cost') + F('tax')), "<CombinedExpression: F(cost) + F(tax)>")
         self.assertEqual(

+ 1 - 1
tests/queries/tests.py

@@ -1312,7 +1312,7 @@ class Queries3Tests(BaseQuerysetTest):
     def test_ticket8683(self):
         # An error should be raised when QuerySet.datetimes() is passed the
         # wrong type of field.
-        with self.assertRaisesMessage(AssertionError, "'name' isn't a DateTimeField."):
+        with self.assertRaisesMessage(AssertionError, "'name' isn't a DateField or DateTimeField."):
             Item.objects.datetimes('name', 'month')
 
     def test_ticket22023(self):