Skip to content

Commit

Permalink
Merge pull request #264 from invenia/rf/utcdatetimes-post-merge-changes
Browse files Browse the repository at this point in the history
Addressing post-merge comments on #263
  • Loading branch information
rofinn authored Feb 23, 2023
2 parents 4306505 + 6458712 commit 2d45074
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 83 deletions.
2 changes: 1 addition & 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.15.0"
version = "1.15.1"

[deps]
CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82"
Expand Down
98 changes: 39 additions & 59 deletions src/parsing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -238,77 +238,66 @@ end
# see https://github.com/invenia/LibPQ.jl/issues/33
_trunc_seconds(str) = replace(str, r"(\.[\d]{3})\d+" => s"\g<1>")

_DEFAULT_TYPE_MAP[:timestamp] = DateTime
const TIMESTAMP_FORMAT = dateformat"y-m-d HH:MM:SS.s" # .s is optional here
function pqparse(::Type{DateTime}, str::AbstractString)
# Utility function for handling "infinity" strings for datetime types to reduce duplication
function _tryparse_datetime_inf(
typ::Type{T}, str, f=typ
)::Union{T, Nothing} where T <: Dates.AbstractDateTime
if str == "infinity"
depwarn_timetype_inf()
return typemax(DateTime)
return f(typemax(DateTime))
elseif str == "-infinity"
depwarn_timetype_inf()
return typemin(DateTime)
return f(typemin(DateTime))
end

# Cut off digits after the third after the decimal point,
# since DateTime in Julia currently handles only milliseconds, see Issue #33
str = replace(str, r"(\.[\d]{3})\d+" => s"\g<1>")
return parse(DateTime, str, TIMESTAMP_FORMAT)
return nothing
end

_DEFAULT_TYPE_MAP[:timestamp] = DateTime
const TIMESTAMP_FORMAT = dateformat"y-m-d HH:MM:SS.s" # .s is optional here
function pqparse(::Type{DateTime}, str::AbstractString)
parsed = _tryparse_datetime_inf(DateTime, str)
isnothing(parsed) || return parsed

parsed = tryparse(DateTime, str, TIMESTAMP_FORMAT)
isnothing(parsed) || return parsed

return parse(DateTime, _trunc_seconds(str), TIMESTAMP_FORMAT)
end

# ISO, YMD
_DEFAULT_TYPE_MAP[:timestamptz] = ZonedDateTime
const TIMESTAMPTZ_ZDT_FORMATS = (
const TIMESTAMPTZ_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{ZonedDateTime}, str::AbstractString)
parsed = _tryparse_datetime_inf(ZonedDateTime, str, Base.Fix2(ZonedDateTime, tz"UTC"))
isnothing(parsed) || return parsed

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
for fmt in TIMESTAMPTZ_FORMATS[1:(end - 1)]
parsed = tryparse(ZonedDateTime, str, fmt)
isnothing(parsed) || return parsed
end

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

function pqparse(::Type{ZonedDateTime}, str::AbstractString)
if str == "infinity"
depwarn_timetype_inf()
return ZonedDateTime(typemax(DateTime), tz"UTC")
elseif str == "-infinity"
depwarn_timetype_inf()
return ZonedDateTime(typemin(DateTime), tz"UTC")
end
function pqparse(::Type{UTCDateTime}, str::AbstractString)
parsed = _tryparse_datetime_inf(UTCDateTime, str)
isnothing(parsed) || return parsed

return _pqparse(ZonedDateTime, str)
end
# Postgres should always give us strings ending with +00 if our timezone is set to UTC
# which is the default
str = replace(str, "+00" => "")

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
parsed = tryparse(UTCDateTime, str, TIMESTAMP_FORMAT)
isnothing(parsed) || return parsed

# 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|:]*$" => ""))
return parse(UTCDateTime, _trunc_seconds(str), TIMESTAMP_FORMAT)
end

_DEFAULT_TYPE_MAP[:date] = Date
Expand Down Expand Up @@ -363,7 +352,7 @@ function Base.parse(::Type{ZonedDateTime}, pqv::PQValue{PQ_SYSTEM_TYPES[:int8]})
end

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

# All postgresql timestamptz are stored in UTC time with the epoch of 2000-01-01.
Expand All @@ -387,16 +376,7 @@ function pqparse(::Type{ZonedDateTime}, ptr::Ptr{UInt8})
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)
return UTCDateTime(pqparse(DateTime, ptr))
end

function pqparse(::Type{DateTime}, ptr::Ptr{UInt8})
Expand All @@ -408,7 +388,7 @@ function pqparse(::Type{DateTime}, ptr::Ptr{UInt8})
depwarn_timetype_inf()
return typemin(DateTime)
end
return POSTGRES_EPOCH_DATETIME + Microsecond(ntoh(unsafe_load(Ptr{Int64}(ptr))))
return POSTGRES_EPOCH_DATETIME + Microsecond(value)
end

function pqparse(::Type{Date}, ptr::Ptr{UInt8})
Expand Down
34 changes: 11 additions & 23 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1327,29 +1327,6 @@ 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 All @@ -1369,6 +1346,13 @@ end
("'foobar'", Symbol, :foobar),
("0::int8", DateTime, DateTime(1970, 1, 1, 0)),
("0::int8", ZonedDateTime, ZonedDateTime(1970, 1, 1, 0, tz"UTC")),
("0::int8", UTCDateTime, UTCDateTime(1970, 1, 1, 0)),
("'2004-10-19 10:23:54+00'::timestamptz", UTCDateTime, UTCDateTime(2004, 10, 19, 10, 23, 54)),
("'2004-10-19 10:23:54-06'::timestamptz", UTCDateTime, UTCDateTime(2004, 10, 19, 16, 23, 54)),
("'[2010-01-01 14:30-00, 2010-01-01 15:30-00)'::tstzrange", Interval{UTCDateTime}, Interval{Closed, Open}(UTCDateTime(2010, 1, 1, 14, 30), UTCDateTime(2010, 1, 1, 15, 30))),
("'[2004-10-19 10:23:54-02, 2004-10-19 11:23:54-02)'::tstzrange", Interval{UTCDateTime}, Interval{Closed, Open}(UTCDateTime(2004, 10, 19, 12, 23, 54), UTCDateTime(2004, 10, 19, 13, 23, 54))),
("'[2004-10-19 10:23:54-02, Infinity)'::tstzrange", Interval{UTCDateTime}, Interval{Closed, Open}(UTCDateTime(2004, 10, 19, 12, 23, 54), UTCDateTime(typemax(DateTime)))),
("'(-Infinity, Infinity)'::tstzrange", Interval{UTCDateTime}, Interval{Open, Open}(UTCDateTime(typemin(DateTime)), UTCDateTime(typemax(DateTime)))),
("'{{{1,2,3},{4,5,6}}}'::int2[]", AbstractArray{Int16}, reshape(Int16[1 2 3; 4 5 6], 1, 2, 3)),
("DATE '2017-01-31'", InfExtendedTime{Date}, InfExtendedTime{Date}(Date(2017, 1, 31))),
("'infinity'::timestamp", InfExtendedTime{Date}, InfExtendedTime{Date}(∞)),
Expand All @@ -1378,6 +1362,10 @@ end
("'-infinity'::timestamptz", InfExtendedTime{ZonedDateTime}, InfExtendedTime{ZonedDateTime}(-∞)),
("'[2004-10-19 10:23:54-02, infinity)'::tstzrange", Interval{InfExtendedTime{ZonedDateTime}}, Interval{Closed, Open}(ZonedDateTime(2004, 10, 19, 12, 23, 54, tz"UTC"), ∞)),
("'(-infinity, infinity)'::tstzrange", Interval{InfExtendedTime{ZonedDateTime}}, Interval{InfExtendedTime{ZonedDateTime}, Open, Open}(-∞, ∞)),
("'infinity'::timestamptz", InfExtendedTime{UTCDateTime}, InfExtendedTime{UTCDateTime}(∞)),
("'-infinity'::timestamptz", InfExtendedTime{UTCDateTime}, InfExtendedTime{UTCDateTime}(-∞)),
("'[2004-10-19 10:23:54-02, infinity)'::tstzrange", Interval{InfExtendedTime{UTCDateTime}}, Interval{Closed, Open}(UTCDateTime(2004, 10, 19, 12, 23, 54), ∞)),
("'(-infinity, infinity)'::tstzrange", Interval{InfExtendedTime{UTCDateTime}}, Interval{InfExtendedTime{UTCDateTime}, Open, Open}(-∞, ∞)),
]

for (test_str, typ, data) in test_data
Expand Down

2 comments on commit 2d45074

@rofinn
Copy link
Contributor Author

@rofinn rofinn commented on 2d45074 Feb 23, 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/78385

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.1 -m "<description of version>" 2d45074678b51041567cf5715223533a76c605e8
git push origin v1.15.1

Please sign in to comment.