Skip to content

Commit 7b6c083

Browse files
Vitaliy Burovoyelprans
Vitaliy Burovoy
authored andcommitted
Fix issues with timetz type I/O
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.
1 parent 1fa12fe commit 7b6c083

File tree

2 files changed

+27
-2
lines changed

2 files changed

+27
-2
lines changed

asyncpg/protocol/codecs/datetime.pyx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,10 @@ cdef timetz_encode(ConnectionSettings settings, WriteBuffer buf, obj):
287287

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

292295

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

316320

317321
cdef timetz_decode_tuple(ConnectionSettings settings, FastReadBuffer buf):

tests/test_codecs.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,27 @@ def _decoder(value):
11291129
finally:
11301130
await conn.close()
11311131

1132+
async def test_timetz_encoding(self):
1133+
try:
1134+
async with self.con.transaction():
1135+
await self.con.execute("SET TIME ZONE 'America/Toronto'")
1136+
# Check decoding:
1137+
row = await self.con.fetchrow(
1138+
'SELECT extract(epoch from now()) AS epoch, '
1139+
'now()::date as date, now()::timetz as time')
1140+
result = datetime.datetime.combine(row['date'], row['time'])
1141+
expected = datetime.datetime.fromtimestamp(row['epoch'],
1142+
tz=result.tzinfo)
1143+
self.assertEqual(result, expected)
1144+
1145+
# Check encoding:
1146+
res = await self.con.fetchval(
1147+
'SELECT now() = ($1::date + $2::timetz)',
1148+
row['date'], row['time'])
1149+
self.assertTrue(res)
1150+
finally:
1151+
await self.con.execute('RESET ALL')
1152+
11321153
async def test_composites_in_arrays(self):
11331154
await self.con.execute('''
11341155
CREATE TYPE t AS (a text, b int);

0 commit comments

Comments
 (0)