fix(pgtype): scan "char" (OID 18) into *string in binary format (#2314)#2571
fix(pgtype): scan "char" (OID 18) into *string in binary format (#2314)#2571luongs3 wants to merge 1 commit into
Conversation
…c#2314) The "char" codec only registered scan plans for *byte and *rune targets. In the text format, scanning into *string happened to work because pgtype.Map.planScan short-circuits any text-format *string target to scanPlanString before consulting the codec. The binary path has no such short-circuit, so the same scan failed with: cannot scan char (OID 18) in binary format into *string That broke a very common pattern when reading system catalogs: SELECT confupdtype FROM pg_constraint LIMIT 1 (or any other pg_*.relkind / contype / etc.) because the default wire format for OID 18 is binary. This change adds a *string scan plan to QCharCodec that mirrors the *byte and *rune plans (single-byte payload, empty src -> zero value, multi-byte src -> error), so both wire formats produce the same result for the same target type. Tests: - pgtype/qchar_test.go: new TestQcharCodecPlanScanString covering text + binary + empty payload + too-long payload, exercising the codec directly so it runs without a Postgres connection. - TestQcharTranscode: added *string round-trip cases for "a", "z", "0", " " so the fix is exercised end-to-end against a real server when PG is available. Local verification: - Reproduced the issue: PlanScan(QCharOID, BinaryFormatCode, *string) returned *scanPlanFail before the fix; returns scanPlanQcharCodecString after. - TestQcharCodecPlanScanString fails on master (binary + empty-binary subtests) and passes on this branch. - gofmt and go vet clean on touched files. Fixes jackc#2314 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the PR! The diagnosis and the routing analysis (text (Also: I gather this was co-authored by Claude — and I reviewed it with Claude too. So this is largely my Claude leaving notes for your Claude. 🤖🤝🤖) What
|
| wire format | scan plan | result | bytes | len |
|---|---|---|---|---|
| text | scanPlanString → string(src) |
"\xc8" |
c8 |
1 |
| binary | this PR → string(src[0]) |
"È" |
c3 88 |
2 |
(reproduced by scanning the same single-byte "char" payload []byte{200} through Map.Scan in each format.)
That means the PR's headline invariant — "Both wire formats now produce the same result for the same target type" — doesn't hold for bytes 128–255, and the binary path no longer matches the raw-byte semantics a text-format *string scan already has.
Suggested fix
Copy the raw byte instead, mirroring the text path exactly:
p := dst.(*string)
// Copy the raw byte so the result matches the text-format *string scan path
// (string(src)) byte-for-byte. Using string(src[0]) would instead UTF-8 encode
// the byte as a code point, diverging for byte values >= 128.
*p = string(src)This also lets the len(src) == 0 special case go away, since string([]byte{}) is already "". With it, text and binary agree for a→"a", 200→"\xc8", 0→"\x00", []→""; your TestQcharCodecPlanScanString, TestQcharTranscode, the full pgtype suite, gofmt, and vet all stay green.
It'd be worth extending TestQcharCodecPlanScanString with a non-ASCII byte (e.g. 0xC8) asserting text and binary produce identical bytes, so this stays locked down.
One genuinely irreducible case to be aware of (not something to fix here): the NUL char. charout emits an empty string for byte 0, so text yields "" while binary yields "\x00". That's rooted in PostgreSQL's text output, and no conversion choice can reconcile it.
Fixes #2314.
Problem
Scanning the PostgreSQL
"char"type (OID 18, single-byte system type) into a Go*stringworked in TextFormat but failed in BinaryFormat:Minimal repro from the issue:
This hits anyone reading common system catalog columns (
relkind,contype,confupdtype, ...) because binary is the default wire format for OID 18.Why text worked:
pgtype.Map.planScanshort-circuits any text-format*stringtarget toscanPlanStringbefore consulting the codec. The binary branch only does that forTextOID/VarcharOID, so for OID 18 it falls through toQCharCodec.PlanScan— which only handled*byteand*rune.Fix
pgtype/qchar.go: add a*stringcase toQCharCodec.PlanScan, returning a newscanPlanQcharCodecStringthat mirrorsscanPlanQcharCodecByte/scanPlanQcharCodecRune(single-byte payload, empty src → zero value, multi-byte src → error).Both wire formats now produce the same result for the same target type.
Tests
pgtype/qchar_test.go: newTestQcharCodecPlanScanStringexercising the codec directly (no DB needed) — covers text format, binary format, empty payload, and over-long payload. The binary + empty-binary subtests fail onmasterand pass on this branch.TestQcharTranscode: added*stringround-trip cases ("a","z","0"," ") so the fix is exercised end-to-end against a real Postgres server when one is available.gofmtandgo vetclean on touched files.What does NOT change
*byteand*runescan paths are untouched.DecodeValue/DecodeDatabaseSQLValueare untouched.scanPlanQcharCodecStringis package-private, same as its sibling plans.Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com