mirror of
https://github.com/jackc/pgx.git
synced 2025-05-28 18:22:15 +00:00
Better number to string handling
Avoid ambiguity of stringWrapper implementing Int64Scanner and Float64Scanner.
This commit is contained in:
parent
8cf6721d66
commit
829babcea9
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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))},
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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))},
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 %>
|
||||
|
@ -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))},
|
||||
})
|
||||
|
@ -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))},
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -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{})},
|
||||
})
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user