diff --git a/README.md b/README.md index 51793d66..8da10ddc 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) @@ -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 diff --git a/spec/pg/decoders/range_decoder_spec.cr b/spec/pg/decoders/range_decoder_spec.cr new file mode 100644 index 00000000..3f1d4a83 --- /dev/null +++ b/spec/pg/decoders/range_decoder_spec.cr @@ -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 + 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 + 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 diff --git a/spec/pg/encoders/range_encoder_spec.cr b/spec/pg/encoders/range_encoder_spec.cr new file mode 100644 index 00000000..26ca345e --- /dev/null +++ b/spec/pg/encoders/range_encoder_spec.cr @@ -0,0 +1,370 @@ +require "../../spec_helper" + +def test_insert_sql_and_read_range(pg_type, pg_value, value) + it "inserts #{pg_type} with value #{pg_value}" do + PG_DB.exec "drop table if exists test_table" + PG_DB.exec "create table test_table (v #{pg_type})" + PG_DB.exec "insert into test_table values ('#{pg_value}'::#{pg_type})" + + actual_value = PG_DB.query_one "select v from test_table", &.read + actual_value.should eq(value) + end +end + +def test_insert_object_and_read_range(pg_type, range, value) + it "inserts #{pg_type} with value #{range}" do + PG_DB.exec "drop table if exists test_table" + PG_DB.exec "create table test_table (v #{pg_type})" + + PG_DB.exec "insert into test_table values ($1)", args: [range] + + actual_value = PG_DB.query_one "select v from test_table", &.read + + actual_value.should eq(value) + end +end + +def test_insert_sql_and_read_multirange(pg_type, pg_value, value) + it "inserts #{pg_type} with value #{pg_value}" do + PG_DB.exec "drop table if exists test_table" + PG_DB.exec "create table test_table (v #{pg_type})" + PG_DB.exec "insert into test_table values ('#{pg_value}'::#{pg_type})" + + actual_value = PG_DB.query_one "select v from test_table", &.read + actual_value.should eq(value) + end +end + +def test_insert_object_and_read_multirange(pg_type, range, value) + it "inserts #{pg_type} with value #{range}" do + PG_DB.exec "drop table if exists test_table" + PG_DB.exec "create table test_table (v #{pg_type})" + PG_DB.exec "insert into test_table values ($1)", args: [range] + + actual_value = PG_DB.query_one "select v from test_table", &.read + actual_value.should eq(value) + end +end + +describe PG::Driver, "encoder" do + describe "ranges" do + context "with raw sql" do + # int4range + test_insert_sql_and_read_range "int4range", "(1,10)", 2...10 + test_insert_sql_and_read_range "int4range", "(1,10]", 2...11 + test_insert_sql_and_read_range "int4range", "[1,10]", 1...11 + test_insert_sql_and_read_range "int4range", "[1,10)", 1...10 + test_insert_sql_and_read_range "int4range", "empty", 0...0 + test_insert_sql_and_read_range "int4range", "[1,)", 1...nil + test_insert_sql_and_read_range "int4range", "(,10]", nil...11 + test_insert_sql_and_read_range "int4range", "(,)", nil...nil + + # int8range + test_insert_sql_and_read_range "int8range", "(1000000000,4000000000)", 1000000001_i64...4000000000_i64 + test_insert_sql_and_read_range "int8range", "(1000000000,4000000000]", 1000000001_i64...4000000001_i64 + test_insert_sql_and_read_range "int8range", "[1000000000,4000000000]", 1000000000_i64...4000000001_i64 + test_insert_sql_and_read_range "int8range", "[1000000000,4000000000)", 1000000000_i64...4000000000_i64 + test_insert_sql_and_read_range "int8range", "empty", 0_i64...0_i64 + test_insert_sql_and_read_range "int8range", "[1000000000,)", 1000000000_i64...nil + test_insert_sql_and_read_range "int8range", "(,4000000000]", nil...4000000001_i64 + test_insert_sql_and_read_range "int8range", "(,)", nil...nil + + # daterange + test_insert_sql_and_read_range "daterange", "(2023-01-01,2023-12-31)", Time.utc(2023, 1, 2)...Time.utc(2023, 12, 31) + test_insert_sql_and_read_range "daterange", "(2023-01-01,2023-12-31]", Time.utc(2023, 1, 2)...Time.utc(2024, 1, 1) + test_insert_sql_and_read_range "daterange", "[2023-01-01,2023-12-31)", Time.utc(2023, 1, 1)...Time.utc(2023, 12, 31) + test_insert_sql_and_read_range "daterange", "[2023-01-01,2023-12-31]", Time.utc(2023, 1, 1)...Time.utc(2024, 1, 1) + test_insert_sql_and_read_range "daterange", "empty", Time.unix(0)...Time.unix(0) + test_insert_sql_and_read_range "daterange", "(,2023-12-31]", nil...Time.utc(2024, 1, 1) + test_insert_sql_and_read_range "daterange", "[2023-01-01,)", Time.utc(2023, 1, 1)...nil + test_insert_sql_and_read_range "daterange", "(,)", nil...nil + + # tsrange + test_insert_sql_and_read_range "tsrange", "(2023-01-01 10:30:00,2023-12-31 15:45:00)", Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tsrange", "(2023-01-01 10:30:00,2023-12-31 15:45:00]", Time.utc(2023, 1, 1, 10, 30, 0)..Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tsrange", "[2023-01-01 10:30:00,2023-12-31 15:45:00)", Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tsrange", "[2023-01-01 10:30:00,2023-12-31 15:45:00]", Time.utc(2023, 1, 1, 10, 30, 0)..Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tsrange", "empty", Time.unix(0)...Time.unix(0) + test_insert_sql_and_read_range "tsrange", "(,2023-12-31 15:45:00]", nil..Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tsrange", "[2023-01-01 10:30:00,)", Time.utc(2023, 1, 1, 10, 30, 0)...nil + test_insert_sql_and_read_range "tsrange", "(,)", nil...nil + + # tstzrange + test_insert_sql_and_read_range "tstzrange", "(2023-01-01 10:30:00+00,2023-12-31 15:45:00+00)", Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tstzrange", "(2023-01-01 10:30:00+00,2023-12-31 15:45:00+00]", Time.utc(2023, 1, 1, 10, 30, 0)..Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tstzrange", "[2023-01-01 10:30:00+00,2023-12-31 15:45:00+00)", Time.utc(2023, 1, 1, 10, 30, 0)...Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tstzrange", "[2023-01-01 10:30:00+00,2023-12-31 15:45:00+00]", Time.utc(2023, 1, 1, 10, 30, 0)..Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tstzrange", "empty", Time.unix(0)...Time.unix(0) + test_insert_sql_and_read_range "tstzrange", "[2023-01-01 10:30:00+00,)", Time.utc(2023, 1, 1, 10, 30, 0)...nil + test_insert_sql_and_read_range "tstzrange", "(,2023-12-31 15:45:00+00]", nil..Time.utc(2023, 12, 31, 15, 45, 0) + test_insert_sql_and_read_range "tstzrange", "(,)", nil...nil + + # numrange + test_insert_sql_and_read_range "numrange", + "(1.5,10.75)", + 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_insert_sql_and_read_range "numrange", + "(1.5,10.75]", + 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_insert_sql_and_read_range "numrange", + "[1.5,10.75)", + 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_insert_sql_and_read_range "numrange", + "[1.5,10.75]", + 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_insert_sql_and_read_range "numrange", + "empty", + 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_insert_sql_and_read_range "numrange", + "(,10.75]", + nil..PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [10_i16, 7500_i16]) + test_insert_sql_and_read_range "numrange", + "[1.5,)", + PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 5000_i16]).as(PG::Numeric?)...nil + test_insert_sql_and_read_range "numrange", + "(,)", + nil...nil + end + + context "with object" do + # int4range + test_insert_object_and_read_range "int4range", 1...10, 1...10 + test_insert_object_and_read_range "int4range", 1..10, 1...11 + test_insert_object_and_read_range "int4range", 1...1, 0...0 # empty range + test_insert_object_and_read_range "int4range", 1..1, 1...2 + test_insert_object_and_read_range "int4range", nil..nil, nil...nil + test_insert_object_and_read_range "int4range", nil...nil, 0...0 # empty range + test_insert_object_and_read_range "int4range", nil...1, nil...1 + test_insert_object_and_read_range "int4range", nil..1, nil...2 + test_insert_object_and_read_range "int4range", 1..nil, 1...nil + test_insert_object_and_read_range "int4range", 1...nil, 1...nil + + # int8range + test_insert_object_and_read_range "int8range", 1000000000_i64...4000000000_i64, 1000000000_i64...4000000000_i64 + test_insert_object_and_read_range "int8range", 1000000000_i64..4000000000_i64, 1000000000_i64...4000000001_i64 + test_insert_object_and_read_range "int8range", 0_i64...0_i64, 0_i64...0_i64 + test_insert_object_and_read_range "int8range", ..5000000000, ...5000000001 + test_insert_object_and_read_range "int8range", 5000000000_i64...nil, 5000000000_i64...nil + + # daterange + test_insert_object_and_read_range "daterange", + Time.utc(2023, 1, 15)..Time.utc(2023, 12, 31), + Time.utc(2023, 1, 15)...Time.utc(2024, 1, 1) + test_insert_object_and_read_range "daterange", + Time.utc(2023, 1, 15)...Time.utc(2023, 12, 31), + Time.utc(2023, 1, 15)...Time.utc(2023, 12, 31) + test_insert_object_and_read_range "daterange", + Time.utc(2023, 6, 15)..nil, + Time.utc(2023, 6, 15)...nil + test_insert_object_and_read_range "daterange", + Time.utc(2023, 6, 15)...Time.utc(2023, 6, 15), + Time::UNIX_EPOCH...Time::UNIX_EPOCH + + # tsrange + test_insert_object_and_read_range "tsrange", + Time.utc(2023, 1, 1)...Time.utc(2023, 1, 2), + Time.utc(2023, 1, 1)...Time.utc(2023, 1, 2) + test_insert_object_and_read_range "tsrange", + Time.utc(2023, 6, 15, 14, 30, 45, nanosecond: 123456000)...Time.utc(2023, 6, 15, 18, 45, 30, nanosecond: 987654000), + Time.utc(2023, 6, 15, 14, 30, 45, nanosecond: 123456000)...Time.utc(2023, 6, 15, 18, 45, 30, nanosecond: 987654000) + test_insert_object_and_read_range "tsrange", + Time.utc(2023, 1, 1, 10, 30)..Time.utc(2023, 1, 1, 15, 30), + Time.utc(2023, 1, 1, 10, 30)..Time.utc(2023, 1, 1, 15, 30) + test_insert_object_and_read_range "tsrange", + Time.utc(2023, 1, 1)...nil, + Time.utc(2023, 1, 1)...nil + test_insert_object_and_read_range "tsrange", + Time.utc(2023, 1, 1)...Time.utc(2023, 1, 1), + Time::UNIX_EPOCH...Time::UNIX_EPOCH + + # tstzrange + test_insert_object_and_read_range "tstzrange", + Time.local(2023, 1, 1, 10, 30, 0, location: DB_LOCATION)...Time.local(2023, 1, 1, 15, 30, 0, location: DB_LOCATION), + Time.local(2023, 1, 1, 10, 30, 0, location: DB_LOCATION)...Time.local(2023, 1, 1, 15, 30, 0, location: DB_LOCATION) + test_insert_object_and_read_range "tstzrange", + Time.local(2023, 1, 1, 10, 30, 45, nanosecond: 123456000, location: DB_LOCATION)...Time.local(2023, 1, 1, 10, 30, 45, nanosecond: 654321000, location: DB_LOCATION), + Time.local(2023, 1, 1, 10, 30, 45, nanosecond: 123456000, location: DB_LOCATION)...Time.local(2023, 1, 1, 10, 30, 45, nanosecond: 654321000, location: DB_LOCATION) + test_insert_object_and_read_range "tstzrange", + Time.local(2023, 1, 1, 10, 30, 45, nanosecond: 123456000, location: DB_LOCATION)...Time.local(2023, 1, 1, 10, 30, 45, nanosecond: 654321000, location: DB_LOCATION), + Time.local(2023, 1, 1, 10, 30, 45, nanosecond: 123456000, location: DB_LOCATION)...Time.local(2023, 1, 1, 10, 30, 45, nanosecond: 654321000, location: DB_LOCATION) + test_insert_object_and_read_range "tstzrange", + nil...Time.local(2023, 12, 31, 15, 45, 0, location: DB_LOCATION), + nil...Time.local(2023, 12, 31, 15, 45, 0, location: DB_LOCATION) + + # numrange + test_insert_object_and_read_range "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]), + 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_insert_object_and_read_range "numrange", + PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 5000_i16])...nil, + PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 5000_i16])...nil + test_insert_object_and_read_range "numrange", + PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [5_i16, 0_i16])...PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [5_i16, 0_i16]), + 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]) + end + end + + if Helper.db_version_gte(14) + describe "multirange" do + context "with raw sql" do + # int4multirange + test_insert_sql_and_read_multirange "int4multirange", + "{}", + [] of Range(Int32?, Int32?) + test_insert_sql_and_read_multirange "int4multirange", + "{[1,10)}", + [1...10] + test_insert_sql_and_read_multirange "int4multirange", + "{[1,5), [10,20), [30,40)}", + [1...5, 10...20, 30...40] + + # int8multirange + test_insert_sql_and_read_multirange "int8multirange", + "{[1000000000,2000000000), [3000000000,4000000000)}", + [1000000000_i64...2000000000_i64, 3000000000_i64...4000000000_i64] + + # datemultirange + test_insert_sql_and_read_multirange "datemultirange", + "{[2023-01-01,2023-06-30), [2023-07-01,2023-12-31)}", + [Time.utc(2023, 1, 1)...Time.utc(2023, 6, 30), Time.utc(2023, 7, 1)...Time.utc(2023, 12, 31)] + + # tsmultirange + test_insert_sql_and_read_multirange "tsmultirange", + "{[\"2023-01-01 10:30:00\",\"2023-06-30 15:45:00\"), [\"2023-07-01 08:00:00\",\"2023-12-31 18:30:00\")}", + [Time.utc(2023, 1, 1, 10, 30)...Time.utc(2023, 6, 30, 15, 45), Time.utc(2023, 7, 1, 8, 0)...Time.utc(2023, 12, 31, 18, 30)] + + # tstzmultirange + test_insert_sql_and_read_multirange "tstzmultirange", + "{[\"2023-01-01 10:30:00+00\",\"2023-06-30 15:45:00+00\"), [\"2023-07-01 08:00:00+00\",\"2023-12-31 18:30:00+00\")}", + [ + Time.utc(2023, 1, 1, 10, 30).in(DB_LOCATION)...Time.utc(2023, 6, 30, 15, 45).in(DB_LOCATION), + Time.utc(2023, 7, 1, 8, 0).in(DB_LOCATION)...Time.utc(2023, 12, 31, 18, 30).in(DB_LOCATION), + ] + + # nummultirange + test_insert_sql_and_read_multirange "nummultirange", + "{[1.5,10.75), [20.25,99.99)}", + [ + 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]), + PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [20_i16, 2500_i16])...PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [99_i16, 9900_i16]), + ] + end + + context "with object" do + # int4multirange + test_insert_object_and_read_multirange "int4multirange", + [] of Range(Int32?, Int32?), + [] of Range(Int32?, Int32?) + test_insert_object_and_read_multirange "int4multirange", + [1...10], + [1...10] + test_insert_object_and_read_multirange "int4multirange", + [1...5, 10...20, 30...40], + [1...5, 10...20, 30...40] + test_insert_object_and_read_multirange "int4multirange", + [nil...100, 200...nil], + [nil...100, 200...nil] + test_insert_object_and_read_multirange "int4multirange", + [1..10, 20...30, 40..50], + [1...11, 20...30, 40...51] + test_insert_object_and_read_multirange "int4multirange", + [1...5, 10...10, 20...30], + [1...5, 20...30] + test_insert_object_and_read_multirange "int4multirange", + [1...5, 10...15, nil...0, 100..nil], + [...0, 1...5, 10...15, 100...] + + # int8multirange + test_insert_object_and_read_multirange "int8multirange", + [] of Range(Int64?, Int64?), + [] of Range(Int64?, Int64?) + test_insert_object_and_read_multirange "int8multirange", + [1000000000_i64...2000000000_i64], + [1000000000_i64...2000000000_i64] + test_insert_object_and_read_multirange "int8multirange", + [1000000000_i64...2000000000_i64, 3000000000_i64...4000000000_i64], + [1000000000_i64...2000000000_i64, 3000000000_i64...4000000000_i64] + test_insert_object_and_read_multirange "int8multirange", + [nil...1000000000_i64, 5000000000_i64...nil], + [nil...1000000000_i64, 5000000000_i64...nil] + test_insert_object_and_read_multirange "int8multirange", + [1000000000_i64..2000000000_i64, 3000000000_i64...4000000000_i64], + [1000000000_i64...2000000001_i64, 3000000000_i64...4000000000_i64] + + # datemultirange + test_insert_object_and_read_multirange "datemultirange", + [] of Range(Time?, Time?), + [] of Range(Time?, Time?) + test_insert_object_and_read_multirange "datemultirange", + [Time.utc(2023, 1, 1)...Time.utc(2023, 6, 30)], + [Time.utc(2023, 1, 1)...Time.utc(2023, 6, 30)] + test_insert_object_and_read_multirange "datemultirange", + [Time.utc(2023, 1, 1)...Time.utc(2023, 6, 30), Time.utc(2023, 7, 1)...Time.utc(2023, 12, 31)], + [Time.utc(2023, 1, 1)...Time.utc(2023, 6, 30), Time.utc(2023, 7, 1)...Time.utc(2023, 12, 31)] + test_insert_object_and_read_multirange "datemultirange", + [Time.utc(2023, 1, 1)..Time.utc(2023, 6, 29), Time.utc(2023, 7, 1)..Time.utc(2023, 12, 31)], + [Time.utc(2023, 1, 1)...Time.utc(2023, 6, 30), Time.utc(2023, 7, 1)...Time.utc(2024, 1, 1)] + test_insert_object_and_read_multirange "datemultirange", + [nil...Time.utc(2023, 6, 30), Time.utc(2023, 7, 1)...nil], + [nil...Time.utc(2023, 6, 30), Time.utc(2023, 7, 1)...nil] + + # tsmultirange + test_insert_object_and_read_multirange "tsmultirange", + [] of Range(Time?, Time?), + [] of Range(Time?, Time?) + test_insert_object_and_read_multirange "tsmultirange", + [Time.utc(2023, 1, 1, 10, 30)...Time.utc(2023, 6, 30, 15, 45)], + [Time.utc(2023, 1, 1, 10, 30)...Time.utc(2023, 6, 30, 15, 45)] + test_insert_object_and_read_multirange "tsmultirange", + [Time.utc(2023, 1, 1, 10, 30)...Time.utc(2023, 6, 30, 15, 45), Time.utc(2023, 7, 1, 8, 0)...Time.utc(2023, 12, 31, 18, 30)], + [Time.utc(2023, 1, 1, 10, 30)...Time.utc(2023, 6, 30, 15, 45), Time.utc(2023, 7, 1, 8, 0)...Time.utc(2023, 12, 31, 18, 30)] + test_insert_object_and_read_multirange "tsmultirange", + [Time.utc(2023, 1, 1, 10, 30)..Time.utc(2023, 6, 30, 15, 45), Time.utc(2023, 7, 1, 8, 0)..Time.utc(2023, 12, 31, 18, 30)], + [Time.utc(2023, 1, 1, 10, 30)..Time.utc(2023, 6, 30, 15, 45), Time.utc(2023, 7, 1, 8, 0)..Time.utc(2023, 12, 31, 18, 30)] + test_insert_object_and_read_multirange "tsmultirange", + [nil...Time.utc(2023, 6, 30, 15, 45), Time.utc(2023, 7, 1, 8, 0)...nil], + [nil...Time.utc(2023, 6, 30, 15, 45), Time.utc(2023, 7, 1, 8, 0)...nil] + + # tstzmultirange + test_insert_object_and_read_multirange "tstzmultirange", + [] of Range(Time?, Time?), + [] of Range(Time?, Time?) + test_insert_object_and_read_multirange "tstzmultirange", + [Time.local(2023, 1, 1, 10, 30, location: DB_LOCATION)...Time.local(2023, 6, 30, 15, 45, location: DB_LOCATION)], + [Time.local(2023, 1, 1, 10, 30, location: DB_LOCATION)...Time.local(2023, 6, 30, 15, 45, location: DB_LOCATION)] + test_insert_object_and_read_multirange "tstzmultirange", + [ + Time.local(2023, 1, 1, 10, 30, location: DB_LOCATION)...Time.local(2023, 6, 30, 15, 45, location: DB_LOCATION), + Time.local(2023, 7, 1, 8, 0, location: DB_LOCATION)...Time.local(2023, 12, 31, 18, 30, location: DB_LOCATION), + ], + [ + Time.local(2023, 1, 1, 10, 30, location: DB_LOCATION)...Time.local(2023, 6, 30, 15, 45, location: DB_LOCATION), + Time.local(2023, 7, 1, 8, 0, location: DB_LOCATION)...Time.local(2023, 12, 31, 18, 30, location: DB_LOCATION), + ] + test_insert_object_and_read_multirange "tstzmultirange", + [nil...Time.local(2023, 6, 30, 15, 45, location: DB_LOCATION), Time.local(2023, 7, 1, 8, 0, location: DB_LOCATION)...nil], + [nil...Time.local(2023, 6, 30, 15, 45, location: DB_LOCATION), Time.local(2023, 7, 1, 8, 0, location: DB_LOCATION)...nil] + + # nummultirange + test_insert_object_and_read_multirange "nummultirange", + [] of Range(PG::Numeric?, PG::Numeric?), + [] of Range(PG::Numeric?, PG::Numeric?) + test_insert_object_and_read_multirange "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, [10_i16, 7500_i16])], + [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_insert_object_and_read_multirange "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, [10_i16, 7500_i16]), + PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [20_i16, 2500_i16])...PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [99_i16, 9900_i16]), + ], + [ + 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]), + PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [20_i16, 2500_i16])...PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [99_i16, 9900_i16]), + ] + test_insert_object_and_read_multirange "nummultirange", + [nil...PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [10_i16, 7500_i16]), PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [20_i16, 2500_i16])...nil], + [nil...PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [10_i16, 7500_i16]), PG::Numeric.new(2_i16, 0_i16, 0_i16, 2_i16, [20_i16, 2500_i16])...nil] + end + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index ba47dbab..3df48848 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -4,6 +4,9 @@ require "../src/pg" DB_URL = ENV["DATABASE_URL"]? || "postgres:///" PG_DB = DB.open(DB_URL) +DB_TIMEZONE = PG_DB.query_one("SHOW timezone", &.read).as(String) +DB_LOCATION = Time::Location.load(DB_TIMEZONE) + def with_db(&) DB.open(DB_URL) do |db| yield db diff --git a/src/pg.cr b/src/pg.cr index 112b5d59..d17f797f 100644 --- a/src/pg.cr +++ b/src/pg.cr @@ -1,6 +1,13 @@ require "db" require "./pg/*" +module DB::MetadataValueConverter + # Log Range as string representation rather than array + def self.arg_to_log(arg : Range) : ::Log::Metadata::Value + ::Log::Metadata::Value.new(arg.to_s) + end +end + module PG # Establish a connection to the database def self.connect(url) diff --git a/src/pg/decoder.cr b/src/pg/decoder.cr index 990d8fc7..33100ddf 100644 --- a/src/pg/decoder.cr +++ b/src/pg/decoder.cr @@ -2,7 +2,7 @@ require "json" require "uuid" module PG - alias PGValue = String | Nil | Bool | Int32 | Float32 | Float64 | Time | JSON::Any | PG::Numeric | UUID + alias PGValue = String | Nil | Bool | Int32 | Float32 | Float64 | Time | JSON::Any | PG::Numeric | UUID | Range(Int32?, Int32?) | Range(Int64?, Int64?) | Range(Time?, Time?) | Range(PG::Numeric?, PG::Numeric?) | Array(Range(Int32?, Int32?)) | Array(Range(Int64?, Int64?)) | Array(Range(Time?, Time?)) | Array(Range(PG::Numeric?, PG::Numeric?)) # :nodoc: module Decoders @@ -537,6 +537,18 @@ module PG register_decoder PolygonDecoder.new register_decoder LineDecoder.new register_decoder CircleDecoder.new + register_decoder Int4RangeDecoder.new + register_decoder Int8RangeDecoder.new + register_decoder DateRangeDecoder.new + register_decoder TsRangeDecoder.new + register_decoder TstzRangeDecoder.new + register_decoder NumRangeDecoder.new + register_decoder Int4MultiRangeDecoder.new + register_decoder Int8MultiRangeDecoder.new + register_decoder DateMultiRangeDecoder.new + register_decoder TsMultiRangeDecoder.new + register_decoder TstzMultiRangeDecoder.new + register_decoder NumMultiRangeDecoder.new end end diff --git a/src/pg/decoders/range_decoder.cr b/src/pg/decoders/range_decoder.cr new file mode 100644 index 00000000..08d04133 --- /dev/null +++ b/src/pg/decoders/range_decoder.cr @@ -0,0 +1,231 @@ +module PG + module Decoders + # Range flags from PostgreSQL range format + RANGE_EMPTY = 0x01 + RANGE_LB_INC = 0x02 # Lower bound inclusive + RANGE_UB_INC = 0x04 # Upper bound inclusive + RANGE_LB_INF = 0x08 # Lower bound infinite + RANGE_UB_INF = 0x10 # Upper bound infinite + + module Decoder + private def decode_range(io, bytesize, oid) + flags = io.read_byte.not_nil! + empty = (flags & RANGE_EMPTY) != 0 + + return empty_range if empty + + lower_bound_inclusive = (flags & RANGE_LB_INC) != 0 + upper_bound_inclusive = (flags & RANGE_UB_INC) != 0 + lower_bound_infinite = (flags & RANGE_LB_INF) != 0 + upper_bound_infinite = (flags & RANGE_UB_INF) != 0 + + lower = if lower_bound_infinite + nil + else + len = read_i32(io) + decode_element(io) + end + + upper = if upper_bound_infinite + nil + else + len = read_i32(io) + decode_element(io) + end + + Range.new(lower, upper, exclusive: !upper_bound_inclusive) + end + end + + # Abstract base class for range decoders with common logic + abstract struct RangeDecoder(T) + include Decoder + + def decode(io, bytesize, oid) + decode_range(io, bytesize, oid) + end + + def type + Range(T?, T?) + end + + abstract def decode_element(io) + abstract def empty_range + end + + struct Int4RangeDecoder < RangeDecoder(Int32) + def_oids [3904] # int4range + + def decode_element(io) + Int32Decoder.new.decode(io, nil, nil) + end + + def empty_range + Range.new(0.as(Int32?), 0.as(Int32?), exclusive: true) + end + end + + struct Int8RangeDecoder < RangeDecoder(Int64) + def_oids [3926] # int8range + + def decode_element(io) + Int64Decoder.new.decode(io, nil, nil) + end + + def empty_range + Range.new(0_i64.as(Int64?), 0_i64.as(Int64?), exclusive: true) + end + end + + struct DateRangeDecoder < RangeDecoder(Time) + def_oids [3912] # daterange + + def decode_element(io) + TimeDecoder.new.decode(io, nil, TimeDecoder::OID::DATE) + end + + def empty_range + Range.new(Time.unix(0).as(Time?), Time.unix(0).as(Time?), exclusive: true) + end + end + + struct TsRangeDecoder < RangeDecoder(Time) + def_oids [3908] # tsrange + + def decode_element(io) + TimeDecoder.new.decode(io, nil, TimeDecoder::OID::TIMESTAMP) + end + + def empty_range + Range.new(Time.unix(0).as(Time?), Time.unix(0).as(Time?), exclusive: true) + end + end + + struct TstzRangeDecoder < RangeDecoder(Time) + def_oids [3910] # tstzrange + + def decode_element(io) + TimeDecoder.new.decode(io, nil, TimeDecoder::OID::TIMESTAMPTZ) + end + + def empty_range + Range.new(Time.unix(0).as(Time?), Time.unix(0).as(Time?), exclusive: true) + end + end + + struct NumRangeDecoder < RangeDecoder(PG::Numeric) + def_oids [3906] # numrange + + def decode_element(io) + NumericDecoder.new.decode(io, nil, nil) + end + + def empty_range + zero_numeric = PG::Numeric.new(1_i16, 0_i16, 0_i16, 0_i16, [0_i16]) + Range.new(zero_numeric.as(PG::Numeric?), zero_numeric.as(PG::Numeric?), exclusive: true) + end + end + + # Abstract base class for multirange decoders with common logic + abstract struct MultiRangeDecoder(T) + include Decoder + + def decode(io, bytesize, oid) + # Multirange format: 4-byte count followed by count range elements + count = read_i32(io) + ranges = Array(Range(T?, T?)).new(count) + + count.times do + # Each range element has a 4-byte length followed by the range data + read_i32(io) + + range = decode_range(io, bytesize, oid) + + ranges << range + end + + ranges + end + + def type + Array(Range(T?, T?)) + end + + abstract def decode_element(io) + abstract def empty_range + end + + struct Int4MultiRangeDecoder < MultiRangeDecoder(Int32) + def_oids [4451] # int4multirange + + def decode_element(io) + Int32Decoder.new.decode(io, nil, nil) + end + + def empty_range + Range.new(0.as(Int32?), 0.as(Int32?), exclusive: true) + end + end + + struct Int8MultiRangeDecoder < MultiRangeDecoder(Int64) + def_oids [4536] # int8multirange + + def decode_element(io) + Int64Decoder.new.decode(io, nil, nil) + end + + def empty_range + Range.new(0_i64.as(Int64?), 0_i64.as(Int64?), exclusive: true) + end + end + + struct DateMultiRangeDecoder < MultiRangeDecoder(Time) + def_oids [4535] # datemultirange + + def decode_element(io) + TimeDecoder.new.decode(io, nil, TimeDecoder::OID::DATE) + end + + def empty_range + Range.new(Time.unix(0).as(Time?), Time.unix(0).as(Time?), exclusive: true) + end + end + + struct TsMultiRangeDecoder < MultiRangeDecoder(Time) + def_oids [4533] # tsmultirange + + def decode_element(io) + TimeDecoder.new.decode(io, nil, TimeDecoder::OID::TIMESTAMP) + end + + def empty_range + Range.new(Time.unix(0).as(Time?), Time.unix(0).as(Time?), exclusive: true) + end + end + + struct TstzMultiRangeDecoder < MultiRangeDecoder(Time) + def_oids [4534] # tstzmultirange + + def decode_element(io) + TimeDecoder.new.decode(io, nil, TimeDecoder::OID::TIMESTAMPTZ) + end + + def empty_range + Range.new(Time.unix(0).as(Time?), Time.unix(0).as(Time?), exclusive: true) + end + end + + struct NumMultiRangeDecoder < MultiRangeDecoder(PG::Numeric) + def_oids [4532] # nummultirange + + def decode_element(io) + NumericDecoder.new.decode(io, nil, nil) + end + + def empty_range + zero_numeric = PG::Numeric.new(1_i16, 0_i16, 0_i16, 0_i16, [0_i16]) + Range.new(zero_numeric.as(PG::Numeric?), zero_numeric.as(PG::Numeric?), exclusive: true) + end + end + end +end diff --git a/src/pq/param.cr b/src/pq/param.cr index 5232d817..fa44605c 100644 --- a/src/pq/param.cr +++ b/src/pq/param.cr @@ -20,7 +20,7 @@ module PQ end def self.encode(val : Time) - text Time::Format::RFC_3339.format(val, fraction_digits: 9) + text format_time(val) end def self.encode(val : Enum) @@ -77,6 +77,94 @@ module PQ text "#{val.months} months #{val.days} days #{val.microseconds} microseconds" end + private def self.format_time(value : Time) + Time::Format::RFC_3339.format(value, fraction_digits: 9) + end + + private def self.format_numeric_range(range) + if range.begin == range.end && range.excludes_end? + "empty" + else + start_bracket = "[" + end_bracket = range.excludes_end? ? ")" : "]" + + begin_str = range.begin.nil? ? "" : range.begin.to_s + end_str = range.end.nil? ? "" : range.end.to_s + + "#{start_bracket}#{begin_str},#{end_str}#{end_bracket}" + end + end + + private def self.format_timestamp_range(range) + if range.begin == range.end && range.excludes_end? + "empty" + else + start_bracket = "[" + end_bracket = range.excludes_end? ? ")" : "]" + + begin_str = range.begin.try { |val| format_time(val) } || "" + end_str = range.end.try { |val| format_time(val) } || "" + + "#{start_bracket}#{begin_str},#{end_str}#{end_bracket}" + end + end + + def self.encode(val : Range(Int32?, Int32?)) + text format_numeric_range(val) + end + + def self.encode(val : Range(Int64?, Int64?)) + text format_numeric_range(val) + end + + def self.encode(val : Range(PG::Numeric?, PG::Numeric?)) + text format_numeric_range(val) + end + + def self.encode(val : Range(Time?, Time?)) + text format_timestamp_range(val) + end + + def self.encode(val : Array(Range(Int32?, Int32?))) + if val.empty? + text "{}" + else + range_strs = val.map { |range| format_numeric_range(range) } + + text "{#{range_strs.join(",")}}" + end + end + + def self.encode(val : Array(Range(Int64?, Int64?))) + if val.empty? + text "{}" + else + range_strs = val.map { |range| format_numeric_range(range) } + + text "{#{range_strs.join(",")}}" + end + end + + def self.encode(val : Array(Range(PG::Numeric?, PG::Numeric?))) + if val.empty? + text "{}" + else + range_strs = val.map { |range| format_numeric_range(range) } + + text "{#{range_strs.join(",")}}" + end + end + + def self.encode(val : Array(Range(Time?, Time?))) + if val.empty? + text "{}" + else + range_strs = val.map { |range| format_timestamp_range(range) } + + text "{#{range_strs.join(",")}}" + end + end + def self.encode(val) text val.to_s end