From 74c6af3195d39d65961d7b97a3f120de84df5f37 Mon Sep 17 00:00:00 2001 From: cruvie <@foxmail.com> Date: Sun, 14 Sep 2025 23:43:00 +0800 Subject: [PATCH 1/4] fix https://github.com/jackc/pgx/issues/1551 --- values.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/values.go b/values.go index 6e2ff3003..08d5b6933 100644 --- a/values.go +++ b/values.go @@ -2,6 +2,7 @@ package pgx import ( "errors" + "reflect" "github.com/jackc/pgx/v5/internal/pgio" "github.com/jackc/pgx/v5/pgtype" @@ -14,6 +15,13 @@ const ( ) func convertSimpleArgument(m *pgtype.Map, arg any) (any, error) { + fieldValue := reflect.ValueOf(arg) + switch fieldValue.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + arg = fieldValue.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + arg = fieldValue.Uint() + } buf, err := m.Encode(0, TextFormatCode, arg, []byte{}) if err != nil { return nil, err From fb6c054606fb7de91a282a7f2cac079a660d808f Mon Sep 17 00:00:00 2001 From: cruvie <@qq> Date: Mon, 29 Sep 2025 10:55:27 +0800 Subject: [PATCH 2/4] fix(values):Fix the issue of time. Duration type being incorrectly converted to int64 --- values.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/values.go b/values.go index 08d5b6933..04e24616e 100644 --- a/values.go +++ b/values.go @@ -3,6 +3,7 @@ package pgx import ( "errors" "reflect" + "time" "github.com/jackc/pgx/v5/internal/pgio" "github.com/jackc/pgx/v5/pgtype" @@ -18,7 +19,9 @@ func convertSimpleArgument(m *pgtype.Map, arg any) (any, error) { fieldValue := reflect.ValueOf(arg) switch fieldValue.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - arg = fieldValue.Int() + if _, ok := arg.(time.Duration); !ok { + arg = fieldValue.Int() + } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: arg = fieldValue.Uint() } From d685c94d63e70291ebb57718d4d99bd9bbefb274 Mon Sep 17 00:00:00 2001 From: cruvie <@qq> Date: Mon, 29 Sep 2025 11:08:14 +0800 Subject: [PATCH 3/4] fix: ensure driver.Valuer.Value() is called for simple arguments The convertSimpleArgument function now properly handles driver.Valuer interface by calling the Value() method when the argument implements driver.Valuer. This ensures that custom value types implementing driver.Valuer are correctly processed before being passed to the PostgreSQL type encoding system. This change makes the behavior consistent between simple and extended query protocols regarding driver.Valuer handling. --- values.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/values.go b/values.go index 04e24616e..ab6e75d4d 100644 --- a/values.go +++ b/values.go @@ -1,6 +1,7 @@ package pgx import ( + "database/sql/driver" "errors" "reflect" "time" @@ -16,6 +17,15 @@ const ( ) func convertSimpleArgument(m *pgtype.Map, arg any) (any, error) { + // If arg implements driver.Valuer, use Value method + if valuer, ok := arg.(driver.Valuer); ok && valuer != nil { + v, err := valuer.Value() + if err != nil { + return nil, err + } + arg = v + } + fieldValue := reflect.ValueOf(arg) switch fieldValue.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: From f4fee4b5adcb21224f4f4531200db3fab2d90c4d Mon Sep 17 00:00:00 2001 From: cruvie <@qq> Date: Thu, 9 Oct 2025 12:13:30 +0800 Subject: [PATCH 4/4] fix(pgtype):Adjust the usage logic of the fmt. Stringer interface to prioritize handling renamed base types - avoiding renamed base types being automatically treated as string types --- pgtype/pgtype.go | 13 +++++++++++- values.go | 21 -------------------- values_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/pgtype/pgtype.go b/pgtype/pgtype.go index b3ef32078..9758575ad 100644 --- a/pgtype/pgtype.go +++ b/pgtype/pgtype.go @@ -1497,7 +1497,18 @@ func TryWrapBuiltinTypeEncodePlan(value any) (plan WrappedEncodePlanNextSetter, case []byte: return &wrapByteSliceEncodePlan{}, byteSliceWrapper(value), true case fmt.Stringer: - return &wrapFmtStringerEncodePlan{}, fmtStringerWrapper{value}, true + // Check if the value is a renamed basic type. If it is, prefer the basic type encoding. + rv := reflect.ValueOf(value) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, reflect.Bool, reflect.String: + // For renamed basic types, don't use Stringer interface automatically + // Let the specific type match above handle it + default: + // For structs and other complex types that implement Stringer, use the Stringer interface + return &wrapFmtStringerEncodePlan{}, fmtStringerWrapper{value}, true + } } return nil, nil, false diff --git a/values.go b/values.go index ab6e75d4d..6e2ff3003 100644 --- a/values.go +++ b/values.go @@ -1,10 +1,7 @@ package pgx import ( - "database/sql/driver" "errors" - "reflect" - "time" "github.com/jackc/pgx/v5/internal/pgio" "github.com/jackc/pgx/v5/pgtype" @@ -17,24 +14,6 @@ const ( ) func convertSimpleArgument(m *pgtype.Map, arg any) (any, error) { - // If arg implements driver.Valuer, use Value method - if valuer, ok := arg.(driver.Valuer); ok && valuer != nil { - v, err := valuer.Value() - if err != nil { - return nil, err - } - arg = v - } - - fieldValue := reflect.ValueOf(arg) - switch fieldValue.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if _, ok := arg.(time.Duration); !ok { - arg = fieldValue.Int() - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - arg = fieldValue.Uint() - } buf, err := m.Encode(0, TextFormatCode, arg, []byte{}) if err != nil { return nil, err diff --git a/values_test.go b/values_test.go index 116577d42..5a0b4ef46 100644 --- a/values_test.go +++ b/values_test.go @@ -1004,6 +1004,57 @@ func TestEncodeTypeRename(t *testing.T) { }) } +// Define custom types that are aliases of basic types but also implement fmt.Stringer +type StringerInt32 int32 +type StringerFloat64 float64 + +// Implement the String() method for these types +func (s StringerInt32) String() string { + return fmt.Sprintf("StringerInt32(%d)", int32(s)) +} + +func (s StringerFloat64) String() string { + return fmt.Sprintf("StringerFloat64(%f)", float64(s)) +} + +// TestStringerTypes tests custom type aliases that implement the fmt.Stringer interface +func TestStringerTypes(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + pgxtest.RunWithQueryExecModes(ctx, t, defaultConnTestRunner, nil, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { + // Test values + inInt := StringerInt32(42) + var outInt StringerInt32 + + inFloat := StringerFloat64(553.36) + var outFloat StringerFloat64 + + // Register types with the connection + conn.TypeMap().RegisterDefaultPgType(inInt, "int4") + conn.TypeMap().RegisterDefaultPgType(inFloat, "float8") + + // Test that the underlying values are properly encoded/decoded, + // not the String() representation + err := conn.QueryRow(context.Background(), "select $1::int4, $2::float8", inInt, inFloat). + Scan(&outInt, &outFloat) + if err != nil { + t.Fatalf("Failed with Stringer types: %v", err) + } + + // Check that the values are correctly preserved (not converted to their String() representation) + if inInt != outInt { + t.Errorf("StringerInt32: expected %v, got %v", inInt, outInt) + } + + if inFloat != outFloat { + t.Errorf("StringerFloat64: expected %v, got %v", inFloat, outFloat) + } + }) +} + // func TestRowDecodeBinary(t *testing.T) { // t.Parallel()