Skip to content
Open
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ querying, etc, can be found at:

### shards

Add this to your `shard.yml` on a generated crystal project,
Add this to your `shard.yml` on a generated crystal project,
and run `shards install`

``` yml
Expand Down Expand Up @@ -147,6 +147,8 @@ Since it uses protocol version 3, older versions probably also work but are not
- varchar
- regtype
- geo types: point, box, path, lseg, polygon, circle, line
- range types: int4range, int8range, daterange, tsrange, tstzrange, numrange (3)
- multirange types: int4multirange, int8multirange, datemultirange, tsmultirange, tstzmultirange, nummultirange (4)
- array types: int8, int4, int2, float8, float4, bool, text, numeric, timestamptz, date, timestamp
- interval (2)

Expand All @@ -161,6 +163,14 @@ Since it uses protocol version 3, older versions probably also work but are not
in Crystal datatype. Therfore we provide a `PG::Interval` type that can be converted to
`Time::Span` and `Time::MonthSpan`.

3: A note on ranges: PostgreSQL range types map to Crystal's `Range` type with support
for all boundary combinations (`[]`, `()`, etc.), empty ranges, and infinite bounds using
beginless/endless syntax (`..10`, `5..`, `..`). Discrete types (int/date) are canonicalized
to `[lower,upper)` form, while continuous types (timestamp/numeric) preserve exact boundaries.

4: A note on multiranges: PostgreSQL multirange types (PostgreSQL 14+) map to Crystal's
`Array(Range)` type, supporting ordered lists of non-contiguous ranges.

# Authentication Methods

By default this driver will accept `scram-sha-256` and `md5`, as well as
Expand Down
109 changes: 109 additions & 0 deletions spec/pg/decoders/range_decoder_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
require "../../spec_helper"

describe PG::Decoders do
describe "ranges" do
describe "int4range" do
test_decode "(lower,upper) - both exclusive", "'(1,10)'::int4range", 2...10 # PostgreSQL canonicalizes discrete ranges to [a,b) form
test_decode "(lower,upper] - exclusive lower, inclusive upper", "'(1,10]'::int4range", 2...11
test_decode "[lower,upper) - inclusive lower, exclusive upper", "'[1,10)'::int4range", 1...10
test_decode "[lower,upper] - both inclusive", "'[1,10]'::int4range", 1...11 # [a,b] becomes [a,b+1) for discrete types
Comment thread
mamantoha marked this conversation as resolved.
test_decode "empty range", "'empty'::int4range", 0...0
test_decode "(-infinity,upper] - infinite lower bound", "'(,10]'::int4range", nil...11
test_decode "[lower,+infinity) - infinite upper bound", "'[1,)'::int4range", 1...nil
test_decode "(-infinity,+infinity) - both bounds infinite", "'(,)'::int4range", nil...nil
end

describe "int8range" do
test_decode "(lower,upper) - both exclusive", "'(3000000000,4000000000)'::int8range", 3_000_000_001...4_000_000_000
test_decode "(lower,upper] - exclusive lower, inclusive upper", "'(3000000000,4000000000]'::int8range", 3_000_000_001...4_000_000_001
test_decode "[lower,upper) - inclusive lower, exclusive upper", "'[3000000000,4000000000)'::int8range", 3_000_000_000...4_000_000_000
test_decode "[lower,upper] - both inclusive", "'[3000000000,4000000000]'::int8range", 3_000_000_000...4_000_000_001
test_decode "empty range", "'empty'::int8range", 0_i64...0_i64
test_decode "(-infinity,upper] - infinite lower bound", "'(,4000000000]'::int8range", nil...4_000_000_001
test_decode "[lower,+infinity) - infinite upper bound", "'[3000000000,)'::int8range", 3_000_000_000...nil
test_decode "(-infinity,+infinity) - both bounds infinite", "'(,)'::int8range", nil...nil
end

describe "daterange" do
test_decode "(lower,upper) - both exclusive", "'(2023-01-01,2023-12-31)'::daterange", Time.utc(2023, 1, 2)...Time.utc(2023, 12, 31)
test_decode "(lower,upper] - exclusive lower, inclusive upper", "'(2023-01-01,2023-12-31]'::daterange", Time.utc(2023, 1, 2)...Time.utc(2024, 1, 1)
test_decode "[lower,upper) - inclusive lower, exclusive upper", "'[2023-01-01,2023-12-31)'::daterange", Time.utc(2023, 1, 1)...Time.utc(2023, 12, 31)
test_decode "[lower,upper] - both inclusive", "'[2023-01-01,2023-12-31]'::daterange", Time.utc(2023, 1, 1)...Time.utc(2024, 1, 1)
test_decode "empty range", "'empty'::daterange", Time.unix(0)...Time.unix(0)
test_decode "(-infinity,upper] - infinite lower bound", "'(,2023-12-31]'::daterange", nil...Time.utc(2024, 1, 1)
test_decode "[lower,+infinity) - infinite upper bound", "'[2023-01-01,)'::daterange", Time.utc(2023, 1, 1)...nil
test_decode "(-infinity,+infinity) - both bounds infinite", "'(,)'::daterange", nil...nil
end

describe "tsrange" do
test_decode "(lower,upper) - both exclusive", "'(2023-01-01 10:30:00,2023-12-31 15:45:00)'::tsrange", Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 12, 31, 15, 45, 0)
test_decode "(lower,upper] - exclusive lower, inclusive upper", "'(2023-01-01 10:30:00,2023-12-31 15:45:00]'::tsrange", Time.utc(2023, 1, 1, 10, 30, 0)..Time.utc(2023, 12, 31, 15, 45, 0) # Crystal can't represent exclusive lower
Comment thread
mamantoha marked this conversation as resolved.
Outdated
test_decode "[lower,upper) - inclusive lower, exclusive upper", "'[2023-01-01 10:30:00,2023-12-31 15:45:00)'::tsrange", Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 12, 31, 15, 45, 0)
test_decode "[lower,upper] - both inclusive", "'[2023-01-01 10:30:00,2023-12-31 15:45:00]'::tsrange", Time.utc(2023, 1, 1, 10, 30, 0)..Time.utc(2023, 12, 31, 15, 45, 0)
test_decode "empty range", "'empty'::tsrange", Time.unix(0)...Time.unix(0)
test_decode "(-infinity,upper] - infinite lower bound", "'(,2023-12-31 15:45:00]'::tsrange", nil..Time.utc(2023, 12, 31, 15, 45, 0)
test_decode "[lower,+infinity) - infinite upper bound", "'[2023-01-01 10:30:00,)'::tsrange", Time.utc(2023, 1, 1, 10, 30, 0)...nil
test_decode "(-infinity,+infinity) - both bounds infinite", "'(,)'::tsrange", nil...nil
end

describe "tstzrange" do
test_decode "(lower,upper) - both exclusive", "'(2023-01-01 10:30:00+00,2023-12-31 15:45:00+00)'::tstzrange", Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 12, 31, 15, 45, 0)
test_decode "(lower,upper] - exclusive lower, inclusive upper", "'(2023-01-01 10:30:00+00,2023-12-31 15:45:00+00]'::tstzrange", Time.utc(2023, 1, 1, 10, 30, 0)..Time.utc(2023, 12, 31, 15, 45, 0)
test_decode "[lower,upper) - inclusive lower, exclusive upper", "'[2023-01-01 10:30:00+00,2023-12-31 15:45:00+00)'::tstzrange", Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 12, 31, 15, 45, 0)
test_decode "[lower,upper] - both inclusive", "'[2023-01-01 10:30:00+00,2023-12-31 15:45:00+00]'::tstzrange", Time.utc(2023, 1, 1, 10, 30, 0)..Time.utc(2023, 12, 31, 15, 45, 0)
test_decode "empty range", "'empty'::tstzrange", Time.unix(0)...Time.unix(0)
test_decode "(-infinity,upper] - infinite lower bound", "'(,2023-12-31 15:45:00+00]'::tstzrange", nil..Time.utc(2023, 12, 31, 15, 45, 0)
test_decode "[lower,+infinity) - infinite upper bound", "'[2023-01-01 10:30:00+00,)'::tstzrange", Time.utc(2023, 1, 1, 10, 30, 0)...nil
test_decode "(-infinity,+infinity) - both bounds infinite", "'(,)'::tstzrange", nil...nil
end

describe "numrange" do
test_decode "(lower,upper) - both exclusive", "'(1.5,10.75)'::numrange", PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 5000_i16])...PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [10_i16, 7500_i16])
test_decode "(lower,upper] - exclusive lower, inclusive upper", "'(1.5,10.75]'::numrange", PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 5000_i16])..PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [10_i16, 7500_i16])
test_decode "[lower,upper) - inclusive lower, exclusive upper", "'[1.5,10.75)'::numrange", PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 5000_i16])...PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [10_i16, 7500_i16])
test_decode "[lower,upper] - both inclusive", "'[1.5,10.75]'::numrange", PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 5000_i16])..PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [10_i16, 7500_i16])
test_decode "empty range", "'empty'::numrange", PG::Numeric.new(1_i16, 0_i16, 0_i16, 0_i16, [0_i16])...PG::Numeric.new(1_i16, 0_i16, 0_i16, 0_i16, [0_i16])
test_decode "(-infinity,upper] - infinite lower bound", "'(,10.75]'::numrange", nil..PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [10_i16, 7500_i16])
test_decode "[lower,+infinity) - infinite upper bound", "'[1.5,)'::numrange", PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 5000_i16])...nil
test_decode "(-infinity,+infinity) - both bounds infinite", "'(,)'::numrange", nil...nil
end
end

if Helper.db_version_gte(14)
describe "multiranges" do
describe "int4multirange" do
test_decode "empty", "'{}'::int4multirange", [] of Range(Int32?, Int32?)
test_decode "single range", "'{[1,5)}'::int4multirange", [1...5]
test_decode "multiple ranges", "'{[1,3), [7,10)}'::int4multirange", [1...3, 7...10]
test_decode "with infinite bounds", "'{(,0), [10,)}'::int4multirange", [nil...0, 10...nil]
end

describe "int8multirange" do
test_decode "empty", "'{}'::int8multirange", [] of Range(Int64?, Int64?)
test_decode "single range", "'{[1,5)}'::int8multirange", [1_i64...5_i64]
test_decode "multiple ranges", "'{[1,3), [7,10)}'::int8multirange", [1_i64...3_i64, 7_i64...10_i64]
end

describe "datemultirange" do
test_decode "empty", "'{}'::datemultirange", [] of Range(Time?, Time?)
test_decode "single range", "'{[2023-01-01,2023-01-05)}'::datemultirange", [Time.utc(2023, 1, 1)...Time.utc(2023, 1, 5)]
test_decode "multiple ranges", "'{[2023-01-01,2023-01-03), [2023-01-07,2023-01-10)}'::datemultirange", [Time.utc(2023, 1, 1)...Time.utc(2023, 1, 3), Time.utc(2023, 1, 7)...Time.utc(2023, 1, 10)]
end

describe "tsmultirange" do
test_decode "empty", "'{}'::tsmultirange", [] of Range(Time?, Time?)
test_decode "single range", "'{[2023-01-01 10:30:00,2023-01-01 15:30:00)}'::tsmultirange", [Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 1, 1, 15, 30, 0)]
end

describe "tstzmultirange" do
test_decode "empty", "'{}'::tstzmultirange", [] of Range(Time?, Time?)
test_decode "single range", "'{[2023-01-01 10:30:00+00,2023-01-01 15:30:00+00)}'::tstzmultirange", [Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 1, 1, 15, 30, 0)]
end

describe "nummultirange" do
test_decode "empty", "'{}'::nummultirange", [] of Range(PG::Numeric?, PG::Numeric?)
test_decode "single range", "'{[1.5,5.75)}'::nummultirange", [PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 5000_i16])...PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [5_i16, 7500_i16])]
end
end
end
end
Loading