Skip to content

Commit 2ac77e8

Browse files
author
James William Pye
committed
Fix issues with timestamptz.
With the utc-only support of timestamptz's, there's little reason to have subjective timestamptz support on TypeIO, so remove that feature and hardwire timestamptz I/O to use UTC. Add warning for intervals with Month fields; datetime.timedelta doesn't support that.
1 parent b821624 commit 2ac77e8

4 files changed

Lines changed: 85 additions & 26 deletions

File tree

postgresql/exceptions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ class DriverWarning(Warning):
6767
source = 'DRIVER'
6868
class IgnoredClientParameterWarning(DriverWarning):
6969
'Warn the user of a valid, but ignored parameter.'
70+
class TypeConversionWarning(DriverWarning):
71+
'Report a potential issue with a conversion.'
7072

7173
class ClusterWarning(Warning):
7274
code = ''

postgresql/protocol/typio.py

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@
3636
time64_noday_io
3737
long-long based time I/O with noday-intervals.
3838
"""
39+
import warnings
3940
import codecs
4041
from ..encodings import aliases as pg_enc_aliases
42+
from .. import exceptions as pg_exc
4143

4244
from operator import itemgetter, add, sub, mul, methodcaller
4345
get0 = itemgetter(0)
@@ -51,6 +53,7 @@
5153
from decimal import Decimal, DecimalTuple
5254
import datetime
5355

56+
from ..exceptions import TypeConversionWarning
5457
from ..python.datetime import UTC, FixedOffset
5558

5659
from ..python.functools import Composition as compose
@@ -71,6 +74,9 @@
7174
# High level type I/O routines.
7275
##
7376
toordinal = methodcaller("toordinal")
77+
convert_to_utc = methodcaller('astimezone', UTC)
78+
remove_tzinfo = methodcaller('replace', tzinfo = None)
79+
set_as_utc = methodcaller('replace', tzinfo = UTC)
7480

7581
date_pack = compose((
7682
toordinal,
@@ -83,12 +89,15 @@
8389
datetime.date.fromordinal
8490
))
8591

92+
seconds_in_day = 24 * 60 * 60
93+
seconds_in_hour = 60 * 60
94+
8695
def timestamp_pack(x):
8796
"""
8897
Create a (seconds, microseconds) pair from a `datetime.datetime` instance.
8998
"""
9099
d = (x - pg_epoch_datetime)
91-
return (d.days * 24 * 60 * 60 + d.seconds, d.microseconds)
100+
return ((d.days * seconds_in_day) + d.seconds, d.microseconds)
92101

93102
def timestamp_unpack(seconds):
94103
"""
@@ -103,7 +112,7 @@ def time_pack(x):
103112
Create a (seconds, microseconds) pair from a `datetime.time` instance.
104113
"""
105114
return (
106-
(x.hour * 60 * 60) + (x.minute * 60) + x.second,
115+
(x.hour * seconds_in_hour) + (x.minute * 60) + x.second,
107116
x.microsecond
108117
)
109118

@@ -132,6 +141,16 @@ def interval_unpack(mds):
132141
`datetime.timedelta` instance.
133142
"""
134143
months, days, seconds_ms = mds
144+
if months != 0:
145+
w = pg_exc.TypeConversionWarning(
146+
"datetime.timedelta cannot represent relative intervals",
147+
details = {
148+
''
149+
'hint': 'An interval was unpacked with a non-zero "month" field.'
150+
},
151+
source = 'DRIVER'
152+
)
153+
warnings.warn(w)
135154
sec, ms = seconds_ms
136155
return datetime.timedelta(
137156
days = days + (months * 30),
@@ -143,7 +162,9 @@ def timetz_pack(x):
143162
Create a ((seconds, microseconds), timezone) tuple from a `datetime.time`
144163
instance.
145164
"""
146-
return (time_pack(x), x.utcoffset())
165+
td = x.tzinfo.utcoffset(x)
166+
seconds = (td.days * seconds_in_day + td.seconds)
167+
return (time_pack(x), seconds)
147168

148169
def timetz_unpack(tstz):
149170
"""
@@ -167,8 +188,8 @@ def timetz_unpack(tstz):
167188
compose((ts.time_unpack, timestamp_unpack)),
168189
),
169190
pg_types.TIMESTAMPTZOID : (
170-
compose((timestamp_pack, ts.time_pack)),
171-
compose((ts.time_unpack, timestamp_unpack)),
191+
compose((convert_to_utc, remove_tzinfo, timestamp_pack, ts.time_pack)),
192+
compose((ts.time_unpack, timestamp_unpack, set_as_utc)),
172193
),
173194
pg_types.INTERVALOID : (
174195
compose((interval_pack, ts.interval_pack)),
@@ -195,8 +216,8 @@ def timetz_unpack(tstz):
195216
compose((ts.time64_unpack, timestamp_unpack)),
196217
),
197218
pg_types.TIMESTAMPTZOID : (
198-
compose((timestamp_pack, ts.time64_pack)),
199-
compose((ts.time64_unpack, timestamp_unpack)),
219+
compose((convert_to_utc, remove_tzinfo, timestamp_pack, ts.time64_pack)),
220+
compose((ts.time64_unpack, timestamp_unpack, set_as_utc)),
200221
),
201222
pg_types.INTERVALOID : (
202223
compose((interval_pack, ts.interval64_pack)),
@@ -564,7 +585,6 @@ def select_time_io(self,
564585
self._time_io = time_io_noday
565586
else:
566587
self._time_io = time_io
567-
self._ts_pack, self._ts_unpack = self._time_io[pg_types.TIMESTAMPOID]
568588

569589
def encode(self, string_data):
570590
return self._encode(string_data)[0]
@@ -636,10 +656,6 @@ def __init__(self):
636656
pg_types.CIDROID : (None, None),
637657
pg_types.INETOID : (None, None),
638658

639-
pg_types.TIMESTAMPTZOID : (
640-
self._pack_timestamptz,
641-
self._unpack_timestamptz,
642-
),
643659
pg_types.XMLOID : (
644660
self.xml_pack, self.xml_unpack
645661
),
@@ -672,20 +688,6 @@ def set_encoding(self, value):
672688
self._encode = ci[0]
673689
self._decode = ci[1]
674690

675-
def _pack_timestamptz(self, dt):
676-
if dt.tzinfo:
677-
return self._ts_pack(
678-
(dt - dt.tzinfo.utcoffset(dt)).replace(tzinfo = None)
679-
)
680-
else:
681-
# If no timezone is specified, assume UTC.
682-
return self._ts_pack(dt)
683-
684-
def _unpack_timestamptz(self, data):
685-
dt = self._ts_unpack(data)
686-
dt = dt.replace(tzinfo = UTC)
687-
return dt
688-
689691
def resolve_descriptor(self, desc, index):
690692
'create a sequence of I/O routines from a pq descriptor'
691693
return [

postgresql/test/test_driver.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from itertools import chain
1313
from operator import itemgetter
1414

15+
from ..python.datetime import FixedOffset
1516
from .. import types as pg_types
1617
from .. import exceptions as pg_exc
1718
from .. import unittest as pg_unittest
@@ -169,6 +170,7 @@
169170
datetime.date(3000,5,20),
170171
datetime.date(2000,1,1),
171172
datetime.date(500,1,1),
173+
datetime.date(1,1,1),
172174
],
173175
),
174176
('time', [
@@ -177,6 +179,58 @@
177179
datetime.time(23,59,59),
178180
],
179181
),
182+
('timestamptz', [
183+
datetime.datetime(1990,5,12,10,10,0, tzinfo=FixedOffset(4000)),
184+
datetime.datetime(1982,5,18,10,10,0, tzinfo=FixedOffset(6000)),
185+
datetime.datetime(1950,1,1,10,10,0, tzinfo=FixedOffset(7000)),
186+
datetime.datetime(1800,1,1,10,10,0, tzinfo=FixedOffset(2000)),
187+
datetime.datetime(2400,1,1,10,10,0, tzinfo=FixedOffset(2000)),
188+
],
189+
),
190+
('timetz', [
191+
datetime.time(10,10,0, tzinfo=FixedOffset(4000)),
192+
datetime.time(10,10,0, tzinfo=FixedOffset(6000)),
193+
datetime.time(10,10,0, tzinfo=FixedOffset(7000)),
194+
datetime.time(10,10,0, tzinfo=FixedOffset(2000)),
195+
],
196+
),
197+
('interval', [
198+
datetime.timedelta(40, 10, 1234),
199+
datetime.timedelta(0, 0),
200+
datetime.timedelta(-100, 0),
201+
datetime.timedelta(-100, -400),
202+
],
203+
),
204+
('point', [
205+
(10, 1234),
206+
(-1, -1),
207+
(0, 0),
208+
(1, 1),
209+
(-100, 0),
210+
(-100, -400),
211+
(-100.02314, -400.930425),
212+
(0xFFFF, 1.3124243),
213+
],
214+
),
215+
('lseg', [
216+
((0,0),(0,0)),
217+
((10,5),(18,293)),
218+
((55,5),(10,293)),
219+
((-1,-1),(-1,-1)),
220+
((-100,0.00231),(50,45.42132)),
221+
((0.123,0.00231),(50,45.42132)),
222+
],
223+
),
224+
('circle', [
225+
((0,0),0),
226+
((0,0),1),
227+
((0,0),1.0011),
228+
((1,1),1.0011),
229+
((-1,-1),1.0011),
230+
((1,-1),1.0011),
231+
((-1,1),1.0011),
232+
],
233+
),
180234
)
181235

182236
class test_driver(pg_unittest.TestCaseWithCluster):

postgresql/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@ def from_mapping(typ, map, attribute_map = {}):
720720
iter = [
721721
map.get(k) for k,_ in sorted(attribute_map.items(), key = get1)
722722
]
723+
return typ(iter, attribute_map)
723724

724725
def __new__(subtype, iter, attribute_map = {}):
725726
rob = tuple.__new__(subtype, iter)

0 commit comments

Comments
 (0)