diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index f4fd1cc379..939c541551 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -35,6 +35,10 @@ except ImportError as exc: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("Error loading either pysqlite2 or sqlite3 modules (tried in that order): %s" % exc) +try: + import pytz +except ImportError: + pytz = None DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError @@ -117,6 +121,10 @@ class DatabaseFeatures(BaseDatabaseFeatures): cursor.execute('DROP TABLE STDDEV_TEST') return has_support + @cached_property + def has_zoneinfo_database(self): + return pytz is not None + class DatabaseOperations(BaseDatabaseOperations): def bulk_batch_size(self, fields, objs): """ @@ -142,10 +150,10 @@ class DatabaseOperations(BaseDatabaseOperations): def date_extract_sql(self, lookup_type, field_name): # sqlite doesn't support extract, so we fake it with the user-defined - # function django_extract that's registered in connect(). Note that + # function django_date_extract that's registered in connect(). Note that # single quotes are used because this is a string (and could otherwise # cause a collision with a field name). - return "django_extract('%s', %s)" % (lookup_type.lower(), field_name) + return "django_date_extract('%s', %s)" % (lookup_type.lower(), field_name) def date_interval_sql(self, sql, connector, timedelta): # It would be more straightforward if we could use the sqlite strftime @@ -154,7 +162,7 @@ class DatabaseOperations(BaseDatabaseOperations): # values differently. So instead we register our own function that # formats the datetime combined with the delta in a manner suitable # for comparisons. - return 'django_format_dtdelta(%s, "%s", "%d", "%d", "%d")' % (sql, + return 'django_format_dtdelta(%s, "%s", "%d", "%d", "%d")' % (sql, connector, timedelta.days, timedelta.seconds, timedelta.microseconds) def date_trunc_sql(self, lookup_type, field_name): @@ -164,6 +172,32 @@ 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): + # 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) + + def datetime_trunc_sql(self, lookup_type, field_name): + # 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) + def drop_foreignkey_sql(self): return "" @@ -214,11 +248,6 @@ class DatabaseOperations(BaseDatabaseOperations): return six.text_type(value) - def year_lookup_bounds(self, value): - first = '%s-01-01' - second = '%s-12-31 23:59:59.999999' - return [first % value, second % value] - def convert_values(self, value, field): """SQLite returns floats when it should be returning decimals, and gets dates and datetimes wrong. @@ -310,9 +339,10 @@ class DatabaseWrapper(BaseDatabaseWrapper): def get_new_connection(self, conn_params): conn = Database.connect(**conn_params) - # Register extract, date_trunc, and regexp functions. - conn.create_function("django_extract", 2, _sqlite_extract) + conn.create_function("django_date_extract", 2, _sqlite_date_extract) conn.create_function("django_date_trunc", 2, _sqlite_date_trunc) + conn.create_function("django_datetime_extract", 3, _sqlite_datetime_extract) + conn.create_function("django_datetime_trunc", 3, _sqlite_datetime_trunc) conn.create_function("regexp", 2, _sqlite_regexp) conn.create_function("django_format_dtdelta", 5, _sqlite_format_dtdelta) return conn @@ -402,7 +432,7 @@ class SQLiteCursorWrapper(Database.Cursor): def convert_query(self, query): return FORMAT_QMARK_REGEX.sub('?', query).replace('%%','%') -def _sqlite_extract(lookup_type, dt): +def _sqlite_date_extract(lookup_type, dt): if dt is None: return None try: @@ -419,12 +449,46 @@ def _sqlite_date_trunc(lookup_type, dt): dt = util.typecast_timestamp(dt) except (ValueError, TypeError): return None + if lookup_type == 'year': + return "%i-01-01" % dt.year + elif lookup_type == 'month': + return "%i-%02i-01" % (dt.year, dt.month) + elif lookup_type == 'day': + return "%i-%02i-%02i" % (dt.year, dt.month, dt.day) + +def _sqlite_datetime_extract(lookup_type, dt, tzname): + if dt is None: + return None + try: + dt = util.typecast_timestamp(dt) + except (ValueError, TypeError): + return None + if tzname is not None: + dt = timezone.localtime(dt, pytz.timezone(tzname)) + if lookup_type == 'week_day': + return (dt.isoweekday() % 7) + 1 + else: + return getattr(dt, lookup_type) + +def _sqlite_datetime_trunc(lookup_type, dt, tzname): + try: + dt = util.typecast_timestamp(dt) + except (ValueError, TypeError): + return None + if tzname is not None: + dt = timezone.localtime(dt, pytz.timezone(tzname)) if lookup_type == 'year': return "%i-01-01 00:00:00" % dt.year elif lookup_type == 'month': return "%i-%02i-01 00:00:00" % (dt.year, dt.month) elif lookup_type == 'day': return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day) + elif lookup_type == 'hour': + return "%i-%02i-%02i %02i:00:00" % (dt.year, dt.month, dt.day, dt.hour) + elif lookup_type == 'minute': + return "%i-%02i-%02i %02i:%02i:00" % (dt.year, dt.month, dt.day, dt.hour, dt.minute) + elif lookup_type == 'second': + return "%i-%02i-%02i %02i:%02i:%02i" % (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) def _sqlite_format_dtdelta(dt, conn, days, secs, usecs): try: