| // Copyright 2011 Google Inc. All Rights Reserved. |
| // Use of this source code is governed by the Apache 2.0 |
| // license that can be found in the LICENSE file. |
| |
| package datastore |
| |
| import ( |
| "errors" |
| "fmt" |
| "reflect" |
| "strings" |
| "testing" |
| |
| "github.com/golang/protobuf/proto" |
| |
| "google.golang.org/appengine/internal" |
| "google.golang.org/appengine/internal/aetesting" |
| pb "google.golang.org/appengine/internal/datastore" |
| ) |
| |
| var ( |
| path1 = &pb.Path{ |
| Element: []*pb.Path_Element{ |
| { |
| Type: proto.String("Gopher"), |
| Id: proto.Int64(6), |
| }, |
| }, |
| } |
| path2 = &pb.Path{ |
| Element: []*pb.Path_Element{ |
| { |
| Type: proto.String("Gopher"), |
| Id: proto.Int64(6), |
| }, |
| { |
| Type: proto.String("Gopher"), |
| Id: proto.Int64(8), |
| }, |
| }, |
| } |
| ) |
| |
| func fakeRunQuery(in *pb.Query, out *pb.QueryResult) error { |
| expectedIn := &pb.Query{ |
| App: proto.String("dev~fake-app"), |
| Kind: proto.String("Gopher"), |
| Compile: proto.Bool(true), |
| } |
| if !proto.Equal(in, expectedIn) { |
| return fmt.Errorf("unsupported argument: got %v want %v", in, expectedIn) |
| } |
| *out = pb.QueryResult{ |
| Result: []*pb.EntityProto{ |
| { |
| Key: &pb.Reference{ |
| App: proto.String("s~test-app"), |
| Path: path1, |
| }, |
| EntityGroup: path1, |
| Property: []*pb.Property{ |
| { |
| Meaning: pb.Property_TEXT.Enum(), |
| Name: proto.String("Name"), |
| Value: &pb.PropertyValue{ |
| StringValue: proto.String("George"), |
| }, |
| }, |
| { |
| Name: proto.String("Height"), |
| Value: &pb.PropertyValue{ |
| Int64Value: proto.Int64(32), |
| }, |
| }, |
| }, |
| }, |
| { |
| Key: &pb.Reference{ |
| App: proto.String("s~test-app"), |
| Path: path2, |
| }, |
| EntityGroup: path1, // ancestor is George |
| Property: []*pb.Property{ |
| { |
| Meaning: pb.Property_TEXT.Enum(), |
| Name: proto.String("Name"), |
| Value: &pb.PropertyValue{ |
| StringValue: proto.String("Rufus"), |
| }, |
| }, |
| // No height for Rufus. |
| }, |
| }, |
| }, |
| MoreResults: proto.Bool(false), |
| } |
| return 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 { |
| if p.Multiple { |
| return errors.New("PropertyMap does not support multiple properties") |
| } |
| m[p.Name] = p |
| } |
| return nil |
| } |
| |
| func (m PropertyMap) Save() ([]Property, error) { |
| props := make([]Property, 0, len(m)) |
| for _, p := range m { |
| if p.Multiple { |
| return nil, errors.New("PropertyMap does not support multiple properties") |
| } |
| 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: "Name", |
| Value: "George", |
| }, |
| { |
| Name: "Height", |
| Value: int64(32), |
| }, |
| } |
| 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 |
| c := aetesting.FakeSingleContext(t, "datastore_v3", "RunQuery", func(in *pb.Query, out *pb.QueryResult) error { |
| nCall++ |
| return fakeRunQuery(in, out) |
| }) |
| c = internal.WithAppIDOverride(c, "dev~fake-app") |
| |
| var ( |
| expectedErr error |
| expectedNCall int |
| ) |
| if tc.want == nil { |
| expectedErr = ErrInvalidEntityType |
| } else { |
| expectedNCall = 1 |
| } |
| keys, err := NewQuery("Gopher").GetAll(c, 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 := NewKey(c, "Gopher", "", 6, nil) |
| expectedKeys := []*Key{ |
| key1, |
| NewKey(c, "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 key.AppID() != "s~test-app" { |
| t.Errorf(`dst type %T: Key #%d's AppID = %q, want "s~test-app"`, tc.dst, i, key.AppID()) |
| continue |
| } |
| if !keysEqual(key, expectedKeys[i]) { |
| t.Errorf("dst type %T: got key #%d %v, want %v", tc.dst, i, key, expectedKeys[i]) |
| continue |
| } |
| } |
| |
| if !reflect.DeepEqual(tc.dst, tc.want) { |
| t.Errorf("dst type %T: Entities got %+v, want %+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.StringID() != b.StringID() || a.IntID() != b.IntID() { |
| 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 !reflect.DeepEqual(q0, q1) { |
| t.Errorf("q0 and q1 were not equal") |
| } |
| if reflect.DeepEqual(q1, q2) { |
| 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 !reflect.DeepEqual(q3, q5) { |
| t.Errorf("q3 and q5 were not equal") |
| } |
| if reflect.DeepEqual(q5, q6) { |
| 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}, |
| } |
| 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 TestQueryToProto(t *testing.T) { |
| // The context is required to make Keys for the test cases. |
| var got *pb.Query |
| NoErr := errors.New("No error") |
| c := aetesting.FakeSingleContext(t, "datastore_v3", "RunQuery", func(in *pb.Query, out *pb.QueryResult) error { |
| got = in |
| return NoErr // return a non-nil error so Run doesn't keep going. |
| }) |
| c = internal.WithAppIDOverride(c, "dev~fake-app") |
| |
| testCases := []struct { |
| desc string |
| query *Query |
| want *pb.Query |
| err string |
| }{ |
| { |
| desc: "empty", |
| query: NewQuery(""), |
| want: &pb.Query{}, |
| }, |
| { |
| desc: "standard query", |
| query: NewQuery("kind").Order("-I").Filter("I >", 17).Filter("U =", "Dave").Limit(7).Offset(42), |
| want: &pb.Query{ |
| Kind: proto.String("kind"), |
| Filter: []*pb.Query_Filter{ |
| { |
| Op: pb.Query_Filter_GREATER_THAN.Enum(), |
| Property: []*pb.Property{ |
| { |
| Name: proto.String("I"), |
| Value: &pb.PropertyValue{Int64Value: proto.Int64(17)}, |
| Multiple: proto.Bool(false), |
| }, |
| }, |
| }, |
| { |
| Op: pb.Query_Filter_EQUAL.Enum(), |
| Property: []*pb.Property{ |
| { |
| Name: proto.String("U"), |
| Value: &pb.PropertyValue{StringValue: proto.String("Dave")}, |
| Multiple: proto.Bool(false), |
| }, |
| }, |
| }, |
| }, |
| Order: []*pb.Query_Order{ |
| { |
| Property: proto.String("I"), |
| Direction: pb.Query_Order_DESCENDING.Enum(), |
| }, |
| }, |
| Limit: proto.Int32(7), |
| Offset: proto.Int32(42), |
| }, |
| }, |
| { |
| desc: "ancestor", |
| query: NewQuery("").Ancestor(NewKey(c, "kind", "Mummy", 0, nil)), |
| want: &pb.Query{ |
| Ancestor: &pb.Reference{ |
| App: proto.String("dev~fake-app"), |
| Path: &pb.Path{ |
| Element: []*pb.Path_Element{{Type: proto.String("kind"), Name: proto.String("Mummy")}}, |
| }, |
| }, |
| }, |
| }, |
| { |
| desc: "projection", |
| query: NewQuery("").Project("A", "B"), |
| want: &pb.Query{ |
| PropertyName: []string{"A", "B"}, |
| }, |
| }, |
| { |
| desc: "projection with distinct", |
| query: NewQuery("").Project("A", "B").Distinct(), |
| want: &pb.Query{ |
| PropertyName: []string{"A", "B"}, |
| GroupByPropertyName: []string{"A", "B"}, |
| }, |
| }, |
| { |
| desc: "keys only", |
| query: NewQuery("").KeysOnly(), |
| want: &pb.Query{ |
| KeysOnly: proto.Bool(true), |
| RequirePerfectPlan: proto.Bool(true), |
| }, |
| }, |
| { |
| desc: "empty filter", |
| query: NewQuery("kind").Filter("=", 17), |
| err: "empty query filter field nam", |
| }, |
| { |
| desc: "bad filter type", |
| query: NewQuery("kind").Filter("M =", map[string]bool{}), |
| err: "bad query filter value type", |
| }, |
| { |
| desc: "bad filter operator", |
| query: NewQuery("kind").Filter("I <<=", 17), |
| err: `invalid operator "<<=" in filter "I <<="`, |
| }, |
| { |
| desc: "empty order", |
| query: NewQuery("kind").Order(""), |
| err: "empty order", |
| }, |
| { |
| desc: "bad order direction", |
| query: NewQuery("kind").Order("+I"), |
| err: `invalid order: "+I`, |
| }, |
| } |
| |
| for _, tt := range testCases { |
| got = nil |
| if _, err := tt.query.Run(c).Next(nil); err != NoErr { |
| if tt.err == "" || !strings.Contains(err.Error(), tt.err) { |
| t.Errorf("%s: error %v, want %q", tt.desc, err, tt.err) |
| } |
| continue |
| } |
| if tt.err != "" { |
| t.Errorf("%s: no error, want %q", tt.desc, tt.err) |
| continue |
| } |
| // Fields that are common to all protos. |
| tt.want.App = proto.String("dev~fake-app") |
| tt.want.Compile = proto.Bool(true) |
| if !proto.Equal(got, tt.want) { |
| t.Errorf("%s:\ngot %v\nwant %v", tt.desc, got, tt.want) |
| } |
| } |
| } |