diff --git a/bigquery/integration_test.go b/bigquery/integration_test.go index 95c3a5180470..239aea03e77e 100644 --- a/bigquery/integration_test.go +++ b/bigquery/integration_test.go @@ -24,6 +24,7 @@ import ( "math/big" "net/http" "os" + "reflect" "sort" "strings" "testing" @@ -2527,6 +2528,37 @@ func TestIntegration_QueryParameters(t *testing.T) { } } +func TestIntegration_QueryEmptyArrays(t *testing.T) { + if client == nil { + t.Skip("Integration tests skipped") + } + ctx := context.Background() + + q := client.Query("SELECT ARRAY[] as a, ARRAY>[] as b") + it, err := q.Read(ctx) + if err != nil { + t.Fatal(err) + } + for { + vals := map[string]Value{} + if err := it.Next(&vals); err != nil { + if errors.Is(err, iterator.Done) { + break + } + } + + valueOfA := reflect.ValueOf(vals["a"]) + if testutil.Equal(vals["a"], nil) || valueOfA.IsNil() { + t.Fatalf("expected empty string array to not return nil, but found %v %v %T", valueOfA, vals["a"], vals["a"]) + } + + valueOfB := reflect.ValueOf(vals["b"]) + if testutil.Equal(vals["b"], nil) || valueOfB.IsNil() { + t.Fatalf("expected empty struct array to not return nil, but found %v %v %T", valueOfB, vals["b"], vals["b"]) + } + } +} + // This test can be merged with the TestIntegration_QueryParameters as soon as support for explicit typed query parameter lands. // To test timestamps with different formats, we need to be able to specify the type explicitly. func TestIntegration_TimestampFormat(t *testing.T) { diff --git a/bigquery/iterator.go b/bigquery/iterator.go index 9d177d1b829c..942be4205d07 100644 --- a/bigquery/iterator.go +++ b/bigquery/iterator.go @@ -140,8 +140,12 @@ type pageFetcher func(ctx context.Context, _ *rowSource, _ Schema, startIndex ui // See https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#numeric-type // for more on NUMERIC. // -// A repeated field corresponds to a slice or array of the element type. A STRUCT -// type (RECORD or nested schema) corresponds to a nested struct or struct pointer. +// A repeated field corresponds to a slice or array of the element type. BigQuery translates +// NULL arrays into an empty array, so we follow that behavior. +// See https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#array_nulls +// for more about NULL and empty arrays. +// +// A STRUCT type (RECORD or nested schema) corresponds to a nested struct or struct pointer. // All calls to Next on the same iterator must use the same struct type. // // It is an error to attempt to read a BigQuery NULL value into a struct field, diff --git a/bigquery/value.go b/bigquery/value.go index c178f8b819db..5c0165f7373c 100644 --- a/bigquery/value.go +++ b/bigquery/value.go @@ -82,6 +82,9 @@ func loadMap(m map[string]Value, vals []Value, s Schema) { } v = vs } + if f.Repeated && (v == nil || reflect.ValueOf(v).IsNil()) { + v = []Value{} + } m[f.Name] = v } diff --git a/bigquery/value_test.go b/bigquery/value_test.go index 7211835b3f82..024f6806d859 100644 --- a/bigquery/value_test.go +++ b/bigquery/value_test.go @@ -825,10 +825,12 @@ func TestValueMap(t *testing.T) { {Name: "i", Type: IntegerFieldType}, {Name: "f", Type: FloatFieldType}, {Name: "b", Type: BooleanFieldType}, - {Name: "n", Type: RecordFieldType, Schema: ns}, + {Name: "sn", Type: StringFieldType, Repeated: true}, + {Name: "r", Type: RecordFieldType, Schema: ns}, {Name: "rn", Type: RecordFieldType, Schema: ns, Repeated: true}, } in := []Value{"x", 7, 3.14, true, + []Value{"a", "b"}, []Value{1, 2}, []Value{[]Value{3, 4}, []Value{5, 6}}, } @@ -837,11 +839,12 @@ func TestValueMap(t *testing.T) { t.Fatal(err) } want := map[string]Value{ - "s": "x", - "i": 7, - "f": 3.14, - "b": true, - "n": map[string]Value{"x": 1, "y": 2}, + "s": "x", + "i": 7, + "f": 3.14, + "b": true, + "sn": []Value{"a", "b"}, + "r": map[string]Value{"x": 1, "y": 2}, "rn": []Value{ map[string]Value{"x": 3, "y": 4}, map[string]Value{"x": 5, "y": 6}, @@ -857,8 +860,9 @@ func TestValueMap(t *testing.T) { "i": nil, "f": nil, "b": nil, - "n": nil, - "rn": nil, + "sn": []Value{}, + "r": nil, + "rn": []Value{}, } var vm2 valueMap if err := vm2.Load(in, schema); err != nil {