- edited description
Problem using custom DateTime type with Postgres
I haven't used SqlAlchemy much beyond the Flask-SqlAlchemy API, so apologies if this is trivial--and not a bug.
The issue occurs when filtering by a datetime field, which uses a Custom Type UTCDateTime. I can reproduce the problem on Postgres but not with SQLite.
postgres version: PostgreSQL 9.4.5 on x86_64-apple-darwin14.5.0, compiled by Apple LLVM version 7.0.0 (clang-700.0.72), 64-bit
tested sqlalchemy versions: 1.0.9
, SQLAlchemy==1.1.0b1.dev0
with psycopg2==2.6.1
from datetime import datetime, timedelta
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import types
from pytz import utc
# setup typical flask-sqlalchemy enabled app
app = Flask(__name__)
app.config.update(
SQLALCHEMY_DATABASE_URI = 'postgresql://taylop53:@localhost:5432/test',
SQLALCHEMY_ECHO = False,
SECRET_KEY = 'a-secret-key',
DEBUG = True
)
db = SQLAlchemy(app)
# custom datetime type
class UTCDateTime(types.TypeDecorator):
""" decorator to enforce tz aware DateTime.
http://stackoverflow.com/questions/79797/how-do-i-convert-local-time-to-utc-in-python
"""
impl = types.DateTime
def process_bind_param(self, value, engine):
if value is not None:
return value.astimezone(utc)
def process_result_value(self, value, engine):
if value is not None:
return value.replace(tzinfo=utc)
# model that uses UTCDateTime
class EventUTC(db.Model):
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(UTCDateTime)
def events_before(self):
return EventUTC.query.filter(
(EventUTC.timestamp <= self.timestamp)
).all()
def create_sample_events():
db.drop_all()
db.create_all()
# create some utc events
now = datetime.utcnow().replace(tzinfo=utc)
events = [
EventUTC(timestamp=now + timedelta(minutes=t))
for t in range(-2, 1)
]
for ev in events: db.session.add(ev)
db.session.commit()
def check_timestamp_is_before(candidate, timestamp):
print 'evaluating: ', candidate.timestamp, ' <= ', timestamp
if not (candidate.timestamp <= timestamp):
print '^^^ failed'
def test_filter_query(event):
events_before = event.events_before()
print 'test for event with timestamp: ', event.timestamp
for test_ev in events_before:
check_timestamp_is_before(test_ev, event.timestamp)
print '\n'
if __name__ == '__main__':
create_sample_events()
all_utc_event = EventUTC.query.all()
# 'for each test event,'
# 'fetch all events that occur before (or at the same time)'
# 'Using tz aware datetimes'
# 'fails when using postgres, succeeds for sqlite'
map(test_filter_query, all_utc_event)
I'm using a TypeDecorator to enforce UTC timezoneaware datetimes, instead of using the standard DateTime type.
This works with postgres when simply storing and accessing the datetime field.
However, it behaves unexpected when filtering on the datetime field.
Comments (7)
-
reporter -
reporter Specific to enforcing timezone aware datetimes, I can avoid this problem by using the ArrowType (http://sqlalchemy-utils.readthedocs.org/en/latest/data_types.html#module-sqlalchemy_utils.types.arrow). ArrowType works as expected with Postgres.
For this to work using then Custom Type is there another perhaps method I need to override?
-
repo owner two things:
-
instead of linking to a whole github repo, please provide a succinct and self contained script as described here: http://stackoverflow.com/help/mcve
-
if the sqlalchemy_utils type is working, examine its source to see why your type is not acting as expected.
-
-
reporter Hi Mike, thanks for the advice - new to this so really appreciate you help.
Re: your message 1. I've updating the Issue with a self contained, simplified version of the script. 2. I shall take a look. At first glance it looks to quite similar, but I'll give it a proper comparison tomorrow
-
reporter - edited description
-
repo owner - removed milestone
any chance you could describe the "unexpected behavior" here? stack trace, or something else? I will see if I can run this to see what we're referring to.
-
repo owner - changed status to closed
this is confusing but there's two things going on.
-
the DateTime type here is timezone-naive, because you aren't specifying "timezone=True". So when you pass a datetime to psycopg2 that has a timezone, it compares to what's in the DB based on the local time of the server. on my side, I get no rows back, on your side you get the wrong ones because I guess you're on the other side of UTC :)
-
if you do put DateTime(timezone=True), now we are closer, but psycopg2 does not persist the timezone as UTC; it persists with a fixed offset, over here its -420, I'm not sure what he origin of that is but it isn't UTC:
-- INPUT: INSERT INTO event (timestamp) VALUES (%(timestamp)s) RETURNING event.id {'timestamp': datetime.datetime(2016, 2, 8, 16, 7, 41, 369721, tzinfo=<UTC>)} INSERT INTO event (timestamp) VALUES (%(timestamp)s) RETURNING event.id {'timestamp': datetime.datetime(2016, 2, 8, 16, 8, 41, 369721, tzinfo=<UTC>)} INSERT INTO event (timestamp) VALUES (%(timestamp)s) RETURNING event.id {'timestamp': datetime.datetime(2016, 2, 8, 16, 9, 41, 369721, tzinfo=<UTC>)} -- OUTPUT: SELECT event.id AS event_id, event.timestamp AS event_timestamp FROM event ('event_id', 'event_timestamp') (1, datetime.datetime(2016, 2, 8, 9, 7, 41, 369721, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=-420, name=None))) (2, datetime.datetime(2016, 2, 8, 9, 8, 41, 369721, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=-420, name=None))) (3, datetime.datetime(2016, 2, 8, 9, 9, 41, 369721, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=-420, name=None)))
In that case, the tests fails because you are getting back datetimes with an actual tzinfo of -420 but the result_processor is forcing it to "UTC" without any modification, hence fails there too.
Solution: use timezone-naive datatypes on the database and do not send tzinfo into any queries:
class UTCDateTime(types.TypeDecorator): impl = types.DateTime def process_bind_param(self, value, engine): if value is not None: return value.replace(tzinfo=None) def process_result_value(self, value, engine): if value is not None: return value.replace(tzinfo=utc)
- Log in to comment