Skip to content

Commit 1ee5574

Browse files
authored
Merge pull request JuliaDatabases#153 from invenia/ne/ranges
Support Intervals.jl parsing for postgres ranges
2 parents dc3144f + debc21e commit 1ee5574

File tree

6 files changed

+199
-87
lines changed

6 files changed

+199
-87
lines changed

Project.toml

+4-2
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.0.7"
4+
version = "1.1.0"
55

66
[deps]
77
BinaryProvider = "b99e7846-7c00-51b0-8f62-c81ae34c0232"
@@ -10,6 +10,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
1010
Decimals = "abce61dc-4473-55a0-ba07-351d65e31d42"
1111
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
1212
FileWatching = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
13+
Intervals = "d8418881-c3e1-53bb-8760-2df7ec849ed5"
1314
IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
1415
LayerDicts = "6f188dcb-512c-564b-bc01-e0f76e72f166"
1516
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
@@ -22,8 +23,9 @@ TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
2223
BinaryProvider = "0.5"
2324
CEnum = "0.2"
2425
DataFrames = "0.14, 0.15, 0.16, 0.17, 0.18, 0.19"
25-
Decimals = "0.3.1, 0.4"
26+
Decimals = "0.4.1"
2627
DocStringExtensions = "0.8.0"
28+
Intervals = "1"
2729
IterTools = "1"
2830
LayerDicts = "1"
2931
Memento = "0.10, 0.11, 0.12, 0.13, 1"

docs/Manifest.toml

+35-23
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82"
1919
version = "0.2.0"
2020

2121
[[CategoricalArrays]]
22-
deps = ["Compat", "DataAPI", "Future", "JSON", "Missings", "Printf", "Reexport", "Unicode"]
23-
git-tree-sha1 = "4d85a015093760ff23b20f3e25fa195b4cb76794"
22+
deps = ["Compat", "DataAPI", "Future", "JSON", "Missings", "Printf", "Reexport", "Statistics", "Unicode"]
23+
git-tree-sha1 = "23d7324164c89638c18f6d7f90d972fa9c4fa9fb"
2424
uuid = "324d7699-5711-5eae-9e2f-1d82baa6b597"
25-
version = "0.7.3"
25+
version = "0.7.7"
2626

2727
[[Compat]]
2828
deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"]
@@ -43,9 +43,9 @@ version = "0.18.4"
4343

4444
[[DataStructures]]
4545
deps = ["InteractiveUtils", "OrderedCollections"]
46-
git-tree-sha1 = "a1b652fb77ae8ca7ea328fa7ba5aa151036e5c10"
46+
git-tree-sha1 = "b7720de347734f4716d1815b00ce5664ed6bbfd4"
4747
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
48-
version = "0.17.6"
48+
version = "0.17.9"
4949

5050
[[DataValueInterfaces]]
5151
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
@@ -57,10 +57,9 @@ deps = ["Printf"]
5757
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
5858

5959
[[Decimals]]
60-
deps = ["Test"]
61-
git-tree-sha1 = "b374d464d64470e743c5f9172c57352e1a9404ac"
60+
git-tree-sha1 = "e98abef36d02a0ec385d68cd7dadbce9b28cbd88"
6261
uuid = "abce61dc-4473-55a0-ba07-351d65e31d42"
63-
version = "0.4.0"
62+
version = "0.4.1"
6463

6564
[[DelimitedFiles]]
6665
deps = ["Mmap"]
@@ -82,11 +81,16 @@ git-tree-sha1 = "580155ffaeb175f37dc0bd31ed6c127663efbc60"
8281
uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
8382
version = "0.22.6"
8483

84+
[[ExprTools]]
85+
git-tree-sha1 = "08c1f74d9ad03acf0ee84c12c9e665ab1a9a6e33"
86+
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
87+
version = "0.1.0"
88+
8589
[[EzXML]]
8690
deps = ["BinaryProvider", "Libdl", "Printf"]
87-
git-tree-sha1 = "469de9cb996a9c03f31905619ff3c33e905711f3"
91+
git-tree-sha1 = "ed53ce1ddb862193b2bd3851dfa108513aab2d5c"
8892
uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
89-
version = "0.9.5"
93+
version = "1.0.0"
9094

9195
[[FileWatching]]
9296
uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
@@ -105,6 +109,12 @@ version = "0.1.1"
105109
deps = ["LinearAlgebra", "Markdown"]
106110
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
107111

112+
[[Intervals]]
113+
deps = ["Dates", "Printf", "TimeZones"]
114+
git-tree-sha1 = "30bce135cd5856edc91f146e3b7e60d736bd067b"
115+
uuid = "d8418881-c3e1-53bb-8760-2df7ec849ed5"
116+
version = "1.0.0"
117+
108118
[[IterTools]]
109119
git-tree-sha1 = "05110a2ab1fc5f932622ffea2a003221f4782c18"
110120
uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
@@ -136,10 +146,10 @@ version = "1.0.0"
136146
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
137147

138148
[[LibPQ]]
139-
deps = ["BinaryProvider", "CEnum", "Dates", "Decimals", "DocStringExtensions", "FileWatching", "IterTools", "LayerDicts", "Libdl", "Memento", "OffsetArrays", "Tables", "TimeZones"]
149+
deps = ["BinaryProvider", "CEnum", "Dates", "Decimals", "DocStringExtensions", "FileWatching", "Intervals", "IterTools", "LayerDicts", "Libdl", "Memento", "OffsetArrays", "Tables", "TimeZones"]
140150
path = ".."
141151
uuid = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
142-
version = "1.0.5"
152+
version = "1.1.0"
143153

144154
[[Libdl]]
145155
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
@@ -158,10 +168,10 @@ uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
158168
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
159169

160170
[[MacroTools]]
161-
deps = ["Compat", "DataStructures", "Test"]
162-
git-tree-sha1 = "82921f0e3bde6aebb8e524efc20f4042373c0c06"
171+
deps = ["DataStructures", "Markdown", "Random"]
172+
git-tree-sha1 = "e2fc7a55bb2224e203bbd8b59f72b91323233458"
163173
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
164-
version = "0.5.2"
174+
version = "0.5.3"
165175

166176
[[Markdown]]
167177
deps = ["Base64"]
@@ -183,14 +193,15 @@ version = "0.4.3"
183193
uuid = "a63ad114-7e13-5084-954f-fe012c677804"
184194

185195
[[Mocking]]
186-
git-tree-sha1 = "bd2623f8b728af988d2afec53d611acb621f3bc4"
196+
deps = ["ExprTools"]
197+
git-tree-sha1 = "916b850daad0d46b8c71f65f719c49957e9513ed"
187198
uuid = "78c3b35d-d492-501b-9361-3d52fe80e533"
188-
version = "0.7.0"
199+
version = "0.7.1"
189200

190201
[[OffsetArrays]]
191-
git-tree-sha1 = "1ae707306f6e33dbb37d0742e08828562772b73f"
202+
git-tree-sha1 = "87d0a91efe29352d5caaa271ae3927083c096e33"
192203
uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
193-
version = "0.11.2"
204+
version = "0.11.4"
194205

195206
[[OrderedCollections]]
196207
deps = ["Random", "Serialization", "Test"]
@@ -203,9 +214,10 @@ deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUID
203214
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
204215

205216
[[PooledArrays]]
206-
git-tree-sha1 = "6e8c38927cb6e9ae144f7277c753714861b27d14"
217+
deps = ["DataAPI"]
218+
git-tree-sha1 = "b1333d4eced1826e15adbdf01a4ecaccca9d353c"
207219
uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720"
208-
version = "0.5.2"
220+
version = "0.5.3"
209221

210222
[[Printf]]
211223
deps = ["Unicode"]
@@ -306,9 +318,9 @@ version = "3.0.5"
306318

307319
[[TimeZones]]
308320
deps = ["Dates", "EzXML", "Mocking", "Printf", "Serialization", "Unicode"]
309-
git-tree-sha1 = "29c7ae3f50f291358043e21db47b3e1a516df891"
321+
git-tree-sha1 = "fec20d06d8139c686e6e3ad026bc9cf60bf0c795"
310322
uuid = "f269a46b-ccf7-5d73-abea-4c690281aa53"
311-
version = "0.10.3"
323+
version = "1.0.0"
312324

313325
[[UUIDs]]
314326
deps = ["Random"]

src/LibPQ.jl

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ using DocStringExtensions
1313
using Decimals
1414
using FileWatching
1515
using Tables
16+
using Intervals
1617
using IterTools: imap
1718
using LayerDicts
1819
using Memento: Memento, getlogger, warn, info, error, debug

src/parsing.jl

+74-41
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ is not in UTF-8.
101101
bytes_view(pqv::PQValue) = unsafe_wrap(Vector{UInt8}, data_pointer(pqv), num_bytes(pqv) + 1)
102102

103103
Base.String(pqv::PQValue) = unsafe_string(pqv)
104-
Base.parse(::Type{String}, pqv::PQValue) = unsafe_string(pqv)
105104
Base.convert(::Type{String}, pqv::PQValue) = String(pqv)
106105
Base.length(pqv::PQValue) = length(string_view(pqv))
107106
Base.lastindex(pqv::PQValue) = lastindex(string_view(pqv))
@@ -115,23 +114,25 @@ Base.lastindex(pqv::PQValue) = lastindex(string_view(pqv))
115114
Parse a value of type `T` from a `PQValue`.
116115
By default, this uses any existing `parse` method for parsing a value of type `T` from a
117116
`String`.
117+
118+
You can implement default PostgreSQL-specific parsing for a given type by overriding
119+
`pqparse`.
118120
"""
119-
Base.parse(::Type{T}, pqv::PQValue) where {T} = parse(T, string_view(pqv))
121+
Base.parse(::Type{T}, pqv::PQValue) where {T} = pqparse(T, string_view(pqv))
120122

121-
# allow parsing as a Symbol anything which works as a String
122-
Base.parse(::Type{Symbol}, pqv::PQValue) = Symbol(string_view(pqv))
123+
"""
124+
LibPQ.pqparse(::Type{T}, str::AbstractString) -> T
123125
124-
function Base.iterate(pqv::PQValue)
125-
sv = string_view(pqv)
126-
iterate(pqv, (sv, ()))
127-
end
128-
function Base.iterate(pqv::PQValue, state)
129-
sv, i = state
130-
iter = iterate(sv, i...)
131-
iter === nothing && return nothing
132-
c, new_sv_state = iter
133-
return (c, (sv, (new_sv_state,)))
134-
end
126+
Parse a value of type `T` from any `AbstractString`.
127+
This is used to parse PostgreSQL's output format.
128+
"""
129+
function pqparse end
130+
131+
# Fallback method
132+
pqparse(::Type{T}, str::AbstractString) where {T} = parse(T, str)
133+
134+
# allow parsing as a Symbol anything which works as a String
135+
pqparse(::Type{Symbol}, str::AbstractString) = Symbol(str)
135136

136137
## integers
137138
_DEFAULT_TYPE_MAP[:int2] = Int16
@@ -152,21 +153,26 @@ _DEFAULT_TYPE_MAP[:numeric] = Decimal
152153

153154
## character
154155
# bpchar is char(n)
155-
function Base.parse(::Type{String}, pqv::PQValue{PQ_SYSTEM_TYPES[:bpchar]})
156-
return String(rstrip(string_view(pqv), ' '))
156+
function pqparse(::Type{String}, str::AbstractString)
157+
return String(rstrip(str, ' '))
157158
end
158159
# char is "char"
159160
_DEFAULT_TYPE_MAP[:char] = PQChar
160-
Base.parse(::Type{PQChar}, pqv::PQValue{PQ_SYSTEM_TYPES[:char]}) = PQChar(first(pqv))
161-
Base.parse(::Type{Char}, pqv::PQValue{PQ_SYSTEM_TYPES[:char]}) = Char(parse(PQChar, pqv))
161+
pqparse(::Type{PQChar}, str::AbstractString) = PQChar(first(str))
162+
pqparse(::Type{Char}, str::AbstractString) = Char(pqparse(PQChar, str))
162163
# varchar, text, and name are all String
163164

164165
## binary data
165166

166167
_DEFAULT_TYPE_MAP[:bytea] = Vector{UInt8}
168+
169+
# Needs it's own `parse` method as it uses bytes_view instead of string_view
167170
function Base.parse(::Type{Vector{UInt8}}, pqv::PQValue{PQ_SYSTEM_TYPES[:bytea]})
171+
pqparse(Vector{UInt8}, bytes_view(pqv))
172+
end
173+
174+
function pqparse(::Type{Vector{UInt8}}, bytes::Array{UInt8,1})
168175
byte_length = Ref{Csize_t}(0)
169-
bytes = bytes_view(pqv)
170176

171177
unescaped_ptr = libpq_c.PQunescapeBytea(bytes, byte_length)
172178

@@ -186,9 +192,7 @@ end
186192
_DEFAULT_TYPE_MAP[:bool] = Bool
187193
const BOOL_TRUE = r"^\s*(t|true|y|yes|on|1)\s*$"i
188194
const BOOL_FALSE = r"^\s*(f|false|n|no|off|0)\s*$"i
189-
function Base.parse(::Type{Bool}, pqv::PQValue{PQ_SYSTEM_TYPES[:bool]})
190-
str = string_view(pqv)
191-
195+
function pqparse(::Type{Bool}, str::AbstractString)
192196
if occursin(BOOL_TRUE, str)
193197
return true
194198
elseif occursin(BOOL_FALSE, str)
@@ -208,9 +212,7 @@ _trunc_seconds(str) = replace(str, r"(\.[\d]{3})\d+" => s"\g<1>")
208212

209213
_DEFAULT_TYPE_MAP[:timestamp] = DateTime
210214
const TIMESTAMP_FORMAT = dateformat"y-m-d HH:MM:SS.s" # .s is optional here
211-
function Base.parse(::Type{DateTime}, pqv::PQValue{PQ_SYSTEM_TYPES[:timestamp]})
212-
str = string_view(pqv)
213-
215+
function pqparse(::Type{DateTime}, str::AbstractString)
214216
if str == "infinity"
215217
return typemax(DateTime)
216218
elseif str == "-infinity"
@@ -231,9 +233,7 @@ const TIMESTAMPTZ_FORMATS = (
231233
dateformat"y-m-d HH:MM:SS.ssz",
232234
dateformat"y-m-d HH:MM:SS.sssz",
233235
)
234-
function Base.parse(::Type{ZonedDateTime}, pqv::PQValue{PQ_SYSTEM_TYPES[:timestamptz]})
235-
str = string_view(pqv)
236-
236+
function pqparse(::Type{ZonedDateTime}, str::AbstractString)
237237
if str == "infinity"
238238
return ZonedDateTime(typemax(DateTime), tz"UTC")
239239
elseif str == "-infinity"
@@ -249,9 +249,7 @@ function Base.parse(::Type{ZonedDateTime}, pqv::PQValue{PQ_SYSTEM_TYPES[:timesta
249249
end
250250

251251
_DEFAULT_TYPE_MAP[:date] = Date
252-
function Base.parse(::Type{Date}, pqv::PQValue{PQ_SYSTEM_TYPES[:date]})
253-
str = string_view(pqv)
254-
252+
function pqparse(::Type{Date}, str::AbstractString)
255253
if str == "infinity"
256254
return typemax(Date)
257255
elseif str == "-infinity"
@@ -262,9 +260,7 @@ function Base.parse(::Type{Date}, pqv::PQValue{PQ_SYSTEM_TYPES[:date]})
262260
end
263261

264262
_DEFAULT_TYPE_MAP[:time] = Time
265-
function Base.parse(::Type{Time}, pqv::PQValue{PQ_SYSTEM_TYPES[:time]})
266-
str = string_view(pqv)
267-
263+
function pqparse(::Type{Time}, str::AbstractString)
268264
try
269265
return parse(Time, str)
270266
catch err
@@ -316,12 +312,12 @@ end
316312

317313
# parse the iso_8601 interval output format
318314
# https://www.postgresql.org/docs/10/datatype-datetime.html#DATATYPE-INTERVAL-OUTPUT
319-
function Base.parse(::Type{Dates.CompoundPeriod}, pqv::PQValue{PQ_SYSTEM_TYPES[:interval]})
315+
function pqparse(::Type{Dates.CompoundPeriod}, str::AbstractString)
320316
interval_regex = INTERVAL_REGEX[]
321-
matched = match(interval_regex, string_view(pqv))
317+
matched = match(interval_regex, str)
322318

323319
if matched === nothing
324-
error("Couldn't parse $(string_view(pqv)) as interval using regex $interval_regex")
320+
error("Couldn't parse $str as interval using regex $interval_regex")
325321
end
326322

327323
periods = Period[]
@@ -364,6 +360,43 @@ function Base.parse(::Type{Dates.CompoundPeriod}, pqv::PQValue{PQ_SYSTEM_TYPES[:
364360
return Dates.CompoundPeriod(periods)
365361
end
366362

363+
## ranges
364+
_DEFAULT_TYPE_MAP[:int4range] = Interval{Int32}
365+
_DEFAULT_TYPE_MAP[:int8range] = Interval{Int64}
366+
_DEFAULT_TYPE_MAP[:numrange] = Interval{Decimal}
367+
_DEFAULT_TYPE_MAP[:tsrange] = Interval{DateTime}
368+
_DEFAULT_TYPE_MAP[:tstzrange] = Interval{ZonedDateTime}
369+
_DEFAULT_TYPE_MAP[:daterange] = Interval{Date}
370+
371+
# Matches anything but the start or end of an interval or a comma
372+
const RANGE_ITEM = "[^\\[\\(\\]\\),]+"
373+
# Makes sure the string starts and ends with a bracket or parentheses, has two items in the
374+
# interval, and a comma to separate the two items.
375+
const RANGE_REGEX = Regex("^([\\[\\(])($RANGE_ITEM),($RANGE_ITEM)([\\]\\)])\$")
376+
get_inclusivity(ch) = ch in ("[", "]") ? true : false
377+
378+
function pqparse(::Type{Interval{T}}, str::AbstractString) where {T}
379+
if str == "empty"
380+
return Interval{T}()
381+
end
382+
383+
matched = match(RANGE_REGEX, str)
384+
if matched === nothing
385+
error("Couldn't parse $str as range using regex $RANGE_REGEX")
386+
else
387+
matched = matched.captures
388+
end
389+
390+
start_inclusivity = get_inclusivity(matched[1])
391+
end_inclusivity = get_inclusivity(matched[4])
392+
393+
# Datetime formats have quotes around them so we strip those out
394+
start = pqparse(T, strip(matched[2], ['"']))
395+
endpoint = pqparse(T, strip(matched[3], ['"']))
396+
397+
return Interval(start, endpoint, start_inclusivity, end_inclusivity)
398+
end
399+
367400
## arrays
368401
# numeric arrays never have double quotes and always use ',' as a separator
369402
parse_numeric_element(::Type{T}, str) where T = parse(T, str)
@@ -443,10 +476,10 @@ for pq_eltype in ("int2", "int4", "int8", "float4", "float8", "oid", "numeric")
443476
_DEFAULT_TYPE_MAP[array_oid] = AbstractArray{jl_missingtype}
444477

445478
for jl_eltype in (jl_type, jl_missingtype)
446-
@eval function Base.parse(
447-
::Type{A}, pqv::PQValue{$array_oid}
479+
@eval function pqparse(
480+
::Type{A}, str::AbstractString
448481
) where A <: AbstractArray{$jl_eltype}
449-
parse_numeric_array($jl_eltype, string_view(pqv))::A
482+
parse_numeric_array($jl_eltype, str)::A
450483
end
451484
end
452485
end

0 commit comments

Comments
 (0)