Skip to content

Commit bc1d5fa

Browse files
authored
Merge pull request #2571 from luongs3/fix/2314-qchar-binary-scan
fix(pgtype): scan "char" (OID 18) into *string in binary format (#2314)
2 parents 4a3eeaf + e2d99a6 commit bc1d5fa

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

pgtype/qchar.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ func (QCharCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPla
6464
return scanPlanQcharCodecByte{}
6565
case *rune:
6666
return scanPlanQcharCodecRune{}
67+
case *string:
68+
return scanPlanQcharCodecString{}
6769
}
6870
}
6971

@@ -114,6 +116,26 @@ func (scanPlanQcharCodecRune) Scan(src []byte, dst any) error {
114116
return nil
115117
}
116118

119+
type scanPlanQcharCodecString struct{}
120+
121+
func (scanPlanQcharCodecString) Scan(src []byte, dst any) error {
122+
if src == nil {
123+
return fmt.Errorf("cannot scan NULL into %T", dst)
124+
}
125+
126+
if len(src) > 1 {
127+
return fmt.Errorf(`invalid length for "char": %v`, len(src))
128+
}
129+
130+
p := dst.(*string)
131+
// Copy the raw byte so the result matches the text-format *string scan path
132+
// (string(src)) byte-for-byte. string(src[0]) would instead UTF-8-encode the
133+
// byte as a code point, diverging for byte values >= 128.
134+
*p = string(src)
135+
136+
return nil
137+
}
138+
117139
func (c QCharCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
118140
if src == nil {
119141
return nil, nil

pgtype/qchar_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"math"
66
"testing"
77

8+
"github.com/jackc/pgx/v5/pgtype"
89
"github.com/jackc/pgx/v5/pgxtest"
910
)
1011

@@ -22,3 +23,71 @@ func TestQcharTranscode(t *testing.T) {
2223
// Can only test with known OIDs as rune and byte would be considered numbers.
2324
pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, pgxtest.KnownOIDQueryExecModes, `"char"`, tests)
2425
}
26+
27+
// TestQcharCodecPlanScanString is a regression test for https://github.com/jackc/pgx/issues/2314.
28+
//
29+
// Scanning a "char" column (OID 18) into *string used to succeed in TextFormat but
30+
// fail in BinaryFormat with "cannot scan char (OID 18) in binary format into *string".
31+
// Both formats must now produce the same result.
32+
func TestQcharCodecPlanScanString(t *testing.T) {
33+
m := pgtype.NewMap()
34+
35+
for _, tt := range []struct {
36+
name string
37+
format int16
38+
}{
39+
{"text", pgtype.TextFormatCode},
40+
{"binary", pgtype.BinaryFormatCode},
41+
} {
42+
t.Run(tt.name, func(t *testing.T) {
43+
var s string
44+
plan := m.PlanScan(pgtype.QCharOID, tt.format, &s)
45+
if plan == nil {
46+
t.Fatalf("PlanScan returned nil plan for *string in %s format", tt.name)
47+
}
48+
if err := plan.Scan([]byte{'a'}, &s); err != nil {
49+
t.Fatalf("Scan failed in %s format: %v", tt.name, err)
50+
}
51+
if s != "a" {
52+
t.Fatalf("Scan result mismatch in %s format: got %q want %q", tt.name, s, "a")
53+
}
54+
})
55+
}
56+
57+
// Empty src must produce empty string (mirrors *byte / *rune zero-value behavior).
58+
t.Run("empty-binary", func(t *testing.T) {
59+
var s string = "x"
60+
plan := m.PlanScan(pgtype.QCharOID, pgtype.BinaryFormatCode, &s)
61+
if err := plan.Scan([]byte{}, &s); err != nil {
62+
t.Fatalf("Scan failed: %v", err)
63+
}
64+
if s != "" {
65+
t.Fatalf("empty src: got %q want %q", s, "")
66+
}
67+
})
68+
69+
// 0xC8 (200): a byte >= 128. Both formats must yield the raw 1-byte string
70+
// "\xc8", not the 2-byte UTF-8 encoding "\xc3\x88". This is the case that
71+
// catches string(src[0]) UTF-8-encoding the byte instead of copying it.
72+
t.Run("non-ascii-byte", func(t *testing.T) {
73+
for _, format := range []int16{pgtype.TextFormatCode, pgtype.BinaryFormatCode} {
74+
var s string
75+
plan := m.PlanScan(pgtype.QCharOID, format, &s)
76+
if err := plan.Scan([]byte{0xC8}, &s); err != nil {
77+
t.Fatalf("format %d: scan failed: %v", format, err)
78+
}
79+
if s != "\xc8" {
80+
t.Fatalf("format %d: got %q (% x) want %q", format, s, s, "\xc8")
81+
}
82+
}
83+
})
84+
85+
// Multi-byte src is an invalid "char" payload and must error.
86+
t.Run("too-long", func(t *testing.T) {
87+
var s string
88+
plan := m.PlanScan(pgtype.QCharOID, pgtype.BinaryFormatCode, &s)
89+
if err := plan.Scan([]byte("ab"), &s); err == nil {
90+
t.Fatalf("expected error for 2-byte src, got nil (s=%q)", s)
91+
}
92+
})
93+
}

0 commit comments

Comments
 (0)