| // Copyright 2018 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // +build go1.7 |
| |
| package spanner |
| |
| import ( |
| "reflect" |
| "testing" |
| "time" |
| |
| "cloud.google.com/go/civil" |
| proto "github.com/golang/protobuf/proto" |
| proto3 "github.com/golang/protobuf/ptypes/struct" |
| "golang.org/x/net/context" |
| sppb "google.golang.org/genproto/googleapis/spanner/v1" |
| ) |
| |
| func TestEncodeStructValueDynamicStructs(t *testing.T) { |
| dynStructType := reflect.StructOf([]reflect.StructField{ |
| {Name: "A", Type: reflect.TypeOf(0), Tag: `spanner:"a"`}, |
| {Name: "B", Type: reflect.TypeOf(""), Tag: `spanner:"b"`}, |
| }) |
| dynNullableStructType := reflect.PtrTo(dynStructType) |
| dynStructArrType := reflect.SliceOf(dynStructType) |
| dynNullableStructArrType := reflect.SliceOf(dynNullableStructType) |
| |
| dynStructValue := reflect.New(dynStructType) |
| dynStructValue.Elem().Field(0).SetInt(10) |
| dynStructValue.Elem().Field(1).SetString("abc") |
| |
| dynStructArrValue := reflect.MakeSlice(dynNullableStructArrType, 2, 2) |
| dynStructArrValue.Index(0).Set(reflect.Zero(dynNullableStructType)) |
| dynStructArrValue.Index(1).Set(dynStructValue) |
| |
| structProtoType := structType( |
| mkField("a", intType()), |
| mkField("b", stringType())) |
| |
| arrProtoType := listType(structProtoType) |
| |
| for _, test := range []encodeTest{ |
| { |
| "Dynanic non-NULL struct value.", |
| dynStructValue.Elem().Interface(), |
| listProto(intProto(10), stringProto("abc")), |
| structProtoType, |
| }, |
| { |
| "Dynanic NULL struct value.", |
| reflect.Zero(dynNullableStructType).Interface(), |
| nullProto(), |
| structProtoType, |
| }, |
| { |
| "Empty array of dynamic structs.", |
| reflect.MakeSlice(dynStructArrType, 0, 0).Interface(), |
| listProto([]*proto3.Value{}...), |
| arrProtoType, |
| }, |
| { |
| "NULL array of non-NULL-able dynamic structs.", |
| reflect.Zero(dynStructArrType).Interface(), |
| nullProto(), |
| arrProtoType, |
| }, |
| { |
| "NULL array of NULL-able(nil) dynamic structs.", |
| reflect.Zero(dynNullableStructArrType).Interface(), |
| nullProto(), |
| arrProtoType, |
| }, |
| { |
| "Array containing NULL(nil) dynamic-typed struct elements.", |
| dynStructArrValue.Interface(), |
| listProto( |
| nullProto(), |
| listProto(intProto(10), stringProto("abc"))), |
| arrProtoType, |
| }, |
| } { |
| encodeStructValue(test, t) |
| } |
| } |
| |
| func TestEncodeStructValueEmptyStruct(t *testing.T) { |
| emptyListValue := listProto([]*proto3.Value{}...) |
| emptyStructType := structType([]*sppb.StructType_Field{}...) |
| emptyStruct := struct{}{} |
| nullEmptyStruct := (*struct{})(nil) |
| |
| dynamicEmptyStructType := reflect.StructOf(make([]reflect.StructField, 0, 0)) |
| dynamicStructArrType := reflect.SliceOf(reflect.PtrTo((dynamicEmptyStructType))) |
| |
| dynamicEmptyStruct := reflect.New(dynamicEmptyStructType) |
| dynamicNullEmptyStruct := reflect.Zero(reflect.PtrTo(dynamicEmptyStructType)) |
| |
| dynamicStructArrValue := reflect.MakeSlice(dynamicStructArrType, 2, 2) |
| dynamicStructArrValue.Index(0).Set(dynamicNullEmptyStruct) |
| dynamicStructArrValue.Index(1).Set(dynamicEmptyStruct) |
| |
| for _, test := range []encodeTest{ |
| { |
| "Go empty struct.", |
| emptyStruct, |
| emptyListValue, |
| emptyStructType, |
| }, |
| { |
| "Dynamic empty struct.", |
| dynamicEmptyStruct.Interface(), |
| emptyListValue, |
| emptyStructType, |
| }, |
| { |
| "Go NULL empty struct.", |
| nullEmptyStruct, |
| nullProto(), |
| emptyStructType, |
| }, |
| { |
| "Dynamic NULL empty struct.", |
| dynamicNullEmptyStruct.Interface(), |
| nullProto(), |
| emptyStructType, |
| }, |
| { |
| "Non-empty array of dynamic NULL and non-NULL empty structs.", |
| dynamicStructArrValue.Interface(), |
| listProto(nullProto(), emptyListValue), |
| listType(emptyStructType), |
| }, |
| { |
| "Non-empty array of nullable empty structs.", |
| []*struct{}{nullEmptyStruct, &emptyStruct}, |
| listProto(nullProto(), emptyListValue), |
| listType(emptyStructType), |
| }, |
| { |
| "Empty array of empty struct.", |
| []struct{}{}, |
| emptyListValue, |
| listType(emptyStructType), |
| }, |
| { |
| "Null array of empty structs.", |
| []struct{}(nil), |
| nullProto(), |
| listType(emptyStructType), |
| }, |
| } { |
| encodeStructValue(test, t) |
| } |
| } |
| |
| func TestEncodeStructValueMixedStructTypes(t *testing.T) { |
| type staticStruct struct { |
| F int `spanner:"fStatic"` |
| } |
| s1 := staticStruct{10} |
| s2 := (*staticStruct)(nil) |
| |
| var f float64 |
| dynStructType := reflect.StructOf([]reflect.StructField{ |
| {Name: "A", Type: reflect.TypeOf(f), Tag: `spanner:"fDynamic"`}, |
| }) |
| s3 := reflect.New(dynStructType) |
| s3.Elem().Field(0).SetFloat(3.14) |
| |
| for _, test := range []encodeTest{ |
| { |
| "'struct' with static and dynamic *struct, []*struct, []struct fields", |
| struct { |
| A []staticStruct |
| B []*staticStruct |
| C interface{} |
| }{ |
| []staticStruct{s1, s1}, |
| []*staticStruct{&s1, s2}, |
| s3.Interface(), |
| }, |
| listProto( |
| listProto(listProto(intProto(10)), listProto(intProto(10))), |
| listProto(listProto(intProto(10)), nullProto()), |
| listProto(floatProto(3.14))), |
| structType( |
| mkField("A", listType(structType(mkField("fStatic", intType())))), |
| mkField("B", listType(structType(mkField("fStatic", intType())))), |
| mkField("C", structType(mkField("fDynamic", floatType())))), |
| }, |
| } { |
| encodeStructValue(test, t) |
| } |
| } |
| |
| func TestBindParamsDynamic(t *testing.T) { |
| // Verify Statement.bindParams generates correct values and types. |
| st := Statement{ |
| SQL: "SELECT id from t_foo WHERE col = @var", |
| Params: map[string]interface{}{"var": nil}, |
| } |
| want := &sppb.ExecuteSqlRequest{ |
| Params: &proto3.Struct{ |
| Fields: map[string]*proto3.Value{"var": nil}, |
| }, |
| ParamTypes: map[string]*sppb.Type{"var": nil}, |
| } |
| var ( |
| t1, _ = time.Parse(time.RFC3339Nano, "2016-11-15T15:04:05.999999999Z") |
| // Boundaries |
| t2, _ = time.Parse(time.RFC3339Nano, "0001-01-01T00:00:00.000000000Z") |
| ) |
| dynamicStructType := reflect.StructOf([]reflect.StructField{ |
| {Name: "A", Type: reflect.TypeOf(t1), Tag: `spanner:"field"`}, |
| {Name: "B", Type: reflect.TypeOf(3.14), Tag: `spanner:""`}, |
| }) |
| dynamicStructArrType := reflect.SliceOf(reflect.PtrTo(dynamicStructType)) |
| dynamicEmptyStructType := reflect.StructOf(make([]reflect.StructField, 0, 0)) |
| |
| dynamicStructTypeProto := structType( |
| mkField("field", timeType()), |
| mkField("", floatType())) |
| |
| s3 := reflect.New(dynamicStructType) |
| s3.Elem().Field(0).Set(reflect.ValueOf(t1)) |
| s3.Elem().Field(1).SetFloat(1.4) |
| |
| s4 := reflect.New(dynamicStructType) |
| s4.Elem().Field(0).Set(reflect.ValueOf(t2)) |
| s4.Elem().Field(1).SetFloat(-13.3) |
| |
| dynamicStructArrayVal := reflect.MakeSlice(dynamicStructArrType, 2, 2) |
| dynamicStructArrayVal.Index(0).Set(s3) |
| dynamicStructArrayVal.Index(1).Set(s4) |
| |
| for i, test := range []struct { |
| val interface{} |
| wantField *proto3.Value |
| wantType *sppb.Type |
| }{ |
| { |
| s3.Interface(), |
| listProto(timeProto(t1), floatProto(1.4)), |
| structType( |
| mkField("field", timeType()), |
| mkField("", floatType())), |
| }, |
| { |
| reflect.Zero(reflect.PtrTo(dynamicEmptyStructType)).Interface(), |
| nullProto(), |
| structType([]*sppb.StructType_Field{}...), |
| }, |
| { |
| dynamicStructArrayVal.Interface(), |
| listProto( |
| listProto(timeProto(t1), floatProto(1.4)), |
| listProto(timeProto(t2), floatProto(-13.3))), |
| listType(dynamicStructTypeProto), |
| }, |
| { |
| []*struct { |
| F1 time.Time `spanner:"field"` |
| F2 float64 `spanner:""` |
| }{ |
| nil, |
| {t1, 1.4}, |
| }, |
| listProto( |
| nullProto(), |
| listProto(timeProto(t1), floatProto(1.4))), |
| listType(dynamicStructTypeProto), |
| }, |
| } { |
| st.Params["var"] = test.val |
| want.Params.Fields["var"] = test.wantField |
| want.ParamTypes["var"] = test.wantType |
| got := &sppb.ExecuteSqlRequest{} |
| if err := st.bindParams(got); err != nil || !proto.Equal(got, want) { |
| // handle NaN |
| if test.wantType.Code == floatType().Code && proto.MarshalTextString(got) == proto.MarshalTextString(want) { |
| continue |
| } |
| t.Errorf("#%d: bind result: \n(%v, %v)\nwant\n(%v, %v)\n", i, got, err, want, nil) |
| } |
| } |
| } |
| |
| func TestStructParametersBind(t *testing.T) { |
| t.Parallel() |
| ctx := context.Background() |
| client, _, tearDown := prepare(ctx, t, nil) |
| defer tearDown() |
| |
| type tRow []interface{} |
| type tRows []struct{ trow tRow } |
| |
| type allFields struct { |
| Stringf string |
| Intf int |
| Boolf bool |
| Floatf float64 |
| Bytef []byte |
| Timef time.Time |
| Datef civil.Date |
| } |
| allColumns := []string{ |
| "Stringf", |
| "Intf", |
| "Boolf", |
| "Floatf", |
| "Bytef", |
| "Timef", |
| "Datef", |
| } |
| s1 := allFields{"abc", 300, false, 3.45, []byte("foo"), t1, d1} |
| s2 := allFields{"def", -300, false, -3.45, []byte("bar"), t2, d2} |
| |
| dynamicStructType := reflect.StructOf([]reflect.StructField{ |
| {Name: "A", Type: reflect.TypeOf(t1), Tag: `spanner:"ff1"`}, |
| }) |
| s3 := reflect.New(dynamicStructType) |
| s3.Elem().Field(0).Set(reflect.ValueOf(t1)) |
| |
| for i, test := range []struct { |
| param interface{} |
| sql string |
| cols []string |
| trows tRows |
| }{ |
| // Struct value. |
| { |
| s1, |
| "SELECT" + |
| " @p.Stringf," + |
| " @p.Intf," + |
| " @p.Boolf," + |
| " @p.Floatf," + |
| " @p.Bytef," + |
| " @p.Timef," + |
| " @p.Datef", |
| allColumns, |
| tRows{ |
| {tRow{"abc", 300, false, 3.45, []byte("foo"), t1, d1}}, |
| }, |
| }, |
| // Array of struct value. |
| { |
| []allFields{s1, s2}, |
| "SELECT * FROM UNNEST(@p)", |
| allColumns, |
| tRows{ |
| {tRow{"abc", 300, false, 3.45, []byte("foo"), t1, d1}}, |
| {tRow{"def", -300, false, -3.45, []byte("bar"), t2, d2}}, |
| }, |
| }, |
| // Null struct. |
| { |
| (*allFields)(nil), |
| "SELECT @p IS NULL", |
| []string{""}, |
| tRows{ |
| {tRow{true}}, |
| }, |
| }, |
| // Null Array of struct. |
| { |
| []allFields(nil), |
| "SELECT @p IS NULL", |
| []string{""}, |
| tRows{ |
| {tRow{true}}, |
| }, |
| }, |
| // Empty struct. |
| { |
| struct{}{}, |
| "SELECT @p IS NULL ", |
| []string{""}, |
| tRows{ |
| {tRow{false}}, |
| }, |
| }, |
| // Empty array of struct. |
| { |
| []allFields{}, |
| "SELECT * FROM UNNEST(@p) ", |
| allColumns, |
| tRows{}, |
| }, |
| // Struct with duplicate fields. |
| { |
| struct { |
| A int `spanner:"field"` |
| B int `spanner:"field"` |
| }{10, 20}, |
| "SELECT * FROM UNNEST([@p]) ", |
| []string{"field", "field"}, |
| tRows{ |
| {tRow{10, 20}}, |
| }, |
| }, |
| // Struct with unnamed fields. |
| { |
| struct { |
| A string `spanner:""` |
| }{"hello"}, |
| "SELECT * FROM UNNEST([@p]) ", |
| []string{""}, |
| tRows{ |
| {tRow{"hello"}}, |
| }, |
| }, |
| // Mixed struct. |
| { |
| struct { |
| DynamicStructField interface{} `spanner:"f1"` |
| ArrayStructField []*allFields `spanner:"f2"` |
| }{ |
| DynamicStructField: s3.Interface(), |
| ArrayStructField: []*allFields{nil}, |
| }, |
| "SELECT @p.f1.ff1, ARRAY_LENGTH(@p.f2), @p.f2[OFFSET(0)] IS NULL ", |
| []string{"ff1", "", ""}, |
| tRows{ |
| {tRow{t1, 1, true}}, |
| }, |
| }, |
| } { |
| iter := client.Single().Query(ctx, Statement{ |
| SQL: test.sql, |
| Params: map[string]interface{}{"p": test.param}, |
| }) |
| var gotRows []*Row |
| err := iter.Do(func(r *Row) error { |
| gotRows = append(gotRows, r) |
| return nil |
| }) |
| if err != nil { |
| t.Errorf("Failed to execute test case %d, error: %v", i, err) |
| } |
| |
| var wantRows []*Row |
| for j, row := range test.trows { |
| r, err := NewRow(test.cols, row.trow) |
| if err != nil { |
| t.Errorf("Invalid row %d in test case %d", j, i) |
| } |
| wantRows = append(wantRows, r) |
| } |
| if !testEqual(gotRows, wantRows) { |
| t.Errorf("%d: Want result %v, got result %v", i, wantRows, gotRows) |
| } |
| } |
| } |