From 0ce5d4feb5ecbb21e41efb3a4dc7aa0442cc16a2 Mon Sep 17 00:00:00 2001 From: Pascal Betz Date: Sun, 4 Oct 2020 00:20:01 +0200 Subject: [PATCH 1/3] add support for ranges --- README.md | 2 +- spec/pg/decoders/range_decoder_spec.cr | 40 +++++++++ src/pg/decoders/range_decoder.cr | 120 +++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 spec/pg/decoders/range_decoder_spec.cr create mode 100644 src/pg/decoders/range_decoder.cr diff --git a/README.md b/README.md index 3754a242..a19e8ee1 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,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 1: A note on numeric: In Postgres this type has arbitrary precision. In this driver, it is represented as a `PG::Numeric` which retains all precision, but @@ -140,4 +141,3 @@ Since it uses protocol version 3, older versions probably also work but are not float first. If you need true arbitrary precision, you can optionally require `pg_ext/big_rational` which adds `#to_big_r`, but requires that you have LibGMP installed. - diff --git a/spec/pg/decoders/range_decoder_spec.cr b/spec/pg/decoders/range_decoder_spec.cr new file mode 100644 index 00000000..63db0d22 --- /dev/null +++ b/spec/pg/decoders/range_decoder_spec.cr @@ -0,0 +1,40 @@ +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)) + + 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 + + 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 + + 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 + + + + # test_decode "int4range ", "'(4,50]'::int4range", 4...4 + # test_decode "int4range ", "'(4,50)'::int4range", 4...4 + # test_decode "int4range ", "'(0,0)'::int4range", 4...4 + # test_decode "int4range ", "'(,10)'::int4range", 4...4 + # test_decode "int4range ", "'(10,)'::int4range", 4...4 + # test_decode "int4range ", "'[,50]'::int4range", 4...4 + # test_decode "int4range ", "'[,50)'::int4range", 4...4 + # test_decode "int4range ", "'(,50]'::int4range", 4...4 + # test_decode "int4range ", "'(,50)'::int4range", 4...4 + # test_decode "int4range ", "'[4,]'::int4range", 4...4 + # test_decode "int4range ", "'[4,)'::int4range", 4...4 + # test_decode "int4range ", "'(4,]'::int4range", 4...4 + # test_decode "int4range ", "'(4,)'::int4range", 4...4 + #test_decode "int4range ", "'(,)'::int4range", 4...4 diff --git a/src/pg/decoders/range_decoder.cr b/src/pg/decoders/range_decoder.cr new file mode 100644 index 00000000..6bd9ad75 --- /dev/null +++ b/src/pg/decoders/range_decoder.cr @@ -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, + 1114, + 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) + + FLAG_EMPTY = 0b00000001 + FLAG_LOWER_INCLUSIVE = 0b00000010 + FLAG_LOWER_INFINITY = 0b00000100 + FLAG_UPPER_INCLUSIVE = 0b00001000 + FLAG_UPPER_INFINITY = 0b00010000 + + def initialize(@oids : Array(Int32)) + end + + {% for key, value in DECODERS_TO_OID %} + private def decode_boundary(io, oid, header, type : {{ key.id }}.class ) + if header.empty + \{% 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, T) + upper = decode_boundary(io, oid, header, 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 From 493367964afda5b97e424f209dd743a4ec19cbd3 Mon Sep 17 00:00:00 2001 From: Pascal Betz Date: Sun, 4 Oct 2020 00:40:58 +0200 Subject: [PATCH 2/3] use correct values for flags --- spec/pg/decoders/range_decoder_spec.cr | 28 +++++++++------------- src/pg/decoders/range_decoder.cr | 33 +++++++++++++------------- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/spec/pg/decoders/range_decoder_spec.cr b/spec/pg/decoders/range_decoder_spec.cr index 63db0d22..cf08e7e1 100644 --- a/spec/pg/decoders/range_decoder_spec.cr +++ b/spec/pg/decoders/range_decoder_spec.cr @@ -8,33 +8,27 @@ describe PG::Decoders do 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 - - - - # test_decode "int4range ", "'(4,50]'::int4range", 4...4 - # test_decode "int4range ", "'(4,50)'::int4range", 4...4 - # test_decode "int4range ", "'(0,0)'::int4range", 4...4 - # test_decode "int4range ", "'(,10)'::int4range", 4...4 - # test_decode "int4range ", "'(10,)'::int4range", 4...4 - # test_decode "int4range ", "'[,50]'::int4range", 4...4 - # test_decode "int4range ", "'[,50)'::int4range", 4...4 - # test_decode "int4range ", "'(,50]'::int4range", 4...4 - # test_decode "int4range ", "'(,50)'::int4range", 4...4 - # test_decode "int4range ", "'[4,]'::int4range", 4...4 - # test_decode "int4range ", "'[4,)'::int4range", 4...4 - # test_decode "int4range ", "'(4,]'::int4range", 4...4 - # test_decode "int4range ", "'(4,)'::int4range", 4...4 - #test_decode "int4range ", "'(,)'::int4range", 4...4 diff --git a/src/pg/decoders/range_decoder.cr b/src/pg/decoders/range_decoder.cr index 6bd9ad75..515e7b30 100644 --- a/src/pg/decoders/range_decoder.cr +++ b/src/pg/decoders/range_decoder.cr @@ -8,7 +8,7 @@ module PG DECODERS_TO_OID = { "Int32" => [3904], "Int64" => [3926], - "Time" => [ + "Time" => [ 3912, 3910, 1114, @@ -19,28 +19,29 @@ module PG # 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 + 3904 => 23, # int4range + 3926 => 20, # int8range + 3912 => 1082, # daterange + 3910 => 1114, # tstzrange + 3908 => 1114, # tsrange, + 3906 => 1700, # numrange } getter oids : Array(Int32) - FLAG_EMPTY = 0b00000001 - FLAG_LOWER_INCLUSIVE = 0b00000010 - FLAG_LOWER_INFINITY = 0b00000100 - FLAG_UPPER_INCLUSIVE = 0b00001000 - FLAG_UPPER_INFINITY = 0b00010000 + # 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, header, type : {{ key.id }}.class ) - if header.empty + private def decode_boundary(io, oid, infinity, type : {{ key.id }}.class ) + if infinity \{% if T.nilable? %} nil \{% else %} @@ -80,8 +81,8 @@ module PG if header.empty empty_range(T) else - lower = decode_boundary(io, oid, header, T) - upper = decode_boundary(io, oid, header, T) + 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 From 4baf28f6c2536cf308aa1d6ec55b4db44dc30c8e Mon Sep 17 00:00:00 2001 From: Pascal Betz Date: Sun, 4 Oct 2020 00:55:46 +0200 Subject: [PATCH 3/3] remove faulty oid --- src/pg/decoders/range_decoder.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pg/decoders/range_decoder.cr b/src/pg/decoders/range_decoder.cr index 515e7b30..1e295b16 100644 --- a/src/pg/decoders/range_decoder.cr +++ b/src/pg/decoders/range_decoder.cr @@ -11,7 +11,6 @@ module PG "Time" => [ 3912, 3910, - 1114, 3908, ], "Numeric" => [3906],