| // 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) |
| } |
| } |
| } |