diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 40b68be32f..03b62f6413 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -542,25 +542,20 @@ class BaseDatabaseOperations(object): """ return "%s" - def datetime_extract_sql(self, lookup_type, field_name): + def datetime_extract_sql(self, lookup_type, field_name, tzname): """ Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or 'second', returns the SQL that extracts a value from the given - datetime field field_name. - - When time zone support is enabled, the SQL should include a '%s' - placeholder for the time zone's name. + datetime field field_name, and a tuple of parameters. """ raise NotImplementedError() - def datetime_trunc_sql(self, lookup_type, field_name): + def datetime_trunc_sql(self, lookup_type, field_name, tzname): """ Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or 'second', returns the SQL that truncates the given datetime field - field_name to a datetime object with only the given specificity. - - When time zone support is enabled, the SQL should include a '%s' - placeholder for the time zone's name. + field_name to a datetime object with only the given specificity, and + a tuple of parameters. """ raise NotImplementedError() diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 9a607bd829..9de2a4d62d 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -225,20 +225,27 @@ class DatabaseOperations(BaseDatabaseOperations): sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) return sql - def datetime_extract_sql(self, lookup_type, field_name): + def datetime_extract_sql(self, lookup_type, field_name, tzname): if settings.USE_TZ: field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name + params = [tzname] + else: + params = [] # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html if lookup_type == 'week_day': # DAYOFWEEK() returns an integer, 1-7, Sunday=1. # Note: WEEKDAY() returns 0-6, Monday=0. - return "DAYOFWEEK(%s)" % field_name + sql = "DAYOFWEEK(%s)" % field_name else: - return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + return sql, params - def datetime_trunc_sql(self, lookup_type, field_name): + def datetime_trunc_sql(self, lookup_type, field_name, tzname): if settings.USE_TZ: field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name + params = [tzname] + else: + params = [] fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') @@ -249,7 +256,7 @@ class DatabaseOperations(BaseDatabaseOperations): else: format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) - return sql + return sql, params def date_interval_sql(self, sql, connector, timedelta): return "(%s %s INTERVAL '%d 0:0:%d:%d' DAY_MICROSECOND)" % (sql, connector, diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index ae123c183f..4762d40bbb 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals import datetime import decimal +import re import sys import warnings @@ -131,8 +132,9 @@ WHEN (new.%(col_name)s IS NULL) if lookup_type == 'week_day': # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. return "TO_CHAR(%s, 'D')" % field_name - # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm - return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + else: + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm + return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) def date_interval_sql(self, sql, connector, timedelta): """ @@ -155,29 +157,52 @@ WHEN (new.%(col_name)s IS NULL) else: return "TRUNC(%s)" % field_name - def datetime_extract_sql(self, lookup_type, field_name): + # Oracle crashes with "ORA-03113: end-of-file on communication channel" + # if the time zone name is passed in parameter. Use interpolation instead. + # https://groups.google.com/forum/#!msg/django-developers/zwQju7hbG78/9l934yelwfsJ + # This regexp matches all time zone names from the zoneinfo database. + _tzname_re = re.compile(r'^[\w/+-]+$') + + def _convert_field_to_tz(self, field_name, tzname): + if not self._tzname_re.match(tzname): + raise ValueError("Invalid time zone name: %s" % tzname) + # Convert from UTC to local time, returning TIMESTAMP WITH TIME ZONE. + result = "(FROM_TZ(%s, 'UTC') AT TIME ZONE '%s')" % (field_name, tzname) + # Extracting from a TIMESTAMP WITH TIME ZONE ignore the time zone. + # Convert to a DATETIME, which is called DATE by Oracle. There's no + # built-in function to do that; the easiest is to go through a string. + result = "TO_CHAR(%s, 'YYYY-MM-DD HH24:MI:SS')" % result + result = "TO_DATE(%s, 'YYYY-MM-DD HH24:MI:SS')" % result + # Re-convert to a TIMESTAMP because EXTRACT only handles the date part + # on DATE values, even though they actually store the time part. + return "CAST(%s AS TIMESTAMP)" % result + + def datetime_extract_sql(self, lookup_type, field_name, tzname): if settings.USE_TZ: - field_name = "CAST((FROM_TZ(%s, 'UTC') AT TIME ZONE (%%s)) AS DATE)" % field_name + field_name = self._convert_field_to_tz(field_name, tzname) if lookup_type == 'week_day': # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. - return "TO_CHAR(%s, 'D')" % field_name - # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm - return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + sql = "TO_CHAR(%s, 'D')" % field_name + else: + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm + sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + return sql, [] - def datetime_trunc_sql(self, lookup_type, field_name): + def datetime_trunc_sql(self, lookup_type, field_name, tzname): if settings.USE_TZ: - field_name = "CAST((FROM_TZ(%s, 'UTC') AT TIME ZONE (%%s)) AS DATE)" % field_name + field_name = self._convert_field_to_tz(field_name, tzname) # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084 if lookup_type in ('year', 'month'): - return "TRUNC(%s, '%s')" % (field_name, lookup_type.upper()) + sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper()) elif lookup_type == 'day': - return "TRUNC(%s)" % field_name + sql = "TRUNC(%s)" % field_name elif lookup_type == 'hour': - return "TRUNC(%s, 'HH24')" % field_name + sql = "TRUNC(%s, 'HH24')" % field_name elif lookup_type == 'minute': - return "TRUNC(%s, 'MI')" % field_name + sql = "TRUNC(%s, 'MI')" % field_name else: - return field_name + sql = field_name # Cast to DATE removes sub-second precision. + return sql, [] def convert_values(self, value, field): if isinstance(value, Database.LOB): diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index 64efba0dbe..8e87ed539f 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -37,21 +37,29 @@ class DatabaseOperations(BaseDatabaseOperations): # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) - def datetime_extract_sql(self, lookup_type, field_name): + def datetime_extract_sql(self, lookup_type, field_name, tzname): if settings.USE_TZ: field_name = "%s AT TIME ZONE %%s" % field_name + params = [tzname] + else: + params = [] # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT if lookup_type == 'week_day': # For consistency across backends, we return Sunday=1, Saturday=7. - return "EXTRACT('dow' FROM %s) + 1" % field_name + sql = "EXTRACT('dow' FROM %s) + 1" % field_name else: - return "EXTRACT('%s' FROM %s)" % (lookup_type, field_name) + sql = "EXTRACT('%s' FROM %s)" % (lookup_type, field_name) + return sql, params - def datetime_trunc_sql(self, lookup_type, field_name): + def datetime_trunc_sql(self, lookup_type, field_name, tzname): if settings.USE_TZ: field_name = "%s AT TIME ZONE %%s" % field_name + params = [tzname] + else: + params = [] # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC - return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) + sql = "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) + return sql, params def deferrable_sql(self): return " DEFERRABLE INITIALLY DEFERRED" diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 939c541551..3b4ff4c5dd 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -172,31 +172,25 @@ class DatabaseOperations(BaseDatabaseOperations): # cause a collision with a field name). return "django_date_trunc('%s', %s)" % (lookup_type.lower(), field_name) - def datetime_extract_sql(self, lookup_type, field_name): + def datetime_extract_sql(self, lookup_type, field_name, tzname): # Same comment as in date_extract_sql. if settings.USE_TZ: if pytz is None: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("This query requires pytz, " "but it isn't installed.") - return "django_datetime_extract('%s', %s, %%s)" % ( - lookup_type.lower(), field_name) - else: - return "django_datetime_extract('%s', %s, NULL)" % ( - lookup_type.lower(), field_name) + return "django_datetime_extract('%s', %s, %%s)" % ( + lookup_type.lower(), field_name), [tzname] - def datetime_trunc_sql(self, lookup_type, field_name): + def datetime_trunc_sql(self, lookup_type, field_name, tzname): # Same comment as in date_trunc_sql. if settings.USE_TZ: if pytz is None: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("This query requires pytz, " "but it isn't installed.") - return "django_datetime_trunc('%s', %s, %%s)" % ( - lookup_type.lower(), field_name) - else: - return "django_datetime_trunc('%s', %s, NULL)" % ( - lookup_type.lower(), field_name) + return "django_datetime_trunc('%s', %s, %%s)" % ( + lookup_type.lower(), field_name), [tzname] def drop_foreignkey_sql(self): return "" diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 1f53810755..1b6654b670 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1034,13 +1034,6 @@ class SQLDateCompiler(SQLCompiler): yield date class SQLDateTimeCompiler(SQLCompiler): - def as_sql(self): - sql, params = super(SQLDateTimeCompiler, self).as_sql() - if settings.USE_TZ: - tzname = timezone._get_timezone_name(self.query.tzinfo) - params = (tzname,) + params - return sql, params - def results_iter(self): """ Returns an iterator over the results from executing this query. diff --git a/django/db/models/sql/datastructures.py b/django/db/models/sql/datastructures.py index 51d4410f59..612eb8f2d9 100644 --- a/django/db/models/sql/datastructures.py +++ b/django/db/models/sql/datastructures.py @@ -26,8 +26,6 @@ class Date(object): """ Add a date selection column. """ - trunc_func = 'date_trunc_sql' - def __init__(self, col, lookup_type): self.col = col self.lookup_type = lookup_type @@ -42,10 +40,25 @@ class Date(object): col = '%s.%s' % tuple([qn(c) for c in self.col]) else: col = self.col - return getattr(connection.ops, self.trunc_func)(self.lookup_type, col), [] + return connection.ops.date_trunc_sql(self.lookup_type, col), [] -class DateTime(Date): +class DateTime(object): """ Add a datetime selection column. """ - trunc_func = 'datetime_trunc_sql' + def __init__(self, col, lookup_type, tzname): + self.col = col + self.lookup_type = lookup_type + self.tzname = tzname + + def relabel_aliases(self, change_map): + c = self.col + if isinstance(c, (list, tuple)): + self.col = (change_map.get(c[0], c[0]), c[1]) + + def as_sql(self, qn, connection): + if isinstance(self.col, (list, tuple)): + col = '%s.%s' % tuple([qn(c) for c in self.col]) + else: + col = self.col + return connection.ops.datetime_trunc_sql(self.lookup_type, col, self.tzname) diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index c543bd4edc..6aac5c898c 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -14,6 +14,7 @@ from django.db.models.sql.where import AND, Constraint from django.utils.functional import Promise from django.utils.encoding import force_text from django.utils import six +from django.utils import timezone __all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'DateQuery', @@ -222,7 +223,6 @@ class DateQuery(Query): """ compiler = 'SQLDateCompiler' - select_type = Date def add_select(self, field_name, lookup_type, order='ASC'): """ @@ -241,7 +241,7 @@ class DateQuery(Query): field = result[0] self._check_field(field) # overridden in DateTimeQuery alias = result[3][-1] - select = self.select_type((alias, field.column), lookup_type) + select = self._get_select((alias, field.column), lookup_type) self.clear_select_clause() self.select = [SelectInfo(select, None)] self.distinct = True @@ -257,6 +257,9 @@ class DateQuery(Query): assert not isinstance(field, DateTimeField), \ "%r is a DateTimeField, not a DateField." % field.name + def _get_select(self, col, lookup_type): + return Date(col, lookup_type) + class DateTimeQuery(DateQuery): """ A DateTimeQuery is like a DateQuery but for a datetime field. If time zone @@ -265,12 +268,18 @@ class DateTimeQuery(DateQuery): """ compiler = 'SQLDateTimeCompiler' - select_type = DateTime def _check_field(self, field): assert isinstance(field, DateTimeField), \ "%r isn't a DateTimeField." % field.name + def _get_select(self, col, lookup_type): + if self.tzinfo is None: + tzname = None + else: + tzname = timezone._get_timezone_name(self.tzinfo) + return DateTime(col, lookup_type, tzname) + class AggregateQuery(Query): """ An AggregateQuery takes another query as a parameter to the FROM diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index a61bc0b929..ef856893b5 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -226,10 +226,9 @@ class WhereNode(tree.Node): return ('%s BETWEEN %%s and %%s' % field_sql, params) elif is_datetime_field and lookup_type in ('month', 'day', 'week_day', 'hour', 'minute', 'second'): - if settings.USE_TZ: - params = [timezone.get_current_timezone_name()] + params - return ('%s = %%s' - % connection.ops.datetime_extract_sql(lookup_type, field_sql), params) + tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None + sql, tz_params = connection.ops.datetime_extract_sql(lookup_type, field_sql, tzname) + return ('%s = %%s' % sql, tz_params + params) elif lookup_type in ('month', 'day', 'week_day'): return ('%s = %%s' % connection.ops.date_extract_sql(lookup_type, field_sql), params)