Skip to content

Commit

Permalink
Fix issues with timetz type I/O
Browse files Browse the repository at this point in the history
For timetz an offset is applied to a time itself, it is not a time zone offset.
This was masked by a symmetrical bug in the encoder.

An offset value is still passed in a tuple (timetz_decode_tuple) as it was
received from Postgres.
  • Loading branch information
Vitaliy Burovoy authored and elprans committed Nov 14, 2017
1 parent 1fa12fe commit 7b6c083
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 2 deletions.
8 changes: 6 additions & 2 deletions asyncpg/protocol/codecs/datetime.pyx
Expand Up @@ -287,7 +287,10 @@ cdef timetz_encode(ConnectionSettings settings, WriteBuffer buf, obj):

buf.write_int32(12)
_encode_time(buf, seconds, microseconds)
buf.write_int32(offset_sec)
# In Python utcoffset() is the difference between the local time
# and the UTC, whereas in PostgreSQL it's the opposite,
# so we need to flip the sign.
buf.write_int32(-offset_sec)


cdef timetz_encode_tuple(ConnectionSettings settings, WriteBuffer buf, obj):
Expand All @@ -311,7 +314,8 @@ cdef timetz_encode_tuple(ConnectionSettings settings, WriteBuffer buf, obj):
cdef timetz_decode(ConnectionSettings settings, FastReadBuffer buf):
time = time_decode(settings, buf)
cdef int32_t offset = <int32_t>(hton.unpack_int32(buf.read(4)) / 60)
return time.replace(tzinfo=datetime.timezone(timedelta(minutes=offset)))
# See the comment in the `timetz_encode` method.
return time.replace(tzinfo=datetime.timezone(timedelta(minutes=-offset)))


cdef timetz_decode_tuple(ConnectionSettings settings, FastReadBuffer buf):
Expand Down
21 changes: 21 additions & 0 deletions tests/test_codecs.py
Expand Up @@ -1129,6 +1129,27 @@ def _decoder(value):
finally:
await conn.close()

async def test_timetz_encoding(self):
try:
async with self.con.transaction():
await self.con.execute("SET TIME ZONE 'America/Toronto'")
# Check decoding:
row = await self.con.fetchrow(
'SELECT extract(epoch from now()) AS epoch, '
'now()::date as date, now()::timetz as time')
result = datetime.datetime.combine(row['date'], row['time'])
expected = datetime.datetime.fromtimestamp(row['epoch'],
tz=result.tzinfo)
self.assertEqual(result, expected)

# Check encoding:
res = await self.con.fetchval(
'SELECT now() = ($1::date + $2::timetz)',
row['date'], row['time'])
self.assertTrue(res)
finally:
await self.con.execute('RESET ALL')

async def test_composites_in_arrays(self):
await self.con.execute('''
CREATE TYPE t AS (a text, b int);
Expand Down

0 comments on commit 7b6c083

Please sign in to comment.