Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #17260 -- Added time zone aware aggregation and lookups.

Thanks Carl Meyer for the review.

Squashed commit of the following:

commit 4f290bd
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Wed Feb 13 21:21:30 2013 +0100

    Used '0:00' instead of 'UTC' which doesn't always exist in Oracle.

    Thanks Ian Kelly for the suggestion.

commit 01b6366
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Wed Feb 13 13:38:43 2013 +0100

    Made tzname a parameter of datetime_extract/trunc_sql.

    This is required to work around a bug in Oracle.

commit 924a144
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Wed Feb 13 14:47:44 2013 +0100

    Added support for parameters in SELECT clauses.

commit b4351d2
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Mon Feb 11 22:30:22 2013 +0100

    Documented backwards incompatibilities in the two previous commits.

commit 91ef847
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Mon Feb 11 09:42:31 2013 +0100

    Used QuerySet.datetimes for the admin's date_hierarchy.

commit 0d0de28
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Mon Feb 11 09:29:38 2013 +0100

    Used QuerySet.datetimes in date-based generic views.

commit 9c0859f
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Sun Feb 10 21:43:25 2013 +0100

    Implemented QuerySet.datetimes on Oracle.

commit 68ab511
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Sun Feb 10 21:43:14 2013 +0100

    Implemented QuerySet.datetimes on MySQL.

commit 22d5268
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Sun Feb 10 21:42:29 2013 +0100

    Implemented QuerySet.datetimes on SQLite.

commit f6800fd
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Sun Feb 10 21:43:03 2013 +0100

    Implemented QuerySet.datetimes on PostgreSQL.

commit 0c829c23f4cf4d6804cadcc93032dd4c26b8c65e
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Sun Feb 10 21:41:08 2013 +0100

    Added datetime-handling infrastructure in the ORM layers.

commit 104d82a7778cf3f0f5d03dfa53709c26df45daad
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Mon Feb 11 10:05:55 2013 +0100

    Updated null_queries tests to avoid clashing with the __second lookup.

commit c01bbb32358201b3ac8cb4291ef87b7612a2b8e6
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Sun Feb 10 23:07:41 2013 +0100

    Updated tests of .dates().

    Replaced .dates() by .datetimes() for DateTimeFields.
    Replaced dates with datetimes in the expected output for DateFields.

commit 50fb7a52462fecf0127b38e7f3df322aeb287c43
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Sun Feb 10 21:40:09 2013 +0100

    Updated and added tests for QuerySet.datetimes.

commit a8451a5004c437190e264667b1e6fb8acc3c1eeb
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Sun Feb 10 22:34:46 2013 +0100

    Documented the new time lookups and updated the date lookups.

commit 29413eab2bd1d5e004598900c0dadc0521bbf4d3
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Sun Feb 10 16:15:49 2013 +0100

    Documented QuerySet.datetimes and updated QuerySet.dates.
  • Loading branch information...
commit e74e207cce54802f897adcb42149440ee154821e 1 parent 91c26ea
Aymeric Augustin authored February 10, 2013

Showing 51 changed files with 1,035 additions and 294 deletions. Show diff stats Hide diff stats

  1. 14  django/contrib/admin/templatetags/admin_list.py
  2. 3  django/contrib/gis/db/backends/mysql/compiler.py
  3. 7  django/contrib/gis/db/backends/mysql/operations.py
  4. 3  django/contrib/gis/db/backends/oracle/compiler.py
  5. 4  django/contrib/gis/db/backends/oracle/operations.py
  6. 2  django/contrib/gis/db/backends/postgis/operations.py
  7. 2  django/contrib/gis/db/backends/spatialite/operations.py
  8. 2  django/contrib/gis/db/backends/util.py
  9. 12  django/contrib/gis/db/models/sql/aggregates.py
  10. 63  django/contrib/gis/db/models/sql/compiler.py
  11. 5  django/contrib/gis/db/models/sql/where.py
  12. 2  django/contrib/gis/tests/geoapp/test_regress.py
  13. 56  django/db/backends/__init__.py
  14. 47  django/db/backends/mysql/base.py
  15. 3  django/db/backends/mysql/compiler.py
  16. 62  django/db/backends/oracle/base.py
  17. 3  django/db/backends/oracle/compiler.py
  18. 25  django/db/backends/postgresql_psycopg2/operations.py
  19. 80  django/db/backends/sqlite3/base.py
  20. 23  django/db/models/fields/__init__.py
  21. 3  django/db/models/manager.py
  22. 51  django/db/models/query.py
  23. 2  django/db/models/query_utils.py
  24. 11  django/db/models/sql/aggregates.py
  25. 99  django/db/models/sql/compiler.py
  26. 3  django/db/models/sql/constants.py
  27. 23  django/db/models/sql/datastructures.py
  28. 4  django/db/models/sql/expressions.py
  29. 48  django/db/models/sql/subqueries.py
  30. 34  django/db/models/sql/where.py
  31. 9  django/views/generic/dates.py
  32. 157  docs/ref/models/querysets.txt
  33. 35  docs/releases/1.6.txt
  34. 8  tests/modeltests/aggregation/tests.py
  35. 18  tests/modeltests/basic/tests.py
  36. 56  tests/modeltests/many_to_one/tests.py
  37. 4  tests/modeltests/reserved_names/tests.py
  38. 128  tests/modeltests/timezones/tests.py
  39. 4  tests/regressiontests/aggregation_regress/tests.py
  40. 6  tests/regressiontests/backends/tests.py
  41. 2  tests/regressiontests/dates/models.py
  42. 38  tests/regressiontests/dates/tests.py
  43. 0  tests/regressiontests/datetimes/__init__.py
  44. 28  tests/regressiontests/datetimes/models.py
  45. 83  tests/regressiontests/datetimes/tests.py
  46. 5  tests/regressiontests/extra_regress/tests.py
  47. 15  tests/regressiontests/generic_views/dates.py
  48. 4  tests/regressiontests/model_inheritance_regress/tests.py
  49. 3  tests/regressiontests/null_queries/models.py
  50. 6  tests/regressiontests/null_queries/tests.py
  51. 24  tests/regressiontests/queries/tests.py
14  django/contrib/admin/templatetags/admin_list.py
@@ -292,6 +292,8 @@ def date_hierarchy(cl):
292 292
     """
293 293
     if cl.date_hierarchy:
294 294
         field_name = cl.date_hierarchy
  295
+        field = cl.opts.get_field_by_name(field_name)[0]
  296
+        dates_or_datetimes = 'datetimes' if isinstance(field, models.DateTimeField) else 'dates'
295 297
         year_field = '%s__year' % field_name
296 298
         month_field = '%s__month' % field_name
297 299
         day_field = '%s__day' % field_name
@@ -323,7 +325,8 @@ def date_hierarchy(cl):
323 325
                 'choices': [{'title': capfirst(formats.date_format(day, 'MONTH_DAY_FORMAT'))}]
324 326
             }
325 327
         elif year_lookup and month_lookup:
326  
-            days = cl.query_set.filter(**{year_field: year_lookup, month_field: month_lookup}).dates(field_name, 'day')
  328
+            days = cl.query_set.filter(**{year_field: year_lookup, month_field: month_lookup})
  329
+            days = getattr(days, dates_or_datetimes)(field_name, 'day')
327 330
             return {
328 331
                 'show': True,
329 332
                 'back': {
@@ -336,11 +339,12 @@ def date_hierarchy(cl):
336 339
                 } for day in days]
337 340
             }
338 341
         elif year_lookup:
339  
-            months = cl.query_set.filter(**{year_field: year_lookup}).dates(field_name, 'month')
  342
+            months = cl.query_set.filter(**{year_field: year_lookup})
  343
+            months = getattr(months, dates_or_datetimes)(field_name, 'month')
340 344
             return {
341  
-                'show' : True,
  345
+                'show': True,
342 346
                 'back': {
343  
-                    'link' : link({}),
  347
+                    'link': link({}),
344 348
                     'title': _('All dates')
345 349
                 },
346 350
                 'choices': [{
@@ -349,7 +353,7 @@ def date_hierarchy(cl):
349 353
                 } for month in months]
350 354
             }
351 355
         else:
352  
-            years = cl.query_set.dates(field_name, 'year')
  356
+            years = getattr(cl.query_set, dates_or_datetimes)(field_name, 'year')
353 357
             return {
354 358
                 'show': True,
355 359
                 'choices': [{
3  django/contrib/gis/db/backends/mysql/compiler.py
@@ -30,3 +30,6 @@ class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler):
30 30
 
31 31
 class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler):
32 32
     pass
  33
+
  34
+class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, GeoSQLCompiler):
  35
+    pass
7  django/contrib/gis/db/backends/mysql/operations.py
@@ -56,12 +56,13 @@ def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
56 56
 
57 57
         lookup_info = self.geometry_functions.get(lookup_type, False)
58 58
         if lookup_info:
59  
-            return "%s(%s, %s)" % (lookup_info, geo_col,
60  
-                                   self.get_geom_placeholder(value, field.srid))
  59
+            sql = "%s(%s, %s)" % (lookup_info, geo_col,
  60
+                                  self.get_geom_placeholder(value, field.srid))
  61
+            return sql, []
61 62
 
62 63
         # TODO: Is this really necessary? MySQL can't handle NULL geometries
63 64
         #  in its spatial indexes anyways.
64 65
         if lookup_type == 'isnull':
65  
-            return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
  66
+            return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), []
66 67
 
67 68
         raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
3  django/contrib/gis/db/backends/oracle/compiler.py
@@ -20,3 +20,6 @@ class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler):
20 20
 
21 21
 class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler):
22 22
     pass
  23
+
  24
+class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, GeoSQLCompiler):
  25
+    pass
4  django/contrib/gis/db/backends/oracle/operations.py
@@ -262,7 +262,7 @@ def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
262 262
                 return lookup_info.as_sql(geo_col, self.get_geom_placeholder(field, value))
263 263
         elif lookup_type == 'isnull':
264 264
             # Handling 'isnull' lookup type
265  
-            return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
  265
+            return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), []
266 266
 
267 267
         raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
268 268
 
@@ -288,7 +288,7 @@ def geometry_columns(self):
288 288
     def spatial_ref_sys(self):
289 289
         from django.contrib.gis.db.backends.oracle.models import SpatialRefSys
290 290
         return SpatialRefSys
291  
-    
  291
+
292 292
     def modify_insert_params(self, placeholders, params):
293 293
         """Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial
294 294
         backend due to #10888
2  django/contrib/gis/db/backends/postgis/operations.py
@@ -560,7 +560,7 @@ def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
560 560
 
561 561
         elif lookup_type == 'isnull':
562 562
             # Handling 'isnull' lookup type
563  
-            return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
  563
+            return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), []
564 564
 
565 565
         raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
566 566
 
2  django/contrib/gis/db/backends/spatialite/operations.py
@@ -358,7 +358,7 @@ def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
358 358
             return op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
359 359
         elif lookup_type == 'isnull':
360 360
             # Handling 'isnull' lookup type
361  
-            return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
  361
+            return "%s IS %sNULL" % (geo_col, ('' if value else 'NOT ')), []
362 362
 
363 363
         raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
364 364
 
2  django/contrib/gis/db/backends/util.py
@@ -16,7 +16,7 @@ def __init__(self, function='', operator='', result='', **kwargs):
16 16
         self.extra = kwargs
17 17
 
18 18
     def as_sql(self, geo_col, geometry='%s'):
19  
-        return self.sql_template % self.params(geo_col, geometry)
  19
+        return self.sql_template % self.params(geo_col, geometry), []
20 20
 
21 21
     def params(self, geo_col, geometry):
22 22
         params = {'function' : self.function,
12  django/contrib/gis/db/models/sql/aggregates.py
@@ -22,13 +22,15 @@ def __init__(self, col, source=None, is_summary=False, tolerance=0.05, **extra):
22 22
             raise ValueError('Geospatial aggregates only allowed on geometry fields.')
23 23
 
24 24
     def as_sql(self, qn, connection):
25  
-        "Return the aggregate, rendered as SQL."
  25
+        "Return the aggregate, rendered as SQL with parameters."
26 26
 
27 27
         if connection.ops.oracle:
28 28
             self.extra['tolerance'] = self.tolerance
29 29
 
  30
+        params = []
  31
+
30 32
         if hasattr(self.col, 'as_sql'):
31  
-            field_name = self.col.as_sql(qn, connection)
  33
+            field_name, params = self.col.as_sql(qn, connection)
32 34
         elif isinstance(self.col, (list, tuple)):
33 35
             field_name = '.'.join([qn(c) for c in self.col])
34 36
         else:
@@ -36,13 +38,13 @@ def as_sql(self, qn, connection):
36 38
 
37 39
         sql_template, sql_function = connection.ops.spatial_aggregate_sql(self)
38 40
 
39  
-        params = {
  41
+        substitutions = {
40 42
             'function': sql_function,
41 43
             'field': field_name
42 44
         }
43  
-        params.update(self.extra)
  45
+        substitutions.update(self.extra)
44 46
 
45  
-        return sql_template % params
  47
+        return sql_template % substitutions, params
46 48
 
47 49
 class Collect(GeoAggregate):
48 50
     pass
63  django/contrib/gis/db/models/sql/compiler.py
... ...
@@ -1,14 +1,16 @@
  1
+import datetime
1 2
 try:
2 3
     from itertools import zip_longest
3 4
 except ImportError:
4 5
     from itertools import izip_longest as zip_longest
5 6
 
6  
-from django.utils.six.moves import zip
7  
-
8  
-from django.db.backends.util import truncate_name, typecast_timestamp
  7
+from django.conf import settings
  8
+from django.db.backends.util import truncate_name, typecast_date, typecast_timestamp
9 9
 from django.db.models.sql import compiler
10 10
 from django.db.models.sql.constants import MULTI
11 11
 from django.utils import six
  12
+from django.utils.six.moves import zip
  13
+from django.utils import timezone
12 14
 
13 15
 SQLCompiler = compiler.SQLCompiler
14 16
 
@@ -31,6 +33,7 @@ def get_columns(self, with_aliases=False):
31 33
         qn2 = self.connection.ops.quote_name
32 34
         result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias))
33 35
                   for alias, col in six.iteritems(self.query.extra_select)]
  36
+        params = []
34 37
         aliases = set(self.query.extra_select.keys())
35 38
         if with_aliases:
36 39
             col_aliases = aliases.copy()
@@ -61,7 +64,9 @@ def get_columns(self, with_aliases=False):
61 64
                         aliases.add(r)
62 65
                         col_aliases.add(col[1])
63 66
                 else:
64  
-                    result.append(col.as_sql(qn, self.connection))
  67
+                    col_sql, col_params = col.as_sql(qn, self.connection)
  68
+                    result.append(col_sql)
  69
+                    params.extend(col_params)
65 70
 
66 71
                     if hasattr(col, 'alias'):
67 72
                         aliases.add(col.alias)
@@ -74,15 +79,13 @@ def get_columns(self, with_aliases=False):
74 79
             aliases.update(new_aliases)
75 80
 
76 81
         max_name_length = self.connection.ops.max_name_length()
77  
-        result.extend([
78  
-                '%s%s' % (
79  
-                    self.get_extra_select_format(alias) % aggregate.as_sql(qn, self.connection),
80  
-                    alias is not None
81  
-                        and ' AS %s' % qn(truncate_name(alias, max_name_length))
82  
-                        or ''
83  
-                    )
84  
-                for alias, aggregate in self.query.aggregate_select.items()
85  
-        ])
  82
+        for alias, aggregate in self.query.aggregate_select.items():
  83
+            agg_sql, agg_params = aggregate.as_sql(qn, self.connection)
  84
+            if alias is None:
  85
+                result.append(agg_sql)
  86
+            else:
  87
+                result.append('%s AS %s' % (agg_sql, qn(truncate_name(alias, max_name_length))))
  88
+            params.extend(agg_params)
86 89
 
87 90
         # This loop customized for GeoQuery.
88 91
         for (table, col), field in self.query.related_select_cols:
@@ -98,7 +101,7 @@ def get_columns(self, with_aliases=False):
98 101
                 col_aliases.add(col)
99 102
 
100 103
         self._select_aliases = aliases
101  
-        return result
  104
+        return result, params
102 105
 
103 106
     def get_default_columns(self, with_aliases=False, col_aliases=None,
104 107
             start_alias=None, opts=None, as_pairs=False, from_parent=None):
@@ -280,5 +283,35 @@ def results_iter(self):
280 283
                 if self.connection.ops.oracle:
281 284
                     date = self.resolve_columns(row, fields)[offset]
282 285
                 elif needs_string_cast:
283  
-                    date = typecast_timestamp(str(date))
  286
+                    date = typecast_date(str(date))
  287
+                if isinstance(date, datetime.datetime):
  288
+                    date = date.date()
284 289
                 yield date
  290
+
  291
+class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, GeoSQLCompiler):
  292
+    """
  293
+    This is overridden for GeoDjango to properly cast date columns, since
  294
+    `GeoQuery.resolve_columns` is used for spatial values.
  295
+    See #14648, #16757.
  296
+    """
  297
+    def results_iter(self):
  298
+        if self.connection.ops.oracle:
  299
+            from django.db.models.fields import DateTimeField
  300
+            fields = [DateTimeField()]
  301
+        else:
  302
+            needs_string_cast = self.connection.features.needs_datetime_string_cast
  303
+
  304
+        offset = len(self.query.extra_select)
  305
+        for rows in self.execute_sql(MULTI):
  306
+            for row in rows:
  307
+                datetime = row[offset]
  308
+                if self.connection.ops.oracle:
  309
+                    datetime = self.resolve_columns(row, fields)[offset]
  310
+                elif needs_string_cast:
  311
+                    datetime = typecast_timestamp(str(datetime))
  312
+                # Datetimes are artifically returned in UTC on databases that
  313
+                # don't support time zone. Restore the zone used in the query.
  314
+                if settings.USE_TZ:
  315
+                    datetime = datetime.replace(tzinfo=None)
  316
+                    datetime = timezone.make_aware(datetime, self.query.tzinfo)
  317
+                yield datetime
5  django/contrib/gis/db/models/sql/where.py
@@ -44,8 +44,9 @@ def make_atom(self, child, qn, connection):
44 44
         lvalue, lookup_type, value_annot, params_or_value = child
45 45
         if isinstance(lvalue, GeoConstraint):
46 46
             data, params = lvalue.process(lookup_type, params_or_value, connection)
47  
-            spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn)
48  
-            return spatial_sql, params
  47
+            spatial_sql, spatial_params = connection.ops.spatial_lookup_sql(
  48
+                    data, lookup_type, params_or_value, lvalue.field, qn)
  49
+            return spatial_sql, spatial_params + params
49 50
         else:
50 51
             return super(GeoWhereNode, self).make_atom(child, qn, connection)
51 52
 
2  django/contrib/gis/tests/geoapp/test_regress.py
@@ -49,7 +49,7 @@ def test_unicode_date(self):
49 49
         founded = datetime(1857, 5, 23)
50 50
         mansfield = PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)',
51 51
                                                     founded=founded)
52  
-        self.assertEqual(founded, PennsylvaniaCity.objects.dates('founded', 'day')[0])
  52
+        self.assertEqual(founded, PennsylvaniaCity.objects.datetimes('founded', 'day')[0])
53 53
         self.assertEqual(founded, PennsylvaniaCity.objects.aggregate(Min('founded'))['founded__min'])
54 54
 
55 55
     def test_empty_count(self):
56  django/db/backends/__init__.py
... ...
@@ -1,3 +1,5 @@
  1
+import datetime
  2
+
1 3
 from django.db.utils import DatabaseError
2 4
 
3 5
 try:
@@ -14,7 +16,7 @@
14 16
 from django.utils.functional import cached_property
15 17
 from django.utils.importlib import import_module
16 18
 from django.utils import six
17  
-from django.utils.timezone import is_aware
  19
+from django.utils import timezone
18 20
 
19 21
 
20 22
 class BaseDatabaseWrapper(object):
@@ -397,6 +399,9 @@ class BaseDatabaseFeatures(object):
397 399
     # Can datetimes with timezones be used?
398 400
     supports_timezones = True
399 401
 
  402
+    # Does the database have a copy of the zoneinfo database?
  403
+    has_zoneinfo_database = True
  404
+
400 405
     # When performing a GROUP BY, is an ORDER BY NULL required
401 406
     # to remove any ordering?
402 407
     requires_explicit_null_ordering_when_grouping = False
@@ -523,7 +528,7 @@ def date_interval_sql(self, sql, connector, timedelta):
523 528
     def date_trunc_sql(self, lookup_type, field_name):
524 529
         """
525 530
         Given a lookup_type of 'year', 'month' or 'day', returns the SQL that
526  
-        truncates the given date field field_name to a DATE object with only
  531
+        truncates the given date field field_name to a date object with only
527 532
         the given specificity.
528 533
         """
529 534
         raise NotImplementedError()
@@ -537,6 +542,23 @@ def datetime_cast_sql(self):
537 542
         """
538 543
         return "%s"
539 544
 
  545
+    def datetime_extract_sql(self, lookup_type, field_name, tzname):
  546
+        """
  547
+        Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or
  548
+        'second', returns the SQL that extracts a value from the given
  549
+        datetime field field_name, and a tuple of parameters.
  550
+        """
  551
+        raise NotImplementedError()
  552
+
  553
+    def datetime_trunc_sql(self, lookup_type, field_name, tzname):
  554
+        """
  555
+        Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or
  556
+        'second', returns the SQL that truncates the given datetime field
  557
+        field_name to a datetime object with only the given specificity, and
  558
+        a tuple of parameters.
  559
+        """
  560
+        raise NotImplementedError()
  561
+
540 562
     def deferrable_sql(self):
541 563
         """
542 564
         Returns the SQL necessary to make a constraint "initially deferred"
@@ -853,7 +875,7 @@ def value_to_db_time(self, value):
853 875
         """
854 876
         if value is None:
855 877
             return None
856  
-        if is_aware(value):
  878
+        if timezone.is_aware(value):
857 879
             raise ValueError("Django does not support timezone-aware times.")
858 880
         return six.text_type(value)
859 881
 
@@ -866,29 +888,33 @@ def value_to_db_decimal(self, value, max_digits, decimal_places):
866 888
             return None
867 889
         return util.format_number(value, max_digits, decimal_places)
868 890
 
869  
-    def year_lookup_bounds(self, value):
  891
+    def year_lookup_bounds_for_date_field(self, value):
870 892
         """
871 893
         Returns a two-elements list with the lower and upper bound to be used
872  
-        with a BETWEEN operator to query a field value using a year lookup
  894
+        with a BETWEEN operator to query a DateField value using a year
  895
+        lookup.
873 896
 
874 897
         `value` is an int, containing the looked-up year.
875 898
         """
876  
-        first = '%s-01-01 00:00:00'
877  
-        second = '%s-12-31 23:59:59.999999'
878  
-        return [first % value, second % value]
  899
+        first = datetime.date(value, 1, 1)
  900
+        second = datetime.date(value, 12, 31)
  901
+        return [first, second]
879 902
 
880  
-    def year_lookup_bounds_for_date_field(self, value):
  903
+    def year_lookup_bounds_for_datetime_field(self, value):
881 904
         """
882 905
         Returns a two-elements list with the lower and upper bound to be used
883  
-        with a BETWEEN operator to query a DateField value using a year lookup
  906
+        with a BETWEEN operator to query a DateTimeField value using a year
  907
+        lookup.
884 908
 
885 909
         `value` is an int, containing the looked-up year.
886  
-
887  
-        By default, it just calls `self.year_lookup_bounds`. Some backends need
888  
-        this hook because on their DB date fields can't be compared to values
889  
-        which include a time part.
890 910
         """
891  
-        return self.year_lookup_bounds(value)
  911
+        first = datetime.datetime(value, 1, 1)
  912
+        second = datetime.datetime(value, 12, 31, 23, 59, 59, 999999)
  913
+        if settings.USE_TZ:
  914
+            tz = timezone.get_current_timezone()
  915
+            first = timezone.make_aware(first, tz)
  916
+            second = timezone.make_aware(second, tz)
  917
+        return [first, second]
892 918
 
893 919
     def convert_values(self, value, field):
894 920
         """
47  django/db/backends/mysql/base.py
@@ -30,6 +30,7 @@
30 30
 from MySQLdb.converters import conversions, Thing2Literal
31 31
 from MySQLdb.constants import FIELD_TYPE, CLIENT
32 32
 
  33
+from django.conf import settings
33 34
 from django.db import utils
34 35
 from django.db.backends import *
35 36
 from django.db.backends.signals import connection_created
@@ -193,6 +194,12 @@ def can_introspect_foreign_keys(self):
193 194
         "Confirm support for introspected foreign keys"
194 195
         return self._mysql_storage_engine != 'MyISAM'
195 196
 
  197
+    @cached_property
  198
+    def has_zoneinfo_database(self):
  199
+        cursor = self.connection.cursor()
  200
+        cursor.execute("SELECT 1 FROM mysql.time_zone LIMIT 1")
  201
+        return cursor.fetchone() is not None
  202
+
196 203
 class DatabaseOperations(BaseDatabaseOperations):
197 204
     compiler_module = "django.db.backends.mysql.compiler"
198 205
 
@@ -218,6 +225,39 @@ def date_trunc_sql(self, lookup_type, field_name):
218 225
             sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str)
219 226
         return sql
220 227
 
  228
+    def datetime_extract_sql(self, lookup_type, field_name, tzname):
  229
+        if settings.USE_TZ:
  230
+            field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name
  231
+            params = [tzname]
  232
+        else:
  233
+            params = []
  234
+        # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
  235
+        if lookup_type == 'week_day':
  236
+            # DAYOFWEEK() returns an integer, 1-7, Sunday=1.
  237
+            # Note: WEEKDAY() returns 0-6, Monday=0.
  238
+            sql = "DAYOFWEEK(%s)" % field_name
  239
+        else:
  240
+            sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
  241
+        return sql, params
  242
+
  243
+    def datetime_trunc_sql(self, lookup_type, field_name, tzname):
  244
+        if settings.USE_TZ:
  245
+            field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name
  246
+            params = [tzname]
  247
+        else:
  248
+            params = []
  249
+        fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
  250
+        format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape.
  251
+        format_def = ('0000-', '01', '-01', ' 00:', '00', ':00')
  252
+        try:
  253
+            i = fields.index(lookup_type) + 1
  254
+        except ValueError:
  255
+            sql = field_name
  256
+        else:
  257
+            format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]])
  258
+            sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str)
  259
+        return sql, params
  260
+
221 261
     def date_interval_sql(self, sql, connector, timedelta):
222 262
         return "(%s %s INTERVAL '%d 0:0:%d:%d' DAY_MICROSECOND)" % (sql, connector,
223 263
                 timedelta.days, timedelta.seconds, timedelta.microseconds)
@@ -314,11 +354,10 @@ def value_to_db_time(self, value):
314 354
         # MySQL doesn't support microseconds
315 355
         return six.text_type(value.replace(microsecond=0))
316 356
 
317  
-    def year_lookup_bounds(self, value):
  357
+    def year_lookup_bounds_for_datetime_field(self, value):
318 358
         # Again, no microseconds
319  
-        first = '%s-01-01 00:00:00'
320  
-        second = '%s-12-31 23:59:59.99'
321  
-        return [first % value, second % value]
  359
+        first, second = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value)
  360
+        return [first.replace(microsecond=0), second.replace(microsecond=0)]
322 361
 
323 362
     def max_name_length(self):
324 363
         return 64
3  django/db/backends/mysql/compiler.py
@@ -31,3 +31,6 @@ class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler):
31 31
 
32 32
 class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler):
33 33
     pass
  34
+
  35
+class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, SQLCompiler):
  36
+    pass
62  django/db/backends/oracle/base.py
@@ -7,6 +7,7 @@
7 7
 
8 8
 import datetime
9 9
 import decimal
  10
+import re
10 11
 import sys
11 12
 import warnings
12 13
 
@@ -128,12 +129,12 @@ def cache_key_culling_sql(self):
128 129
         """
129 130
 
130 131
     def date_extract_sql(self, lookup_type, field_name):
131  
-        # http://download-east.oracle.com/docs/cd/B10501_01/server.920/a96540/functions42a.htm#1017163
132 132
         if lookup_type == 'week_day':
133 133
             # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday.
134 134
             return "TO_CHAR(%s, 'D')" % field_name
135 135
         else:
136  
-            return "EXTRACT(%s FROM %s)" % (lookup_type, field_name)
  136
+            # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm
  137
+            return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
137 138
 
138 139
     def date_interval_sql(self, sql, connector, timedelta):
139 140
         """
@@ -150,13 +151,58 @@ def date_interval_sql(self, sql, connector, timedelta):
150 151
                 timedelta.microseconds, day_precision)
151 152
 
152 153
     def date_trunc_sql(self, lookup_type, field_name):
153  
-        # Oracle uses TRUNC() for both dates and numbers.
154  
-        # http://download-east.oracle.com/docs/cd/B10501_01/server.920/a96540/functions155a.htm#SQLRF06151
155  
-        if lookup_type == 'day':
156  
-            sql = 'TRUNC(%s)' % field_name
  154
+        # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084
  155
+        if lookup_type in ('year', 'month'):
  156
+            return "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
157 157
         else:
158  
-            sql = "TRUNC(%s, '%s')" % (field_name, lookup_type)
159  
-        return sql
  158
+            return "TRUNC(%s)" % field_name
  159
+
  160
+    # Oracle crashes with "ORA-03113: end-of-file on communication channel"
  161
+    # if the time zone name is passed in parameter. Use interpolation instead.
  162
+    # https://groups.google.com/forum/#!msg/django-developers/zwQju7hbG78/9l934yelwfsJ
  163
+    # This regexp matches all time zone names from the zoneinfo database.
  164
+    _tzname_re = re.compile(r'^[\w/:+-]+$')
  165
+
  166
+    def _convert_field_to_tz(self, field_name, tzname):
  167
+        if not self._tzname_re.match(tzname):
  168
+            raise ValueError("Invalid time zone name: %s" % tzname)
  169
+        # Convert from UTC to local time, returning TIMESTAMP WITH TIME ZONE.
  170
+        result = "(FROM_TZ(%s, '0:00') AT TIME ZONE '%s')" % (field_name, tzname)
  171
+        # Extracting from a TIMESTAMP WITH TIME ZONE ignore the time zone.
  172
+        # Convert to a DATETIME, which is called DATE by Oracle. There's no
  173
+        # built-in function to do that; the easiest is to go through a string.
  174
+        result = "TO_CHAR(%s, 'YYYY-MM-DD HH24:MI:SS')" % result
  175
+        result = "TO_DATE(%s, 'YYYY-MM-DD HH24:MI:SS')" % result
  176
+        # Re-convert to a TIMESTAMP because EXTRACT only handles the date part
  177
+        # on DATE values, even though they actually store the time part.
  178
+        return "CAST(%s AS TIMESTAMP)" % result
  179
+
  180
+    def datetime_extract_sql(self, lookup_type, field_name, tzname):
  181
+        if settings.USE_TZ:
  182
+            field_name = self._convert_field_to_tz(field_name, tzname)
  183
+        if lookup_type == 'week_day':
  184
+            # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday.
  185
+            sql = "TO_CHAR(%s, 'D')" % field_name
  186
+        else:
  187
+            # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm
  188
+            sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name)
  189
+        return sql, []
  190
+
  191
+    def datetime_trunc_sql(self, lookup_type, field_name, tzname):
  192
+        if settings.USE_TZ:
  193
+            field_name = self._convert_field_to_tz(field_name, tzname)
  194
+        # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084
  195
+        if lookup_type in ('year', 'month'):
  196
+            sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper())
  197
+        elif lookup_type == 'day':
  198
+            sql = "TRUNC(%s)" % field_name
  199
+        elif lookup_type == 'hour':
  200
+            sql = "TRUNC(%s, 'HH24')" % field_name
  201
+        elif lookup_type == 'minute':
  202
+            sql = "TRUNC(%s, 'MI')" % field_name
  203
+        else:
  204
+            sql = field_name    # Cast to DATE removes sub-second precision.
  205
+        return sql, []
160 206
 
161 207
     def convert_values(self, value, field):
162 208
         if isinstance(value, Database.LOB):
3  django/db/backends/oracle/compiler.py
@@ -71,3 +71,6 @@ class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler):
71 71
 
72 72
 class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler):
73 73
     pass
  74
+
  75
+class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, SQLCompiler):
  76
+    pass
25  django/db/backends/postgresql_psycopg2/operations.py
... ...
@@ -1,5 +1,6 @@
1 1
 from __future__ import unicode_literals
2 2
 
  3
+from django.conf import settings
3 4
 from django.db.backends import BaseDatabaseOperations
4 5
 
5 6
 
@@ -36,6 +37,30 @@ def date_trunc_sql(self, lookup_type, field_name):
36 37
         # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
37 38
         return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name)
38 39
 
  40
+    def datetime_extract_sql(self, lookup_type, field_name, tzname):
  41
+        if settings.USE_TZ:
  42
+            field_name = "%s AT TIME ZONE %%s" % field_name
  43
+            params = [tzname]
  44
+        else:
  45
+            params = []
  46
+        # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
  47
+        if lookup_type == 'week_day':
  48
+            # For consistency across backends, we return Sunday=1, Saturday=7.
  49
+            sql = "EXTRACT('dow' FROM %s) + 1" % field_name
  50
+        else:
  51
+            sql = "EXTRACT('%s' FROM %s)" % (lookup_type, field_name)
  52
+        return sql, params
  53
+
  54
+    def datetime_trunc_sql(self, lookup_type, field_name, tzname):
  55
+        if settings.USE_TZ:
  56
+            field_name = "%s AT TIME ZONE %%s" % field_name
  57
+            params = [tzname]
  58
+        else:
  59
+            params = []
  60
+        # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
  61
+        sql = "DATE_TRUNC('%s', %s)" % (lookup_type, field_name)
  62
+        return sql, params
  63
+
39 64
     def deferrable_sql(self):
40 65
         return " DEFERRABLE INITIALLY DEFERRED"
41 66
 
80  django/db/backends/sqlite3/base.py
@@ -35,6 +35,10 @@
35 35
     from django.core.exceptions import ImproperlyConfigured
36 36
     raise ImproperlyConfigured("Error loading either pysqlite2 or sqlite3 modules (tried in that order): %s" % exc)
37 37
 
  38
+try:
  39
+    import pytz
  40
+except ImportError:
  41
+    pytz = None
38 42
 
39 43
 DatabaseError = Database.DatabaseError
40 44
 IntegrityError = Database.IntegrityError
@@ -117,6 +121,10 @@ def supports_stddev(self):
117 121
         cursor.execute('DROP TABLE STDDEV_TEST')
118 122
         return has_support
119 123
 
  124
+    @cached_property
  125
+    def has_zoneinfo_database(self):
  126
+        return pytz is not None
  127
+
120 128
 class DatabaseOperations(BaseDatabaseOperations):
121 129
     def bulk_batch_size(self, fields, objs):
122 130
         """
@@ -142,10 +150,10 @@ def check_aggregate_support(self, aggregate):
142 150
 
143 151
     def date_extract_sql(self, lookup_type, field_name):
144 152
         # sqlite doesn't support extract, so we fake it with the user-defined
145  
-        # function django_extract that's registered in connect(). Note that
  153
+        # function django_date_extract that's registered in connect(). Note that
146 154
         # single quotes are used because this is a string (and could otherwise
147 155
         # cause a collision with a field name).
148  
-        return "django_extract('%s', %s)" % (lookup_type.lower(), field_name)
  156
+        return "django_date_extract('%s', %s)" % (lookup_type.lower(), field_name)
149 157
 
150 158
     def date_interval_sql(self, sql, connector, timedelta):
151 159
         # It would be more straightforward if we could use the sqlite strftime
@@ -154,7 +162,7 @@ def date_interval_sql(self, sql, connector, timedelta):
154 162
         # values differently. So instead we register our own function that
155 163
         # formats the datetime combined with the delta in a manner suitable
156 164
         # for comparisons.
157  
-        return  'django_format_dtdelta(%s, "%s", "%d", "%d", "%d")' % (sql,
  165
+        return 'django_format_dtdelta(%s, "%s", "%d", "%d", "%d")' % (sql,
158 166
             connector, timedelta.days, timedelta.seconds, timedelta.microseconds)
159 167
 
160 168
     def date_trunc_sql(self, lookup_type, field_name):
@@ -164,6 +172,26 @@ def date_trunc_sql(self, lookup_type, field_name):
164 172
         # cause a collision with a field name).
165 173
         return "django_date_trunc('%s', %s)" % (lookup_type.lower(), field_name)
166 174
 
  175
+    def datetime_extract_sql(self, lookup_type, field_name, tzname):
  176
+        # Same comment as in date_extract_sql.
  177
+        if settings.USE_TZ:
  178
+            if pytz is None:
  179
+                from django.core.exceptions import ImproperlyConfigured
  180
+                raise ImproperlyConfigured("This query requires pytz, "
  181
+                                           "but it isn't installed.")
  182
+        return "django_datetime_extract('%s', %s, %%s)" % (
  183
+            lookup_type.lower(), field_name), [tzname]
  184
+
  185
+    def datetime_trunc_sql(self, lookup_type, field_name, tzname):
  186
+        # Same comment as in date_trunc_sql.
  187
+        if settings.USE_TZ:
  188
+            if pytz is None:
  189
+                from django.core.exceptions import ImproperlyConfigured
  190
+                raise ImproperlyConfigured("This query requires pytz, "
  191
+                                           "but it isn't installed.")
  192
+        return "django_datetime_trunc('%s', %s, %%s)" % (
  193
+            lookup_type.lower(), field_name), [tzname]
  194
+
167 195
     def drop_foreignkey_sql(self):
168 196
         return ""
169 197
 
@@ -214,11 +242,6 @@ def value_to_db_time(self, value):
214 242
 
215 243
         return six.text_type(value)
216 244
 
217  
-    def year_lookup_bounds(self, value):
218  
-        first = '%s-01-01'
219  
-        second = '%s-12-31 23:59:59.999999'
220  
-        return [first % value, second % value]
221  
-
222 245
     def convert_values(self, value, field):
223 246
         """SQLite returns floats when it should be returning decimals,
224 247
         and gets dates and datetimes wrong.
@@ -310,9 +333,10 @@ def get_connection_params(self):
310 333
 
311 334
     def get_new_connection(self, conn_params):
312 335
         conn = Database.connect(**conn_params)
313  
-        # Register extract, date_trunc, and regexp functions.
314  
-        conn.create_function("django_extract", 2, _sqlite_extract)
  336
+        conn.create_function("django_date_extract", 2, _sqlite_date_extract)
315 337
         conn.create_function("django_date_trunc", 2, _sqlite_date_trunc)
  338
+        conn.create_function("django_datetime_extract", 3, _sqlite_datetime_extract)
  339
+        conn.create_function("django_datetime_trunc", 3, _sqlite_datetime_trunc)
316 340
         conn.create_function("regexp", 2, _sqlite_regexp)
317 341
         conn.create_function("django_format_dtdelta", 5, _sqlite_format_dtdelta)
318 342
         return conn
@@ -402,7 +426,7 @@ def executemany(self, query, param_list):
402 426
     def convert_query(self, query):
403 427
         return FORMAT_QMARK_REGEX.sub('?', query).replace('%%','%')
404 428
 
405  
-def _sqlite_extract(lookup_type, dt):
  429
+def _sqlite_date_extract(lookup_type, dt):
406 430
     if dt is None:
407 431
         return None
408 432
     try:
@@ -420,11 +444,45 @@ def _sqlite_date_trunc(lookup_type, dt):
420 444
     except (ValueError, TypeError):
421 445
         return None
422 446
     if lookup_type == 'year':
  447
+        return "%i-01-01" % dt.year
  448
+    elif lookup_type == 'month':
  449
+        return "%i-%02i-01" % (dt.year, dt.month)
  450
+    elif lookup_type == 'day':
  451
+        return "%i-%02i-%02i" % (dt.year, dt.month, dt.day)
  452
+
  453
+def _sqlite_datetime_extract(lookup_type, dt, tzname):
  454
+    if dt is None:
  455
+        return None
  456
+    try:
  457
+        dt = util.typecast_timestamp(dt)
  458
+    except (ValueError, TypeError):
  459
+        return None
  460
+    if tzname is not None:
  461
+        dt = timezone.localtime(dt, pytz.timezone(tzname))
  462
+    if lookup_type == 'week_day':
  463
+        return (dt.isoweekday() % 7) + 1
  464
+    else:
  465
+        return getattr(dt, lookup_type)
  466
+
  467
+def _sqlite_datetime_trunc(lookup_type, dt, tzname):
  468
+    try:
  469
+        dt = util.typecast_timestamp(dt)
  470
+    except (ValueError, TypeError):
  471
+        return None
  472
+    if tzname is not None:
  473
+        dt = timezone.localtime(dt, pytz.timezone(tzname))
  474
+    if lookup_type == 'year':
423 475
         return "%i-01-01 00:00:00" % dt.year
424 476
     elif lookup_type == 'month':
425 477
         return "%i-%02i-01 00:00:00" % (dt.year, dt.month)
426 478
     elif lookup_type == 'day':
427 479
         return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day)
  480
+    elif lookup_type == 'hour':
  481
+        return "%i-%02i-%02i %02i:00:00" % (dt.year, dt.month, dt.day, dt.hour)
  482
+    elif lookup_type == 'minute':
  483
+        return "%i-%02i-%02i %02i:%02i:00" % (dt.year, dt.month, dt.day, dt.hour, dt.minute)
  484
+    elif lookup_type == 'second':
  485
+        return "%i-%02i-%02i %02i:%02i:%02i" % (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
428 486
 
429 487
 def _sqlite_format_dtdelta(dt, conn, days, secs, usecs):
430 488
     try:
23  django/db/models/fields/__init__.py
@@ -312,9 +312,10 @@ def get_prep_lookup(self, lookup_type, value):
312 312
             return value._prepare()
313 313
 
314 314
         if lookup_type in (
315  
-                'regex', 'iregex', 'month', 'day', 'week_day', 'search',
316  
-                'contains', 'icontains', 'iexact', 'startswith', 'istartswith',
317  
-                'endswith', 'iendswith', 'isnull'
  315
+                'iexact', 'contains', 'icontains',
  316
+                'startswith', 'istartswith', 'endswith', 'iendswith',
  317
+                'month', 'day', 'week_day', 'hour', 'minute', 'second',
  318
+                'isnull', 'search', 'regex', 'iregex',
318 319
             ):
319 320
             return value
320 321
         elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'):
@@ -350,8 +351,8 @@ def get_db_prep_lookup(self, lookup_type, value, connection,
350 351
                 sql, params = value._as_sql(connection=connection)
351 352
             return QueryWrapper(('(%s)' % sql), params)
352 353
 
353  
-        if lookup_type in ('regex', 'iregex', 'month', 'day', 'week_day',
354  
-                           'search'):
  354
+        if lookup_type in ('month', 'day', 'week_day', 'hour', 'minute',
  355
+                           'second', 'search', 'regex', 'iregex'):
355 356
             return [value]
356 357
         elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'):
357 358
             return [self.get_db_prep_value(value, connection=connection,
@@ -370,10 +371,12 @@ def get_db_prep_lookup(self, lookup_type, value, connection,
370 371
         elif lookup_type == 'isnull':
371 372
             return []
372 373
         elif lookup_type == 'year':
373  
-            if self.get_internal_type() == 'DateField':
  374
+            if isinstance(self, DateTimeField):
  375
+                return connection.ops.year_lookup_bounds_for_datetime_field(value)
  376
+            elif isinstance(self, DateField):
374 377
                 return connection.ops.year_lookup_bounds_for_date_field(value)
375 378
             else:
376  
-                return connection.ops.year_lookup_bounds(value)
  379
+                return [value]          # this isn't supposed to happen
377 380
 
378 381
     def has_default(self):
379 382
         """
@@ -722,9 +725,9 @@ def contribute_to_class(self, cls, name):
722 725
                       is_next=False))
723 726
 
724 727
     def get_prep_lookup(self, lookup_type, value):
725  
-        # For "__month", "__day", and "__week_day" lookups, convert the value
726  
-        # to an int so the database backend always sees a consistent type.
727  
-        if lookup_type in ('month', 'day', 'week_day'):
  728
+        # For dates lookups, convert the value to an int
  729
+        # so the database backend always sees a consistent type.
  730
+        if lookup_type in ('month', 'day', 'week_day', 'hour', 'minute', 'second'):
728 731
             return int(value)
729 732
         return super(DateField, self).get_prep_lookup(lookup_type, value)
730 733
 
3  django/db/models/manager.py
@@ -130,6 +130,9 @@ def count(self):
130 130
     def dates(self, *args, **kwargs):
131 131
         return self.get_query_set().dates(*args, **kwargs)
132 132
 
  133
+    def datetimes(self, *args, **kwargs):
  134
+        return self.get_query_set().datetimes(*args, **kwargs)
  135
+
133 136
     def distinct(self, *args, **kwargs):
134 137
         return self.get_query_set().distinct(*args, **kwargs)
135 138
 
51  django/db/models/query.py
@@ -7,6 +7,7 @@
7 7
 import sys
8 8
 import warnings
9 9
 
  10
+from django.conf import settings
10 11
 from django.core import exceptions
11 12
 from django.db import connections, router, transaction, IntegrityError
12 13
 from django.db.models.constants import LOOKUP_SEP
@@ -17,6 +18,7 @@
17 18
 from django.db.models import sql
18 19
 from django.utils.functional import partition
19 20
 from django.utils import six
  21
+from django.utils import timezone
20 22
 
21 23
 # Used to control how many objects are worked with at once in some cases (e.g.
22 24
 # when deleting objects).
@@ -629,16 +631,33 @@ def values_list(self, *fields, **kwargs):
629 631
 
630 632
     def dates(self, field_name, kind, order='ASC'):
631 633
         """
632  
-        Returns a list of datetime objects representing all available dates for
  634
+        Returns a list of date objects representing all available dates for
633 635
         the given field_name, scoped to 'kind'.
634 636
         """
635  
-        assert kind in ("month", "year", "day"), \
  637
+        assert kind in ("year", "month", "day"), \
636 638
                 "'kind' must be one of 'year', 'month' or 'day'."
637 639
         assert order in ('ASC', 'DESC'), \
638 640
                 "'order' must be either 'ASC' or 'DESC'."
639 641
         return self._clone(klass=DateQuerySet, setup=True,
640 642
                 _field_name=field_name, _kind=kind, _order=order)
641 643
 
  644
+    def datetimes(self, field_name, kind, order='ASC', tzinfo=None):
  645
+        """
  646
+        Returns a list of datetime objects representing all available
  647
+        datetimes for the given field_name, scoped to 'kind'.
  648
+        """
  649
+        assert kind in ("year", "month", "day", "hour", "minute", "second"), \
  650
+                "'kind' must be one of 'year', 'month', 'day', 'hour', 'minute' or 'second'."
  651
+        assert order in ('ASC', 'DESC'), \
  652
+                "'order' must be either 'ASC' or 'DESC'."
  653
+        if settings.USE_TZ:
  654
+            if tzinfo is None:
  655
+                tzinfo = timezone.get_current_timezone()
  656
+        else:
  657
+            tzinfo = None
  658
+        return self._clone(klass=DateTimeQuerySet, setup=True,
  659
+                _field_name=field_name, _kind=kind, _order=order, _tzinfo=tzinfo)
  660
+
642 661
     def none(self):
643 662
         """
644 663
         Returns an empty QuerySet.
@@ -1187,7 +1206,7 @@ def _setup_query(self):
1187 1206
         self.query.clear_deferred_loading()
1188 1207
         self.query = self.query.clone(klass=sql.DateQuery, setup=True)
1189 1208
         self.query.select = []
1190  
-        self.query.add_date_select(self._field_name, self._kind, self._order)
  1209
+        self.query.add_select(self._field_name, self._kind, self._order)
1191 1210
 
1192 1211
     def _clone(self, klass=None, setup=False, **kwargs):
1193 1212
         c = super(DateQuerySet, self)._clone(klass, False, **kwargs)
@@ -1198,6 +1217,32 @@ def _clone(self, klass=None, setup=False, **kwargs):
1198 1217
         return c
1199 1218
 
1200 1219
 
  1220
+class DateTimeQuerySet(QuerySet):
  1221
+    def iterator(self):
  1222
+        return self.query.get_compiler(self.db).results_iter()
  1223
+
  1224
+    def _setup_query(self):
  1225
+        """
  1226
+        Sets up any special features of the query attribute.
  1227
+
  1228
+        Called by the _clone() method after initializing the rest of the
  1229
+        instance.
  1230
+        """
  1231
+        self.query.clear_deferred_loading()
  1232
+        self.query = self.query.clone(klass=sql.DateTimeQuery, setup=True, tzinfo=self._tzinfo)
  1233
+        self.query.select = []
  1234
+        self.query.add_select(self._field_name, self._kind, self._order)
  1235
+
  1236
+    def _clone(self, klass=None, setup=False, **kwargs):
  1237
+        c = super(DateTimeQuerySet, self)._clone(klass, False, **kwargs)
  1238
+        c._field_name = self._field_name
  1239
+        c._kind = self._kind
  1240
+        c._tzinfo = self._tzinfo
  1241
+        if setup and hasattr(c, '_setup_query'):
  1242
+            c._setup_query()
  1243
+        return c
  1244
+
  1245
+
1201 1246
 def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
1202 1247
                    only_load=None, from_parent=None):
1203 1248
     """
2  django/db/models/query_utils.py
@@ -25,7 +25,7 @@ class QueryWrapper(object):
25 25
     parameters. Can be used to pass opaque data to a where-clause, for example.
26 26
     """
27 27
     def __init__(self, sql, params):
28  
-        self.data = sql, params
  28
+        self.data = sql, list(params)
29 29
 
30 30
     def as_sql(self, qn=None, connection=None):
31 31
         return self.data
11  django/db/models/sql/aggregates.py
@@ -73,22 +73,23 @@ def relabel_aliases(self, change_map):
73 73
             self.col = (change_map.get(self.col[0], self.col[0]), self.col[1])
74 74
 
75 75
     def as_sql(self, qn, connection):
76  
-        "Return the aggregate, rendered as SQL."
  76
+        "Return the aggregate, rendered as SQL with parameters."
  77
+        params = []
77 78
 
78 79
         if hasattr(self.col, 'as_sql'):
79  
-            field_name = self.col.as_sql(qn, connection)
  80
+            field_name, params = self.col.as_sql(qn, connection)
80 81
         elif isinstance(self.col, (list, tuple)):
81 82
             field_name = '.'.join([qn(c) for c in self.col])
82 83
         else:
83 84
             field_name = self.col
84 85
 
85  
-        params = {
  86
+        substitutions = {
86 87
             'function': self.sql_function,
87 88
             'field': field_name
88 89
         }
89  
-        params.update(self.extra)
  90
+        substitutions.update(self.extra)
90 91
 
91  
-        return self.sql_template % params
  92
+        return self.sql_template % substitutions, params
92 93
 
93 94
 
94 95
 class Avg(Aggregate):
99  django/db/models/sql/compiler.py
... ...
@@ -1,5 +1,6 @@
1  
-from django.utils.six.moves import zip
  1
+import datetime
2 2
 
  3
+from django.conf import settings
3 4
 from django.core.exceptions import FieldError
4 5
 from django.db import transaction
5 6
 from django.db.backends.util import truncate_name
@@ -12,6 +13,8 @@
12 13
 from django.db.models.sql.query import get_order_dir, Query
13 14
 from django.db.utils import DatabaseError
14 15
 from django.utils import six
  16
+from django.utils.six.moves import zip
  17
+from django.utils import timezone
15 18
 
16 19
 
17 20
 class SQLCompiler(object):
@@ -71,7 +74,7 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
71 74
         # as the pre_sql_setup will modify query state in a way that forbids
72 75
         # another run of it.
73 76
         self.refcounts_before = self.query.alias_refcount.copy()
74  
-        out_cols = self.get_columns(with_col_aliases)
  77
+        out_cols, s_params = self.get_columns(with_col_aliases)
75 78
         ordering, ordering_group_by = self.get_ordering()
76 79
 
77 80
         distinct_fields = self.get_distinct()
@@ -94,6 +97,7 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
94 97
             result.append(self.connection.ops.distinct_sql(distinct_fields))
95 98
 
96 99
         result.append(', '.join(out_cols + self.query.ordering_aliases))
  100
+        params.extend(s_params)
97 101
 
98 102
         result.append('FROM')
99 103
         result.extend(from_)
@@ -161,9 +165,10 @@ def as_nested_sql(self):
161 165
 
162 166
     def get_columns(self, with_aliases=False):
163 167
         """
164  
-        Returns the list of columns to use in the select statement. If no
165  
-        columns have been specified, returns all columns relating to fields in
166  
-        the model.
  168
+        Returns the list of columns to use in the select statement, as well as
  169
+        a list any extra parameters that need to be included. If no columns
  170
+        have been specified, returns all columns relating to fields in the
  171
+        model.
167 172
 
168 173
         If 'with_aliases' is true, any column names that are duplicated
169 174
         (without the table names) are given unique aliases. This is needed in
@@ -172,6 +177,7 @@ def get_columns(self, with_aliases=False):
172 177
         qn = self.quote_name_unless_alias
173 178
         qn2 = self.connection.ops.quote_name
174 179
         result = ['(%s) AS %s' % (col[0], qn2(alias)) for alias, col in six.iteritems(self.query.extra_select)]
  180
+        params = []
175 181
         aliases = set(self.query.extra_select.keys())
176 182
         if with_aliases:
177 183
             col_aliases = aliases.copy()
@@ -201,7 +207,9 @@ def get_columns(self, with_aliases=False):
201 207
                         aliases.add(r)
202 208
                         col_aliases.add(col[1])
203 209
                 else:
204  
-                    result.append(col.as_sql(qn, self.connection))
  210
+                    col_sql, col_params = col.as_sql(qn, self.connection)
  211
+                    result.append(col_sql)
  212
+                    params.extend(col_params)
205 213
 
206 214
                     if hasattr(col, 'alias'):
207 215
                         aliases.add(col.alias)
@@ -214,15 +222,13 @@ def get_columns(self, with_aliases=False):
214 222
             aliases.update(new_aliases)
215 223
 
216 224
         max_name_length = self.connection.ops.max_name_length()
217  
-        result.extend([
218  
-            '%s%s' % (
219  
-                aggregate.as_sql(qn, self.connection),