Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Since it uses protocol version 3, older versions probably also work but are not
- regtype
- geo types: point, box, path, lseg, polygon, circle, line
- array types: int8, int4, int2, float8, float4, bool, text, numeric, timestamptz, date, timestamp
- range: int4range, int8range, daterange, tsrange, tstzrange, numrange
- interval (2)

1: A note on numeric: In Postgres this type has arbitrary precision. In this
Expand Down
34 changes: 34 additions & 0 deletions spec/pg/decoders/range_decoder_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require "../../spec_helper"

describe PG::Decoders do
# empty ranges
test_decode "int4range ", "'(5, 5)'::int4range", 0..0
test_decode "int4range ", "'[5, 5)'::int4range", 0..0
test_decode "int8range ", "'(5, 5)'::int8range", 0_i64..0_i64
test_decode "int8range ", "'[5, 5)'::int8range", 0_i64..0_i64
test_decode "daterange ", "'(2015-02-03, 2015-02-03)'::daterange", (Time.utc(1970, 1, 1)..Time.utc(1970, 1, 1))

# inclusive/exclusive boundaries
test_decode "int4range ", "'[4, 8]'::int4range", 4...9
test_decode "int4range ", "'[4, 8)'::int4range", 4...8
test_decode "int4range ", "'(4, 8]'::int4range", 5...9
test_decode "int4range ", "'(4, 8)'::int4range", 5...8

# TODO:
# how to deal with Nil, without making every Range Range(T | Nil, T | Nil)
#
# infinity
# test_decode "int4range ", "'(10,]'::int4range", nil..10
# test_decode "int4range ", "'(,10]'::int4range", 10..nil
# test_decode "int4range ", "'(,]'::int4range", nil..nil

# numrange
lower = PG::Numeric.new(ndigits: 1, weight: 0, sign: PG::Numeric::Sign::Pos.value, dscale: 0, digits: [1] of Int16)
upper = PG::Numeric.new(ndigits: 1, weight: 0, sign: PG::Numeric::Sign::Pos.value, dscale: 0, digits: [3] of Int16)
test_decode "numrange ", "'[1, 3)'::numrange", lower...upper

# date/ts
test_decode "daterange ", "'[2015-02-03, 2015-02-04)'::daterange", (Time.utc(2015, 2, 3)...Time.utc(2015, 2, 4))
test_decode "tstzrange ", "'[2015-02-03 16:15:13-01, 2015-02-03 16:15:14-01)'::tstzrange", (Time.utc(2015, 2, 3, 17, 15, 13)...Time.utc(2015, 2, 3, 17, 15, 14))
test_decode "tsrange ", "'[2015-02-03 16:15:13, 2015-02-03 16:15:14)'::tsrange", (Time.utc(2015, 2, 3, 16, 15, 13)...Time.utc(2015, 2, 3, 16, 15, 14))
end
120 changes: 120 additions & 0 deletions src/pg/decoders/range_decoder.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
require "../numeric"

module PG
module Decoders
struct RangeDecoder(T)
include Decoder
# Decoder to use for boundaries => Range oids
DECODERS_TO_OID = {
"Int32" => [3904],
"Int64" => [3926],
"Time" => [
3912,
3910,
3908,
],
"Numeric" => [3906],
}

# Range OID => OID of upper/lower boundary
OIDS_TO_SUBOIDS = {
3904 => 23, # int4range
3926 => 20, # int8range
3912 => 1082, # daterange
3910 => 1114, # tstzrange
3908 => 1114, # tsrange,
3906 => 1700, # numrange
}

getter oids : Array(Int32)

# See https://github.com/postgres/postgres/blob/5cbfce562f7cd2aab0cdc4694ce298ec3567930e/src/include/utils/rangetypes.h#L36
FLAG_EMPTY = 0b00000001
FLAG_LOWER_INCLUSIVE = 0b00000010
FLAG_UPPER_INCLUSIVE = 0b00000100
FLAG_LOWER_INFINITY = 0b00001000
FLAG_UPPER_INFINITY = 0b00010000

def initialize(@oids : Array(Int32))
end

{% for key, value in DECODERS_TO_OID %}
private def decode_boundary(io, oid, infinity, type : {{ key.id }}.class )
if infinity
\{% if T.nilable? %}
nil
\{% else %}
raise PG::RuntimeError.new("Boundary is infinite but #{T} is not nilable")
\{% end %}
else
bytesize = read_i32(io)
suboid = OIDS_TO_SUBOIDS[oid]
Decoders::{{ key.id }}Decoder.new.decode(io, bytesize, suboid)
end
end

PG::Decoders.register_decoder RangeDecoder({{ key.id }}).new({{ value }})
{% end %}

private def empty_range(type : Int32.class)
Range.new(0_i32, 0_i32)
end

private def empty_range(type : Int64.class)
Range.new(0_i64, 0_i64)
end

private def empty_range(type : Time.class)
Range.new(Time.unix(0), Time.unix(0))
end

private def empty_range(type : PG::Numeric.class)
value = PG::Numeric.new(ndigits: 1, weight: 0, sign: PG::Numeric::Sign::Pos.value, dscale: 0, digits: [0] of Int16)

Range.new(value, value)
end

def decode(io, bytesize, oid)
header = decode_range_header(io)

if header.empty
empty_range(T)
else
lower = decode_boundary(io, oid, header.lower_infinity, T)
upper = decode_boundary(io, oid, header.upper_infinity, T)
Range.new(lower, upper, !header.upper_inclusive)
end
end

def type
Range(T, T)
end

def decode_range_header(io)
#
# For discrete types postgres normalizes inclusive/exclusive to
# [a, b)
# (Inclusive lower, exclusive upper) and therefore we do not see FLAG_UPPER_INCLUSIVE
# If lower and/or upper infinity is set, we will represent this with
# beginless/endless Range.
#
flags = io.read_byte.not_nil!

RangeHeader.new(
empty: (FLAG_EMPTY & flags) != 0,
lower_inclusive: (FLAG_LOWER_INCLUSIVE & flags) != 0,
lower_infinity: (FLAG_LOWER_INFINITY & flags) != 0,
upper_inclusive: (FLAG_UPPER_INCLUSIVE & flags) != 0,
upper_infinity: (FLAG_UPPER_INFINITY & flags) != 0
)
end
end

record RangeHeader,
empty : Bool,
lower_inclusive : Bool,
lower_infinity : Bool,
upper_inclusive : Bool,
upper_infinity : Bool
end
end