Skip to content

Commit 900621e

Browse files
author
Samuel Massinon
authored
Binary TimeType and Intervals (#229)
Binary Transfer for DateTime, ZonedDateTime, InfExtendedTime, and Intervals types
1 parent 576a330 commit 900621e

File tree

3 files changed

+145
-17
lines changed

3 files changed

+145
-17
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
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.7.0"
4+
version = "1.8.0"
55

66
[deps]
77
BinaryProvider = "b99e7846-7c00-51b0-8f62-c81ae34c0232"

src/parsing.jl

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,18 @@ function pqparse end
137137
# Fallback method
138138
pqparse(::Type{T}, str::AbstractString) where T = parse(T, str)
139139

140+
function pqparse(::Type{T}, ptr::Ptr{UInt8}) where T<:Number
141+
return ntoh(unsafe_load(Ptr{T}(ptr)))
142+
end
143+
140144
# allow parsing as a Symbol anything which works as a String
141145
pqparse(::Type{Symbol}, str::AbstractString) = Symbol(str)
142146

143147
function generate_binary_parser(symbol)
144148
@eval function Base.parse(
145149
::Type{T}, pqv::PQBinaryValue{$(oid(symbol))}
146150
) where T<:Number
147-
return convert(
148-
T, ntoh(unsafe_load(Ptr{$(_DEFAULT_TYPE_MAP[symbol])}(data_pointer(pqv))))
149-
)
151+
return convert(T, pqparse($(_DEFAULT_TYPE_MAP[symbol]), data_pointer(pqv)))
150152
end
151153
end
152154

@@ -316,14 +318,92 @@ function pqparse(::Type{InfExtendedTime{T}}, str::AbstractString) where T<:Dates
316318
end
317319

318320
# UNIX timestamps
319-
function Base.parse(::Type{DateTime}, pqv::PQTextValue{PQ_SYSTEM_TYPES[:int8]})
321+
function Base.parse(::Type{DateTime}, pqv::PQValue{PQ_SYSTEM_TYPES[:int8]})
320322
return unix2datetime(parse(Int64, pqv))
321323
end
322324

323-
function Base.parse(::Type{ZonedDateTime}, pqv::PQTextValue{PQ_SYSTEM_TYPES[:int8]})
325+
function Base.parse(::Type{ZonedDateTime}, pqv::PQValue{PQ_SYSTEM_TYPES[:int8]})
324326
return TimeZones.unix2zdt(parse(Int64, pqv))
325327
end
326328

329+
# All postgresql timestamptz are stored in UTC time with the epoch of 2000-01-01.
330+
const POSTGRES_EPOCH_DATE = Date("2000-01-01")
331+
const POSTGRES_EPOCH_DATETIME = DateTime("2000-01-01")
332+
333+
# Note: Because postgresql stores the values as a Microsecond in Int64, the max (infinite)
334+
# value of date time in postgresql when querying binary is 294277-01-09T04:00:54.775
335+
# and the minimum is -290278-12-22T19:59:05.225.
336+
function pqparse(::Type{ZonedDateTime}, ptr::Ptr{UInt8})
337+
value = ntoh(unsafe_load(Ptr{Int64}(ptr)))
338+
if value == typemax(Int64)
339+
depwarn_timetype_inf()
340+
return ZonedDateTime(typemax(DateTime), tz"UTC")
341+
elseif value == typemin(Int64)
342+
depwarn_timetype_inf()
343+
return ZonedDateTime(typemin(DateTime), tz"UTC")
344+
end
345+
dt = POSTGRES_EPOCH_DATETIME + Microsecond(value)
346+
return ZonedDateTime(dt, tz"UTC"; from_utc=true)
347+
end
348+
349+
function pqparse(::Type{DateTime}, ptr::Ptr{UInt8})
350+
value = ntoh(unsafe_load(Ptr{Int64}(ptr)))
351+
if value == typemax(Int64)
352+
depwarn_timetype_inf()
353+
return typemax(DateTime)
354+
elseif value == typemin(Int64)
355+
depwarn_timetype_inf()
356+
return typemin(DateTime)
357+
end
358+
return POSTGRES_EPOCH_DATETIME + Microsecond(ntoh(unsafe_load(Ptr{Int64}(ptr))))
359+
end
360+
361+
function pqparse(::Type{Date}, ptr::Ptr{UInt8})
362+
value = ntoh(unsafe_load(Ptr{Int32}(ptr)))
363+
if value == typemax(Int32)
364+
depwarn_timetype_inf()
365+
return typemax(Date)
366+
elseif value == typemin(Int32)
367+
depwarn_timetype_inf()
368+
return typemin(Date)
369+
end
370+
return POSTGRES_EPOCH_DATE + Day(value)
371+
end
372+
373+
function pqparse(
374+
::Type{InfExtendedTime{T}}, ptr::Ptr{UInt8}
375+
) where T<:Dates.AbstractDateTime
376+
microseconds = ntoh(unsafe_load(Ptr{Int64}(ptr)))
377+
if microseconds == typemax(Int64)
378+
return InfExtendedTime{T}(∞)
379+
elseif microseconds == typemin(Int64)
380+
return InfExtendedTime{T}(-∞)
381+
end
382+
383+
return InfExtendedTime{T}(pqparse(T, ptr))
384+
end
385+
386+
function pqparse(::Type{InfExtendedTime{T}}, ptr::Ptr{UInt8}) where T<:Date
387+
microseconds = ntoh(unsafe_load(Ptr{Int32}(ptr)))
388+
if microseconds == typemax(Int32)
389+
return InfExtendedTime{T}(∞)
390+
elseif microseconds == typemin(Int32)
391+
return InfExtendedTime{T}(-∞)
392+
end
393+
394+
return InfExtendedTime{T}(pqparse(T, ptr))
395+
end
396+
397+
function generate_binary_date_parser(symbol)
398+
@eval function Base.parse(
399+
::Type{T}, pqv::PQBinaryValue{$(oid(symbol))}
400+
) where T<:TimeType
401+
return pqparse(T, data_pointer(pqv))
402+
end
403+
end
404+
405+
foreach(generate_binary_date_parser, (:timestamptz, :timestamp, :date))
406+
327407
## intervals
328408
# iso_8601
329409
_DEFAULT_TYPE_MAP[:interval] = Dates.CompoundPeriod
@@ -417,6 +497,56 @@ function pqparse(::Type{Interval{T}}, str::AbstractString) where T
417497
return parse(Interval{T}, str; element_parser=pqparse)
418498
end
419499

500+
# How to parse range binary fetch is shown here
501+
# https://github.com/postgres/postgres/blob/31079a4a8e66e56e48bad94d380fa6224e9ffa0d/src/backend/utils/adt/rangetypes.c#L162
502+
const RANGE_EMPTY = 0b00000001
503+
const RANGE_LOWER_BOUND_INCLUSIVE = 0b00000010
504+
const RANGE_UPPER_BOUND_INCLUSIVE = 0b00000100
505+
const RANGE_LOWER_BOUND_INFINITIY = 0b00001000
506+
const RANGE_UPPER_BOUND_INFINITIY = 0b00010000
507+
const RANGE_LOWER_BOUND_NULL = 0b00100000
508+
const RANGE_UPPER_BOUND_NULL = 0b01000000
509+
510+
function generate_range_binary_parser(symbol)
511+
@eval function Base.parse(
512+
::Type{Interval{T}}, pqv::PQBinaryValue{$(oid(symbol))}
513+
) where T
514+
current_pointer = data_pointer(pqv)
515+
flags = ntoh(unsafe_load(Ptr{UInt8}(current_pointer)))
516+
current_pointer += sizeof(UInt8)
517+
518+
Bool(flags & RANGE_EMPTY) && return Interval{T}()
519+
520+
lower_value = nothing
521+
lower_bound = Unbounded
522+
# if there is a lower bound
523+
if iszero(flags & (RANGE_LOWER_BOUND_INFINITIY | RANGE_LOWER_BOUND_NULL))
524+
lower_value_length = ntoh(unsafe_load(Ptr{UInt32}(current_pointer)))
525+
current_pointer += sizeof(UInt32)
526+
lower_value = pqparse(T, current_pointer)
527+
current_pointer += lower_value_length
528+
lower_bound = !iszero(flags & RANGE_LOWER_BOUND_INCLUSIVE) ? Closed : Open
529+
end
530+
531+
upper_value = nothing
532+
upper_bound = Unbounded
533+
# if there is a upper bound
534+
if iszero(flags & (RANGE_UPPER_BOUND_INFINITIY | RANGE_UPPER_BOUND_NULL))
535+
upper_value_length = ntoh(unsafe_load(Ptr{UInt32}(current_pointer)))
536+
current_pointer += sizeof(UInt32)
537+
upper_value = pqparse(T, current_pointer)
538+
current_pointer += upper_value_length
539+
upper_bound = !iszero(flags & RANGE_UPPER_BOUND_INCLUSIVE) ? Closed : Open
540+
end
541+
542+
return Interval{T,lower_bound,upper_bound}(lower_value, upper_value)
543+
end
544+
end
545+
546+
foreach(
547+
generate_range_binary_parser, (:int4range, :int8range, :tsrange, :tstzrange, :daterange)
548+
)
549+
420550
## arrays
421551
# numeric arrays never have double quotes and always use ',' as a separator
422552
parse_numeric_element(::Type{T}, str) where T = parse(T, str)

test/runtests.jl

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,22 +1149,13 @@ end
11491149

11501150
@testset "Parsing" begin
11511151

1152-
binary_not_implemented_pgtypes = [
1153-
"numeric",
1154-
"timestamp",
1155-
"timestamptz",
1156-
"tstzrange",
1157-
]
1152+
binary_not_implemented_pgtypes = ["numeric", "numrange"]
11581153
binary_not_implemented_types = [
11591154
Decimal,
1160-
DateTime,
1161-
ZonedDateTime,
1162-
Date,
11631155
Time,
11641156
Dates.CompoundPeriod,
11651157
Array,
11661158
OffsetArray,
1167-
Interval,
11681159
]
11691160

11701161
@testset for binary_format in (LibPQ.TEXT, LibPQ.BINARY)
@@ -1218,11 +1209,13 @@ end
12181209
("TIMESTAMP '2004-10-19 10:23:54.123'", DateTime(2004, 10, 19, 10, 23, 54,123)),
12191210
("TIMESTAMP '2004-10-19 10:23:54.1234'", DateTime(2004, 10, 19, 10, 23, 54,123)),
12201211
("'infinity'::timestamp", typemax(DateTime)),
1212+
("'infinity'::timestamp", typemax(DateTime)),
12211213
("'-infinity'::timestamp", typemin(DateTime)),
12221214
("'epoch'::timestamp", DateTime(1970, 1, 1, 0, 0, 0)),
12231215
("TIMESTAMP WITH TIME ZONE '2004-10-19 10:23:54-00'", ZonedDateTime(2004, 10, 19, 10, 23, 54, tz"UTC")),
12241216
("TIMESTAMP WITH TIME ZONE '2004-10-19 10:23:54-02'", ZonedDateTime(2004, 10, 19, 10, 23, 54, tz"UTC-2")),
12251217
("TIMESTAMP WITH TIME ZONE '2004-10-19 10:23:54+10'", ZonedDateTime(2004, 10, 19, 10, 23, 54, tz"UTC+10")),
1218+
("TIMESTAMP WITH TIME ZONE '294276-12-31T23:59:59.999'", ZonedDateTime(294276, 12, 31, 23, 59, 59, 999, tz"UTC")),
12261219
("'infinity'::timestamptz", ZonedDateTime(typemax(DateTime), tz"UTC")),
12271220
("'-infinity'::timestamptz", ZonedDateTime(typemin(DateTime), tz"UTC")),
12281221
("'epoch'::timestamptz", ZonedDateTime(1970, 1, 1, 0, 0, 0, tz"UTC")),
@@ -1301,7 +1294,10 @@ end
13011294

13021295
oid = LibPQ.column_oids(result)[1]
13031296
func = result.column_funcs[1]
1304-
if binary_format && any(T -> data isa T, binary_not_implemented_types)
1297+
if binary_format && (
1298+
any(T -> data isa T, binary_not_implemented_types) ||
1299+
any(occursin.(binary_not_implemented_pgtypes, test_str))
1300+
)
13051301
@test_broken parsed = func(LibPQ.PQValue{oid}(result, 1, 1))
13061302
@test_broken isequal(parsed, data)
13071303
@test_broken typeof(parsed) == typeof(data)
@@ -1339,6 +1335,8 @@ end
13391335
("0::int8", DateTime, DateTime(1970, 1, 1, 0)),
13401336
("0::int8", ZonedDateTime, ZonedDateTime(1970, 1, 1, 0, tz"UTC")),
13411337
("'{{{1,2,3},{4,5,6}}}'::int2[]", AbstractArray{Int16}, reshape(Int16[1 2 3; 4 5 6], 1, 2, 3)),
1338+
("DATE '2017-01-31'", InfExtendedTime{Date}, InfExtendedTime{Date}(Date(2017, 1, 31))),
1339+
("'infinity'::timestamp", InfExtendedTime{Date}, InfExtendedTime{Date}(∞)),
13421340
("'infinity'::timestamp", InfExtendedTime{Date}, InfExtendedTime{Date}(∞)),
13431341
("'-infinity'::timestamp", InfExtendedTime{Date}, InfExtendedTime{Date}(-∞)),
13441342
("'infinity'::timestamptz", InfExtendedTime{ZonedDateTime}, InfExtendedTime{ZonedDateTime}(∞)),

0 commit comments

Comments
 (0)