| // Copyright 2014 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. | |
| package datastore | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "reflect" | |
| "sort" | |
| "testing" | |
| "cloud.google.com/go/internal/testutil" | |
| "github.com/golang/protobuf/proto" | |
| "github.com/google/go-cmp/cmp" | |
| pb "google.golang.org/genproto/googleapis/datastore/v1" | |
| "google.golang.org/grpc" | |
| ) | |
| var ( | |
| key1 = &pb.Key{ | |
| Path: []*pb.Key_PathElement{ | |
| { | |
| Kind: "Gopher", | |
| IdType: &pb.Key_PathElement_Id{Id: 6}, | |
| }, | |
| }, | |
| } | |
| key2 = &pb.Key{ | |
| Path: []*pb.Key_PathElement{ | |
| { | |
| Kind: "Gopher", | |
| IdType: &pb.Key_PathElement_Id{Id: 6}, | |
| }, | |
| { | |
| Kind: "Gopher", | |
| IdType: &pb.Key_PathElement_Id{Id: 8}, | |
| }, | |
| }, | |
| } | |
| ) | |
| type fakeClient struct { | |
| pb.DatastoreClient | |
| queryFn func(*pb.RunQueryRequest) (*pb.RunQueryResponse, error) | |
| commitFn func(*pb.CommitRequest) (*pb.CommitResponse, error) | |
| } | |
| func (c *fakeClient) RunQuery(_ context.Context, req *pb.RunQueryRequest, _ ...grpc.CallOption) (*pb.RunQueryResponse, error) { | |
| return c.queryFn(req) | |
| } | |
| func (c *fakeClient) Commit(_ context.Context, req *pb.CommitRequest, _ ...grpc.CallOption) (*pb.CommitResponse, error) { | |
| return c.commitFn(req) | |
| } | |
| func fakeRunQuery(in *pb.RunQueryRequest) (*pb.RunQueryResponse, error) { | |
| expectedIn := &pb.RunQueryRequest{ | |
| QueryType: &pb.RunQueryRequest_Query{Query: &pb.Query{ | |
| Kind: []*pb.KindExpression{{Name: "Gopher"}}, | |
| }}, | |
| } | |
| if !proto.Equal(in, expectedIn) { | |
| return nil, fmt.Errorf("unsupported argument: got %v want %v", in, expectedIn) | |
| } | |
| return &pb.RunQueryResponse{ | |
| Batch: &pb.QueryResultBatch{ | |
| MoreResults: pb.QueryResultBatch_NO_MORE_RESULTS, | |
| EntityResultType: pb.EntityResult_FULL, | |
| EntityResults: []*pb.EntityResult{ | |
| { | |
| Entity: &pb.Entity{ | |
| Key: key1, | |
| Properties: map[string]*pb.Value{ | |
| "Name": {ValueType: &pb.Value_StringValue{StringValue: "George"}}, | |
| "Height": {ValueType: &pb.Value_IntegerValue{IntegerValue: 32}}, | |
| }, | |
| }, | |
| }, | |
| { | |
| Entity: &pb.Entity{ | |
| Key: key2, | |
| Properties: map[string]*pb.Value{ | |
| "Name": {ValueType: &pb.Value_StringValue{StringValue: "Rufus"}}, | |
| // No height for Rufus. | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, nil | |
| } | |
| type StructThatImplementsPLS struct{} | |
| func (StructThatImplementsPLS) Load(p []Property) error { return nil } | |
| func (StructThatImplementsPLS) Save() ([]Property, error) { return nil, nil } | |
| var _ PropertyLoadSaver = StructThatImplementsPLS{} | |
| type StructPtrThatImplementsPLS struct{} | |
| func (*StructPtrThatImplementsPLS) Load(p []Property) error { return nil } | |
| func (*StructPtrThatImplementsPLS) Save() ([]Property, error) { return nil, nil } | |
| var _ PropertyLoadSaver = &StructPtrThatImplementsPLS{} | |
| type PropertyMap map[string]Property | |
| func (m PropertyMap) Load(props []Property) error { | |
| for _, p := range props { | |
| m[p.Name] = p | |
| } | |
| return nil | |
| } | |
| func (m PropertyMap) Save() ([]Property, error) { | |
| props := make([]Property, 0, len(m)) | |
| for _, p := range m { | |
| props = append(props, p) | |
| } | |
| return props, nil | |
| } | |
| var _ PropertyLoadSaver = PropertyMap{} | |
| type Gopher struct { | |
| Name string | |
| Height int | |
| } | |
| // typeOfEmptyInterface is the type of interface{}, but we can't use | |
| // reflect.TypeOf((interface{})(nil)) directly because TypeOf takes an | |
| // interface{}. | |
| var typeOfEmptyInterface = reflect.TypeOf((*interface{})(nil)).Elem() | |
| func TestCheckMultiArg(t *testing.T) { | |
| testCases := []struct { | |
| v interface{} | |
| mat multiArgType | |
| elemType reflect.Type | |
| }{ | |
| // Invalid cases. | |
| {nil, multiArgTypeInvalid, nil}, | |
| {Gopher{}, multiArgTypeInvalid, nil}, | |
| {&Gopher{}, multiArgTypeInvalid, nil}, | |
| {PropertyList{}, multiArgTypeInvalid, nil}, // This is a special case. | |
| {PropertyMap{}, multiArgTypeInvalid, nil}, | |
| {[]*PropertyList(nil), multiArgTypeInvalid, nil}, | |
| {[]*PropertyMap(nil), multiArgTypeInvalid, nil}, | |
| {[]**Gopher(nil), multiArgTypeInvalid, nil}, | |
| {[]*interface{}(nil), multiArgTypeInvalid, nil}, | |
| // Valid cases. | |
| { | |
| []PropertyList(nil), | |
| multiArgTypePropertyLoadSaver, | |
| reflect.TypeOf(PropertyList{}), | |
| }, | |
| { | |
| []PropertyMap(nil), | |
| multiArgTypePropertyLoadSaver, | |
| reflect.TypeOf(PropertyMap{}), | |
| }, | |
| { | |
| []StructThatImplementsPLS(nil), | |
| multiArgTypePropertyLoadSaver, | |
| reflect.TypeOf(StructThatImplementsPLS{}), | |
| }, | |
| { | |
| []StructPtrThatImplementsPLS(nil), | |
| multiArgTypePropertyLoadSaver, | |
| reflect.TypeOf(StructPtrThatImplementsPLS{}), | |
| }, | |
| { | |
| []Gopher(nil), | |
| multiArgTypeStruct, | |
| reflect.TypeOf(Gopher{}), | |
| }, | |
| { | |
| []*Gopher(nil), | |
| multiArgTypeStructPtr, | |
| reflect.TypeOf(Gopher{}), | |
| }, | |
| { | |
| []interface{}(nil), | |
| multiArgTypeInterface, | |
| typeOfEmptyInterface, | |
| }, | |
| } | |
| for _, tc := range testCases { | |
| mat, elemType := checkMultiArg(reflect.ValueOf(tc.v)) | |
| if mat != tc.mat || elemType != tc.elemType { | |
| t.Errorf("checkMultiArg(%T): got %v, %v want %v, %v", | |
| tc.v, mat, elemType, tc.mat, tc.elemType) | |
| } | |
| } | |
| } | |
| func TestSimpleQuery(t *testing.T) { | |
| struct1 := Gopher{Name: "George", Height: 32} | |
| struct2 := Gopher{Name: "Rufus"} | |
| pList1 := PropertyList{ | |
| { | |
| Name: "Height", | |
| Value: int64(32), | |
| }, | |
| { | |
| Name: "Name", | |
| Value: "George", | |
| }, | |
| } | |
| pList2 := PropertyList{ | |
| { | |
| Name: "Name", | |
| Value: "Rufus", | |
| }, | |
| } | |
| pMap1 := PropertyMap{ | |
| "Name": Property{ | |
| Name: "Name", | |
| Value: "George", | |
| }, | |
| "Height": Property{ | |
| Name: "Height", | |
| Value: int64(32), | |
| }, | |
| } | |
| pMap2 := PropertyMap{ | |
| "Name": Property{ | |
| Name: "Name", | |
| Value: "Rufus", | |
| }, | |
| } | |
| testCases := []struct { | |
| dst interface{} | |
| want interface{} | |
| }{ | |
| // The destination must have type *[]P, *[]S or *[]*S, for some non-interface | |
| // type P such that *P implements PropertyLoadSaver, or for some struct type S. | |
| {new([]Gopher), &[]Gopher{struct1, struct2}}, | |
| {new([]*Gopher), &[]*Gopher{&struct1, &struct2}}, | |
| {new([]PropertyList), &[]PropertyList{pList1, pList2}}, | |
| {new([]PropertyMap), &[]PropertyMap{pMap1, pMap2}}, | |
| // Any other destination type is invalid. | |
| {0, nil}, | |
| {Gopher{}, nil}, | |
| {PropertyList{}, nil}, | |
| {PropertyMap{}, nil}, | |
| {[]int{}, nil}, | |
| {[]Gopher{}, nil}, | |
| {[]PropertyList{}, nil}, | |
| {new(int), nil}, | |
| {new(Gopher), nil}, | |
| {new(PropertyList), nil}, // This is a special case. | |
| {new(PropertyMap), nil}, | |
| {new([]int), nil}, | |
| {new([]map[int]int), nil}, | |
| {new([]map[string]Property), nil}, | |
| {new([]map[string]interface{}), nil}, | |
| {new([]*int), nil}, | |
| {new([]*map[int]int), nil}, | |
| {new([]*map[string]Property), nil}, | |
| {new([]*map[string]interface{}), nil}, | |
| {new([]**Gopher), nil}, | |
| {new([]*PropertyList), nil}, | |
| {new([]*PropertyMap), nil}, | |
| } | |
| for _, tc := range testCases { | |
| nCall := 0 | |
| client := &Client{ | |
| client: &fakeClient{ | |
| queryFn: func(req *pb.RunQueryRequest) (*pb.RunQueryResponse, error) { | |
| nCall++ | |
| return fakeRunQuery(req) | |
| }, | |
| }, | |
| } | |
| ctx := context.Background() | |
| var ( | |
| expectedErr error | |
| expectedNCall int | |
| ) | |
| if tc.want == nil { | |
| expectedErr = ErrInvalidEntityType | |
| } else { | |
| expectedNCall = 1 | |
| } | |
| keys, err := client.GetAll(ctx, NewQuery("Gopher"), tc.dst) | |
| if err != expectedErr { | |
| t.Errorf("dst type %T: got error %v, want %v", tc.dst, err, expectedErr) | |
| continue | |
| } | |
| if nCall != expectedNCall { | |
| t.Errorf("dst type %T: Context.Call was called an incorrect number of times: got %d want %d", tc.dst, nCall, expectedNCall) | |
| continue | |
| } | |
| if err != nil { | |
| continue | |
| } | |
| key1 := IDKey("Gopher", 6, nil) | |
| expectedKeys := []*Key{ | |
| key1, | |
| IDKey("Gopher", 8, key1), | |
| } | |
| if l1, l2 := len(keys), len(expectedKeys); l1 != l2 { | |
| t.Errorf("dst type %T: got %d keys, want %d keys", tc.dst, l1, l2) | |
| continue | |
| } | |
| for i, key := range keys { | |
| if !keysEqual(key, expectedKeys[i]) { | |
| t.Errorf("dst type %T: got key #%d %v, want %v", tc.dst, i, key, expectedKeys[i]) | |
| continue | |
| } | |
| } | |
| // Make sure we sort any PropertyList items (the order is not deterministic). | |
| if pLists, ok := tc.dst.(*[]PropertyList); ok { | |
| for _, p := range *pLists { | |
| sort.Sort(byName(p)) | |
| } | |
| } | |
| if !testutil.Equal(tc.dst, tc.want) { | |
| t.Errorf("dst type %T: Entities\ngot %+v\nwant %+v", tc.dst, tc.dst, tc.want) | |
| continue | |
| } | |
| } | |
| } | |
| // keysEqual is like (*Key).Equal, but ignores the App ID. | |
| func keysEqual(a, b *Key) bool { | |
| for a != nil && b != nil { | |
| if a.Kind != b.Kind || a.Name != b.Name || a.ID != b.ID { | |
| return false | |
| } | |
| a, b = a.Parent, b.Parent | |
| } | |
| return a == b | |
| } | |
| func TestQueriesAreImmutable(t *testing.T) { | |
| // Test that deriving q2 from q1 does not modify q1. | |
| q0 := NewQuery("foo") | |
| q1 := NewQuery("foo") | |
| q2 := q1.Offset(2) | |
| if !testutil.Equal(q0, q1, cmp.AllowUnexported(Query{})) { | |
| t.Errorf("q0 and q1 were not equal") | |
| } | |
| if testutil.Equal(q1, q2, cmp.AllowUnexported(Query{})) { | |
| t.Errorf("q1 and q2 were equal") | |
| } | |
| // Test that deriving from q4 twice does not conflict, even though | |
| // q4 has a long list of order clauses. This tests that the arrays | |
| // backed by a query's slice of orders are not shared. | |
| f := func() *Query { | |
| q := NewQuery("bar") | |
| // 47 is an ugly number that is unlikely to be near a re-allocation | |
| // point in repeated append calls. For example, it's not near a power | |
| // of 2 or a multiple of 10. | |
| for i := 0; i < 47; i++ { | |
| q = q.Order(fmt.Sprintf("x%d", i)) | |
| } | |
| return q | |
| } | |
| q3 := f().Order("y") | |
| q4 := f() | |
| q5 := q4.Order("y") | |
| q6 := q4.Order("z") | |
| if !testutil.Equal(q3, q5, cmp.AllowUnexported(Query{})) { | |
| t.Errorf("q3 and q5 were not equal") | |
| } | |
| if testutil.Equal(q5, q6, cmp.AllowUnexported(Query{})) { | |
| t.Errorf("q5 and q6 were equal") | |
| } | |
| } | |
| func TestFilterParser(t *testing.T) { | |
| testCases := []struct { | |
| filterStr string | |
| wantOK bool | |
| wantFieldName string | |
| wantOp operator | |
| }{ | |
| // Supported ops. | |
| {"x<", true, "x", lessThan}, | |
| {"x <", true, "x", lessThan}, | |
| {"x <", true, "x", lessThan}, | |
| {" x < ", true, "x", lessThan}, | |
| {"x <=", true, "x", lessEq}, | |
| {"x =", true, "x", equal}, | |
| {"x >=", true, "x", greaterEq}, | |
| {"x >", true, "x", greaterThan}, | |
| {"in >", true, "in", greaterThan}, | |
| {"in>", true, "in", greaterThan}, | |
| // Valid but (currently) unsupported ops. | |
| {"x!=", false, "", 0}, | |
| {"x !=", false, "", 0}, | |
| {" x != ", false, "", 0}, | |
| {"x IN", false, "", 0}, | |
| {"x in", false, "", 0}, | |
| // Invalid ops. | |
| {"x EQ", false, "", 0}, | |
| {"x lt", false, "", 0}, | |
| {"x <>", false, "", 0}, | |
| {"x >>", false, "", 0}, | |
| {"x ==", false, "", 0}, | |
| {"x =<", false, "", 0}, | |
| {"x =>", false, "", 0}, | |
| {"x !", false, "", 0}, | |
| {"x ", false, "", 0}, | |
| {"x", false, "", 0}, | |
| // Quoted and interesting field names. | |
| {"x > y =", true, "x > y", equal}, | |
| {"` x ` =", true, " x ", equal}, | |
| {`" x " =`, true, " x ", equal}, | |
| {`" \"x " =`, true, ` "x `, equal}, | |
| {`" x =`, false, "", 0}, | |
| {`" x ="`, false, "", 0}, | |
| {"` x \" =", false, "", 0}, | |
| } | |
| for _, tc := range testCases { | |
| q := NewQuery("foo").Filter(tc.filterStr, 42) | |
| if ok := q.err == nil; ok != tc.wantOK { | |
| t.Errorf("%q: ok=%t, want %t", tc.filterStr, ok, tc.wantOK) | |
| continue | |
| } | |
| if !tc.wantOK { | |
| continue | |
| } | |
| if len(q.filter) != 1 { | |
| t.Errorf("%q: len=%d, want %d", tc.filterStr, len(q.filter), 1) | |
| continue | |
| } | |
| got, want := q.filter[0], filter{tc.wantFieldName, tc.wantOp, 42} | |
| if got != want { | |
| t.Errorf("%q: got %v, want %v", tc.filterStr, got, want) | |
| continue | |
| } | |
| } | |
| } | |
| func TestNamespaceQuery(t *testing.T) { | |
| gotNamespace := make(chan string, 1) | |
| ctx := context.Background() | |
| client := &Client{ | |
| client: &fakeClient{ | |
| queryFn: func(req *pb.RunQueryRequest) (*pb.RunQueryResponse, error) { | |
| if part := req.PartitionId; part != nil { | |
| gotNamespace <- part.NamespaceId | |
| } else { | |
| gotNamespace <- "" | |
| } | |
| return nil, errors.New("not implemented") | |
| }, | |
| }, | |
| } | |
| var gs []Gopher | |
| // Ignore errors for the rest of this test. | |
| client.GetAll(ctx, NewQuery("gopher"), &gs) | |
| if got, want := <-gotNamespace, ""; got != want { | |
| t.Errorf("GetAll: got namespace %q, want %q", got, want) | |
| } | |
| client.Count(ctx, NewQuery("gopher")) | |
| if got, want := <-gotNamespace, ""; got != want { | |
| t.Errorf("Count: got namespace %q, want %q", got, want) | |
| } | |
| const ns = "not_default" | |
| client.GetAll(ctx, NewQuery("gopher").Namespace(ns), &gs) | |
| if got, want := <-gotNamespace, ns; got != want { | |
| t.Errorf("GetAll: got namespace %q, want %q", got, want) | |
| } | |
| client.Count(ctx, NewQuery("gopher").Namespace(ns)) | |
| if got, want := <-gotNamespace, ns; got != want { | |
| t.Errorf("Count: got namespace %q, want %q", got, want) | |
| } | |
| } | |
| func TestReadOptions(t *testing.T) { | |
| tid := []byte{1} | |
| for _, test := range []struct { | |
| q *Query | |
| want *pb.ReadOptions | |
| }{ | |
| { | |
| q: NewQuery(""), | |
| want: nil, | |
| }, | |
| { | |
| q: NewQuery("").Transaction(nil), | |
| want: nil, | |
| }, | |
| { | |
| q: NewQuery("").Transaction(&Transaction{id: tid}), | |
| want: &pb.ReadOptions{ | |
| ConsistencyType: &pb.ReadOptions_Transaction{ | |
| Transaction: tid, | |
| }, | |
| }, | |
| }, | |
| { | |
| q: NewQuery("").EventualConsistency(), | |
| want: &pb.ReadOptions{ | |
| ConsistencyType: &pb.ReadOptions_ReadConsistency_{ | |
| ReadConsistency: pb.ReadOptions_EVENTUAL, | |
| }, | |
| }, | |
| }, | |
| } { | |
| req := &pb.RunQueryRequest{} | |
| if err := test.q.toProto(req); err != nil { | |
| t.Fatalf("%+v: got %v, want no error", test.q, err) | |
| } | |
| if got := req.ReadOptions; !proto.Equal(got, test.want) { | |
| t.Errorf("%+v:\ngot %+v\nwant %+v", test.q, got, test.want) | |
| } | |
| } | |
| // Test errors. | |
| for _, q := range []*Query{ | |
| NewQuery("").Transaction(&Transaction{id: nil}), | |
| NewQuery("").Transaction(&Transaction{id: tid}).EventualConsistency(), | |
| } { | |
| req := &pb.RunQueryRequest{} | |
| if err := q.toProto(req); err == nil { | |
| t.Errorf("%+v: got nil, wanted error", q) | |
| } | |
| } | |
| } | |
| func TestInvalidFilters(t *testing.T) { | |
| client := &Client{ | |
| client: &fakeClient{ | |
| queryFn: func(req *pb.RunQueryRequest) (*pb.RunQueryResponse, error) { | |
| return fakeRunQuery(req) | |
| }, | |
| }, | |
| } | |
| // Used for an invalid type | |
| type MyType int | |
| var v MyType = 1 | |
| for _, q := range []*Query{ | |
| NewQuery("SomeKey").Filter("", 0), | |
| NewQuery("SomeKey").Filter("fld=", v), | |
| } { | |
| if _, err := client.Count(context.Background(), q); err == nil { | |
| t.Errorf("%+v: got nil, wanted error", q) | |
| } | |
| } | |
| } |