From 75446032b914bb0be5e07da29c976034c0a666cf Mon Sep 17 00:00:00 2001 From: Torkel Rogstad Date: Fri, 12 Mar 2021 14:42:51 +0100 Subject: [PATCH 01/16] Normalize UTC timestamps to comply with stdlib --- timestamptz.go | 22 ++++++++++++++++++++-- timestamptz_test.go | 6 ++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/timestamptz.go b/timestamptz.go index 299a8668..58701970 100644 --- a/timestamptz.go +++ b/timestamptz.go @@ -124,7 +124,7 @@ func (dst *Timestamptz) DecodeText(ci *ConnInfo, src []byte) error { return err } - *dst = Timestamptz{Time: tim, Status: Present} + *dst = Timestamptz{Time: normalizePotentialUTC(tim), Status: Present} } return nil @@ -231,6 +231,9 @@ func (src Timestamptz) Value() (driver.Value, error) { if src.InfinityModifier != None { return src.InfinityModifier.String(), nil } + if src.Time.Location().String() == time.UTC.String() { + return src.Time.UTC(), nil + } return src.Time, nil case Null: return nil, nil @@ -289,8 +292,23 @@ func (dst *Timestamptz) UnmarshalJSON(b []byte) error { return err } - *dst = Timestamptz{Time: tim, Status: Present} + *dst = Timestamptz{Time: normalizePotentialUTC(tim), Status: Present} } return nil } + +// Normalize timestamps in UTC location to behave similarly to how the Golang +// standard library does it: UTC timestamps lack a .loc value. +// +// Reason for this: when comparing two timestamps with reflect.DeepEqual (generally +// speaking not a good idea, but several testing libraries (for example testify) +// does this), their location data needs to be equal for them to be considered +// equal. +func normalizePotentialUTC(timestamp time.Time) time.Time { + if timestamp.Location().String() != time.UTC.String() { + return timestamp + } + + return timestamp.UTC() +} diff --git a/timestamptz_test.go b/timestamptz_test.go index c3f63967..2ff326bb 100644 --- a/timestamptz_test.go +++ b/timestamptz_test.go @@ -70,8 +70,7 @@ func TestTimestamptzNanosecondsTruncated(t *testing.T) { t.Errorf("%d. EncodeText failed - %v", i, err) } - tstz.DecodeText(nil, buf) - if err != nil { + if err := tstz.DecodeText(nil, buf); err != nil { t.Errorf("%d. DecodeText failed - %v", i, err) } @@ -87,8 +86,7 @@ func TestTimestamptzNanosecondsTruncated(t *testing.T) { t.Errorf("%d. EncodeBinary failed - %v", i, err) } - tstz.DecodeBinary(nil, buf) - if err != nil { + if err := tstz.DecodeBinary(nil, buf); err != nil { t.Errorf("%d. DecodeBinary failed - %v", i, err) } From ccc7cc2931b8f0b3df41e1f5c1e7322d708b6623 Mon Sep 17 00:00:00 2001 From: Oleg Lomaka Date: Tue, 4 Jan 2022 16:25:19 +0200 Subject: [PATCH 02/16] Assign Numeric to *big.Rat --- numeric.go | 26 ++++++++++++++++++++++++++ numeric_test.go | 13 +++++++++++++ 2 files changed, 39 insertions(+) diff --git a/numeric.go b/numeric.go index a939625b..cd057749 100644 --- a/numeric.go +++ b/numeric.go @@ -369,6 +369,12 @@ func (src *Numeric) AssignTo(dst interface{}) error { return fmt.Errorf("%d is greater than maximum value for %T", normalizedInt, *v) } *v = normalizedInt.Uint64() + case *big.Rat: + rat, err := src.toBigRat() + if err != nil { + return err + } + v.Set(rat) default: if nextDst, retry := GetAssignToDstType(dst); retry { return src.AssignTo(nextDst) @@ -406,6 +412,26 @@ func (dst *Numeric) toBigInt() (*big.Int, error) { return num, nil } +func (dst *Numeric) toBigRat() (*big.Rat, error) { + if dst.NaN { + return nil, fmt.Errorf("%v is not a number", dst) + } else if dst.InfinityModifier == Infinity { + return nil, fmt.Errorf("%v is infinity", dst) + } else if dst.InfinityModifier == NegativeInfinity { + return nil, fmt.Errorf("%v is -infinity", dst) + } + + num := new(big.Rat).SetInt(dst.Int) + if dst.Exp > 0 { + mul := new(big.Int).Exp(big10, big.NewInt(int64(dst.Exp)), nil) + num.Mul(num, new(big.Rat).SetInt(mul)) + } else if dst.Exp < 0 { + mul := new(big.Int).Exp(big10, big.NewInt(int64(-dst.Exp)), nil) + num.Quo(num, new(big.Rat).SetInt(mul)) + } + return num, nil +} + func (src *Numeric) toFloat64() (float64, error) { if src.NaN { return math.NaN(), nil diff --git a/numeric_test.go b/numeric_test.go index 455c3ac3..83334a04 100644 --- a/numeric_test.go +++ b/numeric_test.go @@ -263,6 +263,7 @@ func TestNumericAssignTo(t *testing.T) { var f64 float64 var pf32 *float32 var pf64 *float64 + var br = new(big.Rat) simpleTests := []struct { src *pgtype.Numeric @@ -293,6 +294,9 @@ func TestNumericAssignTo(t *testing.T) { {src: &pgtype.Numeric{Status: pgtype.Present, InfinityModifier: pgtype.Infinity}, dst: &f32, expected: float32(math.Inf(1))}, {src: &pgtype.Numeric{Status: pgtype.Present, InfinityModifier: pgtype.NegativeInfinity}, dst: &f64, expected: math.Inf(-1)}, {src: &pgtype.Numeric{Status: pgtype.Present, InfinityModifier: pgtype.NegativeInfinity}, dst: &f32, expected: float32(math.Inf(-1))}, + {src: &pgtype.Numeric{Int: big.NewInt(-1023), Exp: -2, Status: pgtype.Present}, dst: br, expected: big.NewRat(-1023, 100)}, + {src: &pgtype.Numeric{Int: big.NewInt(-1023), Exp: 2, Status: pgtype.Present}, dst: br, expected: big.NewRat(-102300, 1)}, + {src: &pgtype.Numeric{Int: big.NewInt(23), Exp: 0, Status: pgtype.Present}, dst: br, expected: big.NewRat(23, 1)}, } for i, tt := range simpleTests { @@ -317,6 +321,11 @@ func TestNumericAssignTo(t *testing.T) { } else if !nanExpected && dst != tt.expected { t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, dst) } + case big.Rat: + if (&dstTyped).Cmp(tt.expected.(*big.Rat)) != 0 { + t.Errorf("%d: expected %v to assign %v, but result was %v", + i, tt.src, tt.expected, dst) + } default: if dst != tt.expected { t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, dst) @@ -356,6 +365,10 @@ func TestNumericAssignTo(t *testing.T) { {src: &pgtype.Numeric{Int: big.NewInt(-1), Status: pgtype.Present}, dst: &ui64}, {src: &pgtype.Numeric{Int: big.NewInt(-1), Status: pgtype.Present}, dst: &ui}, {src: &pgtype.Numeric{Int: big.NewInt(0), Status: pgtype.Null}, dst: &i32}, + {src: &pgtype.Numeric{Int: big.NewInt(0), Status: pgtype.Null}, dst: br}, + {src: &pgtype.Numeric{Int: big.NewInt(0), Status: pgtype.Present, NaN: true}, dst: br}, + {src: &pgtype.Numeric{Int: big.NewInt(0), Status: pgtype.Present, InfinityModifier: pgtype.Infinity}, dst: br}, + {src: &pgtype.Numeric{Int: big.NewInt(0), Status: pgtype.Present, InfinityModifier: pgtype.NegativeInfinity}, dst: br}, } for i, tt := range errorTests { From 94e10b98b1558e160816e82090e68dd9a7e8b66c Mon Sep 17 00:00:00 2001 From: Pinank Solanki Date: Wed, 2 Feb 2022 03:23:29 +0530 Subject: [PATCH 03/16] Fix typo in float8 --- float8.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/float8.go b/float8.go index 4d9e7116..6297ab5e 100644 --- a/float8.go +++ b/float8.go @@ -204,7 +204,7 @@ func (dst *Float8) DecodeBinary(ci *ConnInfo, src []byte) error { } if len(src) != 8 { - return fmt.Errorf("invalid length for float4: %v", len(src)) + return fmt.Errorf("invalid length for float8: %v", len(src)) } n := int64(binary.BigEndian.Uint64(src)) From f4252a58be6acfa4acdb5c5aa18d6e40d77b5b76 Mon Sep 17 00:00:00 2001 From: Collin Forsyth Date: Wed, 2 Feb 2022 23:37:56 -0500 Subject: [PATCH 04/16] correctly Scan type aliases for floating point types --- convert.go | 4 ++++ float4_test.go | 7 +++++++ float8_test.go | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/convert.go b/convert.go index de9ba9ba..f7219bd4 100644 --- a/convert.go +++ b/convert.go @@ -337,6 +337,10 @@ func float64AssignTo(srcVal float64, srcStatus Status, dst interface{}) error { if v := reflect.ValueOf(dst); v.Kind() == reflect.Ptr { el := v.Elem() switch el.Kind() { + // if dst is a type alias of a float32 or 64, set dst val + case reflect.Float32, reflect.Float64: + el.SetFloat(srcVal) + return nil // if dst is a pointer to pointer, strip the pointer and try again case reflect.Ptr: if el.IsNil() { diff --git a/float4_test.go b/float4_test.go index d2524cda..1977f194 100644 --- a/float4_test.go +++ b/float4_test.go @@ -56,6 +56,9 @@ func TestFloat4Set(t *testing.T) { } func TestFloat4AssignTo(t *testing.T) { + type aliasf32 float32 + type aliasf64 float64 + var i8 int8 var i16 int16 var i32 int32 @@ -73,6 +76,8 @@ func TestFloat4AssignTo(t *testing.T) { var f64 float64 var pf32 *float32 var pf64 *float64 + var a32 aliasf32 + var a64 aliasf64 simpleTests := []struct { src pgtype.Float4 @@ -91,6 +96,8 @@ func TestFloat4AssignTo(t *testing.T) { {src: pgtype.Float4{Float: 42, Status: pgtype.Present}, dst: &ui64, expected: uint64(42)}, {src: pgtype.Float4{Float: 42, Status: pgtype.Present}, dst: &ui, expected: uint(42)}, {src: pgtype.Float4{Float: 42, Status: pgtype.Present}, dst: &_i8, expected: _int8(42)}, + {src: pgtype.Float4{Float: 42, Status: pgtype.Present}, dst: &a32, expected: aliasf32(42)}, + {src: pgtype.Float4{Float: 42, Status: pgtype.Present}, dst: &a64, expected: aliasf64(42)}, {src: pgtype.Float4{Float: 0, Status: pgtype.Null}, dst: &pi8, expected: ((*int8)(nil))}, {src: pgtype.Float4{Float: 0, Status: pgtype.Null}, dst: &_pi8, expected: ((*_int8)(nil))}, } diff --git a/float8_test.go b/float8_test.go index 6bc7c652..c21f00d0 100644 --- a/float8_test.go +++ b/float8_test.go @@ -56,6 +56,9 @@ func TestFloat8Set(t *testing.T) { } func TestFloat8AssignTo(t *testing.T) { + type aliasf32 float32 + type aliasf64 float64 + var i8 int8 var i16 int16 var i32 int32 @@ -73,6 +76,8 @@ func TestFloat8AssignTo(t *testing.T) { var f64 float64 var pf32 *float32 var pf64 *float64 + var a32 aliasf32 + var a64 aliasf64 simpleTests := []struct { src pgtype.Float8 @@ -91,6 +96,9 @@ func TestFloat8AssignTo(t *testing.T) { {src: pgtype.Float8{Float: 42, Status: pgtype.Present}, dst: &ui64, expected: uint64(42)}, {src: pgtype.Float8{Float: 42, Status: pgtype.Present}, dst: &ui, expected: uint(42)}, {src: pgtype.Float8{Float: 42, Status: pgtype.Present}, dst: &_i8, expected: _int8(42)}, + {src: pgtype.Float8{Float: 42, Status: pgtype.Present}, dst: &a32, expected: aliasf32(42)}, + {src: pgtype.Float8{Float: 42, Status: pgtype.Present}, dst: &a64, expected: aliasf64(42)}, + {src: pgtype.Float8{Float: 0, Status: pgtype.Null}, dst: &pi8, expected: ((*int8)(nil))}, {src: pgtype.Float8{Float: 0, Status: pgtype.Null}, dst: &_pi8, expected: ((*_int8)(nil))}, } From 202542ead5c88f0d66fddc0159b70b4c6c948170 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Mon, 7 Feb 2022 10:51:03 -0600 Subject: [PATCH 05/16] Release v1.10.0 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e34c7979..73126cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.10.0 (February 7, 2022) + +* Normalize UTC timestamps to comply with stdlib (Torkel Rogstad) +* Assign Numeric to *big.Rat (Oleg Lomaka) +* Fix typo in float8 error message (Pinank Solanki) +* Scan type aliases for floating point types (Collin Forsyth) + # 1.9.1 (November 28, 2021) * Fix: binary timestamp is assumed to be in UTC (restored behavior changed in v1.9.0) From a365c9a3c2c71facc0874f0a422a9b91703695db Mon Sep 17 00:00:00 2001 From: Vu Date: Tue, 1 Mar 2022 23:18:55 +0800 Subject: [PATCH 06/16] Add multirange support for num, int4 and int8 type --- int4_multirange.go | 239 ++++++++++++++++++++++++++++++++++++++++ int4_multirange_test.go | 81 ++++++++++++++ int8_multirange.go | 239 ++++++++++++++++++++++++++++++++++++++++ int8_multirange_test.go | 81 ++++++++++++++ multirange.go | 83 ++++++++++++++ multirange_test.go | 51 +++++++++ num_multirange.go | 239 ++++++++++++++++++++++++++++++++++++++++ num_multirange_test.go | 55 +++++++++ pgtype.go | 143 +++++++++++++----------- typed_multirange.go.erb | 239 ++++++++++++++++++++++++++++++++++++++++ typed_multirange_gen.sh | 8 ++ 11 files changed, 1391 insertions(+), 67 deletions(-) create mode 100644 int4_multirange.go create mode 100644 int4_multirange_test.go create mode 100644 int8_multirange.go create mode 100644 int8_multirange_test.go create mode 100644 multirange.go create mode 100644 multirange_test.go create mode 100644 num_multirange.go create mode 100644 num_multirange_test.go create mode 100644 typed_multirange.go.erb create mode 100755 typed_multirange_gen.sh diff --git a/int4_multirange.go b/int4_multirange.go new file mode 100644 index 00000000..c3432ce6 --- /dev/null +++ b/int4_multirange.go @@ -0,0 +1,239 @@ +package pgtype + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + + "github.com/jackc/pgio" +) + +type Int4multirange struct { + Ranges []Int4range + Status Status +} + +func (dst *Int4multirange) Set(src interface{}) error { + //untyped nil and typed nil interfaces are different + if src == nil { + *dst = Int4multirange{Status: Null} + return nil + } + + switch value := src.(type) { + case Int4multirange: + *dst = value + case *Int4multirange: + *dst = *value + case string: + return dst.DecodeText(nil, []byte(value)) + case []Int4range: + if value == nil { + *dst = Int4multirange{Status: Null} + } else if len(value) == 0 { + *dst = Int4multirange{Status: Present} + } else { + elements := make([]Int4range, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = Int4multirange{ + Ranges: elements, + Status: Present, + } + } + case []*Int4range: + if value == nil { + *dst = Int4multirange{Status: Null} + } else if len(value) == 0 { + *dst = Int4multirange{Status: Present} + } else { + elements := make([]Int4range, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = Int4multirange{ + Ranges: elements, + Status: Present, + } + } + default: + return fmt.Errorf("cannot convert %v to Int4multirange", src) + } + + return nil + +} + +func (dst Int4multirange) Get() interface{} { + switch dst.Status { + case Present: + return dst + case Null: + return nil + default: + return dst.Status + } +} + +func (src *Int4multirange) AssignTo(dst interface{}) error { + return fmt.Errorf("cannot assign %v to %T", src, dst) +} + +func (dst *Int4multirange) DecodeText(ci *ConnInfo, src []byte) error { + if src == nil { + *dst = Int4multirange{Status: Null} + return nil + } + + utmr, err := ParseUntypedTextMultirange(string(src)) + if err != nil { + return err + } + + var elements []Int4range + + if len(utmr.Elements) > 0 { + elements = make([]Int4range, len(utmr.Elements)) + + for i, s := range utmr.Elements { + var elem Int4range + + elemSrc := []byte(s) + + err = elem.DecodeText(ci, elemSrc) + if err != nil { + return err + } + + elements[i] = elem + } + } + + *dst = Int4multirange{Ranges: elements, Status: Present} + + return nil +} + +func (dst *Int4multirange) DecodeBinary(ci *ConnInfo, src []byte) error { + if src == nil { + *dst = Int4multirange{Status: Null} + return nil + } + + rp := 0 + + numElems := int(binary.BigEndian.Uint32(src[rp:])) + rp += 4 + + if numElems == 0 { + *dst = Int4multirange{Status: Present} + return nil + } + + elements := make([]Int4range, numElems) + + for i := range elements { + elemLen := int(int32(binary.BigEndian.Uint32(src[rp:]))) + rp += 4 + var elemSrc []byte + if elemLen >= 0 { + elemSrc = src[rp : rp+elemLen] + rp += elemLen + } + err := elements[i].DecodeBinary(ci, elemSrc) + if err != nil { + return err + } + } + + *dst = Int4multirange{Ranges: elements, Status: Present} + return nil +} + +func (src Int4multirange) EncodeText(ci *ConnInfo, buf []byte) ([]byte, error) { + switch src.Status { + case Null: + return nil, nil + case Undefined: + return nil, errUndefined + } + + buf = append(buf, '{') + + inElemBuf := make([]byte, 0, 32) + for i, elem := range src.Ranges { + if i > 0 { + buf = append(buf, ',') + } + + elemBuf, err := elem.EncodeText(ci, inElemBuf) + if err != nil { + return nil, err + } + if elemBuf == nil { + return nil, fmt.Errorf("multi-range does not allow null range") + } else { + buf = append(buf, string(elemBuf)...) + } + + } + + buf = append(buf, '}') + + return buf, nil +} + +func (src Int4multirange) EncodeBinary(ci *ConnInfo, buf []byte) ([]byte, error) { + switch src.Status { + case Null: + return nil, nil + case Undefined: + return nil, errUndefined + } + + buf = pgio.AppendInt32(buf, int32(len(src.Ranges))) + + for i := range src.Ranges { + sp := len(buf) + buf = pgio.AppendInt32(buf, -1) + + elemBuf, err := src.Ranges[i].EncodeBinary(ci, buf) + if err != nil { + return nil, err + } + if elemBuf != nil { + buf = elemBuf + pgio.SetInt32(buf[sp:], int32(len(buf[sp:])-4)) + } + } + + return buf, nil +} + +// Scan implements the database/sql Scanner interface. +func (dst *Int4multirange) Scan(src interface{}) error { + if src == nil { + return dst.DecodeText(nil, nil) + } + + switch src := src.(type) { + case string: + return dst.DecodeText(nil, []byte(src)) + case []byte: + srcCopy := make([]byte, len(src)) + copy(srcCopy, src) + return dst.DecodeText(nil, srcCopy) + } + + return fmt.Errorf("cannot scan %T", src) +} + +// Value implements the database/sql/driver Valuer interface. +func (src Int4multirange) Value() (driver.Value, error) { + return EncodeValueText(src) +} diff --git a/int4_multirange_test.go b/int4_multirange_test.go new file mode 100644 index 00000000..e123c402 --- /dev/null +++ b/int4_multirange_test.go @@ -0,0 +1,81 @@ +package pgtype_test + +import ( + "testing" + + "github.com/jackc/pgtype" + "github.com/jackc/pgtype/testutil" +) + +func TestInt4multirangeTranscode(t *testing.T) { + testutil.TestSuccessfulTranscode(t, "int4multirange", []interface{}{ + &pgtype.Int4multirange{ + Ranges: nil, + Status: pgtype.Present, + }, + &pgtype.Int4multirange{ + Ranges: []pgtype.Int4range{ + { + Lower: pgtype.Int4{Int: -543, Status: pgtype.Present}, + Upper: pgtype.Int4{Int: 342, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + }, + &pgtype.Int4multirange{ + Ranges: []pgtype.Int4range{ + { + Lower: pgtype.Int4{Int: -42, Status: pgtype.Present}, + Upper: pgtype.Int4{Int: -5, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + { + Lower: pgtype.Int4{Int: 5, Status: pgtype.Present}, + Upper: pgtype.Int4{Int: 42, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + { + Lower: pgtype.Int4{Int: 52, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Unbounded, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + }, + }) +} + +func TestInt4multirangeNormalize(t *testing.T) { + testutil.TestSuccessfulNormalize(t, []testutil.NormalizeTest{ + { + SQL: "select int4multirange(int4range(1, 14, '(]'), int4range(20, 25, '()'))", + Value: pgtype.Int4multirange{ + Ranges: []pgtype.Int4range{ + { + Lower: pgtype.Int4{Int: 2, Status: pgtype.Present}, + Upper: pgtype.Int4{Int: 15, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + { + Lower: pgtype.Int4{Int: 21, Status: pgtype.Present}, + Upper: pgtype.Int4{Int: 25, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + }, + }, + }) +} diff --git a/int8_multirange.go b/int8_multirange.go new file mode 100644 index 00000000..e0976427 --- /dev/null +++ b/int8_multirange.go @@ -0,0 +1,239 @@ +package pgtype + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + + "github.com/jackc/pgio" +) + +type Int8multirange struct { + Ranges []Int8range + Status Status +} + +func (dst *Int8multirange) Set(src interface{}) error { + //untyped nil and typed nil interfaces are different + if src == nil { + *dst = Int8multirange{Status: Null} + return nil + } + + switch value := src.(type) { + case Int8multirange: + *dst = value + case *Int8multirange: + *dst = *value + case string: + return dst.DecodeText(nil, []byte(value)) + case []Int8range: + if value == nil { + *dst = Int8multirange{Status: Null} + } else if len(value) == 0 { + *dst = Int8multirange{Status: Present} + } else { + elements := make([]Int8range, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = Int8multirange{ + Ranges: elements, + Status: Present, + } + } + case []*Int8range: + if value == nil { + *dst = Int8multirange{Status: Null} + } else if len(value) == 0 { + *dst = Int8multirange{Status: Present} + } else { + elements := make([]Int8range, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = Int8multirange{ + Ranges: elements, + Status: Present, + } + } + default: + return fmt.Errorf("cannot convert %v to Int8multirange", src) + } + + return nil + +} + +func (dst Int8multirange) Get() interface{} { + switch dst.Status { + case Present: + return dst + case Null: + return nil + default: + return dst.Status + } +} + +func (src *Int8multirange) AssignTo(dst interface{}) error { + return fmt.Errorf("cannot assign %v to %T", src, dst) +} + +func (dst *Int8multirange) DecodeText(ci *ConnInfo, src []byte) error { + if src == nil { + *dst = Int8multirange{Status: Null} + return nil + } + + utmr, err := ParseUntypedTextMultirange(string(src)) + if err != nil { + return err + } + + var elements []Int8range + + if len(utmr.Elements) > 0 { + elements = make([]Int8range, len(utmr.Elements)) + + for i, s := range utmr.Elements { + var elem Int8range + + elemSrc := []byte(s) + + err = elem.DecodeText(ci, elemSrc) + if err != nil { + return err + } + + elements[i] = elem + } + } + + *dst = Int8multirange{Ranges: elements, Status: Present} + + return nil +} + +func (dst *Int8multirange) DecodeBinary(ci *ConnInfo, src []byte) error { + if src == nil { + *dst = Int8multirange{Status: Null} + return nil + } + + rp := 0 + + numElems := int(binary.BigEndian.Uint32(src[rp:])) + rp += 4 + + if numElems == 0 { + *dst = Int8multirange{Status: Present} + return nil + } + + elements := make([]Int8range, numElems) + + for i := range elements { + elemLen := int(int32(binary.BigEndian.Uint32(src[rp:]))) + rp += 4 + var elemSrc []byte + if elemLen >= 0 { + elemSrc = src[rp : rp+elemLen] + rp += elemLen + } + err := elements[i].DecodeBinary(ci, elemSrc) + if err != nil { + return err + } + } + + *dst = Int8multirange{Ranges: elements, Status: Present} + return nil +} + +func (src Int8multirange) EncodeText(ci *ConnInfo, buf []byte) ([]byte, error) { + switch src.Status { + case Null: + return nil, nil + case Undefined: + return nil, errUndefined + } + + buf = append(buf, '{') + + inElemBuf := make([]byte, 0, 32) + for i, elem := range src.Ranges { + if i > 0 { + buf = append(buf, ',') + } + + elemBuf, err := elem.EncodeText(ci, inElemBuf) + if err != nil { + return nil, err + } + if elemBuf == nil { + return nil, fmt.Errorf("multi-range does not allow null range") + } else { + buf = append(buf, string(elemBuf)...) + } + + } + + buf = append(buf, '}') + + return buf, nil +} + +func (src Int8multirange) EncodeBinary(ci *ConnInfo, buf []byte) ([]byte, error) { + switch src.Status { + case Null: + return nil, nil + case Undefined: + return nil, errUndefined + } + + buf = pgio.AppendInt32(buf, int32(len(src.Ranges))) + + for i := range src.Ranges { + sp := len(buf) + buf = pgio.AppendInt32(buf, -1) + + elemBuf, err := src.Ranges[i].EncodeBinary(ci, buf) + if err != nil { + return nil, err + } + if elemBuf != nil { + buf = elemBuf + pgio.SetInt32(buf[sp:], int32(len(buf[sp:])-4)) + } + } + + return buf, nil +} + +// Scan implements the database/sql Scanner interface. +func (dst *Int8multirange) Scan(src interface{}) error { + if src == nil { + return dst.DecodeText(nil, nil) + } + + switch src := src.(type) { + case string: + return dst.DecodeText(nil, []byte(src)) + case []byte: + srcCopy := make([]byte, len(src)) + copy(srcCopy, src) + return dst.DecodeText(nil, srcCopy) + } + + return fmt.Errorf("cannot scan %T", src) +} + +// Value implements the database/sql/driver Valuer interface. +func (src Int8multirange) Value() (driver.Value, error) { + return EncodeValueText(src) +} diff --git a/int8_multirange_test.go b/int8_multirange_test.go new file mode 100644 index 00000000..a4233384 --- /dev/null +++ b/int8_multirange_test.go @@ -0,0 +1,81 @@ +package pgtype_test + +import ( + "testing" + + "github.com/jackc/pgtype" + "github.com/jackc/pgtype/testutil" +) + +func TestInt8multirangeTranscode(t *testing.T) { + testutil.TestSuccessfulTranscode(t, "int8multirange", []interface{}{ + &pgtype.Int8multirange{ + Ranges: nil, + Status: pgtype.Present, + }, + &pgtype.Int8multirange{ + Ranges: []pgtype.Int8range{ + { + Lower: pgtype.Int8{Int: -543, Status: pgtype.Present}, + Upper: pgtype.Int8{Int: 342, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + }, + &pgtype.Int8multirange{ + Ranges: []pgtype.Int8range{ + { + Lower: pgtype.Int8{Int: -42, Status: pgtype.Present}, + Upper: pgtype.Int8{Int: -5, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + { + Lower: pgtype.Int8{Int: 5, Status: pgtype.Present}, + Upper: pgtype.Int8{Int: 42, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + { + Lower: pgtype.Int8{Int: 52, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Unbounded, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + }, + }) +} + +func TestInt8multirangeNormalize(t *testing.T) { + testutil.TestSuccessfulNormalize(t, []testutil.NormalizeTest{ + { + SQL: "select int8multirange(int8range(1, 14, '(]'), int8range(20, 25, '()'))", + Value: pgtype.Int8multirange{ + Ranges: []pgtype.Int8range{ + { + Lower: pgtype.Int8{Int: 2, Status: pgtype.Present}, + Upper: pgtype.Int8{Int: 15, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + { + Lower: pgtype.Int8{Int: 21, Status: pgtype.Present}, + Upper: pgtype.Int8{Int: 25, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + }, + }, + }) +} diff --git a/multirange.go b/multirange.go new file mode 100644 index 00000000..beb11f70 --- /dev/null +++ b/multirange.go @@ -0,0 +1,83 @@ +package pgtype + +import ( + "bytes" + "fmt" +) + +type UntypedTextMultirange struct { + Elements []string +} + +func ParseUntypedTextMultirange(src string) (*UntypedTextMultirange, error) { + utmr := &UntypedTextMultirange{} + utmr.Elements = make([]string, 0) + + buf := bytes.NewBufferString(src) + + skipWhitespace(buf) + + r, _, err := buf.ReadRune() + if err != nil { + return nil, fmt.Errorf("invalid array: %v", err) + } + + if r != '{' { + return nil, fmt.Errorf("invalid multirange, expected '{': %v", err) + } + +parseValueLoop: + for { + r, _, err = buf.ReadRune() + if err != nil { + return nil, fmt.Errorf("invalid multirange: %v", err) + } + + switch r { + case ',': // skip range separator + case '}': + break parseValueLoop + default: + buf.UnreadRune() + value, err := parseRange(buf) + if err != nil { + return nil, fmt.Errorf("invalid multirange value: %v", err) + } + utmr.Elements = append(utmr.Elements, value) + } + } + + skipWhitespace(buf) + + if buf.Len() > 0 { + return nil, fmt.Errorf("unexpected trailing data: %v", buf.String()) + } + + return utmr, nil + +} + +func parseRange(buf *bytes.Buffer) (string, error) { + + s := &bytes.Buffer{} + + boundSepRead := false + for { + r, _, err := buf.ReadRune() + if err != nil { + return "", err + } + + switch r { + case ',', '}': + if r == ',' && !boundSepRead { + boundSepRead = true + break + } + buf.UnreadRune() + return s.String(), nil + } + + s.WriteRune(r) + } +} diff --git a/multirange_test.go b/multirange_test.go new file mode 100644 index 00000000..4991aecf --- /dev/null +++ b/multirange_test.go @@ -0,0 +1,51 @@ +package pgtype + +import ( + "reflect" + "testing" +) + +func TestParseUntypedTextMultirange(t *testing.T) { + tests := []struct { + src string + result UntypedTextMultirange + err error + }{ + { + src: `{[1,2)}`, + result: UntypedTextMultirange{Elements: []string{`[1,2)`}}, + err: nil, + }, + { + src: `{[,),["foo", "bar"]}`, + result: UntypedTextMultirange{Elements: []string{`[,)`, `["foo", "bar"]`}}, + err: nil, + }, + { + src: `{}`, + result: UntypedTextMultirange{Elements: []string{}}, + err: nil, + }, + { + src: ` { (,) , [1,2] } `, + result: UntypedTextMultirange{Elements: []string{` (,) `, ` [1,2] `}}, + err: nil, + }, + { + src: `{["f""oo","b""ar")}`, + result: UntypedTextMultirange{Elements: []string{`["f""oo","b""ar")`}}, + err: nil, + }, + } + for i, tt := range tests { + r, err := ParseUntypedTextMultirange(tt.src) + if err != tt.err { + t.Errorf("%d. `%v`: expected err %v, got %v", i, tt.src, tt.err, err) + continue + } + + if !reflect.DeepEqual(*r, tt.result) { + t.Errorf("%d: expected %+v to be parsed to %+v, but it was %+v", i, tt.src, tt.result, *r) + } + } +} diff --git a/num_multirange.go b/num_multirange.go new file mode 100644 index 00000000..cbabc8ac --- /dev/null +++ b/num_multirange.go @@ -0,0 +1,239 @@ +package pgtype + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + + "github.com/jackc/pgio" +) + +type Nummultirange struct { + Ranges []Numrange + Status Status +} + +func (dst *Nummultirange) Set(src interface{}) error { + //untyped nil and typed nil interfaces are different + if src == nil { + *dst = Nummultirange{Status: Null} + return nil + } + + switch value := src.(type) { + case Nummultirange: + *dst = value + case *Nummultirange: + *dst = *value + case string: + return dst.DecodeText(nil, []byte(value)) + case []Numrange: + if value == nil { + *dst = Nummultirange{Status: Null} + } else if len(value) == 0 { + *dst = Nummultirange{Status: Present} + } else { + elements := make([]Numrange, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = Nummultirange{ + Ranges: elements, + Status: Present, + } + } + case []*Numrange: + if value == nil { + *dst = Nummultirange{Status: Null} + } else if len(value) == 0 { + *dst = Nummultirange{Status: Present} + } else { + elements := make([]Numrange, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = Nummultirange{ + Ranges: elements, + Status: Present, + } + } + default: + return fmt.Errorf("cannot convert %v to Nummultirange", src) + } + + return nil + +} + +func (dst Nummultirange) Get() interface{} { + switch dst.Status { + case Present: + return dst + case Null: + return nil + default: + return dst.Status + } +} + +func (src *Nummultirange) AssignTo(dst interface{}) error { + return fmt.Errorf("cannot assign %v to %T", src, dst) +} + +func (dst *Nummultirange) DecodeText(ci *ConnInfo, src []byte) error { + if src == nil { + *dst = Nummultirange{Status: Null} + return nil + } + + utmr, err := ParseUntypedTextMultirange(string(src)) + if err != nil { + return err + } + + var elements []Numrange + + if len(utmr.Elements) > 0 { + elements = make([]Numrange, len(utmr.Elements)) + + for i, s := range utmr.Elements { + var elem Numrange + + elemSrc := []byte(s) + + err = elem.DecodeText(ci, elemSrc) + if err != nil { + return err + } + + elements[i] = elem + } + } + + *dst = Nummultirange{Ranges: elements, Status: Present} + + return nil +} + +func (dst *Nummultirange) DecodeBinary(ci *ConnInfo, src []byte) error { + if src == nil { + *dst = Nummultirange{Status: Null} + return nil + } + + rp := 0 + + numElems := int(binary.BigEndian.Uint32(src[rp:])) + rp += 4 + + if numElems == 0 { + *dst = Nummultirange{Status: Present} + return nil + } + + elements := make([]Numrange, numElems) + + for i := range elements { + elemLen := int(int32(binary.BigEndian.Uint32(src[rp:]))) + rp += 4 + var elemSrc []byte + if elemLen >= 0 { + elemSrc = src[rp : rp+elemLen] + rp += elemLen + } + err := elements[i].DecodeBinary(ci, elemSrc) + if err != nil { + return err + } + } + + *dst = Nummultirange{Ranges: elements, Status: Present} + return nil +} + +func (src Nummultirange) EncodeText(ci *ConnInfo, buf []byte) ([]byte, error) { + switch src.Status { + case Null: + return nil, nil + case Undefined: + return nil, errUndefined + } + + buf = append(buf, '{') + + inElemBuf := make([]byte, 0, 32) + for i, elem := range src.Ranges { + if i > 0 { + buf = append(buf, ',') + } + + elemBuf, err := elem.EncodeText(ci, inElemBuf) + if err != nil { + return nil, err + } + if elemBuf == nil { + return nil, fmt.Errorf("multi-range does not allow null range") + } else { + buf = append(buf, string(elemBuf)...) + } + + } + + buf = append(buf, '}') + + return buf, nil +} + +func (src Nummultirange) EncodeBinary(ci *ConnInfo, buf []byte) ([]byte, error) { + switch src.Status { + case Null: + return nil, nil + case Undefined: + return nil, errUndefined + } + + buf = pgio.AppendInt32(buf, int32(len(src.Ranges))) + + for i := range src.Ranges { + sp := len(buf) + buf = pgio.AppendInt32(buf, -1) + + elemBuf, err := src.Ranges[i].EncodeBinary(ci, buf) + if err != nil { + return nil, err + } + if elemBuf != nil { + buf = elemBuf + pgio.SetInt32(buf[sp:], int32(len(buf[sp:])-4)) + } + } + + return buf, nil +} + +// Scan implements the database/sql Scanner interface. +func (dst *Nummultirange) Scan(src interface{}) error { + if src == nil { + return dst.DecodeText(nil, nil) + } + + switch src := src.(type) { + case string: + return dst.DecodeText(nil, []byte(src)) + case []byte: + srcCopy := make([]byte, len(src)) + copy(srcCopy, src) + return dst.DecodeText(nil, srcCopy) + } + + return fmt.Errorf("cannot scan %T", src) +} + +// Value implements the database/sql/driver Valuer interface. +func (src Nummultirange) Value() (driver.Value, error) { + return EncodeValueText(src) +} diff --git a/num_multirange_test.go b/num_multirange_test.go new file mode 100644 index 00000000..f4289794 --- /dev/null +++ b/num_multirange_test.go @@ -0,0 +1,55 @@ +package pgtype_test + +import ( + "math/big" + "testing" + + "github.com/jackc/pgtype" + "github.com/jackc/pgtype/testutil" +) + +func TestNumericMultirangeTranscode(t *testing.T) { + testutil.TestSuccessfulTranscode(t, "nummultirange", []interface{}{ + &pgtype.Nummultirange{ + Ranges: nil, + Status: pgtype.Present, + }, + &pgtype.Nummultirange{ + Ranges: []pgtype.Numrange{ + { + Lower: pgtype.Numeric{Int: big.NewInt(-543), Exp: 3, Status: pgtype.Present}, + Upper: pgtype.Numeric{Int: big.NewInt(342), Exp: 1, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + }, + &pgtype.Nummultirange{ + Ranges: []pgtype.Numrange{ + { + Lower: pgtype.Numeric{Int: big.NewInt(-42), Exp: 1, Status: pgtype.Present}, + Upper: pgtype.Numeric{Int: big.NewInt(-5), Exp: 0, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + { + Lower: pgtype.Numeric{Int: big.NewInt(5), Exp: 1, Status: pgtype.Present}, + Upper: pgtype.Numeric{Int: big.NewInt(42), Exp: 1, Status: pgtype.Present}, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Inclusive, + Status: pgtype.Present, + }, + { + Lower: pgtype.Numeric{Int: big.NewInt(42), Exp: 2, Status: pgtype.Present}, + LowerType: pgtype.Exclusive, + UpperType: pgtype.Unbounded, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + }, + }) +} diff --git a/pgtype.go b/pgtype.go index 200fb562..eba09fa5 100644 --- a/pgtype.go +++ b/pgtype.go @@ -74,12 +74,15 @@ const ( JSONBArrayOID = 3807 DaterangeOID = 3912 Int4rangeOID = 3904 + Int4multirangeOID = 4451 NumrangeOID = 3906 + NummultirangeOID = 4532 TsrangeOID = 3908 TsrangeArrayOID = 3909 TstzrangeOID = 3910 TstzrangeArrayOID = 3911 Int8rangeOID = 3926 + Int8multirangeOID = 4536 ) type Status byte @@ -288,8 +291,10 @@ func NewConnInfo() *ConnInfo { ci.RegisterDataType(DataType{Value: &Int2{}, Name: "int2", OID: Int2OID}) ci.RegisterDataType(DataType{Value: &Int4{}, Name: "int4", OID: Int4OID}) ci.RegisterDataType(DataType{Value: &Int4range{}, Name: "int4range", OID: Int4rangeOID}) + ci.RegisterDataType(DataType{Value: &Int4multirange{}, Name: "int4multirange", OID: Int4multirangeOID}) ci.RegisterDataType(DataType{Value: &Int8{}, Name: "int8", OID: Int8OID}) ci.RegisterDataType(DataType{Value: &Int8range{}, Name: "int8range", OID: Int8rangeOID}) + ci.RegisterDataType(DataType{Value: &Int8multirange{}, Name: "int8multirange", OID: Int8multirangeOID}) ci.RegisterDataType(DataType{Value: &Interval{}, Name: "interval", OID: IntervalOID}) ci.RegisterDataType(DataType{Value: &JSON{}, Name: "json", OID: JSONOID}) ci.RegisterDataType(DataType{Value: &JSONB{}, Name: "jsonb", OID: JSONBOID}) @@ -300,6 +305,7 @@ func NewConnInfo() *ConnInfo { ci.RegisterDataType(DataType{Value: &Name{}, Name: "name", OID: NameOID}) ci.RegisterDataType(DataType{Value: &Numeric{}, Name: "numeric", OID: NumericOID}) ci.RegisterDataType(DataType{Value: &Numrange{}, Name: "numrange", OID: NumrangeOID}) + ci.RegisterDataType(DataType{Value: &Nummultirange{}, Name: "nummultirange", OID: NummultirangeOID}) ci.RegisterDataType(DataType{Value: &OIDValue{}, Name: "oid", OID: OIDOID}) ci.RegisterDataType(DataType{Value: &Path{}, Name: "path", OID: PathOID}) ci.RegisterDataType(DataType{Value: &Point{}, Name: "point", OID: PointOID}) @@ -873,72 +879,75 @@ var nameValues map[string]Value func init() { nameValues = map[string]Value{ - "_aclitem": &ACLItemArray{}, - "_bool": &BoolArray{}, - "_bpchar": &BPCharArray{}, - "_bytea": &ByteaArray{}, - "_cidr": &CIDRArray{}, - "_date": &DateArray{}, - "_float4": &Float4Array{}, - "_float8": &Float8Array{}, - "_inet": &InetArray{}, - "_int2": &Int2Array{}, - "_int4": &Int4Array{}, - "_int8": &Int8Array{}, - "_numeric": &NumericArray{}, - "_text": &TextArray{}, - "_timestamp": &TimestampArray{}, - "_timestamptz": &TimestamptzArray{}, - "_uuid": &UUIDArray{}, - "_varchar": &VarcharArray{}, - "_jsonb": &JSONBArray{}, - "aclitem": &ACLItem{}, - "bit": &Bit{}, - "bool": &Bool{}, - "box": &Box{}, - "bpchar": &BPChar{}, - "bytea": &Bytea{}, - "char": &QChar{}, - "cid": &CID{}, - "cidr": &CIDR{}, - "circle": &Circle{}, - "date": &Date{}, - "daterange": &Daterange{}, - "float4": &Float4{}, - "float8": &Float8{}, - "hstore": &Hstore{}, - "inet": &Inet{}, - "int2": &Int2{}, - "int4": &Int4{}, - "int4range": &Int4range{}, - "int8": &Int8{}, - "int8range": &Int8range{}, - "interval": &Interval{}, - "json": &JSON{}, - "jsonb": &JSONB{}, - "line": &Line{}, - "lseg": &Lseg{}, - "macaddr": &Macaddr{}, - "name": &Name{}, - "numeric": &Numeric{}, - "numrange": &Numrange{}, - "oid": &OIDValue{}, - "path": &Path{}, - "point": &Point{}, - "polygon": &Polygon{}, - "record": &Record{}, - "text": &Text{}, - "tid": &TID{}, - "timestamp": &Timestamp{}, - "timestamptz": &Timestamptz{}, - "tsrange": &Tsrange{}, - "_tsrange": &TsrangeArray{}, - "tstzrange": &Tstzrange{}, - "_tstzrange": &TstzrangeArray{}, - "unknown": &Unknown{}, - "uuid": &UUID{}, - "varbit": &Varbit{}, - "varchar": &Varchar{}, - "xid": &XID{}, + "_aclitem": &ACLItemArray{}, + "_bool": &BoolArray{}, + "_bpchar": &BPCharArray{}, + "_bytea": &ByteaArray{}, + "_cidr": &CIDRArray{}, + "_date": &DateArray{}, + "_float4": &Float4Array{}, + "_float8": &Float8Array{}, + "_inet": &InetArray{}, + "_int2": &Int2Array{}, + "_int4": &Int4Array{}, + "_int8": &Int8Array{}, + "_numeric": &NumericArray{}, + "_text": &TextArray{}, + "_timestamp": &TimestampArray{}, + "_timestamptz": &TimestamptzArray{}, + "_uuid": &UUIDArray{}, + "_varchar": &VarcharArray{}, + "_jsonb": &JSONBArray{}, + "aclitem": &ACLItem{}, + "bit": &Bit{}, + "bool": &Bool{}, + "box": &Box{}, + "bpchar": &BPChar{}, + "bytea": &Bytea{}, + "char": &QChar{}, + "cid": &CID{}, + "cidr": &CIDR{}, + "circle": &Circle{}, + "date": &Date{}, + "daterange": &Daterange{}, + "float4": &Float4{}, + "float8": &Float8{}, + "hstore": &Hstore{}, + "inet": &Inet{}, + "int2": &Int2{}, + "int4": &Int4{}, + "int4range": &Int4range{}, + "int4multirange": &Int4multirange{}, + "int8": &Int8{}, + "int8range": &Int8range{}, + "int8multirange": &Int8multirange{}, + "interval": &Interval{}, + "json": &JSON{}, + "jsonb": &JSONB{}, + "line": &Line{}, + "lseg": &Lseg{}, + "macaddr": &Macaddr{}, + "name": &Name{}, + "numeric": &Numeric{}, + "numrange": &Numrange{}, + "nummultirange": &Nummultirange{}, + "oid": &OIDValue{}, + "path": &Path{}, + "point": &Point{}, + "polygon": &Polygon{}, + "record": &Record{}, + "text": &Text{}, + "tid": &TID{}, + "timestamp": &Timestamp{}, + "timestamptz": &Timestamptz{}, + "tsrange": &Tsrange{}, + "_tsrange": &TsrangeArray{}, + "tstzrange": &Tstzrange{}, + "_tstzrange": &TstzrangeArray{}, + "unknown": &Unknown{}, + "uuid": &UUID{}, + "varbit": &Varbit{}, + "varchar": &Varchar{}, + "xid": &XID{}, } } diff --git a/typed_multirange.go.erb b/typed_multirange.go.erb new file mode 100644 index 00000000..84c8299f --- /dev/null +++ b/typed_multirange.go.erb @@ -0,0 +1,239 @@ +package pgtype + +import ( + "database/sql/driver" + "encoding/binary" + "fmt" + + "github.com/jackc/pgio" +) + +type <%= multirange_type %> struct { + Ranges []<%= range_type %> + Status Status +} + +func (dst *<%= multirange_type %>) Set(src interface{}) error { + //untyped nil and typed nil interfaces are different + if src == nil { + *dst = <%= multirange_type %>{Status: Null} + return nil + } + + switch value := src.(type) { + case <%= multirange_type %>: + *dst = value + case *<%= multirange_type %>: + *dst = *value + case string: + return dst.DecodeText(nil, []byte(value)) + case []<%= range_type %>: + if value == nil { + *dst = <%= multirange_type %>{Status: Null} + } else if len(value) == 0 { + *dst = <%= multirange_type %>{Status: Present} + } else { + elements := make([]<%= range_type %>, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = <%= multirange_type %>{ + Ranges: elements, + Status: Present, + } + } + case []*<%= range_type %>: + if value == nil { + *dst = <%= multirange_type %>{Status: Null} + } else if len(value) == 0 { + *dst = <%= multirange_type %>{Status: Present} + } else { + elements := make([]<%= range_type %>, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = <%= multirange_type %>{ + Ranges: elements, + Status: Present, + } + } + default: + return fmt.Errorf("cannot convert %v to <%= multirange_type %>", src) + } + + return nil + +} + +func (dst <%= multirange_type %>) Get() interface{} { + switch dst.Status { + case Present: + return dst + case Null: + return nil + default: + return dst.Status + } +} + +func (src *<%= multirange_type %>) AssignTo(dst interface{}) error { + return fmt.Errorf("cannot assign %v to %T", src, dst) +} + +func (dst *<%= multirange_type %>) DecodeText(ci *ConnInfo, src []byte) error { + if src == nil { + *dst = <%= multirange_type %>{Status: Null} + return nil + } + + utmr, err := ParseUntypedTextMultirange(string(src)) + if err != nil { + return err + } + + var elements []<%= range_type %> + + if len(utmr.Elements) > 0 { + elements = make([]<%= range_type %>, len(utmr.Elements)) + + for i, s := range utmr.Elements { + var elem <%= range_type %> + + elemSrc := []byte(s) + + err = elem.DecodeText(ci, elemSrc) + if err != nil { + return err + } + + elements[i] = elem + } + } + + *dst = <%= multirange_type %>{Ranges: elements, Status: Present} + + return nil +} + +func (dst *<%= multirange_type %>) DecodeBinary(ci *ConnInfo, src []byte) error { + if src == nil { + *dst = <%= multirange_type %>{Status: Null} + return nil + } + + rp := 0 + + numElems := int(binary.BigEndian.Uint32(src[rp:])) + rp += 4 + + if numElems == 0 { + *dst = <%= multirange_type %>{Status: Present} + return nil + } + + elements := make([]<%= range_type %>, numElems) + + for i := range elements { + elemLen := int(int32(binary.BigEndian.Uint32(src[rp:]))) + rp += 4 + var elemSrc []byte + if elemLen >= 0 { + elemSrc = src[rp : rp+elemLen] + rp += elemLen + } + err := elements[i].DecodeBinary(ci, elemSrc) + if err != nil { + return err + } + } + + *dst = <%= multirange_type %>{Ranges: elements, Status: Present} + return nil +} + +func (src <%= multirange_type %>) EncodeText(ci *ConnInfo, buf []byte) ([]byte, error) { + switch src.Status { + case Null: + return nil, nil + case Undefined: + return nil, errUndefined + } + + buf = append(buf, '{') + + inElemBuf := make([]byte, 0, 32) + for i, elem := range src.Ranges { + if i > 0 { + buf = append(buf, ',') + } + + elemBuf, err := elem.EncodeText(ci, inElemBuf) + if err != nil { + return nil, err + } + if elemBuf == nil { + return nil, fmt.Errorf("multi-range does not allow null range") + } else { + buf = append(buf, string(elemBuf)...) + } + + } + + buf = append(buf, '}') + + return buf, nil +} + +func (src <%= multirange_type %>) EncodeBinary(ci *ConnInfo, buf []byte) ([]byte, error) { + switch src.Status { + case Null: + return nil, nil + case Undefined: + return nil, errUndefined + } + + buf = pgio.AppendInt32(buf, int32(len(src.Ranges))) + + for i := range src.Ranges { + sp := len(buf) + buf = pgio.AppendInt32(buf, -1) + + elemBuf, err := src.Ranges[i].EncodeBinary(ci, buf) + if err != nil { + return nil, err + } + if elemBuf != nil { + buf = elemBuf + pgio.SetInt32(buf[sp:], int32(len(buf[sp:])-4)) + } + } + + return buf, nil +} + +// Scan implements the database/sql Scanner interface. +func (dst *<%= multirange_type %>) Scan(src interface{}) error { + if src == nil { + return dst.DecodeText(nil, nil) + } + + switch src := src.(type) { + case string: + return dst.DecodeText(nil, []byte(src)) + case []byte: + srcCopy := make([]byte, len(src)) + copy(srcCopy, src) + return dst.DecodeText(nil, srcCopy) + } + + return fmt.Errorf("cannot scan %T", src) +} + +// Value implements the database/sql/driver Valuer interface. +func (src <%= multirange_type %>) Value() (driver.Value, error) { + return EncodeValueText(src) +} diff --git a/typed_multirange_gen.sh b/typed_multirange_gen.sh new file mode 100755 index 00000000..610f40a1 --- /dev/null +++ b/typed_multirange_gen.sh @@ -0,0 +1,8 @@ +erb range_type=Numrange multirange_type=Nummultirange typed_multirange.go.erb > num_multirange.go +erb range_type=Int4range multirange_type=Int4multirange typed_multirange.go.erb > int4_multirange.go +erb range_type=Int8range multirange_type=Int8multirange typed_multirange.go.erb > int8_multirange.go +# TODO +# erb range_type=Tsrange multirange_type=Tsmultirange typed_multirange.go.erb > ts_multirange.go +# erb range_type=Tstzrange multirange_type=Tstzmultirange typed_multirange.go.erb > tstz_multirange.go +# erb range_type=Daterange multirange_type=Datemultirange typed_multirange.go.erb > date_multirange.go +goimports -w *multirange.go \ No newline at end of file From b103a6efbda898f3e26b9562da71837b1163f498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Sun, 20 Mar 2022 15:05:19 +0100 Subject: [PATCH 07/16] test: jsonbarray set failing test cases --- jsonb_array_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/jsonb_array_test.go b/jsonb_array_test.go index 65f1777a..e63d0c00 100644 --- a/jsonb_array_test.go +++ b/jsonb_array_test.go @@ -1,6 +1,8 @@ package pgtype_test import ( + "encoding/json" + "reflect" "testing" "github.com/jackc/pgtype" @@ -34,3 +36,53 @@ func TestJSONBArrayTranscode(t *testing.T) { }, }) } + +func TestJSONBArraySet(t *testing.T) { + successfulTests := []struct { + source interface{} + result pgtype.JSONBArray + }{ + {source: []string{"{}"}, result: pgtype.JSONBArray{ + Elements: []pgtype.JSONB{pgtype.JSONB{Bytes: []byte("{}"), Status: pgtype.Present}}, + Dimensions: []pgtype.ArrayDimension{pgtype.ArrayDimension{Length: 1, LowerBound: 1}}, + Status: pgtype.Present, + }}, + {source: [][]byte{[]byte("{}")}, result: pgtype.JSONBArray{ + Elements: []pgtype.JSONB{pgtype.JSONB{Bytes: []byte("{}"), Status: pgtype.Present}}, + Dimensions: []pgtype.ArrayDimension{pgtype.ArrayDimension{Length: 1, LowerBound: 1}}, + Status: pgtype.Present, + }}, + {source: [][]byte{[]byte(`{"foo":1}`), []byte(`{"bar":2}`)}, result: pgtype.JSONBArray{ + Elements: []pgtype.JSONB{pgtype.JSONB{Bytes: []byte(`{"foo":1}`), Status: pgtype.Present}, pgtype.JSONB{Bytes: []byte(`{"bar":2}`), Status: pgtype.Present}}, + Dimensions: []pgtype.ArrayDimension{pgtype.ArrayDimension{Length: 2, LowerBound: 1}}, + Status: pgtype.Present, + }}, + {source: []json.RawMessage{json.RawMessage(`{"foo":1}`), json.RawMessage(`{"bar":2}`)}, result: pgtype.JSONBArray{ + Elements: []pgtype.JSONB{pgtype.JSONB{Bytes: []byte(`{"foo":1}`), Status: pgtype.Present}, pgtype.JSONB{Bytes: []byte(`{"bar":2}`), Status: pgtype.Present}}, + Dimensions: []pgtype.ArrayDimension{pgtype.ArrayDimension{Length: 2, LowerBound: 1}}, + Status: pgtype.Present, + }}, + {source: []json.RawMessage{json.RawMessage(`{"foo":12}`), json.RawMessage(`{"bar":2}`)}, result: pgtype.JSONBArray{ + Elements: []pgtype.JSONB{pgtype.JSONB{Bytes: []byte(`{"foo":12}`), Status: pgtype.Present}, pgtype.JSONB{Bytes: []byte(`{"bar":2}`), Status: pgtype.Present}}, + Dimensions: []pgtype.ArrayDimension{pgtype.ArrayDimension{Length: 2, LowerBound: 1}}, + Status: pgtype.Present, + }}, + {source: []json.RawMessage{json.RawMessage(`{"foo":1}`), json.RawMessage(`{"bar":{"x":2}}`)}, result: pgtype.JSONBArray{ + Elements: []pgtype.JSONB{pgtype.JSONB{Bytes: []byte(`{"foo":1}`), Status: pgtype.Present}, pgtype.JSONB{Bytes: []byte(`{"bar":{"x":2}}`), Status: pgtype.Present}}, + Dimensions: []pgtype.ArrayDimension{pgtype.ArrayDimension{Length: 2, LowerBound: 1}}, + Status: pgtype.Present, + }}, + } + + for i, tt := range successfulTests { + var d pgtype.JSONBArray + err := d.Set(tt.source) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if !reflect.DeepEqual(d, tt.result) { + t.Errorf("%d: expected %+v to convert to %+v, but it was %+v", i, tt.source, tt.result, d) + } + } +} From 4c6f1b1dc49de587531c756b1120fc877c739cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Emil=20Schulz=20=C3=98stergaard?= Date: Sun, 20 Mar 2022 15:05:43 +0100 Subject: [PATCH 08/16] fix: add json rawmessage to typed_array_gen.sh --- jsonb_array.go | 29 +++++++++++++++++++++++++++++ typed_array_gen.sh | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/jsonb_array.go b/jsonb_array.go index c4b7cd3d..e78ad377 100644 --- a/jsonb_array.go +++ b/jsonb_array.go @@ -5,6 +5,7 @@ package pgtype import ( "database/sql/driver" "encoding/binary" + "encoding/json" "fmt" "reflect" @@ -72,6 +73,25 @@ func (dst *JSONBArray) Set(src interface{}) error { } } + case []json.RawMessage: + if value == nil { + *dst = JSONBArray{Status: Null} + } else if len(value) == 0 { + *dst = JSONBArray{Status: Present} + } else { + elements := make([]JSONB, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = JSONBArray{ + Elements: elements, + Dimensions: []ArrayDimension{{Length: int32(len(elements)), LowerBound: 1}}, + Status: Present, + } + } + case []JSONB: if value == nil { *dst = JSONBArray{Status: Null} @@ -214,6 +234,15 @@ func (src *JSONBArray) AssignTo(dst interface{}) error { } return nil + case *[]json.RawMessage: + *v = make([]json.RawMessage, len(src.Elements)) + for i := range src.Elements { + if err := src.Elements[i].AssignTo(&((*v)[i])); err != nil { + return err + } + } + return nil + } } diff --git a/typed_array_gen.sh b/typed_array_gen.sh index ea28be07..1f4098c7 100755 --- a/typed_array_gen.sh +++ b/typed_array_gen.sh @@ -20,7 +20,7 @@ erb pgtype_array_type=ACLItemArray pgtype_element_type=ACLItem go_array_types=[] erb pgtype_array_type=HstoreArray pgtype_element_type=Hstore go_array_types=[]map[string]string element_type_name=hstore text_null=NULL binary_format=true typed_array.go.erb > hstore_array.go erb pgtype_array_type=NumericArray pgtype_element_type=Numeric go_array_types=[]float32,[]*float32,[]float64,[]*float64,[]int64,[]*int64,[]uint64,[]*uint64 element_type_name=numeric text_null=NULL binary_format=true typed_array.go.erb > numeric_array.go erb pgtype_array_type=UUIDArray pgtype_element_type=UUID go_array_types=[][16]byte,[][]byte,[]string,[]*string element_type_name=uuid text_null=NULL binary_format=true typed_array.go.erb > uuid_array.go -erb pgtype_array_type=JSONBArray pgtype_element_type=JSONB go_array_types=[]string,[][]byte element_type_name=jsonb text_null=NULL binary_format=true typed_array.go.erb > jsonb_array.go +erb pgtype_array_type=JSONBArray pgtype_element_type=JSONB go_array_types=[]string,[][]byte,[]json.RawMessage element_type_name=jsonb text_null=NULL binary_format=true typed_array.go.erb > jsonb_array.go # While the binary format is theoretically possible it is only practical to use the text format. erb pgtype_array_type=EnumArray pgtype_element_type=GenericText go_array_types=[]string,[]*string text_null=NULL binary_format=false typed_array.go.erb > enum_array.go From 5ece2efd4c610fe2c0078f49695d91025ae758fa Mon Sep 17 00:00:00 2001 From: WGH Date: Mon, 28 Mar 2022 05:08:30 +0300 Subject: [PATCH 09/16] Fix typo in Record type documentation --- record.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/record.go b/record.go index 718c3570..5cf2c93a 100644 --- a/record.go +++ b/record.go @@ -6,7 +6,7 @@ import ( ) // Record is the generic PostgreSQL record type such as is created with the -// "row" function. Record only implements BinaryEncoder and Value. The text +// "row" function. Record only implements BinaryDecoder and Value. The text // format output format from PostgreSQL does not include type information and is // therefore impossible to decode. No encoders are implemented because // PostgreSQL does not support input of generic records. From 71648e3d78faa726cd7c2bd8f18239da276d3f84 Mon Sep 17 00:00:00 2001 From: WGH Date: Mon, 28 Mar 2022 04:57:54 +0300 Subject: [PATCH 10/16] Add defaults for typed_array.go.erb template parameters Most of the time binary_format is "true", and text_null is "NULL", so it makes sense to not repeat that. --- typed_array.go.erb | 7 +++++++ typed_array_gen.sh | 48 +++++++++++++++++++++++----------------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/typed_array.go.erb b/typed_array.go.erb index 5788626b..b89ae164 100644 --- a/typed_array.go.erb +++ b/typed_array.go.erb @@ -1,5 +1,12 @@ // Code generated by erb. DO NOT EDIT. +<% + # defaults when not explicitly set on command line + + binary_format ||= "true" + text_null ||= "NULL" +%> + package pgtype import ( diff --git a/typed_array_gen.sh b/typed_array_gen.sh index 1f4098c7..a9090cd9 100755 --- a/typed_array_gen.sh +++ b/typed_array_gen.sh @@ -1,28 +1,28 @@ -erb pgtype_array_type=Int2Array pgtype_element_type=Int2 go_array_types=[]int16,[]*int16,[]uint16,[]*uint16,[]int32,[]*int32,[]uint32,[]*uint32,[]int64,[]*int64,[]uint64,[]*uint64,[]int,[]*int,[]uint,[]*uint element_type_name=int2 text_null=NULL binary_format=true typed_array.go.erb > int2_array.go -erb pgtype_array_type=Int4Array pgtype_element_type=Int4 go_array_types=[]int16,[]*int16,[]uint16,[]*uint16,[]int32,[]*int32,[]uint32,[]*uint32,[]int64,[]*int64,[]uint64,[]*uint64,[]int,[]*int,[]uint,[]*uint element_type_name=int4 text_null=NULL binary_format=true typed_array.go.erb > int4_array.go -erb pgtype_array_type=Int8Array pgtype_element_type=Int8 go_array_types=[]int16,[]*int16,[]uint16,[]*uint16,[]int32,[]*int32,[]uint32,[]*uint32,[]int64,[]*int64,[]uint64,[]*uint64,[]int,[]*int,[]uint,[]*uint element_type_name=int8 text_null=NULL binary_format=true typed_array.go.erb > int8_array.go -erb pgtype_array_type=BoolArray pgtype_element_type=Bool go_array_types=[]bool,[]*bool element_type_name=bool text_null=NULL binary_format=true typed_array.go.erb > bool_array.go -erb pgtype_array_type=DateArray pgtype_element_type=Date go_array_types=[]time.Time,[]*time.Time element_type_name=date text_null=NULL binary_format=true typed_array.go.erb > date_array.go -erb pgtype_array_type=TimestamptzArray pgtype_element_type=Timestamptz go_array_types=[]time.Time,[]*time.Time element_type_name=timestamptz text_null=NULL binary_format=true typed_array.go.erb > timestamptz_array.go -erb pgtype_array_type=TstzrangeArray pgtype_element_type=Tstzrange go_array_types=[]Tstzrange element_type_name=tstzrange text_null=NULL binary_format=true typed_array.go.erb > tstzrange_array.go -erb pgtype_array_type=TsrangeArray pgtype_element_type=Tsrange go_array_types=[]Tsrange element_type_name=tsrange text_null=NULL binary_format=true typed_array.go.erb > tsrange_array.go -erb pgtype_array_type=TimestampArray pgtype_element_type=Timestamp go_array_types=[]time.Time,[]*time.Time element_type_name=timestamp text_null=NULL binary_format=true typed_array.go.erb > timestamp_array.go -erb pgtype_array_type=Float4Array pgtype_element_type=Float4 go_array_types=[]float32,[]*float32 element_type_name=float4 text_null=NULL binary_format=true typed_array.go.erb > float4_array.go -erb pgtype_array_type=Float8Array pgtype_element_type=Float8 go_array_types=[]float64,[]*float64 element_type_name=float8 text_null=NULL binary_format=true typed_array.go.erb > float8_array.go -erb pgtype_array_type=InetArray pgtype_element_type=Inet go_array_types=[]*net.IPNet,[]net.IP,[]*net.IP element_type_name=inet text_null=NULL binary_format=true typed_array.go.erb > inet_array.go -erb pgtype_array_type=MacaddrArray pgtype_element_type=Macaddr go_array_types=[]net.HardwareAddr,[]*net.HardwareAddr element_type_name=macaddr text_null=NULL binary_format=true typed_array.go.erb > macaddr_array.go -erb pgtype_array_type=CIDRArray pgtype_element_type=CIDR go_array_types=[]*net.IPNet,[]net.IP,[]*net.IP element_type_name=cidr text_null=NULL binary_format=true typed_array.go.erb > cidr_array.go -erb pgtype_array_type=TextArray pgtype_element_type=Text go_array_types=[]string,[]*string element_type_name=text text_null=NULL binary_format=true typed_array.go.erb > text_array.go -erb pgtype_array_type=VarcharArray pgtype_element_type=Varchar go_array_types=[]string,[]*string element_type_name=varchar text_null=NULL binary_format=true typed_array.go.erb > varchar_array.go -erb pgtype_array_type=BPCharArray pgtype_element_type=BPChar go_array_types=[]string,[]*string element_type_name=bpchar text_null=NULL binary_format=true typed_array.go.erb > bpchar_array.go -erb pgtype_array_type=ByteaArray pgtype_element_type=Bytea go_array_types=[][]byte element_type_name=bytea text_null=NULL binary_format=true typed_array.go.erb > bytea_array.go -erb pgtype_array_type=ACLItemArray pgtype_element_type=ACLItem go_array_types=[]string,[]*string element_type_name=aclitem text_null=NULL binary_format=false typed_array.go.erb > aclitem_array.go -erb pgtype_array_type=HstoreArray pgtype_element_type=Hstore go_array_types=[]map[string]string element_type_name=hstore text_null=NULL binary_format=true typed_array.go.erb > hstore_array.go -erb pgtype_array_type=NumericArray pgtype_element_type=Numeric go_array_types=[]float32,[]*float32,[]float64,[]*float64,[]int64,[]*int64,[]uint64,[]*uint64 element_type_name=numeric text_null=NULL binary_format=true typed_array.go.erb > numeric_array.go -erb pgtype_array_type=UUIDArray pgtype_element_type=UUID go_array_types=[][16]byte,[][]byte,[]string,[]*string element_type_name=uuid text_null=NULL binary_format=true typed_array.go.erb > uuid_array.go -erb pgtype_array_type=JSONBArray pgtype_element_type=JSONB go_array_types=[]string,[][]byte,[]json.RawMessage element_type_name=jsonb text_null=NULL binary_format=true typed_array.go.erb > jsonb_array.go +erb pgtype_array_type=Int2Array pgtype_element_type=Int2 go_array_types=[]int16,[]*int16,[]uint16,[]*uint16,[]int32,[]*int32,[]uint32,[]*uint32,[]int64,[]*int64,[]uint64,[]*uint64,[]int,[]*int,[]uint,[]*uint element_type_name=int2 typed_array.go.erb > int2_array.go +erb pgtype_array_type=Int4Array pgtype_element_type=Int4 go_array_types=[]int16,[]*int16,[]uint16,[]*uint16,[]int32,[]*int32,[]uint32,[]*uint32,[]int64,[]*int64,[]uint64,[]*uint64,[]int,[]*int,[]uint,[]*uint element_type_name=int4 typed_array.go.erb > int4_array.go +erb pgtype_array_type=Int8Array pgtype_element_type=Int8 go_array_types=[]int16,[]*int16,[]uint16,[]*uint16,[]int32,[]*int32,[]uint32,[]*uint32,[]int64,[]*int64,[]uint64,[]*uint64,[]int,[]*int,[]uint,[]*uint element_type_name=int8 typed_array.go.erb > int8_array.go +erb pgtype_array_type=BoolArray pgtype_element_type=Bool go_array_types=[]bool,[]*bool element_type_name=bool typed_array.go.erb > bool_array.go +erb pgtype_array_type=DateArray pgtype_element_type=Date go_array_types=[]time.Time,[]*time.Time element_type_name=date typed_array.go.erb > date_array.go +erb pgtype_array_type=TimestamptzArray pgtype_element_type=Timestamptz go_array_types=[]time.Time,[]*time.Time element_type_name=timestamptz typed_array.go.erb > timestamptz_array.go +erb pgtype_array_type=TstzrangeArray pgtype_element_type=Tstzrange go_array_types=[]Tstzrange element_type_name=tstzrange typed_array.go.erb > tstzrange_array.go +erb pgtype_array_type=TsrangeArray pgtype_element_type=Tsrange go_array_types=[]Tsrange element_type_name=tsrange typed_array.go.erb > tsrange_array.go +erb pgtype_array_type=TimestampArray pgtype_element_type=Timestamp go_array_types=[]time.Time,[]*time.Time element_type_name=timestamp typed_array.go.erb > timestamp_array.go +erb pgtype_array_type=Float4Array pgtype_element_type=Float4 go_array_types=[]float32,[]*float32 element_type_name=float4 typed_array.go.erb > float4_array.go +erb pgtype_array_type=Float8Array pgtype_element_type=Float8 go_array_types=[]float64,[]*float64 element_type_name=float8 typed_array.go.erb > float8_array.go +erb pgtype_array_type=InetArray pgtype_element_type=Inet go_array_types=[]*net.IPNet,[]net.IP,[]*net.IP element_type_name=inet typed_array.go.erb > inet_array.go +erb pgtype_array_type=MacaddrArray pgtype_element_type=Macaddr go_array_types=[]net.HardwareAddr,[]*net.HardwareAddr element_type_name=macaddr typed_array.go.erb > macaddr_array.go +erb pgtype_array_type=CIDRArray pgtype_element_type=CIDR go_array_types=[]*net.IPNet,[]net.IP,[]*net.IP element_type_name=cidr typed_array.go.erb > cidr_array.go +erb pgtype_array_type=TextArray pgtype_element_type=Text go_array_types=[]string,[]*string element_type_name=text typed_array.go.erb > text_array.go +erb pgtype_array_type=VarcharArray pgtype_element_type=Varchar go_array_types=[]string,[]*string element_type_name=varchar typed_array.go.erb > varchar_array.go +erb pgtype_array_type=BPCharArray pgtype_element_type=BPChar go_array_types=[]string,[]*string element_type_name=bpchar typed_array.go.erb > bpchar_array.go +erb pgtype_array_type=ByteaArray pgtype_element_type=Bytea go_array_types=[][]byte element_type_name=bytea typed_array.go.erb > bytea_array.go +erb pgtype_array_type=ACLItemArray pgtype_element_type=ACLItem go_array_types=[]string,[]*string element_type_name=aclitem binary_format=false typed_array.go.erb > aclitem_array.go +erb pgtype_array_type=HstoreArray pgtype_element_type=Hstore go_array_types=[]map[string]string element_type_name=hstore typed_array.go.erb > hstore_array.go +erb pgtype_array_type=NumericArray pgtype_element_type=Numeric go_array_types=[]float32,[]*float32,[]float64,[]*float64,[]int64,[]*int64,[]uint64,[]*uint64 element_type_name=numeric typed_array.go.erb > numeric_array.go +erb pgtype_array_type=UUIDArray pgtype_element_type=UUID go_array_types=[][16]byte,[][]byte,[]string,[]*string element_type_name=uuid typed_array.go.erb > uuid_array.go +erb pgtype_array_type=JSONBArray pgtype_element_type=JSONB go_array_types=[]string,[][]byte,[]json.RawMessage element_type_name=jsonb typed_array.go.erb > jsonb_array.go # While the binary format is theoretically possible it is only practical to use the text format. -erb pgtype_array_type=EnumArray pgtype_element_type=GenericText go_array_types=[]string,[]*string text_null=NULL binary_format=false typed_array.go.erb > enum_array.go +erb pgtype_array_type=EnumArray pgtype_element_type=GenericText go_array_types=[]string,[]*string binary_format=false typed_array.go.erb > enum_array.go goimports -w *_array.go From 5db1de5fc18703ddf645024dff9ff73c98bc9107 Mon Sep 17 00:00:00 2001 From: WGH Date: Mon, 28 Mar 2022 05:00:22 +0300 Subject: [PATCH 11/16] Make text format for type_array.go.erb opt-out Some types, like RECORD, don't have sane text format. If we want to have arrays of such types, we don't want to generate text format for such arrays either. --- typed_array.go.erb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/typed_array.go.erb b/typed_array.go.erb index b89ae164..31debcd8 100644 --- a/typed_array.go.erb +++ b/typed_array.go.erb @@ -4,6 +4,8 @@ # defaults when not explicitly set on command line binary_format ||= "true" + text_format ||= "true" + text_null ||= "NULL" %> @@ -286,6 +288,7 @@ func (src *<%= pgtype_array_type %>) assignToRecursive(value reflect.Value, inde return index, nil } +<% if text_format == "true" %> func (dst *<%= pgtype_array_type %>) DecodeText(ci *ConnInfo, src []byte) error { if src == nil { *dst = <%= pgtype_array_type %>{Status: Null} @@ -321,6 +324,7 @@ func (dst *<%= pgtype_array_type %>) DecodeText(ci *ConnInfo, src []byte) error return nil } +<% end %> <% if binary_format == "true" %> func (dst *<%= pgtype_array_type %>) DecodeBinary(ci *ConnInfo, src []byte) error { @@ -366,6 +370,7 @@ func (dst *<%= pgtype_array_type %>) DecodeBinary(ci *ConnInfo, src []byte) erro } <% end %> +<% if text_format == "true" %> func (src <%= pgtype_array_type %>) EncodeText(ci *ConnInfo, buf []byte) ([]byte, error) { switch src.Status { case Null: @@ -422,6 +427,7 @@ func (src <%= pgtype_array_type %>) EncodeText(ci *ConnInfo, buf []byte) ([]byte return buf, nil } +<% end %> <% if binary_format == "true" %> func (src <%= pgtype_array_type %>) EncodeBinary(ci *ConnInfo, buf []byte) ([]byte, error) { @@ -469,6 +475,7 @@ func (src <%= pgtype_array_type %>) EncodeText(ci *ConnInfo, buf []byte) ([]byte } <% end %> +<% if text_format == "true" %> // Scan implements the database/sql Scanner interface. func (dst *<%= pgtype_array_type %>) Scan(src interface{}) error { if src == nil { @@ -499,3 +506,4 @@ func (src <%= pgtype_array_type %>) Value() (driver.Value, error) { return string(buf), nil } +<% end %> From 3e230ba7313cffe1aa245f913c30c0105509f822 Mon Sep 17 00:00:00 2001 From: WGH Date: Mon, 28 Mar 2022 05:03:18 +0300 Subject: [PATCH 12/16] Split encode_binary and decode_binary in typed_array.go.erb Again, RECORD, for example, has binary decoding, but no binary encoding. --- typed_array.go.erb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/typed_array.go.erb b/typed_array.go.erb index 31debcd8..e8433c04 100644 --- a/typed_array.go.erb +++ b/typed_array.go.erb @@ -7,6 +7,9 @@ text_format ||= "true" text_null ||= "NULL" + + encode_binary ||= binary_format + decode_binary ||= binary_format %> package pgtype @@ -326,7 +329,7 @@ func (dst *<%= pgtype_array_type %>) DecodeText(ci *ConnInfo, src []byte) error } <% end %> -<% if binary_format == "true" %> +<% if decode_binary == "true" %> func (dst *<%= pgtype_array_type %>) DecodeBinary(ci *ConnInfo, src []byte) error { if src == nil { *dst = <%= pgtype_array_type %>{Status: Null} @@ -429,7 +432,7 @@ func (src <%= pgtype_array_type %>) EncodeText(ci *ConnInfo, buf []byte) ([]byte } <% end %> -<% if binary_format == "true" %> +<% if encode_binary == "true" %> func (src <%= pgtype_array_type %>) EncodeBinary(ci *ConnInfo, buf []byte) ([]byte, error) { switch src.Status { case Null: From ccb207cba5b4dd520b9cc40994eeb161ce140737 Mon Sep 17 00:00:00 2001 From: WGH Date: Mon, 28 Mar 2022 05:06:16 +0300 Subject: [PATCH 13/16] Add support for record array Like Record itself, it only implements BinaryDecoder, doesn't implement BinaryEncoder, and has no support for the text protocol. --- record_array.go | 318 +++++++++++++++++++++++++++++++++++++++++++ record_array_test.go | 104 ++++++++++++++ typed_array_gen.sh | 2 + 3 files changed, 424 insertions(+) create mode 100644 record_array.go create mode 100644 record_array_test.go diff --git a/record_array.go b/record_array.go new file mode 100644 index 00000000..2271717a --- /dev/null +++ b/record_array.go @@ -0,0 +1,318 @@ +// Code generated by erb. DO NOT EDIT. + +package pgtype + +import ( + "encoding/binary" + "fmt" + "reflect" +) + +type RecordArray struct { + Elements []Record + Dimensions []ArrayDimension + Status Status +} + +func (dst *RecordArray) Set(src interface{}) error { + // untyped nil and typed nil interfaces are different + if src == nil { + *dst = RecordArray{Status: Null} + return nil + } + + if value, ok := src.(interface{ Get() interface{} }); ok { + value2 := value.Get() + if value2 != value { + return dst.Set(value2) + } + } + + // Attempt to match to select common types: + switch value := src.(type) { + + case [][]Value: + if value == nil { + *dst = RecordArray{Status: Null} + } else if len(value) == 0 { + *dst = RecordArray{Status: Present} + } else { + elements := make([]Record, len(value)) + for i := range value { + if err := elements[i].Set(value[i]); err != nil { + return err + } + } + *dst = RecordArray{ + Elements: elements, + Dimensions: []ArrayDimension{{Length: int32(len(elements)), LowerBound: 1}}, + Status: Present, + } + } + + case []Record: + if value == nil { + *dst = RecordArray{Status: Null} + } else if len(value) == 0 { + *dst = RecordArray{Status: Present} + } else { + *dst = RecordArray{ + Elements: value, + Dimensions: []ArrayDimension{{Length: int32(len(value)), LowerBound: 1}}, + Status: Present, + } + } + default: + // Fallback to reflection if an optimised match was not found. + // The reflection is necessary for arrays and multidimensional slices, + // but it comes with a 20-50% performance penalty for large arrays/slices + reflectedValue := reflect.ValueOf(src) + if !reflectedValue.IsValid() || reflectedValue.IsZero() { + *dst = RecordArray{Status: Null} + return nil + } + + dimensions, elementsLength, ok := findDimensionsFromValue(reflectedValue, nil, 0) + if !ok { + return fmt.Errorf("cannot find dimensions of %v for RecordArray", src) + } + if elementsLength == 0 { + *dst = RecordArray{Status: Present} + return nil + } + if len(dimensions) == 0 { + if originalSrc, ok := underlyingSliceType(src); ok { + return dst.Set(originalSrc) + } + return fmt.Errorf("cannot convert %v to RecordArray", src) + } + + *dst = RecordArray{ + Elements: make([]Record, elementsLength), + Dimensions: dimensions, + Status: Present, + } + elementCount, err := dst.setRecursive(reflectedValue, 0, 0) + if err != nil { + // Maybe the target was one dimension too far, try again: + if len(dst.Dimensions) > 1 { + dst.Dimensions = dst.Dimensions[:len(dst.Dimensions)-1] + elementsLength = 0 + for _, dim := range dst.Dimensions { + if elementsLength == 0 { + elementsLength = int(dim.Length) + } else { + elementsLength *= int(dim.Length) + } + } + dst.Elements = make([]Record, elementsLength) + elementCount, err = dst.setRecursive(reflectedValue, 0, 0) + if err != nil { + return err + } + } else { + return err + } + } + if elementCount != len(dst.Elements) { + return fmt.Errorf("cannot convert %v to RecordArray, expected %d dst.Elements, but got %d instead", src, len(dst.Elements), elementCount) + } + } + + return nil +} + +func (dst *RecordArray) setRecursive(value reflect.Value, index, dimension int) (int, error) { + switch value.Kind() { + case reflect.Array: + fallthrough + case reflect.Slice: + if len(dst.Dimensions) == dimension { + break + } + + valueLen := value.Len() + if int32(valueLen) != dst.Dimensions[dimension].Length { + return 0, fmt.Errorf("multidimensional arrays must have array expressions with matching dimensions") + } + for i := 0; i < valueLen; i++ { + var err error + index, err = dst.setRecursive(value.Index(i), index, dimension+1) + if err != nil { + return 0, err + } + } + + return index, nil + } + if !value.CanInterface() { + return 0, fmt.Errorf("cannot convert all values to RecordArray") + } + if err := dst.Elements[index].Set(value.Interface()); err != nil { + return 0, fmt.Errorf("%v in RecordArray", err) + } + index++ + + return index, nil +} + +func (dst RecordArray) Get() interface{} { + switch dst.Status { + case Present: + return dst + case Null: + return nil + default: + return dst.Status + } +} + +func (src *RecordArray) AssignTo(dst interface{}) error { + switch src.Status { + case Present: + if len(src.Dimensions) <= 1 { + // Attempt to match to select common types: + switch v := dst.(type) { + + case *[][]Value: + *v = make([][]Value, len(src.Elements)) + for i := range src.Elements { + if err := src.Elements[i].AssignTo(&((*v)[i])); err != nil { + return err + } + } + return nil + + } + } + + // Try to convert to something AssignTo can use directly. + if nextDst, retry := GetAssignToDstType(dst); retry { + return src.AssignTo(nextDst) + } + + // Fallback to reflection if an optimised match was not found. + // The reflection is necessary for arrays and multidimensional slices, + // but it comes with a 20-50% performance penalty for large arrays/slices + value := reflect.ValueOf(dst) + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + + switch value.Kind() { + case reflect.Array, reflect.Slice: + default: + return fmt.Errorf("cannot assign %T to %T", src, dst) + } + + if len(src.Elements) == 0 { + if value.Kind() == reflect.Slice { + value.Set(reflect.MakeSlice(value.Type(), 0, 0)) + return nil + } + } + + elementCount, err := src.assignToRecursive(value, 0, 0) + if err != nil { + return err + } + if elementCount != len(src.Elements) { + return fmt.Errorf("cannot assign %v, needed to assign %d elements, but only assigned %d", dst, len(src.Elements), elementCount) + } + + return nil + case Null: + return NullAssignTo(dst) + } + + return fmt.Errorf("cannot decode %#v into %T", src, dst) +} + +func (src *RecordArray) assignToRecursive(value reflect.Value, index, dimension int) (int, error) { + switch kind := value.Kind(); kind { + case reflect.Array: + fallthrough + case reflect.Slice: + if len(src.Dimensions) == dimension { + break + } + + length := int(src.Dimensions[dimension].Length) + if reflect.Array == kind { + typ := value.Type() + if typ.Len() != length { + return 0, fmt.Errorf("expected size %d array, but %s has size %d array", length, typ, typ.Len()) + } + value.Set(reflect.New(typ).Elem()) + } else { + value.Set(reflect.MakeSlice(value.Type(), length, length)) + } + + var err error + for i := 0; i < length; i++ { + index, err = src.assignToRecursive(value.Index(i), index, dimension+1) + if err != nil { + return 0, err + } + } + + return index, nil + } + if len(src.Dimensions) != dimension { + return 0, fmt.Errorf("incorrect dimensions, expected %d, found %d", len(src.Dimensions), dimension) + } + if !value.CanAddr() { + return 0, fmt.Errorf("cannot assign all values from RecordArray") + } + addr := value.Addr() + if !addr.CanInterface() { + return 0, fmt.Errorf("cannot assign all values from RecordArray") + } + if err := src.Elements[index].AssignTo(addr.Interface()); err != nil { + return 0, err + } + index++ + return index, nil +} + +func (dst *RecordArray) DecodeBinary(ci *ConnInfo, src []byte) error { + if src == nil { + *dst = RecordArray{Status: Null} + return nil + } + + var arrayHeader ArrayHeader + rp, err := arrayHeader.DecodeBinary(ci, src) + if err != nil { + return err + } + + if len(arrayHeader.Dimensions) == 0 { + *dst = RecordArray{Dimensions: arrayHeader.Dimensions, Status: Present} + return nil + } + + elementCount := arrayHeader.Dimensions[0].Length + for _, d := range arrayHeader.Dimensions[1:] { + elementCount *= d.Length + } + + elements := make([]Record, elementCount) + + for i := range elements { + elemLen := int(int32(binary.BigEndian.Uint32(src[rp:]))) + rp += 4 + var elemSrc []byte + if elemLen >= 0 { + elemSrc = src[rp : rp+elemLen] + rp += elemLen + } + err = elements[i].DecodeBinary(ci, elemSrc) + if err != nil { + return err + } + } + + *dst = RecordArray{Elements: elements, Dimensions: arrayHeader.Dimensions, Status: Present} + return nil +} diff --git a/record_array_test.go b/record_array_test.go new file mode 100644 index 00000000..9c92e333 --- /dev/null +++ b/record_array_test.go @@ -0,0 +1,104 @@ +package pgtype_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/jackc/pgtype" + "github.com/jackc/pgtype/testutil" + "github.com/jackc/pgx/v4" +) + +var recordArrayTests = []struct { + sql string + expected pgtype.RecordArray +}{ + { + sql: `select array_agg((x::int4, x+100::int8)) from generate_series(0, 1) x;`, + expected: pgtype.RecordArray{ + Dimensions: []pgtype.ArrayDimension{ + {LowerBound: 1, Length: 2}, + }, + Elements: []pgtype.Record{ + { + Fields: []pgtype.Value{ + &pgtype.Int4{Int: 0, Status: pgtype.Present}, + &pgtype.Int8{Int: 100, Status: pgtype.Present}, + }, + Status: pgtype.Present, + }, + { + Fields: []pgtype.Value{ + &pgtype.Int4{Int: 1, Status: pgtype.Present}, + &pgtype.Int8{Int: 101, Status: pgtype.Present}, + }, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + }, + }, +} + +func TestRecordArrayTranscode(t *testing.T) { + conn := testutil.MustConnectPgx(t) + defer testutil.MustCloseContext(t, conn) + + for i, tt := range recordArrayTests { + psName := fmt.Sprintf("test%d", i) + _, err := conn.Prepare(context.Background(), psName, tt.sql) + require.NoError(t, err) + + t.Run(tt.sql, func(t *testing.T) { + var result pgtype.RecordArray + err := conn.QueryRow(context.Background(), psName, pgx.QueryResultFormats{pgx.BinaryFormatCode}).Scan(&result) + require.NoError(t, err) + + require.Equal(t, tt.expected, result) + }) + + } +} + +func TestRecordArrayAssignTo(t *testing.T) { + src := pgtype.RecordArray{ + Dimensions: []pgtype.ArrayDimension{ + {LowerBound: 1, Length: 2}, + }, + Elements: []pgtype.Record{ + { + Fields: []pgtype.Value{ + &pgtype.Int4{Int: 0, Status: pgtype.Present}, + &pgtype.Int8{Int: 100, Status: pgtype.Present}, + }, + Status: pgtype.Present, + }, + { + Fields: []pgtype.Value{ + &pgtype.Int4{Int: 1, Status: pgtype.Present}, + &pgtype.Int8{Int: 101, Status: pgtype.Present}, + }, + Status: pgtype.Present, + }, + }, + Status: pgtype.Present, + } + dst := [][]pgtype.Value{} + err := src.AssignTo(&dst) + require.NoError(t, err) + + expected := [][]pgtype.Value{ + { + &pgtype.Int4{Int: 0, Status: pgtype.Present}, + &pgtype.Int8{Int: 100, Status: pgtype.Present}, + }, + { + &pgtype.Int4{Int: 1, Status: pgtype.Present}, + &pgtype.Int8{Int: 101, Status: pgtype.Present}, + }, + } + require.Equal(t, expected, dst) +} diff --git a/typed_array_gen.sh b/typed_array_gen.sh index a9090cd9..d922f1cb 100755 --- a/typed_array_gen.sh +++ b/typed_array_gen.sh @@ -25,4 +25,6 @@ erb pgtype_array_type=JSONBArray pgtype_element_type=JSONB go_array_types=[]stri # While the binary format is theoretically possible it is only practical to use the text format. erb pgtype_array_type=EnumArray pgtype_element_type=GenericText go_array_types=[]string,[]*string binary_format=false typed_array.go.erb > enum_array.go +erb pgtype_array_type=RecordArray pgtype_element_type=Record go_array_types=[][]Value element_type_name=record text_null=NULL encode_binary=false text_format=false typed_array.go.erb > record_array.go + goimports -w *_array.go From 25558de3bd1bd2441cd3442394502b567ea94fbe Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 16 Apr 2022 07:07:31 -0500 Subject: [PATCH 14/16] Add UnmarshalJSON to pgtype.Int2 fixes https://github.com/jackc/pgtype/issues/153 --- int2.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/int2.go b/int2.go index 3eb5aeb5..0775882a 100644 --- a/int2.go +++ b/int2.go @@ -3,6 +3,7 @@ package pgtype import ( "database/sql/driver" "encoding/binary" + "encoding/json" "fmt" "math" "strconv" @@ -302,3 +303,19 @@ func (src Int2) MarshalJSON() ([]byte, error) { return nil, errBadStatus } + +func (dst *Int2) UnmarshalJSON(b []byte) error { + var n *int16 + err := json.Unmarshal(b, &n) + if err != nil { + return err + } + + if n == nil { + *dst = Int2{Status: Null} + } else { + *dst = Int2{Int: *n, Status: Present} + } + + return nil +} From c63f912615930b25fd5c496e779d015c3bfb67fe Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Thu, 21 Apr 2022 19:19:32 -0500 Subject: [PATCH 15/16] Hstore.Set accepts map[string]Text --- hstore.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hstore.go b/hstore.go index f46eeaf6..706a3964 100644 --- a/hstore.go +++ b/hstore.go @@ -50,6 +50,8 @@ func (dst *Hstore) Set(src interface{}) error { } } *dst = Hstore{Map: m, Status: Present} + case map[string]Text: + *dst = Hstore{Map: value, Status: Present} default: return fmt.Errorf("cannot convert %v to Hstore", src) } From c5a0faca99221f74833133537e4b3d80f84d2cc6 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Thu, 21 Apr 2022 19:58:17 -0500 Subject: [PATCH 16/16] Release v1.11.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73126cf3..253f42c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 1.11.0 (April 21, 2022) + +* Add multirange for numeric, int4, and int8 (Vu) +* JSONBArray now supports json.RawMessage (Jens Emil Schulz Østergaard) +* Add RecordArray (WGH) +* Add UnmarshalJSON to pgtype.Int2 +* Hstore.Set accepts map[string]Text + # 1.10.0 (February 7, 2022) * Normalize UTC timestamps to comply with stdlib (Torkel Rogstad)