Skip to content

Commit 4306505

Browse files
authored
Merge pull request #263 from invenia/rf/utcdatetimes
Added support for parsing timestamptz as UTCDateTime
2 parents e65ee52 + 5e03173 commit 4306505

File tree

4 files changed

+81
-6
lines changed

4 files changed

+81
-6
lines changed

Project.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "LibPQ"
22
uuid = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
33
license = "MIT"
4-
version = "1.14.1"
4+
version = "1.15.0"
55

66
[deps]
77
CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82"
@@ -20,6 +20,7 @@ OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
2020
SQLStrings = "af517c2e-c243-48fa-aab8-efac3db270f5"
2121
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
2222
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
23+
UTCDateTimes = "0f7cfa37-7abf-4834-b969-a8aa512401c2"
2324

2425
[compat]
2526
CEnum = "0.2, 0.3, 0.4"
@@ -36,6 +37,7 @@ OffsetArrays = "0.9.1, 0.10, 0.11, 1"
3637
SQLStrings = "0.1"
3738
Tables = "0.2, 1"
3839
TimeZones = "0.9.2, 0.10, 0.11, 1"
40+
UTCDateTimes = "1.5"
3941
julia = "1.6"
4042

4143
[extras]

src/LibPQ.jl

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ using Memento: Memento, getlogger, warn, info, error, debug
2828
using OffsetArrays
2929
using SQLStrings
3030
using TimeZones
31+
using UTCDateTimes
3132

3233
const Parameter = Union{String,Missing}
3334
const LOGGER = getlogger(@__MODULE__)

src/parsing.jl

+53-5
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,32 @@ end
257257

258258
# ISO, YMD
259259
_DEFAULT_TYPE_MAP[:timestamptz] = ZonedDateTime
260-
const TIMESTAMPTZ_FORMATS = (
260+
const TIMESTAMPTZ_ZDT_FORMATS = (
261261
dateformat"y-m-d HH:MM:SSz",
262262
dateformat"y-m-d HH:MM:SS.sz",
263263
dateformat"y-m-d HH:MM:SS.ssz",
264264
dateformat"y-m-d HH:MM:SS.sssz",
265265
)
266+
const TIMESTAMPTZ_UTC_FORMATS = (
267+
dateformat"y-m-d HH:MM:SS",
268+
dateformat"y-m-d HH:MM:SS.s",
269+
dateformat"y-m-d HH:MM:SS.ss",
270+
dateformat"y-m-d HH:MM:SS.sss",
271+
)
272+
273+
timestamptz_formats(::Type{ZonedDateTime}) = TIMESTAMPTZ_ZDT_FORMATS
274+
timestamptz_formats(::Type{UTCDateTime}) = TIMESTAMPTZ_UTC_FORMATS
275+
276+
function _pqparse(::Type{T}, str::AbstractString) where T<:Union{UTCDateTime, ZonedDateTime}
277+
formats = timestamptz_formats(T)
278+
for fmt in formats[1:(end - 1)]
279+
parsed = tryparse(T, str, fmt)
280+
parsed !== nothing && return parsed
281+
end
282+
283+
return parse(T, _trunc_seconds(str), formats[end])
284+
end
285+
266286
function pqparse(::Type{ZonedDateTime}, str::AbstractString)
267287
if str == "infinity"
268288
depwarn_timetype_inf()
@@ -272,12 +292,23 @@ function pqparse(::Type{ZonedDateTime}, str::AbstractString)
272292
return ZonedDateTime(typemin(DateTime), tz"UTC")
273293
end
274294

275-
for fmt in TIMESTAMPTZ_FORMATS[1:(end - 1)]
276-
parsed = tryparse(ZonedDateTime, str, fmt)
277-
parsed !== nothing && return parsed
295+
return _pqparse(ZonedDateTime, str)
296+
end
297+
298+
function pqparse(::Type{UTCDateTime}, str::AbstractString)
299+
if str == "infinity"
300+
depwarn_timetype_inf()
301+
return UTCDateTime(typemax(DateTime))
302+
elseif str == "-infinity"
303+
depwarn_timetype_inf()
304+
return UTCDateTime(typemin(DateTime))
278305
end
279306

280-
return parse(ZonedDateTime, _trunc_seconds(str), TIMESTAMPTZ_FORMATS[end])
307+
# Postgres should give us strings ending with +00, +00:00, -00:00
308+
# We use the regex below to strip these character off before parsing, iff,
309+
# the values after the `-`/`+` are `0` or `:`. This means parsing will fail if
310+
# we're asked to parse a non-UTC string like +04:00.
311+
return _pqparse(UTCDateTime, replace(str, r"[-|\+][0|:]*$" => ""))
281312
end
282313

283314
_DEFAULT_TYPE_MAP[:date] = Date
@@ -331,6 +362,10 @@ function Base.parse(::Type{ZonedDateTime}, pqv::PQValue{PQ_SYSTEM_TYPES[:int8]})
331362
return TimeZones.unix2zdt(parse(Int64, pqv))
332363
end
333364

365+
function Base.parse(::Type{UTCDateTime}, pqv::PQValue{PQ_SYSTEM_TYPES[:int8]})
366+
return UTCDateTime(unix2datetime(parse(Int64, pqv)))
367+
end
368+
334369
# All postgresql timestamptz are stored in UTC time with the epoch of 2000-01-01.
335370
const POSTGRES_EPOCH_DATE = Date("2000-01-01")
336371
const POSTGRES_EPOCH_DATETIME = DateTime("2000-01-01")
@@ -351,6 +386,19 @@ function pqparse(::Type{ZonedDateTime}, ptr::Ptr{UInt8})
351386
return ZonedDateTime(dt, tz"UTC"; from_utc=true)
352387
end
353388

389+
function pqparse(::Type{UTCDateTime}, ptr::Ptr{UInt8})
390+
value = ntoh(unsafe_load(Ptr{Int64}(ptr)))
391+
if value == typemax(Int64)
392+
depwarn_timetype_inf()
393+
return UTCDateTime(typemax(DateTime))
394+
elseif value == typemin(Int64)
395+
depwarn_timetype_inf()
396+
return UTCDateTime(typemin(DateTime))
397+
end
398+
dt = POSTGRES_EPOCH_DATETIME + Microsecond(value)
399+
return UTCDateTime(dt)
400+
end
401+
354402
function pqparse(::Type{DateTime}, ptr::Ptr{UInt8})
355403
value = ntoh(unsafe_load(Ptr{Int64}(ptr)))
356404
if value == typemax(Int64)

test/runtests.jl

+24
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ using OffsetArrays
1313
using SQLStrings
1414
using TimeZones
1515
using Tables
16+
using UTCDateTimes
1617

1718
Memento.config!("critical")
1819

@@ -1326,6 +1327,29 @@ end
13261327
finally
13271328
close(result)
13281329
end
1330+
1331+
# Test parsing timestamptz as UTCDateTime
1332+
if data isa ZonedDateTime
1333+
try
1334+
result = execute(
1335+
conn,
1336+
"SELECT $test_str;";
1337+
binary_format=binary_format,
1338+
type_map=Dict(:timestamptz => UTCDateTime),
1339+
)
1340+
1341+
oid = LibPQ.column_oids(result)[1]
1342+
func = result.column_funcs[1]
1343+
parsed = func(LibPQ.PQValue{oid}(result, 1, 1))
1344+
@test isequal(parsed, data)
1345+
@test typeof(parsed) == UTCDateTime
1346+
parsed_no_oid = func(LibPQ.PQValue(result, 1, 1))
1347+
@test isequal(parsed_no_oid, data)
1348+
@test typeof(parsed_no_oid) == UTCDateTime
1349+
finally
1350+
close(result)
1351+
end
1352+
end
13291353
end
13301354

13311355
close(conn)

0 commit comments

Comments
 (0)