Skip to content

Commit bdef72d

Browse files
committed
sql/pgwire: decode multi-byte "char" datums correctly
Historically, our `"char"` type has differed from Postgres. We allow multi-byte UTF-8 characters, while in Postgres `"char"` values are always a single byte. This inconsistency is difficult to change at this point. In #70942 we began truncating `"char"` values to a single byte when decoding them in pgwire. This caused odd behavior with these values because the rest of the system truncates `"char"` values to a single UTF-8 character instead. This commit updates the pgwire decoding to make it consistent with other SQL logic. Fixes #149427 Release note (bug fix): Fixes a minor bug that caused inconsistent behavior with the very rarely used `"char"` type (not to be confused with `CHAR`).
1 parent 923e3b0 commit bdef72d

File tree

3 files changed

+86
-6
lines changed

3 files changed

+86
-6
lines changed

pkg/sql/pgwire/pgwirebase/encoding.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -922,14 +922,20 @@ func DecodeDatum(
922922
sv := strings.TrimRight(bs, " ")
923923
return da.NewDString(tree.DString(sv)), nil
924924
case oid.T_char:
925-
sv := bs
926-
// Always truncate to 1 byte, and handle the null byte specially.
927-
if len(b) >= 1 {
928-
if b[0] == 0 {
929-
sv = ""
930-
} else {
925+
var sv string
926+
if len(b) == 1 {
927+
// Take a single byte as-is, even if it is not a valid UTF-8
928+
// character. The null byte represents an empty string.
929+
if b[0] != 0 {
931930
sv = string(b[:1])
932931
}
932+
} else if len(b) > 1 {
933+
// If there is more than one byte, decode the first UTF-8 character.
934+
r, _ := utf8.DecodeRune(b)
935+
if r == utf8.RuneError {
936+
return nil, invalidUTF8Error
937+
}
938+
sv = string(r)
933939
}
934940
return da.NewDString(tree.DString(sv)), nil
935941
case oid.T_name:

pkg/sql/pgwire/testdata/encodings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@
104104
"TextAsBinary": [],
105105
"Binary": [0]
106106
},
107+
{
108+
"SQL": "'☃'::\"char\"",
109+
"Oid": 18,
110+
"Text": "",
111+
"TextAsBinary": [226, 152, 131],
112+
"Binary": [226, 152, 131]
113+
},
107114
{
108115
"SQL": "''::text",
109116
"Oid": 25,

pkg/sql/pgwire/testdata/pgtest/char

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,70 @@ ReadyForQuery
208208
{"Type":"DataRow","Values":[{"binary":"00"}]}
209209
{"Type":"CommandComplete","CommandTag":"SELECT 1"}
210210
{"Type":"ReadyForQuery","TxStatus":"I"}
211+
212+
# Regression test for #149427. "char" datums should be truncated to 1 UTF-8
213+
# character.
214+
send
215+
Query {"String": "CREATE TABLE t149427 (c \"char\" PRIMARY KEY)"}
216+
----
217+
218+
until
219+
ReadyForQuery
220+
----
221+
{"Type":"CommandComplete","CommandTag":"CREATE TABLE"}
222+
{"Type":"ReadyForQuery","TxStatus":"I"}
223+
224+
send
225+
Parse {"Query": "INSERT INTO t149427 VALUES ($1)", "Name": "i149427"}
226+
Describe {"Name": "i149427", "ObjectType": "S"}
227+
Bind {"ParameterFormatCodes": [0], "PreparedStatement": "i149427", "Parameters": [{"text":"☃"}]}
228+
Execute
229+
Sync
230+
----
231+
232+
until
233+
ReadyForQuery
234+
----
235+
{"Type":"ParseComplete"}
236+
{"Type":"ParameterDescription","ParameterOIDs":[18]}
237+
{"Type":"NoData"}
238+
{"Type":"BindComplete"}
239+
{"Type":"CommandComplete","CommandTag":"INSERT 0 1"}
240+
{"Type":"ReadyForQuery","TxStatus":"I"}
241+
242+
send
243+
Parse {"Query": "SELECT * FROM t149427", "Name": "s149427"}
244+
Describe {"Name": "s149427", "ObjectType": "S"}
245+
Bind {"PreparedStatement": "s149427", "ResultFormatCodes": [0]}
246+
Execute
247+
Sync
248+
----
249+
250+
until
251+
ReadyForQuery
252+
----
253+
{"Type":"ParseComplete"}
254+
{"Type":"ParameterDescription","ParameterOIDs":null}
255+
{"Type":"RowDescription","Fields":[{"Name":"c","TableOID":105,"TableAttributeNumber":1,"DataTypeOID":18,"DataTypeSize":1,"TypeModifier":-1,"Format":0}]}
256+
{"Type":"BindComplete"}
257+
{"Type":"DataRow","Values":[{"text":"☃"}]}
258+
{"Type":"CommandComplete","CommandTag":"SELECT 1"}
259+
{"Type":"ReadyForQuery","TxStatus":"I"}
260+
261+
send
262+
Parse {"Query": "DELETE FROM t149427 WHERE c = $1", "Name": "d149427"}
263+
Describe {"Name": "d149427", "ObjectType": "S"}
264+
Bind {"ParameterFormatCodes": [0], "PreparedStatement": "d149427", "Parameters": [{"text":"☃"}]}
265+
Execute
266+
Sync
267+
----
268+
269+
until
270+
ReadyForQuery
271+
----
272+
{"Type":"ParseComplete"}
273+
{"Type":"ParameterDescription","ParameterOIDs":[25]}
274+
{"Type":"NoData"}
275+
{"Type":"BindComplete"}
276+
{"Type":"CommandComplete","CommandTag":"DELETE 1"}
277+
{"Type":"ReadyForQuery","TxStatus":"I"}

0 commit comments

Comments
 (0)