Skip to content

Commit e535ffa

Browse files
author
James William Pye
committed
Fix infinity and -infinity values for timestamp, timestamptz, and date.
Issue reported by Axel Rau. Fixes #28
1 parent 87277bb commit e535ffa

7 files changed

Lines changed: 125 additions & 43 deletions

File tree

postgresql/documentation/changes.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ Changes
2121
protocol.element3.Tuple
2222
* Alter Statement.chunks() to return chunks of builtins.tuple. Being
2323
an interface intended for speed, types.Row() impedes its performance.
24+
* Fix handling of infinity values with timestamptz, timestamp, and date.
25+
Bug reported by Axel Rau.

postgresql/driver/pq3.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2050,8 +2050,8 @@ def connect(self):
20502050
# manual binding
20512051
self.sys = pg_lib.Binding(self, pg_lib.sys)
20522052

2053-
if self.version_info <= (8,0):
2054-
meth = self.sys.startup_data_no_start
2053+
if self.version_info[:2] <= (8,0):
2054+
meth = self.sys.startup_data_only_version
20552055
else:
20562056
meth = self.sys.startup_data
20572057
# connection info

postgresql/lib/libsys.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ SELECT
130130
client_port
131131
FROM pg_catalog.pg_stat_activity WHERE procpid = pg_catalog.pg_backend_pid();
132132

133+
[startup_data_only_version:transient:first]
134+
SELECT
135+
pg_catalog.version()::text,
136+
NULL::text AS backend_start,
137+
NULL::text AS client_addr,
138+
NULL::text AS client_port;
139+
133140
[sizeof_db:transient:first]
134141
SELECT pg_catalog.pg_database_size(current_database())::bigint
135142

postgresql/python/datetime.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
##
2-
# copyright 2009, James William Pye
3-
# http://python.projects.postgresql.org
2+
# python.datetime - parts needed to use stdlib.datetime
43
##
5-
"""
6-
datetime extras
7-
"""
84
import datetime
95

6+
##
7+
# stdlib.datetime representation of PostgreSQL 'infinity' and '-infinity'.
8+
infinity_datetime = datetime.datetime(datetime.MAXYEAR, 12, 31, 23, 59, 59, 999999)
9+
negative_infinity_datetime = datetime.datetime(datetime.MINYEAR, 1, 1, 0, 0, 0, 0)
10+
11+
infinity_date = datetime.date(datetime.MAXYEAR, 12, 31)
12+
negative_infinity_date = datetime.date(datetime.MINYEAR, 1, 1)
13+
1014
class FixedOffset(datetime.tzinfo):
1115
def __init__(self, offset_in_seconds, tzname = None):
1216
self._tzname = tzname

postgresql/test/test_driver.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from itertools import chain, islice
1515
from operator import itemgetter
1616

17-
from ..python.datetime import FixedOffset
17+
from ..python.datetime import FixedOffset, \
18+
negative_infinity_datetime, infinity_datetime, \
19+
negative_infinity_date, infinity_date
1820
from .. import types as pg_types
1921
from ..types.io.stdlib_xml_etree import etree
2022
from .. import exceptions as pg_exc
@@ -172,6 +174,8 @@
172174
datetime.datetime(2000,1,1,5,25,10),
173175
datetime.datetime(500,1,1,5,25,10),
174176
datetime.datetime(250,1,1,5,25,10),
177+
infinity_datetime,
178+
negative_infinity_datetime,
175179
],
176180
),
177181
('date', [
@@ -195,6 +199,8 @@
195199
datetime.datetime(1950,1,1,10,10,0, tzinfo=FixedOffset(7000)),
196200
datetime.datetime(1800,1,1,10,10,0, tzinfo=FixedOffset(2000)),
197201
datetime.datetime(2400,1,1,10,10,0, tzinfo=FixedOffset(2000)),
202+
infinity_datetime,
203+
negative_infinity_datetime,
198204
],
199205
),
200206
('timetz', [
@@ -203,11 +209,13 @@
203209
datetime.time(10,10,0, tzinfo=FixedOffset(6000)),
204210
datetime.time(10,10,0, tzinfo=FixedOffset(7000)),
205211
datetime.time(10,10,0, tzinfo=FixedOffset(2000)),
212+
datetime.time(22,30,0, tzinfo=FixedOffset(0)),
206213
],
207214
),
208215
('interval', [
209216
# no months :(
210217
datetime.timedelta(40, 10, 1234),
218+
datetime.timedelta(0, 0, 4321),
211219
datetime.timedelta(0, 0),
212220
datetime.timedelta(-100, 0),
213221
datetime.timedelta(-100, -400),
@@ -273,10 +281,6 @@
273281
Varbit('010111101111'),
274282
],
275283
),
276-
('uuid', [
277-
uuid.uuid1(),
278-
],
279-
),
280284
]
281285

282286
if False:
@@ -1024,6 +1028,30 @@ def testXML(self):
10241028
(tostr(foo), tostr(bar))
10251029
)
10261030

1031+
def testUUID(self):
1032+
# doesn't exist in all versions supported by py-postgresql.
1033+
has_uuid = self.db.prepare(
1034+
"select true from pg_type where lower(typname) = 'uuid'").first()
1035+
if has_uuid:
1036+
ps = self.db.prepare('select $1::uuid').first
1037+
x = uuid.uuid1()
1038+
self.failUnlessEqual(ps(x), x)
1039+
1040+
def testInfinity_stdlib_datetime(self):
1041+
ps = self.db.prepare('SELECT $1::timestamp, $2::timestamptz').first
1042+
# Can't test the special text case because we don't get the text back.
1043+
ts, tstz = ps('infinity', 'infinity')
1044+
self.failUnlessEqual(ts, infinity_datetime)
1045+
self.failUnlessEqual(tstz, infinity_datetime)
1046+
ts, tstz = ps('-infinity', '-infinity')
1047+
self.failUnlessEqual(ts, negative_infinity_datetime)
1048+
self.failUnlessEqual(tstz, negative_infinity_datetime)
1049+
1050+
def testInfinity_stdlib_date(self):
1051+
ps = self.db.prepare('SELECT $1::date::text').first
1052+
self.failUnlessEqual(ps('infinity'), 'infinity')
1053+
self.failUnlessEqual(ps('-infinity'), '-infinity')
1054+
10271055
def testTypeIOError(self):
10281056
original = dict(self.db.typio._cache)
10291057
ps = self.db.prepare('SELECT $1::numeric')

postgresql/types/io/lib.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ def path_unpack(data, long_unpack = long_unpack, unpack = struct.unpack):
6060
return unpack("!4x%dd" %(long_unpack(data[:4]),), data)
6161
polygon_pack, polygon_unpack = path_pack, path_unpack
6262

63+
##
64+
# Binary representations of infinity for datetimes.
65+
time_infinity = b'\x7f\xf0\x00\x00\x00\x00\x00\x00'
66+
time_negative_infinity = b'\xff\xf0\x00\x00\x00\x00\x00\x00'
67+
time64_infinity = b'\x7f\xff\xff\xff\xff\xff\xff\xff'
68+
time64_negative_infinity = b'\x80\x00\x00\x00\x00\x00\x00\x00'
69+
date_infinity = b'\x7f\xff\xff\xff'
70+
date_negative_infinity = b'\x80\x00\x00\x00'
71+
6372
# time types
6473
date_pack, date_unpack = long_pack, long_unpack
6574

postgresql/types/io/stdlib_datetime.py

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
11
##
2-
# datetime - timewise
2+
# stdlib_datetime - support for the stdlib's datetime.
33
#
44
# I/O routines for date, time, timetz, timestamp, timestamptz, and interval.
55
# Supported by the datetime module.
66
##
7-
"""
8-
time_io
9-
Floating-point based time I/O.
10-
11-
time_noday_io
12-
Floating-point based time I/O with noday-intervals.
13-
14-
time64_io
15-
long-long based time I/O.
16-
17-
time64_noday_io
18-
long-long based time I/O with noday-intervals.
19-
"""
207
import datetime
218
import warnings
229
from functools import partial
2310
from operator import methodcaller, add
2411

25-
from ...python.datetime import UTC, FixedOffset
12+
from ...python.datetime import UTC, FixedOffset, \
13+
infinity_date, infinity_datetime, \
14+
negative_infinity_date, negative_infinity_datetime
2615
from ...python.functools import Composition as compose
2716
from ...exceptions import TypeConversionWarning
2817

@@ -47,18 +36,51 @@
4736
pg_epoch_datetime = datetime.datetime(2000, 1, 1)
4837
pg_epoch_date = pg_epoch_datetime.date()
4938
pg_date_offset = pg_epoch_date.toordinal()
39+
pg_minus_date_offset = -pg_date_offset
5040

5141
## Difference between PostgreSQL epoch and Unix epoch.
5242
## Used to convert a PostgreSQL ordinal to an ordinal usable by datetime
5343
pg_time_days = (pg_date_offset - datetime.date(1970, 1, 1).toordinal())
5444

55-
toordinal = methodcaller("toordinal")
5645
convert_to_utc = methodcaller('astimezone', UTC)
5746
remove_tzinfo = methodcaller('replace', tzinfo = None)
5847
set_as_utc = methodcaller('replace', tzinfo = UTC)
5948

49+
##
50+
# Constants used to special case infinity and -infinity.
51+
time64_pack_constants = {
52+
infinity_datetime: lib.time64_infinity,
53+
negative_infinity_datetime: lib.time64_negative_infinity,
54+
'infinity': lib.time64_infinity,
55+
'-infinity': lib.time64_negative_infinity,
56+
}
57+
time_pack_constants = {
58+
infinity_datetime: lib.time_infinity,
59+
negative_infinity_datetime: lib.time_negative_infinity,
60+
'infinity': lib.time_infinity,
61+
'-infinity': lib.time_negative_infinity,
62+
}
63+
date_pack_constants = {
64+
infinity_date: lib.date_infinity,
65+
negative_infinity_date: lib.date_negative_infinity,
66+
'infinity': lib.date_infinity,
67+
'-infinity': lib.date_negative_infinity,
68+
}
69+
time64_unpack_constants = {
70+
lib.time64_infinity: infinity_datetime,
71+
lib.time64_negative_infinity: negative_infinity_datetime,
72+
}
73+
time_unpack_constants = {
74+
lib.time_infinity: infinity_datetime,
75+
lib.time_negative_infinity: negative_infinity_datetime,
76+
}
77+
date_unpack_constants = {
78+
lib.date_infinity: infinity_date,
79+
lib.date_negative_infinity: negative_infinity_date,
80+
}
81+
6082
date_pack = compose((
61-
toordinal, partial(add, -pg_date_offset), lib.date_pack,
83+
methodcaller("toordinal"), partial(add, pg_minus_date_offset), lib.date_pack,
6284
))
6385
date_unpack = compose((
6486
lib.date_unpack, partial(add, pg_date_offset), datetime.date.fromordinal
@@ -91,7 +113,7 @@ def time_pack(x):
91113
x.microsecond
92114
)
93115

94-
def time_unpack(seconds_ms, time = datetime.time):
116+
def time_unpack(seconds_ms, time = datetime.time, divmod = divmod):
95117
"""
96118
Create a `datetime.time` instance from a (seconds, microseconds) pair.
97119
Seconds being offset from epoch.
@@ -106,9 +128,7 @@ def interval_pack(x):
106128
Create a (months, days, (seconds, microseconds)) tuple from a
107129
`datetime.timedelta` instance.
108130
"""
109-
return (
110-
0, x.days, (x.seconds, x.microseconds)
111-
)
131+
return (0, x.days, (x.seconds, x.microseconds))
112132

113133
def interval_unpack(mds, timedelta = datetime.timedelta):
114134
"""
@@ -117,6 +137,7 @@ def interval_unpack(mds, timedelta = datetime.timedelta):
117137
"""
118138
months, days, seconds_ms = mds
119139
if months != 0:
140+
# XXX: Should this raise an exception?
120141
w = pg_exc.TypeConversionWarning(
121142
"datetime.timedelta cannot represent relative intervals",
122143
details = {
@@ -153,6 +174,13 @@ def timetz_unpack(tstz):
153174
NoDay = True
154175
WithDay = False
155176

177+
# Used to handle the special cases: infinity and -infinity.
178+
def proc_when_not_in(proc, dict):
179+
def _proc(x):
180+
r = dict.get(x)
181+
return r or proc(x)
182+
return _proc
183+
156184
id_to_io = {
157185
(FloatTimes, TIMEOID) : (
158186
compose((time_pack, lib.time_pack)),
@@ -165,13 +193,13 @@ def timetz_unpack(tstz):
165193
datetime.time
166194
),
167195
(FloatTimes, TIMESTAMPOID) : (
168-
compose((timestamp_pack, lib.time_pack)),
169-
compose((lib.time_unpack, timestamp_unpack)),
196+
proc_when_not_in(compose((timestamp_pack, lib.time_pack)), time_pack_constants),
197+
proc_when_not_in(compose((lib.time_unpack, timestamp_unpack)), time_unpack_constants),
170198
datetime.datetime
171199
),
172200
(FloatTimes, TIMESTAMPTZOID) : (
173-
compose((convert_to_utc, remove_tzinfo, timestamp_pack, lib.time_pack)),
174-
compose((lib.time_unpack, timestamp_unpack, set_as_utc)),
201+
proc_when_not_in(compose((convert_to_utc, remove_tzinfo, timestamp_pack, lib.time_pack)), time_pack_constants),
202+
proc_when_not_in(compose((lib.time_unpack, timestamp_unpack, set_as_utc)), time_unpack_constants),
175203
datetime.datetime
176204
),
177205
(FloatTimes, WithDay, INTERVALOID): (
@@ -196,13 +224,13 @@ def timetz_unpack(tstz):
196224
datetime.time
197225
),
198226
(IntTimes, TIMESTAMPOID) : (
199-
compose((timestamp_pack, lib.time64_pack)),
200-
compose((lib.time64_unpack, timestamp_unpack)),
227+
proc_when_not_in(compose((timestamp_pack, lib.time64_pack)), time64_pack_constants),
228+
proc_when_not_in(compose((lib.time64_unpack, timestamp_unpack)), time64_unpack_constants),
201229
datetime.datetime
202230
),
203231
(IntTimes, TIMESTAMPTZOID) : (
204-
compose((convert_to_utc, remove_tzinfo, timestamp_pack, lib.time64_pack)),
205-
compose((lib.time64_unpack, timestamp_unpack, set_as_utc)),
232+
proc_when_not_in(compose((convert_to_utc, remove_tzinfo, timestamp_pack, lib.time64_pack)), time64_pack_constants),
233+
proc_when_not_in(compose((lib.time64_unpack, timestamp_unpack, set_as_utc)), time64_unpack_constants),
206234
datetime.datetime
207235
),
208236
(IntTimes, WithDay, INTERVALOID) : (
@@ -233,10 +261,14 @@ def select_format(oid, typio, get = id_to_io.__getitem__):
233261
return get((time_type(typio), oid))
234262

235263
def select_day_format(oid, typio, get = id_to_io.__getitem__):
236-
return get((time_type(typio), typio.database.version_info <= (8,0), oid))
264+
return get((time_type(typio), typio.database.version_info[:2] <= (8,0), oid))
237265

238266
oid_to_io = {
239-
DATEOID : (date_pack, date_unpack, datetime.date),
267+
DATEOID : (
268+
proc_when_not_in(date_pack, date_pack_constants),
269+
proc_when_not_in(date_unpack, date_unpack_constants),
270+
datetime.date,
271+
),
240272
TIMEOID : select_format,
241273
TIMETZOID : select_format,
242274
TIMESTAMPOID : select_format,

0 commit comments

Comments
 (0)