diff --git a/conn_test.go b/conn_test.go index a618b936..41d04b55 100644 --- a/conn_test.go +++ b/conn_test.go @@ -841,7 +841,9 @@ func TestDomainType(t *testing.T) { // Domain type uint64 is a PostgreSQL domain of underlying type numeric. - // Unregistered type can be used as string. + // In the extended protocol preparing "select $1::uint64" appears to create a statement that expects a param OID of + // uint64 but a result OID of the underlying numeric. + var s string err := conn.QueryRow(context.Background(), "select $1::uint64", "24").Scan(&s) require.NoError(t, err) diff --git a/pgtype/builtin_wrappers.go b/pgtype/builtin_wrappers.go index 30a88465..0f12ada3 100644 --- a/pgtype/builtin_wrappers.go +++ b/pgtype/builtin_wrappers.go @@ -5,7 +5,6 @@ import ( "math" "net" "reflect" - "strconv" "time" ) @@ -341,16 +340,6 @@ func (w stringWrapper) TextValue() (Text, error) { return Text{String: string(w), Valid: true}, nil } -func (w *stringWrapper) ScanInt64(v Int8) error { - if !v.Valid { - return fmt.Errorf("cannot scan NULL into *string") - } - - *w = stringWrapper(strconv.FormatInt(v.Int64, 10)) - - return nil -} - type timeWrapper time.Time func (w *timeWrapper) ScanDate(v Date) error { diff --git a/pgtype/float4.go b/pgtype/float4.go index fb84c124..2c628011 100644 --- a/pgtype/float4.go +++ b/pgtype/float4.go @@ -156,6 +156,8 @@ func (Float4Codec) PlanScan(m *Map, oid uint32, format int16, target interface{} return scanPlanBinaryFloat4ToFloat64Scanner{} case Int64Scanner: return scanPlanBinaryFloat4ToInt64Scanner{} + case TextScanner: + return scanPlanBinaryFloat4ToTextScanner{} } case TextFormatCode: switch target.(type) { @@ -229,6 +231,25 @@ func (scanPlanBinaryFloat4ToInt64Scanner) Scan(src []byte, dst interface{}) erro return s.ScanInt64(Int8{Int64: i64, Valid: true}) } +type scanPlanBinaryFloat4ToTextScanner struct{} + +func (scanPlanBinaryFloat4ToTextScanner) Scan(src []byte, dst interface{}) error { + s := (dst).(TextScanner) + + if src == nil { + return s.ScanText(Text{}) + } + + if len(src) != 4 { + return fmt.Errorf("invalid length for float4: %v", len(src)) + } + + ui32 := int32(binary.BigEndian.Uint32(src)) + f32 := math.Float32frombits(uint32(ui32)) + + return s.ScanText(Text{String: strconv.FormatFloat(float64(f32), 'f', -1, 32), Valid: true}) +} + type scanPlanTextAnyToFloat32 struct{} func (scanPlanTextAnyToFloat32) Scan(src []byte, dst interface{}) error { diff --git a/pgtype/float4_test.go b/pgtype/float4_test.go index 00b9addf..f155ed97 100644 --- a/pgtype/float4_test.go +++ b/pgtype/float4_test.go @@ -17,6 +17,7 @@ func TestFloat4Codec(t *testing.T) { {float32(9999.99), new(float32), isExpectedEq(float32(9999.99))}, {pgtype.Float4{}, new(pgtype.Float4), isExpectedEq(pgtype.Float4{})}, {int64(1), new(int64), isExpectedEq(int64(1))}, + {"1.23", new(string), isExpectedEq("1.23")}, {nil, new(*float32), isExpectedEq((*float32)(nil))}, }) } diff --git a/pgtype/float8.go b/pgtype/float8.go index 664fb9f8..b7c6177e 100644 --- a/pgtype/float8.go +++ b/pgtype/float8.go @@ -194,6 +194,8 @@ func (Float8Codec) PlanScan(m *Map, oid uint32, format int16, target interface{} return scanPlanBinaryFloat8ToFloat64Scanner{} case Int64Scanner: return scanPlanBinaryFloat8ToInt64Scanner{} + case TextScanner: + return scanPlanBinaryFloat8ToTextScanner{} } case TextFormatCode: switch target.(type) { @@ -267,6 +269,25 @@ func (scanPlanBinaryFloat8ToInt64Scanner) Scan(src []byte, dst interface{}) erro return s.ScanInt64(Int8{Int64: i64, Valid: true}) } +type scanPlanBinaryFloat8ToTextScanner struct{} + +func (scanPlanBinaryFloat8ToTextScanner) Scan(src []byte, dst interface{}) error { + s := (dst).(TextScanner) + + if src == nil { + return s.ScanText(Text{}) + } + + if len(src) != 8 { + return fmt.Errorf("invalid length for float8: %v", len(src)) + } + + ui64 := int64(binary.BigEndian.Uint64(src)) + f64 := math.Float64frombits(uint64(ui64)) + + return s.ScanText(Text{String: strconv.FormatFloat(f64, 'f', -1, 64), Valid: true}) +} + type scanPlanTextAnyToFloat64 struct{} func (scanPlanTextAnyToFloat64) Scan(src []byte, dst interface{}) error { diff --git a/pgtype/float8_test.go b/pgtype/float8_test.go index 9c269072..496b718b 100644 --- a/pgtype/float8_test.go +++ b/pgtype/float8_test.go @@ -17,6 +17,7 @@ func TestFloat8Codec(t *testing.T) { {float64(9999.99), new(float64), isExpectedEq(float64(9999.99))}, {pgtype.Float8{}, new(pgtype.Float8), isExpectedEq(pgtype.Float8{})}, {int64(1), new(int64), isExpectedEq(int64(1))}, + {"1.23", new(string), isExpectedEq("1.23")}, {nil, new(*float64), isExpectedEq((*float64)(nil))}, }) } diff --git a/pgtype/int.go b/pgtype/int.go index ee4ab932..b3eabceb 100644 --- a/pgtype/int.go +++ b/pgtype/int.go @@ -233,6 +233,8 @@ func (Int2Codec) PlanScan(m *Map, oid uint32, format int16, target interface{}) return scanPlanBinaryInt2ToUint{} case Int64Scanner: return scanPlanBinaryInt2ToInt64Scanner{} + case TextScanner: + return scanPlanBinaryInt2ToTextScanner{} } case TextFormatCode: switch target.(type) { @@ -557,6 +559,27 @@ func (scanPlanBinaryInt2ToInt64Scanner) Scan(src []byte, dst interface{}) error return s.ScanInt64(Int8{Int64: n, Valid: true}) } +type scanPlanBinaryInt2ToTextScanner struct{} + +func (scanPlanBinaryInt2ToTextScanner) Scan(src []byte, dst interface{}) error { + s, ok := (dst).(TextScanner) + if !ok { + return ErrScanTargetTypeChanged + } + + if src == nil { + return s.ScanText(Text{}) + } + + if len(src) != 2 { + return fmt.Errorf("invalid length for int2: %v", len(src)) + } + + n := int64(int16(binary.BigEndian.Uint16(src))) + + return s.ScanText(Text{String: strconv.FormatInt(n, 10), Valid: true}) +} + type Int4 struct { Int32 int32 Valid bool @@ -770,6 +793,8 @@ func (Int4Codec) PlanScan(m *Map, oid uint32, format int16, target interface{}) return scanPlanBinaryInt4ToUint{} case Int64Scanner: return scanPlanBinaryInt4ToInt64Scanner{} + case TextScanner: + return scanPlanBinaryInt4ToTextScanner{} } case TextFormatCode: switch target.(type) { @@ -1105,6 +1130,27 @@ func (scanPlanBinaryInt4ToInt64Scanner) Scan(src []byte, dst interface{}) error return s.ScanInt64(Int8{Int64: n, Valid: true}) } +type scanPlanBinaryInt4ToTextScanner struct{} + +func (scanPlanBinaryInt4ToTextScanner) Scan(src []byte, dst interface{}) error { + s, ok := (dst).(TextScanner) + if !ok { + return ErrScanTargetTypeChanged + } + + if src == nil { + return s.ScanText(Text{}) + } + + if len(src) != 4 { + return fmt.Errorf("invalid length for int4: %v", len(src)) + } + + n := int64(int32(binary.BigEndian.Uint32(src))) + + return s.ScanText(Text{String: strconv.FormatInt(n, 10), Valid: true}) +} + type Int8 struct { Int64 int64 Valid bool @@ -1318,6 +1364,8 @@ func (Int8Codec) PlanScan(m *Map, oid uint32, format int16, target interface{}) return scanPlanBinaryInt8ToUint{} case Int64Scanner: return scanPlanBinaryInt8ToInt64Scanner{} + case TextScanner: + return scanPlanBinaryInt8ToTextScanner{} } case TextFormatCode: switch target.(type) { @@ -1675,6 +1723,27 @@ func (scanPlanBinaryInt8ToInt64Scanner) Scan(src []byte, dst interface{}) error return s.ScanInt64(Int8{Int64: n, Valid: true}) } +type scanPlanBinaryInt8ToTextScanner struct{} + +func (scanPlanBinaryInt8ToTextScanner) Scan(src []byte, dst interface{}) error { + s, ok := (dst).(TextScanner) + if !ok { + return ErrScanTargetTypeChanged + } + + if src == nil { + return s.ScanText(Text{}) + } + + if len(src) != 8 { + return fmt.Errorf("invalid length for int8: %v", len(src)) + } + + n := int64(int64(binary.BigEndian.Uint64(src))) + + return s.ScanText(Text{String: strconv.FormatInt(n, 10), Valid: true}) +} + type scanPlanTextAnyToInt8 struct{} func (scanPlanTextAnyToInt8) Scan(src []byte, dst interface{}) error { diff --git a/pgtype/int.go.erb b/pgtype/int.go.erb index 81f28bba..aa1db7fc 100644 --- a/pgtype/int.go.erb +++ b/pgtype/int.go.erb @@ -234,6 +234,8 @@ func (Int<%= pg_byte_size %>Codec) PlanScan(m *Map, oid uint32, format int16, ta return scanPlanBinaryInt<%= pg_byte_size %>ToUint{} case Int64Scanner: return scanPlanBinaryInt<%= pg_byte_size %>ToInt64Scanner{} + case TextScanner: + return scanPlanBinaryInt<%= pg_byte_size %>ToTextScanner{} } case TextFormatCode: switch target.(type) { @@ -443,6 +445,29 @@ func (scanPlanBinaryInt<%= pg_byte_size %>ToInt64Scanner) Scan(src []byte, dst i return s.ScanInt64(Int8{Int64: n, Valid: true}) } + +<%# PostgreSQL binary format integer to Go TextScanner %> +type scanPlanBinaryInt<%= pg_byte_size %>ToTextScanner struct{} + +func (scanPlanBinaryInt<%= pg_byte_size %>ToTextScanner) Scan(src []byte, dst interface{}) error { + s, ok := (dst).(TextScanner) + if !ok { + return ErrScanTargetTypeChanged + } + + if src == nil { + return s.ScanText(Text{}) + } + + if len(src) != <%= pg_byte_size %> { + return fmt.Errorf("invalid length for int<%= pg_byte_size %>: %v", len(src)) + } + + + n := int64(int<%= pg_bit_size %>(binary.BigEndian.Uint<%= pg_bit_size %>(src))) + + return s.ScanText(Text{String: strconv.FormatInt(n, 10), Valid: true}) +} <% end %> <%# Any text to all integer types %> diff --git a/pgtype/int_test.go b/pgtype/int_test.go index c779bdc9..73294b3c 100644 --- a/pgtype/int_test.go +++ b/pgtype/int_test.go @@ -45,6 +45,7 @@ func TestInt2Codec(t *testing.T) { {1, new(int16), isExpectedEq(int16(1))}, {math.MaxInt16, new(int16), isExpectedEq(int16(math.MaxInt16))}, {1, new(pgtype.Int2), isExpectedEq(pgtype.Int2{Int16: 1, Valid: true})}, + {"1", new(string), isExpectedEq("1")}, {pgtype.Int2{}, new(pgtype.Int2), isExpectedEq(pgtype.Int2{})}, {nil, new(*int16), isExpectedEq((*int16)(nil))}, }) @@ -126,6 +127,7 @@ func TestInt4Codec(t *testing.T) { {1, new(int32), isExpectedEq(int32(1))}, {math.MaxInt32, new(int32), isExpectedEq(int32(math.MaxInt32))}, {1, new(pgtype.Int4), isExpectedEq(pgtype.Int4{Int32: 1, Valid: true})}, + {"1", new(string), isExpectedEq("1")}, {pgtype.Int4{}, new(pgtype.Int4), isExpectedEq(pgtype.Int4{})}, {nil, new(*int32), isExpectedEq((*int32)(nil))}, }) @@ -207,6 +209,7 @@ func TestInt8Codec(t *testing.T) { {1, new(int64), isExpectedEq(int64(1))}, {math.MaxInt64, new(int64), isExpectedEq(int64(math.MaxInt64))}, {1, new(pgtype.Int8), isExpectedEq(pgtype.Int8{Int64: 1, Valid: true})}, + {"1", new(string), isExpectedEq("1")}, {pgtype.Int8{}, new(pgtype.Int8), isExpectedEq(pgtype.Int8{})}, {nil, new(*int64), isExpectedEq((*int64)(nil))}, }) diff --git a/pgtype/int_test.go.erb b/pgtype/int_test.go.erb index d72d6bbd..ac9a3f14 100644 --- a/pgtype/int_test.go.erb +++ b/pgtype/int_test.go.erb @@ -44,6 +44,7 @@ func TestInt<%= pg_byte_size %>Codec(t *testing.T) { {1, new(int<%= pg_bit_size %>), isExpectedEq(int<%= pg_bit_size %>(1))}, {math.MaxInt<%= pg_bit_size %>, new(int<%= pg_bit_size %>), isExpectedEq(int<%= pg_bit_size %>(math.MaxInt<%= pg_bit_size %>))}, {1, new(pgtype.Int<%= pg_byte_size %>), isExpectedEq(pgtype.Int<%= pg_byte_size %>{Int<%= pg_bit_size %>: 1, Valid: true})}, + {"1", new(string), isExpectedEq("1")}, {pgtype.Int<%= pg_byte_size %>{}, new(pgtype.Int<%= pg_byte_size %>), isExpectedEq(pgtype.Int<%= pg_byte_size %>{})}, {nil, new(*int<%= pg_bit_size %>), isExpectedEq((*int<%= pg_bit_size %>)(nil))}, }) diff --git a/pgtype/numeric.go b/pgtype/numeric.go index b9827b63..5ca7d077 100644 --- a/pgtype/numeric.go +++ b/pgtype/numeric.go @@ -237,6 +237,11 @@ func (n Numeric) MarshalJSON() ([]byte, error) { return []byte(`"NaN"`), nil } + return n.numberTextBytes(), nil +} + +// numberString returns a string of the number. undefined if NaN, infinite, or NULL +func (n Numeric) numberTextBytes() []byte { intStr := n.Int.String() buf := &bytes.Buffer{} exp := int(n.Exp) @@ -263,7 +268,7 @@ func (n Numeric) MarshalJSON() ([]byte, error) { buf.WriteString(intStr) } - return buf.Bytes(), nil + return buf.Bytes() } type NumericCodec struct{} @@ -520,19 +525,7 @@ func encodeNumericText(n Numeric, buf []byte) (newBuf []byte, err error) { return buf, nil } - digits := n.Int.String() - if n.Exp >= 0 { - buf = append(buf, digits...) - if n.Exp > 0 { - for i := int32(0); i < n.Exp; i++ { - buf = append(buf, '0') - } - } - } else { - buf = append(buf, digits...) - buf = append(buf, 'e') - buf = append(buf, strconv.FormatInt(int64(n.Exp), 10)...) - } + buf = append(buf, n.numberTextBytes()...) return buf, nil } @@ -548,6 +541,8 @@ func (NumericCodec) PlanScan(m *Map, oid uint32, format int16, target interface{ return scanPlanBinaryNumericToFloat64Scanner{} case Int64Scanner: return scanPlanBinaryNumericToInt64Scanner{} + case TextScanner: + return scanPlanBinaryNumericToTextScanner{} } case TextFormatCode: switch target.(type) { @@ -721,6 +716,30 @@ func (scanPlanBinaryNumericToInt64Scanner) Scan(src []byte, dst interface{}) err return scanner.ScanInt64(Int8{Int64: bigInt.Int64(), Valid: true}) } +type scanPlanBinaryNumericToTextScanner struct{} + +func (scanPlanBinaryNumericToTextScanner) Scan(src []byte, dst interface{}) error { + scanner := (dst).(TextScanner) + + if src == nil { + return scanner.ScanText(Text{}) + } + + var n Numeric + + err := scanPlanBinaryNumericToNumericScanner{}.Scan(src, &n) + if err != nil { + return err + } + + sbuf, err := encodeNumericText(n, nil) + if err != nil { + return err + } + + return scanner.ScanText(Text{String: string(sbuf), Valid: true}) +} + type scanPlanTextAnyToNumericScanner struct{} func (scanPlanTextAnyToNumericScanner) Scan(src []byte, dst interface{}) error { diff --git a/pgtype/numeric_test.go b/pgtype/numeric_test.go index 3c37ae18..d95deaa5 100644 --- a/pgtype/numeric_test.go +++ b/pgtype/numeric_test.go @@ -110,6 +110,7 @@ func TestNumericCodec(t *testing.T) { {int64(math.MinInt64 + 1), new(pgtype.Numeric), isExpectedEqNumeric(mustParseNumeric(t, strconv.FormatInt(math.MinInt64+1, 10)))}, {int64(math.MaxInt64), new(pgtype.Numeric), isExpectedEqNumeric(mustParseNumeric(t, strconv.FormatInt(math.MaxInt64, 10)))}, {int64(math.MaxInt64 - 1), new(pgtype.Numeric), isExpectedEqNumeric(mustParseNumeric(t, strconv.FormatInt(math.MaxInt64-1, 10)))}, + {"1.23", new(string), isExpectedEq("1.23")}, {pgtype.Numeric{}, new(pgtype.Numeric), isExpectedEq(pgtype.Numeric{})}, {nil, new(pgtype.Numeric), isExpectedEq(pgtype.Numeric{})}, }) diff --git a/values_test.go b/values_test.go index 4282880c..04441b72 100644 --- a/values_test.go +++ b/values_test.go @@ -1022,6 +1022,7 @@ func TestScanIntoByteSlice(t *testing.T) { output []byte }{ {"int - text", "select 42", pgx.TextFormatCode, []byte("42")}, + {"int - binary", "select 42", pgx.BinaryFormatCode, []byte("42")}, {"text - text", "select 'hi'", pgx.TextFormatCode, []byte("hi")}, {"text - binary", "select 'hi'", pgx.BinaryFormatCode, []byte("hi")}, {"json - text", "select '{}'::json", pgx.TextFormatCode, []byte("{}")}, @@ -1036,19 +1037,4 @@ func TestScanIntoByteSlice(t *testing.T) { require.Equal(t, tt.output, buf) }) } - - // Failure cases - for _, tt := range []struct { - name string - sql string - err string - }{ - {"int binary", "select 42::int4", "can't scan into dest[0]: cannot scan OID 23 in binary format into *[]uint8"}, - } { - t.Run(tt.name, func(t *testing.T) { - var buf []byte - err := conn.QueryRow(context.Background(), tt.sql, pgx.QueryResultFormats{pgx.BinaryFormatCode}).Scan(&buf) - require.EqualError(t, err, tt.err) - }) - } }