Skip to content

Commit a7eaf2b

Browse files
committed
Fix decoding of fractional timestamps before Postgres epoch
The datetime decoder currently incorrectly uses an unsigned integer type for microseconds, which leads to incorrect decoding of timestamps with fractional seconds which represent dates before Jan 1 2000. Same issue affects negative intervals with fractional seconds. Fixes: #363
1 parent 687127e commit a7eaf2b

File tree

2 files changed

+36
-14
lines changed

2 files changed

+36
-14
lines changed

asyncpg/protocol/codecs/datetime.pyx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,17 @@ cdef inline _encode_time(WriteBuffer buf, int64_t seconds,
7979

8080

8181
cdef inline int32_t _decode_time(FastReadBuffer buf, int64_t *seconds,
82-
uint32_t *microseconds):
83-
# XXX: add support for double timestamps
84-
# int64 timestamps,
82+
int32_t *microseconds):
8583
cdef int64_t ts = hton.unpack_int64(buf.read(8))
8684

8785
if ts == pg_time64_infinity:
8886
return 1
8987
elif ts == pg_time64_negative_infinity:
9088
return -1
91-
92-
seconds[0] = <int64_t>(ts / 1000000)
93-
microseconds[0] = <uint32_t>(ts % 1000000)
94-
95-
return 0
89+
else:
90+
seconds[0] = ts // 1000000
91+
microseconds[0] = <int32_t>(ts % 1000000)
92+
return 0
9693

9794

9895
cdef date_encode(ConnectionSettings settings, WriteBuffer buf, obj):
@@ -181,7 +178,7 @@ cdef timestamp_encode_tuple(ConnectionSettings settings, WriteBuffer buf, obj):
181178
cdef timestamp_decode(ConnectionSettings settings, FastReadBuffer buf):
182179
cdef:
183180
int64_t seconds = 0
184-
uint32_t microseconds = 0
181+
int32_t microseconds = 0
185182
int32_t inf = _decode_time(buf, &seconds, &microseconds)
186183

187184
if inf > 0:
@@ -242,7 +239,7 @@ cdef timestamptz_encode(ConnectionSettings settings, WriteBuffer buf, obj):
242239
cdef timestamptz_decode(ConnectionSettings settings, FastReadBuffer buf):
243240
cdef:
244241
int64_t seconds = 0
245-
uint32_t microseconds = 0
242+
int32_t microseconds = 0
246243
int32_t inf = _decode_time(buf, &seconds, &microseconds)
247244

248245
if inf > 0:
@@ -285,7 +282,7 @@ cdef time_encode_tuple(ConnectionSettings settings, WriteBuffer buf, obj):
285282
cdef time_decode(ConnectionSettings settings, FastReadBuffer buf):
286283
cdef:
287284
int64_t seconds = 0
288-
uint32_t microseconds = 0
285+
int32_t microseconds = 0
289286

290287
_decode_time(buf, &seconds, &microseconds)
291288

@@ -400,9 +397,10 @@ cdef interval_decode(ConnectionSettings settings, FastReadBuffer buf):
400397
int32_t months
401398
int32_t years
402399
int64_t seconds = 0
403-
uint32_t microseconds = 0
400+
int32_t microseconds = 0
404401

405402
_decode_time(buf, &seconds, &microseconds)
403+
406404
days = hton.unpack_int32(buf.read(4))
407405
months = hton.unpack_int32(buf.read(4))
408406

tests/test_codecs.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,11 @@ def _system_timezone():
169169
{'textinput': 'infinity', 'output': infinity_datetime},
170170
{'textinput': '-infinity', 'output': negative_infinity_datetime},
171171
{'input': datetime.date(2000, 1, 1),
172-
'output': datetime.datetime(2000, 1, 1)}
172+
'output': datetime.datetime(2000, 1, 1)},
173+
{'textinput': '1970-01-01 20:31:23.648',
174+
'output': datetime.datetime(1970, 1, 1, 20, 31, 23, 648000)},
175+
{'input': datetime.datetime(1970, 1, 1, 20, 31, 23, 648000),
176+
'textoutput': '1970-01-01 20:31:23.648'},
173177
]),
174178
('date', 'date', [
175179
datetime.date(3000, 5, 20),
@@ -215,12 +219,29 @@ def _system_timezone():
215219
datetime.time(22, 30, 0, tzinfo=_timezone(0)),
216220
]),
217221
('interval', 'interval', [
218-
# no months :(
219222
datetime.timedelta(40, 10, 1234),
220223
datetime.timedelta(0, 0, 4321),
221224
datetime.timedelta(0, 0),
222225
datetime.timedelta(-100, 0),
223226
datetime.timedelta(-100, -400),
227+
{
228+
'textinput': '-2 years -11 months -10 days '
229+
'-2 hours -800 milliseconds',
230+
'output': datetime.timedelta(
231+
days=(-2 * 365) + (-11 * 30) - 10,
232+
seconds=(-2 * 3600),
233+
milliseconds=-800
234+
),
235+
},
236+
{
237+
'query': 'SELECT justify_hours($1::interval)::text',
238+
'input': datetime.timedelta(
239+
days=(-2 * 365) + (-11 * 30) - 10,
240+
seconds=(-2 * 3600),
241+
milliseconds=-800
242+
),
243+
'textoutput': '-1070 days -02:00:00.8',
244+
},
224245
]),
225246
('uuid', 'uuid', [
226247
uuid.UUID('38a4ff5a-3a56-11e6-a6c2-c8f73323c6d4'),
@@ -458,6 +479,9 @@ async def test_standard_codecs(self):
458479
stmt = text_out
459480
else:
460481
outputval = sample['output']
482+
483+
if sample.get('query'):
484+
stmt = await self.con.prepare(sample['query'])
461485
else:
462486
inputval = outputval = sample
463487

0 commit comments

Comments
 (0)