pgx/hstore_array_test.go
Jack Christensen af0ca3a39b Fix simple protocol empty array and original recursive empty array issue
Original issue https://github.com/jackc/pgtype/issues/68

This crash occurred in the recursive assignment system used to support
multidimensional arrays.

This was fixed in 9639a69d451f55456f598c1aa8b93053f8df3088. However,
that fix incorrectly used nil instead of an empty slice.

In hindsight, it appears the fundamental error is that an assignment to
a slice of a type that is not specified is handled with the recursive /
reflection path. Or another way of looking at it is as an unexpected
feature where []T can now be scanned if individual elements are
assignable to T even if []T is not specifically handled.

But this new reflection / recursive path did not handle empty arrays.

This fix handles the reflection path for an empty slice by allocating an
empty slice.
2020-10-31 17:12:16 -05:00

437 lines
14 KiB
Go

package pgtype_test
import (
"context"
"reflect"
"testing"
"github.com/jackc/pgtype"
"github.com/jackc/pgtype/testutil"
"github.com/jackc/pgx/v4"
)
func TestHstoreArrayTranscode(t *testing.T) {
conn := testutil.MustConnectPgx(t)
defer testutil.MustCloseContext(t, conn)
var hstoreOID uint32
err := conn.QueryRow(context.Background(), "select t.oid from pg_type t where t.typname='hstore';").Scan(&hstoreOID)
if err != nil {
t.Fatalf("did not find hstore OID, %v", err)
}
conn.ConnInfo().RegisterDataType(pgtype.DataType{Value: &pgtype.Hstore{}, Name: "hstore", OID: hstoreOID})
var hstoreArrayOID uint32
err = conn.QueryRow(context.Background(), "select t.oid from pg_type t where t.typname='_hstore';").Scan(&hstoreArrayOID)
if err != nil {
t.Fatalf("did not find _hstore OID, %v", err)
}
conn.ConnInfo().RegisterDataType(pgtype.DataType{Value: &pgtype.HstoreArray{}, Name: "_hstore", OID: hstoreArrayOID})
text := func(s string) pgtype.Text {
return pgtype.Text{String: s, Status: pgtype.Present}
}
values := []pgtype.Hstore{
{Map: map[string]pgtype.Text{}, Status: pgtype.Present},
{Map: map[string]pgtype.Text{"foo": text("bar")}, Status: pgtype.Present},
{Map: map[string]pgtype.Text{"foo": text("bar"), "baz": text("quz")}, Status: pgtype.Present},
{Map: map[string]pgtype.Text{"NULL": text("bar")}, Status: pgtype.Present},
{Map: map[string]pgtype.Text{"foo": text("NULL")}, Status: pgtype.Present},
{Status: pgtype.Null},
}
specialStrings := []string{
`"`,
`'`,
`\`,
`\\`,
`=>`,
` `,
`\ / / \\ => " ' " '`,
}
for _, s := range specialStrings {
// Special key values
values = append(values, pgtype.Hstore{Map: map[string]pgtype.Text{s + "foo": text("bar")}, Status: pgtype.Present}) // at beginning
values = append(values, pgtype.Hstore{Map: map[string]pgtype.Text{"foo" + s + "bar": text("bar")}, Status: pgtype.Present}) // in middle
values = append(values, pgtype.Hstore{Map: map[string]pgtype.Text{"foo" + s: text("bar")}, Status: pgtype.Present}) // at end
values = append(values, pgtype.Hstore{Map: map[string]pgtype.Text{s: text("bar")}, Status: pgtype.Present}) // is key
// Special value values
values = append(values, pgtype.Hstore{Map: map[string]pgtype.Text{"foo": text(s + "bar")}, Status: pgtype.Present}) // at beginning
values = append(values, pgtype.Hstore{Map: map[string]pgtype.Text{"foo": text("foo" + s + "bar")}, Status: pgtype.Present}) // in middle
values = append(values, pgtype.Hstore{Map: map[string]pgtype.Text{"foo": text("foo" + s)}, Status: pgtype.Present}) // at end
values = append(values, pgtype.Hstore{Map: map[string]pgtype.Text{"foo": text(s)}, Status: pgtype.Present}) // is key
}
src := &pgtype.HstoreArray{
Elements: values,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(values)), LowerBound: 1}},
Status: pgtype.Present,
}
_, err = conn.Prepare(context.Background(), "test", "select $1::hstore[]")
if err != nil {
t.Fatal(err)
}
formats := []struct {
name string
formatCode int16
}{
{name: "TextFormat", formatCode: pgx.TextFormatCode},
{name: "BinaryFormat", formatCode: pgx.BinaryFormatCode},
}
for _, fc := range formats {
queryResultFormats := pgx.QueryResultFormats{fc.formatCode}
vEncoder := testutil.ForceEncoder(src, fc.formatCode)
if vEncoder == nil {
t.Logf("%#v does not implement %v", src, fc.name)
continue
}
var result pgtype.HstoreArray
err := conn.QueryRow(context.Background(), "test", queryResultFormats, vEncoder).Scan(&result)
if err != nil {
t.Errorf("%v: %v", fc.name, err)
continue
}
if result.Status != src.Status {
t.Errorf("%v: expected Status %v, got %v", fc.formatCode, src.Status, result.Status)
continue
}
if len(result.Elements) != len(src.Elements) {
t.Errorf("%v: expected %v elements, got %v", fc.formatCode, len(src.Elements), len(result.Elements))
continue
}
for i := range result.Elements {
a := src.Elements[i]
b := result.Elements[i]
if a.Status != b.Status {
t.Errorf("%v element idx %d: expected status %v, got %v", fc.formatCode, i, a.Status, b.Status)
}
if len(a.Map) != len(b.Map) {
t.Errorf("%v element idx %d: expected %v pairs, got %v", fc.formatCode, i, len(a.Map), len(b.Map))
}
for k := range a.Map {
if a.Map[k] != b.Map[k] {
t.Errorf("%v element idx %d: expected key %v to be %v, got %v", fc.formatCode, i, k, a.Map[k], b.Map[k])
}
}
}
}
}
func TestHstoreArraySet(t *testing.T) {
successfulTests := []struct {
src interface{}
result pgtype.HstoreArray
}{
{
src: []map[string]string{{"foo": "bar"}},
result: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 1}},
Status: pgtype.Present,
},
},
{
src: [][]map[string]string{{{"foo": "bar"}}, {{"baz": "quz"}}},
result: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"baz": {String: "quz", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 2}, {LowerBound: 1, Length: 1}},
Status: pgtype.Present,
},
},
{
src: [][][][]map[string]string{
{{{{"foo": "bar"}, {"baz": "quz"}, {"bar": "baz"}}}},
{{{{"wibble": "wobble"}, {"wubble": "wabble"}, {"wabble": "wobble"}}}}},
result: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"baz": {String: "quz", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"bar": {String: "baz", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wibble": {String: "wobble", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wubble": {String: "wabble", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wabble": {String: "wobble", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{
{LowerBound: 1, Length: 2},
{LowerBound: 1, Length: 1},
{LowerBound: 1, Length: 1},
{LowerBound: 1, Length: 3}},
Status: pgtype.Present,
},
},
{
src: [2][1]map[string]string{{{"foo": "bar"}}, {{"baz": "quz"}}},
result: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"baz": {String: "quz", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 2}, {LowerBound: 1, Length: 1}},
Status: pgtype.Present,
},
},
{
src: [2][1][1][3]map[string]string{
{{{{"foo": "bar"}, {"baz": "quz"}, {"bar": "baz"}}}},
{{{{"wibble": "wobble"}, {"wubble": "wabble"}, {"wabble": "wobble"}}}}},
result: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"baz": {String: "quz", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"bar": {String: "baz", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wibble": {String: "wobble", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wubble": {String: "wabble", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wabble": {String: "wobble", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{
{LowerBound: 1, Length: 2},
{LowerBound: 1, Length: 1},
{LowerBound: 1, Length: 1},
{LowerBound: 1, Length: 3}},
Status: pgtype.Present,
},
},
}
for i, tt := range successfulTests {
var dst pgtype.HstoreArray
err := dst.Set(tt.src)
if err != nil {
t.Errorf("%d: %v", i, err)
}
if !reflect.DeepEqual(dst, tt.result) {
t.Errorf("%d: expected %v to convert to %v, but it was %v", i, tt.src, tt.result, dst)
}
}
}
func TestHstoreArrayAssignTo(t *testing.T) {
var hstoreSlice []map[string]string
var hstoreSliceDim2 [][]map[string]string
var hstoreSliceDim4 [][][][]map[string]string
var hstoreArrayDim2 [2][1]map[string]string
var hstoreArrayDim4 [2][1][1][3]map[string]string
simpleTests := []struct {
src pgtype.HstoreArray
dst interface{}
expected interface{}
}{
{
src: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 1}},
Status: pgtype.Present,
},
dst: &hstoreSlice,
expected: []map[string]string{{"foo": "bar"}}},
{
src: pgtype.HstoreArray{Status: pgtype.Null}, dst: &hstoreSlice, expected: (([]map[string]string)(nil)),
},
{
src: pgtype.HstoreArray{Status: pgtype.Present}, dst: &hstoreSlice, expected: []map[string]string{},
},
{
src: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"baz": {String: "quz", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 2}, {LowerBound: 1, Length: 1}},
Status: pgtype.Present,
},
dst: &hstoreSliceDim2,
expected: [][]map[string]string{{{"foo": "bar"}}, {{"baz": "quz"}}},
},
{
src: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"baz": {String: "quz", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"bar": {String: "baz", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wibble": {String: "wobble", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wubble": {String: "wabble", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wabble": {String: "wobble", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{
{LowerBound: 1, Length: 2},
{LowerBound: 1, Length: 1},
{LowerBound: 1, Length: 1},
{LowerBound: 1, Length: 3}},
Status: pgtype.Present,
},
dst: &hstoreSliceDim4,
expected: [][][][]map[string]string{
{{{{"foo": "bar"}, {"baz": "quz"}, {"bar": "baz"}}}},
{{{{"wibble": "wobble"}, {"wubble": "wabble"}, {"wabble": "wobble"}}}}},
},
{
src: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"baz": {String: "quz", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 2}, {LowerBound: 1, Length: 1}},
Status: pgtype.Present,
},
dst: &hstoreArrayDim2,
expected: [2][1]map[string]string{{{"foo": "bar"}}, {{"baz": "quz"}}},
},
{
src: pgtype.HstoreArray{
Elements: []pgtype.Hstore{
{
Map: map[string]pgtype.Text{"foo": {String: "bar", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"baz": {String: "quz", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"bar": {String: "baz", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wibble": {String: "wobble", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wubble": {String: "wabble", Status: pgtype.Present}},
Status: pgtype.Present,
},
{
Map: map[string]pgtype.Text{"wabble": {String: "wobble", Status: pgtype.Present}},
Status: pgtype.Present,
},
},
Dimensions: []pgtype.ArrayDimension{
{LowerBound: 1, Length: 2},
{LowerBound: 1, Length: 1},
{LowerBound: 1, Length: 1},
{LowerBound: 1, Length: 3}},
Status: pgtype.Present,
},
dst: &hstoreArrayDim4,
expected: [2][1][1][3]map[string]string{
{{{{"foo": "bar"}, {"baz": "quz"}, {"bar": "baz"}}}},
{{{{"wibble": "wobble"}, {"wubble": "wabble"}, {"wabble": "wobble"}}}}},
},
}
for i, tt := range simpleTests {
err := tt.src.AssignTo(tt.dst)
if err != nil {
t.Errorf("%d: %v", i, err)
}
if dst := reflect.ValueOf(tt.dst).Elem().Interface(); !reflect.DeepEqual(dst, tt.expected) {
t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, dst)
}
}
}