Better number to string handling

Avoid ambiguity of stringWrapper implementing Int64Scanner and
Float64Scanner.
This commit is contained in:
Jack Christensen 2022-04-09 09:09:46 -05:00
parent 8cf6721d66
commit 829babcea9
13 changed files with 180 additions and 41 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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))},
})
}

View File

@ -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 {

View File

@ -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))},
})
}

View File

@ -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 {

View File

@ -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 %>

View File

@ -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))},
})

View File

@ -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))},
})

View File

@ -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 {

View File

@ -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{})},
})

View File

@ -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)
})
}
}