Skip to content

Commit

Permalink
Merge pull request #263 from invenia/rf/utcdatetimes
Browse files Browse the repository at this point in the history
Added support for parsing timestamptz as UTCDateTime
  • Loading branch information
rofinn authored Feb 22, 2023
2 parents e65ee52 + 5e03173 commit 4306505
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 6 deletions.
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "LibPQ"
uuid = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
license = "MIT"
version = "1.14.1"
version = "1.15.0"

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

[compat]
CEnum = "0.2, 0.3, 0.4"
Expand All @@ -36,6 +37,7 @@ OffsetArrays = "0.9.1, 0.10, 0.11, 1"
SQLStrings = "0.1"
Tables = "0.2, 1"
TimeZones = "0.9.2, 0.10, 0.11, 1"
UTCDateTimes = "1.5"
julia = "1.6"

[extras]
Expand Down
1 change: 1 addition & 0 deletions src/LibPQ.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ using Memento: Memento, getlogger, warn, info, error, debug
using OffsetArrays
using SQLStrings
using TimeZones
using UTCDateTimes

const Parameter = Union{String,Missing}
const LOGGER = getlogger(@__MODULE__)
Expand Down
58 changes: 53 additions & 5 deletions src/parsing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,32 @@ end

# ISO, YMD
_DEFAULT_TYPE_MAP[:timestamptz] = ZonedDateTime
const TIMESTAMPTZ_FORMATS = (
const TIMESTAMPTZ_ZDT_FORMATS = (
dateformat"y-m-d HH:MM:SSz",
dateformat"y-m-d HH:MM:SS.sz",
dateformat"y-m-d HH:MM:SS.ssz",
dateformat"y-m-d HH:MM:SS.sssz",
)
const TIMESTAMPTZ_UTC_FORMATS = (
dateformat"y-m-d HH:MM:SS",
dateformat"y-m-d HH:MM:SS.s",
dateformat"y-m-d HH:MM:SS.ss",
dateformat"y-m-d HH:MM:SS.sss",
)

timestamptz_formats(::Type{ZonedDateTime}) = TIMESTAMPTZ_ZDT_FORMATS
timestamptz_formats(::Type{UTCDateTime}) = TIMESTAMPTZ_UTC_FORMATS

function _pqparse(::Type{T}, str::AbstractString) where T<:Union{UTCDateTime, ZonedDateTime}
formats = timestamptz_formats(T)
for fmt in formats[1:(end - 1)]
parsed = tryparse(T, str, fmt)
parsed !== nothing && return parsed
end

return parse(T, _trunc_seconds(str), formats[end])
end

function pqparse(::Type{ZonedDateTime}, str::AbstractString)
if str == "infinity"
depwarn_timetype_inf()
Expand All @@ -272,12 +292,23 @@ function pqparse(::Type{ZonedDateTime}, str::AbstractString)
return ZonedDateTime(typemin(DateTime), tz"UTC")
end

for fmt in TIMESTAMPTZ_FORMATS[1:(end - 1)]
parsed = tryparse(ZonedDateTime, str, fmt)
parsed !== nothing && return parsed
return _pqparse(ZonedDateTime, str)
end

function pqparse(::Type{UTCDateTime}, str::AbstractString)
if str == "infinity"
depwarn_timetype_inf()
return UTCDateTime(typemax(DateTime))
elseif str == "-infinity"
depwarn_timetype_inf()
return UTCDateTime(typemin(DateTime))
end

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

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

function Base.parse(::Type{UTCDateTime}, pqv::PQValue{PQ_SYSTEM_TYPES[:int8]})
return UTCDateTime(unix2datetime(parse(Int64, pqv)))
end

# All postgresql timestamptz are stored in UTC time with the epoch of 2000-01-01.
const POSTGRES_EPOCH_DATE = Date("2000-01-01")
const POSTGRES_EPOCH_DATETIME = DateTime("2000-01-01")
Expand All @@ -351,6 +386,19 @@ function pqparse(::Type{ZonedDateTime}, ptr::Ptr{UInt8})
return ZonedDateTime(dt, tz"UTC"; from_utc=true)
end

function pqparse(::Type{UTCDateTime}, ptr::Ptr{UInt8})
value = ntoh(unsafe_load(Ptr{Int64}(ptr)))
if value == typemax(Int64)
depwarn_timetype_inf()
return UTCDateTime(typemax(DateTime))
elseif value == typemin(Int64)
depwarn_timetype_inf()
return UTCDateTime(typemin(DateTime))
end
dt = POSTGRES_EPOCH_DATETIME + Microsecond(value)
return UTCDateTime(dt)
end

function pqparse(::Type{DateTime}, ptr::Ptr{UInt8})
value = ntoh(unsafe_load(Ptr{Int64}(ptr)))
if value == typemax(Int64)
Expand Down
24 changes: 24 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ using OffsetArrays
using SQLStrings
using TimeZones
using Tables
using UTCDateTimes

Memento.config!("critical")

Expand Down Expand Up @@ -1326,6 +1327,29 @@ end
finally
close(result)
end

# Test parsing timestamptz as UTCDateTime
if data isa ZonedDateTime
try
result = execute(
conn,
"SELECT $test_str;";
binary_format=binary_format,
type_map=Dict(:timestamptz => UTCDateTime),
)

oid = LibPQ.column_oids(result)[1]
func = result.column_funcs[1]
parsed = func(LibPQ.PQValue{oid}(result, 1, 1))
@test isequal(parsed, data)
@test typeof(parsed) == UTCDateTime
parsed_no_oid = func(LibPQ.PQValue(result, 1, 1))
@test isequal(parsed_no_oid, data)
@test typeof(parsed_no_oid) == UTCDateTime
finally
close(result)
end
end
end

close(conn)
Expand Down

2 comments on commit 4306505

@rofinn
Copy link
Contributor Author

@rofinn rofinn commented on 4306505 Feb 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/78271

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v1.15.0 -m "<description of version>" 43065052195f350933a456a1786485b77a9125dc
git push origin v1.15.0

Please sign in to comment.