diff --git a/pgtype/hstore.go b/pgtype/hstore.go index f3ae78380..ca0c3f6f7 100644 --- a/pgtype/hstore.go +++ b/pgtype/hstore.go @@ -6,7 +6,6 @@ import ( "encoding/binary" "errors" "fmt" - "strings" "unicode" "unicode/utf8" @@ -137,13 +136,20 @@ func (encodePlanHstoreCodecText) Encode(value any, buf []byte) (newBuf []byte, e buf = append(buf, ',') } - buf = append(buf, quoteHstoreElementIfNeeded(k)...) + // unconditionally quote hstore keys/values like Postgres does + // this avoids a Mac OS X Postgres hstore parsing bug: + // https://www.postgresql.org/message-id/CA%2BHWA9awUW0%2BRV_gO9r1ABZwGoZxPztcJxPy8vMFSTbTfi4jig%40mail.gmail.com + buf = append(buf, '"') + buf = append(buf, quoteArrayReplacer.Replace(k)...) + buf = append(buf, '"') buf = append(buf, "=>"...) if v == nil { buf = append(buf, "NULL"...) } else { - buf = append(buf, quoteHstoreElementIfNeeded(*v)...) + buf = append(buf, '"') + buf = append(buf, quoteArrayReplacer.Replace(*v)...) + buf = append(buf, '"') } } @@ -271,19 +277,6 @@ func (c HstoreCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) ( return hstore, nil } -func quoteHstoreElementIfNeeded(src string) string { - // Double-quote keys and values that include whitespace, commas, =s or >s. To include a double - // quote or a backslash in a key or value, escape it with a backslash. - // From: https://www.postgresql.org/docs/current/hstore.html - // whitespace appears to be defined as the isspace() C function: \t\n\v\f\r\n and space - const quoteRequiredChars = `,"\=> ` + "\t\n\v\f\r" - if src == "" || (len(src) == 4 && strings.ToLower(src) == "null") || strings.ContainsAny(src, quoteRequiredChars) { - return quoteArrayElement(src) - } - - return src -} - const ( hsPre = iota hsKey diff --git a/pgtype/hstore_test.go b/pgtype/hstore_test.go index aa6881c5c..e2b6bb4ef 100644 --- a/pgtype/hstore_test.go +++ b/pgtype/hstore_test.go @@ -129,6 +129,9 @@ func TestHstoreCodec(t *testing.T) { "form\\ffeed", "carriage\rreturn", "curly{}braces", + // Postgres on Mac OS X hstore parsing bug: + // ą = "\xc4\x85" in UTF-8; isspace(0x85) on Mac OS X returns true instead of false + "mac_bugą", } for _, s := range specialStrings { // Special key values @@ -255,3 +258,33 @@ func TestParseInvalidInputs(t *testing.T) { } } } + +func BenchmarkHstoreEncode(b *testing.B) { + stringPtr := func(s string) *string { + return &s + } + h := pgtype.Hstore{"a x": stringPtr("100"), "b": stringPtr("200"), "c": stringPtr("300"), + "d": stringPtr("400"), "e": stringPtr("500")} + + serializeConfigs := []struct { + name string + encodePlan pgtype.EncodePlan + }{ + {"text", pgtype.HstoreCodec{}.PlanEncode(nil, 0, pgtype.TextFormatCode, h)}, + {"binary", pgtype.HstoreCodec{}.PlanEncode(nil, 0, pgtype.BinaryFormatCode, h)}, + } + + for _, serializeConfig := range serializeConfigs { + var buf []byte + b.Run(serializeConfig.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + var err error + buf, err = serializeConfig.encodePlan.Encode(h, buf) + if err != nil { + b.Fatal(err) + } + buf = buf[:0] + } + }) + } +}