Made tzname a parameter of datetime_extract/trunc_sql.

This is required to work around a bug in Oracle.
This commit is contained in:
Aymeric Augustin 2013-02-13 13:38:43 +01:00
parent 924a144ef8
commit 01b6366f3c
9 changed files with 108 additions and 65 deletions

View File

@ -542,25 +542,20 @@ class BaseDatabaseOperations(object):
""" """
return "%s" 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 Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or
'second', returns the SQL that extracts a value from the given 'second', returns the SQL that extracts a value from the given
datetime field field_name. datetime field field_name, and a tuple of parameters.
When time zone support is enabled, the SQL should include a '%s'
placeholder for the time zone's name.
""" """
raise NotImplementedError() 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 Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or
'second', returns the SQL that truncates the given datetime field 'second', returns the SQL that truncates the given datetime field
field_name to a datetime object with only the given specificity. field_name to a datetime object with only the given specificity, and
a tuple of parameters.
When time zone support is enabled, the SQL should include a '%s'
placeholder for the time zone's name.
""" """
raise NotImplementedError() raise NotImplementedError()

View File

@ -225,20 +225,27 @@ class DatabaseOperations(BaseDatabaseOperations):
sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str)
return sql 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: if settings.USE_TZ:
field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name 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 # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
if lookup_type == 'week_day': if lookup_type == 'week_day':
# DAYOFWEEK() returns an integer, 1-7, Sunday=1. # DAYOFWEEK() returns an integer, 1-7, Sunday=1.
# Note: WEEKDAY() returns 0-6, Monday=0. # Note: WEEKDAY() returns 0-6, Monday=0.
return "DAYOFWEEK(%s)" % field_name sql = "DAYOFWEEK(%s)" % field_name
else: 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: if settings.USE_TZ:
field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name
params = [tzname]
else:
params = []
fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] fields = ['year', 'month', 'day', 'hour', 'minute', 'second']
format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape.
format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') format_def = ('0000-', '01', '-01', ' 00:', '00', ':00')
@ -249,7 +256,7 @@ class DatabaseOperations(BaseDatabaseOperations):
else: else:
format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) 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) 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): def date_interval_sql(self, sql, connector, timedelta):
return "(%s %s INTERVAL '%d 0:0:%d:%d' DAY_MICROSECOND)" % (sql, connector, return "(%s %s INTERVAL '%d 0:0:%d:%d' DAY_MICROSECOND)" % (sql, connector,

View File

@ -7,6 +7,7 @@ from __future__ import unicode_literals
import datetime import datetime
import decimal import decimal
import re
import sys import sys
import warnings import warnings
@ -131,8 +132,9 @@ WHEN (new.%(col_name)s IS NULL)
if lookup_type == 'week_day': if lookup_type == 'week_day':
# TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday.
return "TO_CHAR(%s, 'D')" % field_name return "TO_CHAR(%s, 'D')" % field_name
# http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm else:
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) # 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): def date_interval_sql(self, sql, connector, timedelta):
""" """
@ -155,29 +157,52 @@ WHEN (new.%(col_name)s IS NULL)
else: else:
return "TRUNC(%s)" % field_name 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: 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': if lookup_type == 'week_day':
# TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday.
return "TO_CHAR(%s, 'D')" % field_name sql = "TO_CHAR(%s, 'D')" % field_name
# http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm else:
return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) # 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: 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 # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084
if lookup_type in ('year', 'month'): 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': elif lookup_type == 'day':
return "TRUNC(%s)" % field_name sql = "TRUNC(%s)" % field_name
elif lookup_type == 'hour': elif lookup_type == 'hour':
return "TRUNC(%s, 'HH24')" % field_name sql = "TRUNC(%s, 'HH24')" % field_name
elif lookup_type == 'minute': elif lookup_type == 'minute':
return "TRUNC(%s, 'MI')" % field_name sql = "TRUNC(%s, 'MI')" % field_name
else: else:
return field_name sql = field_name # Cast to DATE removes sub-second precision.
return sql, []
def convert_values(self, value, field): def convert_values(self, value, field):
if isinstance(value, Database.LOB): if isinstance(value, Database.LOB):

View File

@ -37,21 +37,29 @@ class DatabaseOperations(BaseDatabaseOperations):
# http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) 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: if settings.USE_TZ:
field_name = "%s AT TIME ZONE %%s" % field_name 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 # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
if lookup_type == 'week_day': if lookup_type == 'week_day':
# For consistency across backends, we return Sunday=1, Saturday=7. # 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: 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: if settings.USE_TZ:
field_name = "%s AT TIME ZONE %%s" % field_name 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 # 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): def deferrable_sql(self):
return " DEFERRABLE INITIALLY DEFERRED" return " DEFERRABLE INITIALLY DEFERRED"

View File

@ -172,31 +172,25 @@ class DatabaseOperations(BaseDatabaseOperations):
# cause a collision with a field name). # cause a collision with a field name).
return "django_date_trunc('%s', %s)" % (lookup_type.lower(), 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. # Same comment as in date_extract_sql.
if settings.USE_TZ: if settings.USE_TZ:
if pytz is None: if pytz is None:
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
raise ImproperlyConfigured("This query requires pytz, " raise ImproperlyConfigured("This query requires pytz, "
"but it isn't installed.") "but it isn't installed.")
return "django_datetime_extract('%s', %s, %%s)" % ( return "django_datetime_extract('%s', %s, %%s)" % (
lookup_type.lower(), field_name) lookup_type.lower(), field_name), [tzname]
else:
return "django_datetime_extract('%s', %s, NULL)" % (
lookup_type.lower(), field_name)
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. # Same comment as in date_trunc_sql.
if settings.USE_TZ: if settings.USE_TZ:
if pytz is None: if pytz is None:
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
raise ImproperlyConfigured("This query requires pytz, " raise ImproperlyConfigured("This query requires pytz, "
"but it isn't installed.") "but it isn't installed.")
return "django_datetime_trunc('%s', %s, %%s)" % ( return "django_datetime_trunc('%s', %s, %%s)" % (
lookup_type.lower(), field_name) lookup_type.lower(), field_name), [tzname]
else:
return "django_datetime_trunc('%s', %s, NULL)" % (
lookup_type.lower(), field_name)
def drop_foreignkey_sql(self): def drop_foreignkey_sql(self):
return "" return ""

View File

@ -1034,13 +1034,6 @@ class SQLDateCompiler(SQLCompiler):
yield date yield date
class SQLDateTimeCompiler(SQLCompiler): 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): def results_iter(self):
""" """
Returns an iterator over the results from executing this query. Returns an iterator over the results from executing this query.

View File

@ -26,8 +26,6 @@ class Date(object):
""" """
Add a date selection column. Add a date selection column.
""" """
trunc_func = 'date_trunc_sql'
def __init__(self, col, lookup_type): def __init__(self, col, lookup_type):
self.col = col self.col = col
self.lookup_type = lookup_type self.lookup_type = lookup_type
@ -42,10 +40,25 @@ class Date(object):
col = '%s.%s' % tuple([qn(c) for c in self.col]) col = '%s.%s' % tuple([qn(c) for c in self.col])
else: else:
col = self.col 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. 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)

View File

@ -14,6 +14,7 @@ from django.db.models.sql.where import AND, Constraint
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils import six from django.utils import six
from django.utils import timezone
__all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'DateQuery', __all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'DateQuery',
@ -222,7 +223,6 @@ class DateQuery(Query):
""" """
compiler = 'SQLDateCompiler' compiler = 'SQLDateCompiler'
select_type = Date
def add_select(self, field_name, lookup_type, order='ASC'): def add_select(self, field_name, lookup_type, order='ASC'):
""" """
@ -241,7 +241,7 @@ class DateQuery(Query):
field = result[0] field = result[0]
self._check_field(field) # overridden in DateTimeQuery self._check_field(field) # overridden in DateTimeQuery
alias = result[3][-1] 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.clear_select_clause()
self.select = [SelectInfo(select, None)] self.select = [SelectInfo(select, None)]
self.distinct = True self.distinct = True
@ -257,6 +257,9 @@ class DateQuery(Query):
assert not isinstance(field, DateTimeField), \ assert not isinstance(field, DateTimeField), \
"%r is a DateTimeField, not a DateField." % field.name "%r is a DateTimeField, not a DateField." % field.name
def _get_select(self, col, lookup_type):
return Date(col, lookup_type)
class DateTimeQuery(DateQuery): class DateTimeQuery(DateQuery):
""" """
A DateTimeQuery is like a DateQuery but for a datetime field. If time zone A DateTimeQuery is like a DateQuery but for a datetime field. If time zone
@ -265,12 +268,18 @@ class DateTimeQuery(DateQuery):
""" """
compiler = 'SQLDateTimeCompiler' compiler = 'SQLDateTimeCompiler'
select_type = DateTime
def _check_field(self, field): def _check_field(self, field):
assert isinstance(field, DateTimeField), \ assert isinstance(field, DateTimeField), \
"%r isn't a DateTimeField." % field.name "%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): class AggregateQuery(Query):
""" """
An AggregateQuery takes another query as a parameter to the FROM An AggregateQuery takes another query as a parameter to the FROM

View File

@ -226,10 +226,9 @@ class WhereNode(tree.Node):
return ('%s BETWEEN %%s and %%s' % field_sql, params) return ('%s BETWEEN %%s and %%s' % field_sql, params)
elif is_datetime_field and lookup_type in ('month', 'day', 'week_day', elif is_datetime_field and lookup_type in ('month', 'day', 'week_day',
'hour', 'minute', 'second'): 'hour', 'minute', 'second'):
if settings.USE_TZ: tzname = timezone.get_current_timezone_name() if settings.USE_TZ else None
params = [timezone.get_current_timezone_name()] + params sql, tz_params = connection.ops.datetime_extract_sql(lookup_type, field_sql, tzname)
return ('%s = %%s' return ('%s = %%s' % sql, tz_params + params)
% connection.ops.datetime_extract_sql(lookup_type, field_sql), params)
elif lookup_type in ('month', 'day', 'week_day'): elif lookup_type in ('month', 'day', 'week_day'):
return ('%s = %%s' return ('%s = %%s'
% connection.ops.date_extract_sql(lookup_type, field_sql), params) % connection.ops.date_extract_sql(lookup_type, field_sql), params)