From 2ecbb8e36cd1658f0ceb05785180dfb21dd6f3f4 Mon Sep 17 00:00:00 2001 From: JP Roemer <2822534+0rax@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:09:34 +0200 Subject: [PATCH 1/2] fix: handle empty hstore gracefuly in pgdialect --- dialect/pgdialect/hstore_parser.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dialect/pgdialect/hstore_parser.go b/dialect/pgdialect/hstore_parser.go index df9918219..fec401786 100644 --- a/dialect/pgdialect/hstore_parser.go +++ b/dialect/pgdialect/hstore_parser.go @@ -16,7 +16,7 @@ type hstoreParser struct { func newHStoreParser(b []byte) *hstoreParser { p := new(hstoreParser) - if len(b) < 6 || b[0] != '"' { + if len(b) != 0 && (len(b) < 6 || b[0] != '"') { p.err = fmt.Errorf("pgdialect: can't parse hstore: %q", b) return p } @@ -83,7 +83,7 @@ func (p *hstoreParser) readNext() error { default: value := p.p.ReadLiteral(ch) if bytes.Equal(value, []byte("NULL")) { - value = nil + p.value = "" } p.skipComma() return nil From 1d90ace98e34262062eb3db0c6882ea4f7fece2f Mon Sep 17 00:00:00 2001 From: JP Roemer <2822534+0rax@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:10:23 +0200 Subject: [PATCH 2/2] chore: add test cases for hstore (empty hstore & query appender) --- dialect/pgdialect/append_test.go | 39 +++++++++++++++++++++++++ dialect/pgdialect/hstore_parser_test.go | 2 ++ internal/dbtest/pg_test.go | 16 ++++++++++ 3 files changed, 57 insertions(+) create mode 100644 dialect/pgdialect/append_test.go diff --git a/dialect/pgdialect/append_test.go b/dialect/pgdialect/append_test.go new file mode 100644 index 000000000..eeadb5e24 --- /dev/null +++ b/dialect/pgdialect/append_test.go @@ -0,0 +1,39 @@ +package pgdialect + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + "github.com/uptrace/bun/schema" +) + +func TestHStoreAppender(t *testing.T) { + tests := []struct { + input map[string]string + expectedIn []string // maps being unsorted, multiple expected output are valid + }{ + {nil, []string{`NULL`}}, + {map[string]string{}, []string{`''`}}, + + {map[string]string{"": ""}, []string{`'""=>""'`}}, + {map[string]string{`\`: `\`}, []string{`'"\\"=>"\\"'`}}, + {map[string]string{"'": "'"}, []string{`'"''"=>"''"'`}}, + {map[string]string{`'"{}`: `'"{}`}, []string{`'"''\"{}"=>"''\"{}"'`}}, + + {map[string]string{"1": "2", "3": "4"}, []string{`'"1"=>"2","3"=>"4"'`, `'"3"=>"4","1"=>"2"'`}}, + {map[string]string{"1": ""}, []string{`'"1"=>""'`}}, + {map[string]string{"1": "NULL"}, []string{`'"1"=>"NULL"'`}}, + {map[string]string{"{1}": "{2}", "{3}": "{4}"}, []string{`'"{1}"=>"{2}","{3}"=>"{4}"'`, `'"{3}"=>"{4}","{1}"=>"{2}"'`}}, + } + + appendFunc := pgDialect.hstoreAppender(reflect.TypeOf(map[string]string{})) + + for i, test := range tests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + got := appendFunc(schema.NewFormatter(pgDialect), []byte{}, reflect.ValueOf(test.input)) + require.Contains(t, test.expectedIn, string(got)) + }) + } +} diff --git a/dialect/pgdialect/hstore_parser_test.go b/dialect/pgdialect/hstore_parser_test.go index 2323611c2..6b3d663a6 100644 --- a/dialect/pgdialect/hstore_parser_test.go +++ b/dialect/pgdialect/hstore_parser_test.go @@ -12,6 +12,8 @@ func TestHStoreParser(t *testing.T) { s string m map[string]string }{ + {``, map[string]string{}}, + {`""=>""`, map[string]string{"": ""}}, {`"\\"=>"\\"`, map[string]string{`\`: `\`}}, {`"'"=>"'"`, map[string]string{"'": "'"}}, diff --git a/internal/dbtest/pg_test.go b/internal/dbtest/pg_test.go index 103d68170..21e80b852 100644 --- a/internal/dbtest/pg_test.go +++ b/internal/dbtest/pg_test.go @@ -750,6 +750,22 @@ func TestPostgresHStoreQuote(t *testing.T) { require.Equal(t, wanted, m) } +func TestPostgresHStoreEmpty(t *testing.T) { + db := pg(t) + t.Cleanup(func() { db.Close() }) + + _, err := db.Exec(`CREATE EXTENSION IF NOT EXISTS HSTORE;`) + require.NoError(t, err) + + wanted := map[string]string{} + m := make(map[string]string) + err = db.NewSelect(). + ColumnExpr("?::hstore", pgdialect.HStore(wanted)). + Scan(ctx, pgdialect.HStore(&m)) + require.NoError(t, err) + require.Equal(t, wanted, m) +} + func TestPostgresSkipupdateField(t *testing.T) { type Model struct { ID int64 `bun:",pk,autoincrement"`