// Copyright 2014 Google Inc. All Rights Reserved. // // 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 ( "encoding/json" "errors" "fmt" "reflect" "sort" "strings" "testing" "time" "cloud.google.com/go/internal/testutil" "github.com/golang/protobuf/proto" "github.com/google/go-cmp/cmp" "golang.org/x/net/context" pb "google.golang.org/genproto/googleapis/datastore/v1" "google.golang.org/grpc" ) type ( myBlob []byte myByte byte myString string ) func makeMyByteSlice(n int) []myByte { b := make([]myByte, n) for i := range b { b[i] = myByte(i) } return b } func makeInt8Slice(n int) []int8 { b := make([]int8, n) for i := range b { b[i] = int8(i) } return b } func makeUint8Slice(n int) []uint8 { b := make([]uint8, n) for i := range b { b[i] = uint8(i) } return b } func newKey(stringID string, parent *Key) *Key { return NameKey("kind", stringID, parent) } var ( testKey0 = newKey("name0", nil) testKey1a = newKey("name1", nil) testKey1b = newKey("name1", nil) testKey2a = newKey("name2", testKey0) testKey2b = newKey("name2", testKey0) testGeoPt0 = GeoPoint{Lat: 1.2, Lng: 3.4} testGeoPt1 = GeoPoint{Lat: 5, Lng: 10} testBadGeoPt = GeoPoint{Lat: 1000, Lng: 34} ts = time.Unix(1e9, 0).UTC() ) type B0 struct { B []byte `datastore:",noindex"` } type B1 struct { B []int8 } type B2 struct { B myBlob `datastore:",noindex"` } type B3 struct { B []myByte `datastore:",noindex"` } type B4 struct { B [][]byte } type C0 struct { I int C chan int } type C1 struct { I int C *chan int } type C2 struct { I int C []chan int } type C3 struct { C string } type c4 struct { C string } type E struct{} type G0 struct { G GeoPoint } type G1 struct { G []GeoPoint } type K0 struct { K *Key } type K1 struct { K []*Key } type S struct { St string } type NoOmit struct { A string B int `datastore:"Bb"` C bool `datastore:",noindex"` } type OmitAll struct { A string `datastore:",omitempty"` B int `datastore:"Bb,omitempty"` C bool `datastore:",omitempty,noindex"` F []int `datastore:",omitempty"` } type Omit struct { A string `datastore:",omitempty"` B int `datastore:"Bb,omitempty"` C bool `datastore:",omitempty,noindex"` F []int `datastore:",omitempty"` S `datastore:",omitempty"` } type NoOmits struct { No []NoOmit `datastore:",omitempty"` S `datastore:",omitempty"` Ss S `datastore:",omitempty"` } type N0 struct { X0 Nonymous X0 Ignore string `datastore:"-"` Other string } type N1 struct { X0 Nonymous []X0 Ignore string `datastore:"-"` Other string } type N2 struct { N1 `datastore:"red"` Green N1 `datastore:"green"` Blue N1 White N1 `datastore:"-"` } type N3 struct { C3 `datastore:"red"` } type N4 struct { c4 } type N5 struct { c4 `datastore:"red"` } type O0 struct { I int64 } type O1 struct { I int32 } type U0 struct { U uint } type U1 struct { U string } type T struct { T time.Time } type X0 struct { S string I int i int } type X1 struct { S myString I int32 J int64 } type X2 struct { Z string i int } type X3 struct { S bool I int } type Y0 struct { B bool F []float64 G []float64 } type Y1 struct { B bool F float64 } type Y2 struct { B bool F []int64 } type Pointers struct { Pi *int Ps *string Pb *bool Pf *float64 Pg *GeoPoint Pt *time.Time } type PointersOmitEmpty struct { Pi *int `datastore:",omitempty"` Ps *string `datastore:",omitempty"` Pb *bool `datastore:",omitempty"` Pf *float64 `datastore:",omitempty"` Pg *GeoPoint `datastore:",omitempty"` Pt *time.Time `datastore:",omitempty"` } func populatedPointers() *Pointers { var ( i int s string b bool f float64 g GeoPoint t time.Time ) return &Pointers{ Pi: &i, Ps: &s, Pb: &b, Pf: &f, Pg: &g, Pt: &t, } } type Tagged struct { A int `datastore:"a,noindex"` B []int `datastore:"b"` C int `datastore:",noindex"` D int `datastore:""` E int I int `datastore:"-"` J int `datastore:",noindex" json:"j"` Y0 `datastore:"-"` Z chan int `datastore:"-"` } type InvalidTagged1 struct { I int `datastore:"\t"` } type InvalidTagged2 struct { I int J int `datastore:"I"` } type InvalidTagged3 struct { X string `datastore:"-,noindex"` } type InvalidTagged4 struct { X string `datastore:",garbage"` } type Inner1 struct { W int32 X string } type Inner2 struct { Y float64 } type Inner3 struct { Z bool } type Inner5 struct { WW int } type Inner4 struct { X Inner5 } type Outer struct { A int16 I []Inner1 J Inner2 Inner3 } type OuterFlatten struct { A int16 I []Inner1 `datastore:",flatten"` J Inner2 `datastore:",flatten,noindex"` Inner3 `datastore:",flatten"` K Inner4 `datastore:",flatten"` } type OuterEquivalent struct { A int16 IDotW []int32 `datastore:"I.W"` IDotX []string `datastore:"I.X"` JDotY float64 `datastore:"J.Y"` Z bool } type Dotted struct { A DottedA `datastore:"A0.A1.A2"` } type DottedA struct { B DottedB `datastore:"B3"` } type DottedB struct { C int `datastore:"C4.C5"` } type SliceOfSlices struct { I int S []struct { J int F []float64 } `datastore:",flatten"` } type Recursive struct { I int R []Recursive } type MutuallyRecursive0 struct { I int R []MutuallyRecursive1 } type MutuallyRecursive1 struct { I int R []MutuallyRecursive0 } type EntityWithKey struct { I int S string K *Key `datastore:"__key__"` } type EntityWithKey2 EntityWithKey type WithNestedEntityWithKey struct { N EntityWithKey } type WithNonKeyField struct { I int K string `datastore:"__key__"` } type NestedWithNonKeyField struct { N WithNonKeyField } type Basic struct { A string } type PtrToStructField struct { B *Basic C *Basic `datastore:"c,noindex"` *Basic D []*Basic } var two int = 2 type EmbeddedTime struct { time.Time } type SpecialTime struct { MyTime EmbeddedTime } type Doubler struct { S string I int64 B bool } type Repeat struct { Key string Value []byte } type Repeated struct { Repeats []Repeat } func (d *Doubler) Load(props []Property) error { return LoadStruct(d, props) } func (d *Doubler) Save() ([]Property, error) { // Save the default Property slice to an in-memory buffer (a PropertyList). props, err := SaveStruct(d) if err != nil { return nil, err } var list PropertyList if err := list.Load(props); err != nil { return nil, err } // Edit that PropertyList, and send it on. for i := range list { switch v := list[i].Value.(type) { case string: // + means string concatenation. list[i].Value = v + v case int64: // + means integer addition. list[i].Value = v + v } } return list.Save() } var _ PropertyLoadSaver = (*Doubler)(nil) type Deriver struct { S, Derived, Ignored string } func (e *Deriver) Load(props []Property) error { for _, p := range props { if p.Name != "S" { continue } e.S = p.Value.(string) e.Derived = "derived+" + e.S } return nil } func (e *Deriver) Save() ([]Property, error) { return []Property{ { Name: "S", Value: e.S, }, }, nil } var _ PropertyLoadSaver = (*Deriver)(nil) type BadMultiPropEntity struct{} func (e *BadMultiPropEntity) Load(props []Property) error { return errors.New("unimplemented") } func (e *BadMultiPropEntity) Save() ([]Property, error) { // Write multiple properties with the same name "I". var props []Property for i := 0; i < 3; i++ { props = append(props, Property{ Name: "I", Value: int64(i), }) } return props, nil } var _ PropertyLoadSaver = (*BadMultiPropEntity)(nil) type testCase struct { desc string src interface{} want interface{} putErr string getErr string } var testCases = []testCase{ { "chan save fails", &C0{I: -1}, &E{}, "unsupported struct field", "", }, { "*chan save fails", &C1{I: -1}, &E{}, "unsupported struct field", "", }, { "[]chan save fails", &C2{I: -1, C: make([]chan int, 8)}, &E{}, "unsupported struct field", "", }, { "chan load fails", &C3{C: "not a chan"}, &C0{}, "", "type mismatch", }, { "*chan load fails", &C3{C: "not a *chan"}, &C1{}, "", "type mismatch", }, { "[]chan load fails", &C3{C: "not a []chan"}, &C2{}, "", "type mismatch", }, { "empty struct", &E{}, &E{}, "", "", }, { "geopoint", &G0{G: testGeoPt0}, &G0{G: testGeoPt0}, "", "", }, { "geopoint invalid", &G0{G: testBadGeoPt}, &G0{}, "invalid GeoPoint value", "", }, { "geopoint as props", &G0{G: testGeoPt0}, &PropertyList{ Property{Name: "G", Value: testGeoPt0, NoIndex: false}, }, "", "", }, { "geopoint slice", &G1{G: []GeoPoint{testGeoPt0, testGeoPt1}}, &G1{G: []GeoPoint{testGeoPt0, testGeoPt1}}, "", "", }, { "omit empty, all", &OmitAll{}, new(PropertyList), "", "", }, { "omit empty", &Omit{}, &PropertyList{ Property{Name: "St", Value: "", NoIndex: false}, }, "", "", }, { "omit empty, fields populated", &Omit{ A: "a", B: 10, C: true, F: []int{11}, }, &PropertyList{ Property{Name: "A", Value: "a", NoIndex: false}, Property{Name: "Bb", Value: int64(10), NoIndex: false}, Property{Name: "C", Value: true, NoIndex: true}, Property{Name: "F", Value: []interface{}{int64(11)}, NoIndex: false}, Property{Name: "St", Value: "", NoIndex: false}, }, "", "", }, { "omit empty, fields populated", &Omit{ A: "a", B: 10, C: true, F: []int{11}, S: S{St: "string"}, }, &PropertyList{ Property{Name: "A", Value: "a", NoIndex: false}, Property{Name: "Bb", Value: int64(10), NoIndex: false}, Property{Name: "C", Value: true, NoIndex: true}, Property{Name: "F", Value: []interface{}{int64(11)}, NoIndex: false}, Property{Name: "St", Value: "string", NoIndex: false}, }, "", "", }, { "omit empty does not propagate", &NoOmits{ No: []NoOmit{ NoOmit{}, }, S: S{}, Ss: S{}, }, &PropertyList{ Property{Name: "No", Value: []interface{}{ &Entity{ Properties: []Property{ Property{Name: "A", Value: "", NoIndex: false}, Property{Name: "Bb", Value: int64(0), NoIndex: false}, Property{Name: "C", Value: false, NoIndex: true}, }, }, }, NoIndex: false}, Property{Name: "Ss", Value: &Entity{ Properties: []Property{ Property{Name: "St", Value: "", NoIndex: false}, }, }, NoIndex: false}, Property{Name: "St", Value: "", NoIndex: false}, }, "", "", }, { "key", &K0{K: testKey1a}, &K0{K: testKey1b}, "", "", }, { "key with parent", &K0{K: testKey2a}, &K0{K: testKey2b}, "", "", }, { "nil key", &K0{}, &K0{}, "", "", }, { "all nil keys in slice", &K1{[]*Key{nil, nil}}, &K1{[]*Key{nil, nil}}, "", "", }, { "some nil keys in slice", &K1{[]*Key{testKey1a, nil, testKey2a}}, &K1{[]*Key{testKey1b, nil, testKey2b}}, "", "", }, { "overflow", &O0{I: 1 << 48}, &O1{}, "", "overflow", }, { "time", &T{T: time.Unix(1e9, 0)}, &T{T: time.Unix(1e9, 0)}, "", "", }, { "time as props", &T{T: time.Unix(1e9, 0)}, &PropertyList{ Property{Name: "T", Value: time.Unix(1e9, 0), NoIndex: false}, }, "", "", }, { "uint save", &U0{U: 1}, &U0{}, "unsupported struct field", "", }, { "uint load", &U1{U: "not a uint"}, &U0{}, "", "type mismatch", }, { "zero", &X0{}, &X0{}, "", "", }, { "basic", &X0{S: "one", I: 2, i: 3}, &X0{S: "one", I: 2}, "", "", }, { "save string/int load myString/int32", &X0{S: "one", I: 2, i: 3}, &X1{S: "one", I: 2}, "", "", }, { "missing fields", &X0{S: "one", I: 2, i: 3}, &X2{}, "", "no such struct field", }, { "save string load bool", &X0{S: "one", I: 2, i: 3}, &X3{I: 2}, "", "type mismatch", }, { "basic slice", &Y0{B: true, F: []float64{7, 8, 9}}, &Y0{B: true, F: []float64{7, 8, 9}}, "", "", }, { "save []float64 load float64", &Y0{B: true, F: []float64{7, 8, 9}}, &Y1{B: true}, "", "requires a slice", }, { "save []float64 load []int64", &Y0{B: true, F: []float64{7, 8, 9}}, &Y2{B: true}, "", "type mismatch", }, { "single slice is too long", &Y0{F: make([]float64, maxIndexedProperties+1)}, &Y0{}, "too many indexed properties", "", }, { "two slices are too long", &Y0{F: make([]float64, maxIndexedProperties), G: make([]float64, maxIndexedProperties)}, &Y0{}, "too many indexed properties", "", }, { "one slice and one scalar are too long", &Y0{F: make([]float64, maxIndexedProperties), B: true}, &Y0{}, "too many indexed properties", "", }, { "slice of slices of bytes", &Repeated{ Repeats: []Repeat{ { Key: "key 1", Value: []byte("value 1"), }, { Key: "key 2", Value: []byte("value 2"), }, }, }, &Repeated{ Repeats: []Repeat{ { Key: "key 1", Value: []byte("value 1"), }, { Key: "key 2", Value: []byte("value 2"), }, }, }, "", "", }, { "long blob", &B0{B: makeUint8Slice(maxIndexedProperties + 1)}, &B0{B: makeUint8Slice(maxIndexedProperties + 1)}, "", "", }, { "long []int8 is too long", &B1{B: makeInt8Slice(maxIndexedProperties + 1)}, &B1{}, "too many indexed properties", "", }, { "short []int8", &B1{B: makeInt8Slice(3)}, &B1{B: makeInt8Slice(3)}, "", "", }, { "long myBlob", &B2{B: makeUint8Slice(maxIndexedProperties + 1)}, &B2{B: makeUint8Slice(maxIndexedProperties + 1)}, "", "", }, { "short myBlob", &B2{B: makeUint8Slice(3)}, &B2{B: makeUint8Slice(3)}, "", "", }, { "long []myByte", &B3{B: makeMyByteSlice(maxIndexedProperties + 1)}, &B3{B: makeMyByteSlice(maxIndexedProperties + 1)}, "", "", }, { "short []myByte", &B3{B: makeMyByteSlice(3)}, &B3{B: makeMyByteSlice(3)}, "", "", }, { "slice of blobs", &B4{B: [][]byte{ makeUint8Slice(3), makeUint8Slice(4), makeUint8Slice(5), }}, &B4{B: [][]byte{ makeUint8Slice(3), makeUint8Slice(4), makeUint8Slice(5), }}, "", "", }, { "[]byte must be noindex", &PropertyList{ Property{Name: "B", Value: makeUint8Slice(1501), NoIndex: false}, }, nil, "[]byte property too long to index", "", }, { "string must be noindex", &PropertyList{ Property{Name: "B", Value: strings.Repeat("x", 1501), NoIndex: false}, }, nil, "string property too long to index", "", }, { "slice of []byte must be noindex", &PropertyList{ Property{Name: "B", Value: []interface{}{ []byte("short"), makeUint8Slice(1501), }, NoIndex: false}, }, nil, "[]byte property too long to index", "", }, { "slice of string must be noindex", &PropertyList{ Property{Name: "B", Value: []interface{}{ "short", strings.Repeat("x", 1501), }, NoIndex: false}, }, nil, "string property too long to index", "", }, { "save tagged load props", &Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7}, &PropertyList{ // A and B are renamed to a and b; A and C are noindex, I is ignored. // Order is sorted as per byName. Property{Name: "C", Value: int64(3), NoIndex: true}, Property{Name: "D", Value: int64(4), NoIndex: false}, Property{Name: "E", Value: int64(5), NoIndex: false}, Property{Name: "J", Value: int64(7), NoIndex: true}, Property{Name: "a", Value: int64(1), NoIndex: true}, Property{Name: "b", Value: []interface{}{int64(21), int64(22), int64(23)}, NoIndex: false}, }, "", "", }, { "save tagged load tagged", &Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, I: 6, J: 7}, &Tagged{A: 1, B: []int{21, 22, 23}, C: 3, D: 4, E: 5, J: 7}, "", "", }, { "invalid tagged1", &InvalidTagged1{I: 1}, &InvalidTagged1{}, "struct tag has invalid property name", "", }, { "invalid tagged2", &InvalidTagged2{I: 1, J: 2}, &InvalidTagged2{J: 2}, "", "", }, { "invalid tagged3", &InvalidTagged3{X: "hello"}, &InvalidTagged3{}, "struct tag has invalid property name: \"-\"", "", }, { "invalid tagged4", &InvalidTagged4{X: "hello"}, &InvalidTagged4{}, "struct tag has invalid option: \"garbage\"", "", }, { "doubler", &Doubler{S: "s", I: 1, B: true}, &Doubler{S: "ss", I: 2, B: true}, "", "", }, { "save struct load props", &X0{S: "s", I: 1}, &PropertyList{ Property{Name: "I", Value: int64(1), NoIndex: false}, Property{Name: "S", Value: "s", NoIndex: false}, }, "", "", }, { "save props load struct", &PropertyList{ Property{Name: "I", Value: int64(1), NoIndex: false}, Property{Name: "S", Value: "s", NoIndex: false}, }, &X0{S: "s", I: 1}, "", "", }, { "nil-value props", &PropertyList{ Property{Name: "I", Value: nil, NoIndex: false}, Property{Name: "B", Value: nil, NoIndex: false}, Property{Name: "S", Value: nil, NoIndex: false}, Property{Name: "F", Value: nil, NoIndex: false}, Property{Name: "K", Value: nil, NoIndex: false}, Property{Name: "T", Value: nil, NoIndex: false}, Property{Name: "J", Value: []interface{}{nil, int64(7), nil}, NoIndex: false}, }, &struct { I int64 B bool S string F float64 K *Key T time.Time J []int64 }{ J: []int64{0, 7, 0}, }, "", "", }, { "save outer load props flatten", &OuterFlatten{ A: 1, I: []Inner1{ {10, "ten"}, {20, "twenty"}, {30, "thirty"}, }, J: Inner2{ Y: 3.14, }, Inner3: Inner3{ Z: true, }, K: Inner4{ X: Inner5{ WW: 12, }, }, }, &PropertyList{ Property{Name: "A", Value: int64(1), NoIndex: false}, Property{Name: "I.W", Value: []interface{}{int64(10), int64(20), int64(30)}, NoIndex: false}, Property{Name: "I.X", Value: []interface{}{"ten", "twenty", "thirty"}, NoIndex: false}, Property{Name: "J.Y", Value: float64(3.14), NoIndex: true}, Property{Name: "K.X.WW", Value: int64(12), NoIndex: false}, Property{Name: "Z", Value: true, NoIndex: false}, }, "", "", }, { "load outer props flatten", &PropertyList{ Property{Name: "A", Value: int64(1), NoIndex: false}, Property{Name: "I.W", Value: []interface{}{int64(10), int64(20), int64(30)}, NoIndex: false}, Property{Name: "I.X", Value: []interface{}{"ten", "twenty", "thirty"}, NoIndex: false}, Property{Name: "J.Y", Value: float64(3.14), NoIndex: true}, Property{Name: "Z", Value: true, NoIndex: false}, }, &OuterFlatten{ A: 1, I: []Inner1{ {10, "ten"}, {20, "twenty"}, {30, "thirty"}, }, J: Inner2{ Y: 3.14, }, Inner3: Inner3{ Z: true, }, }, "", "", }, { "save outer load props", &Outer{ A: 1, I: []Inner1{ {10, "ten"}, {20, "twenty"}, {30, "thirty"}, }, J: Inner2{ Y: 3.14, }, Inner3: Inner3{ Z: true, }, }, &PropertyList{ Property{Name: "A", Value: int64(1), NoIndex: false}, Property{Name: "I", Value: []interface{}{ &Entity{ Properties: []Property{ Property{Name: "W", Value: int64(10), NoIndex: false}, Property{Name: "X", Value: "ten", NoIndex: false}, }, }, &Entity{ Properties: []Property{ Property{Name: "W", Value: int64(20), NoIndex: false}, Property{Name: "X", Value: "twenty", NoIndex: false}, }, }, &Entity{ Properties: []Property{ Property{Name: "W", Value: int64(30), NoIndex: false}, Property{Name: "X", Value: "thirty", NoIndex: false}, }, }, }, NoIndex: false}, Property{Name: "J", Value: &Entity{ Properties: []Property{ Property{Name: "Y", Value: float64(3.14), NoIndex: false}, }, }, NoIndex: false}, Property{Name: "Z", Value: true, NoIndex: false}, }, "", "", }, { "save props load outer-equivalent", &PropertyList{ Property{Name: "A", Value: int64(1), NoIndex: false}, Property{Name: "I.W", Value: []interface{}{int64(10), int64(20), int64(30)}, NoIndex: false}, Property{Name: "I.X", Value: []interface{}{"ten", "twenty", "thirty"}, NoIndex: false}, Property{Name: "J.Y", Value: float64(3.14), NoIndex: false}, Property{Name: "Z", Value: true, NoIndex: false}, }, &OuterEquivalent{ A: 1, IDotW: []int32{10, 20, 30}, IDotX: []string{"ten", "twenty", "thirty"}, JDotY: 3.14, Z: true, }, "", "", }, { "dotted names save", &Dotted{A: DottedA{B: DottedB{C: 88}}}, &PropertyList{ Property{Name: "A0.A1.A2", Value: &Entity{ Properties: []Property{ Property{Name: "B3", Value: &Entity{ Properties: []Property{ Property{Name: "C4.C5", Value: int64(88), NoIndex: false}, }, }, NoIndex: false}, }, }, NoIndex: false}, }, "", "", }, { "dotted names load", &PropertyList{ Property{Name: "A0.A1.A2", Value: &Entity{ Properties: []Property{ Property{Name: "B3", Value: &Entity{ Properties: []Property{ Property{Name: "C4.C5", Value: 99, NoIndex: false}, }, }, NoIndex: false}, }, }, NoIndex: false}, }, &Dotted{A: DottedA{B: DottedB{C: 99}}}, "", "", }, { "save struct load deriver", &X0{S: "s", I: 1}, &Deriver{S: "s", Derived: "derived+s"}, "", "", }, { "save deriver load struct", &Deriver{S: "s", Derived: "derived+s", Ignored: "ignored"}, &X0{S: "s"}, "", "", }, { "zero time.Time", &T{T: time.Time{}}, &T{T: time.Time{}}, "", "", }, { "time.Time near Unix zero time", &T{T: time.Unix(0, 4e3)}, &T{T: time.Unix(0, 4e3)}, "", "", }, { "time.Time, far in the future", &T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)}, &T{T: time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC)}, "", "", }, { "time.Time, very far in the past", &T{T: time.Date(-300000, 1, 1, 0, 0, 0, 0, time.UTC)}, &T{}, "time value out of range", "", }, { "time.Time, very far in the future", &T{T: time.Date(294248, 1, 1, 0, 0, 0, 0, time.UTC)}, &T{}, "time value out of range", "", }, { "structs", &N0{ X0: X0{S: "one", I: 2, i: 3}, Nonymous: X0{S: "four", I: 5, i: 6}, Ignore: "ignore", Other: "other", }, &N0{ X0: X0{S: "one", I: 2}, Nonymous: X0{S: "four", I: 5}, Other: "other", }, "", "", }, { "slice of structs", &N1{ X0: X0{S: "one", I: 2, i: 3}, Nonymous: []X0{ {S: "four", I: 5, i: 6}, {S: "seven", I: 8, i: 9}, {S: "ten", I: 11, i: 12}, {S: "thirteen", I: 14, i: 15}, }, Ignore: "ignore", Other: "other", }, &N1{ X0: X0{S: "one", I: 2}, Nonymous: []X0{ {S: "four", I: 5}, {S: "seven", I: 8}, {S: "ten", I: 11}, {S: "thirteen", I: 14}, }, Other: "other", }, "", "", }, { "structs with slices of structs", &N2{ N1: N1{ X0: X0{S: "rouge"}, Nonymous: []X0{ {S: "rosso0"}, {S: "rosso1"}, }, }, Green: N1{ X0: X0{S: "vert"}, Nonymous: []X0{ {S: "verde0"}, {S: "verde1"}, {S: "verde2"}, }, }, Blue: N1{ X0: X0{S: "bleu"}, Nonymous: []X0{ {S: "blu0"}, {S: "blu1"}, {S: "blu2"}, {S: "blu3"}, }, }, }, &N2{ N1: N1{ X0: X0{S: "rouge"}, Nonymous: []X0{ {S: "rosso0"}, {S: "rosso1"}, }, }, Green: N1{ X0: X0{S: "vert"}, Nonymous: []X0{ {S: "verde0"}, {S: "verde1"}, {S: "verde2"}, }, }, Blue: N1{ X0: X0{S: "bleu"}, Nonymous: []X0{ {S: "blu0"}, {S: "blu1"}, {S: "blu2"}, {S: "blu3"}, }, }, }, "", "", }, { "save structs load props", &N2{ N1: N1{ X0: X0{S: "rouge"}, Nonymous: []X0{ {S: "rosso0"}, {S: "rosso1"}, }, }, Green: N1{ X0: X0{S: "vert"}, Nonymous: []X0{ {S: "verde0"}, {S: "verde1"}, {S: "verde2"}, }, }, Blue: N1{ X0: X0{S: "bleu"}, Nonymous: []X0{ {S: "blu0"}, {S: "blu1"}, {S: "blu2"}, {S: "blu3"}, }, }, }, &PropertyList{ Property{Name: "Blue", Value: &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "Nonymous", Value: []interface{}{ &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "S", Value: "blu0", NoIndex: false}, }, }, &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "S", Value: "blu1", NoIndex: false}, }, }, &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "S", Value: "blu2", NoIndex: false}, }, }, &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "S", Value: "blu3", NoIndex: false}, }, }, }, NoIndex: false}, Property{Name: "Other", Value: "", NoIndex: false}, Property{Name: "S", Value: "bleu", NoIndex: false}, }, }, NoIndex: false}, Property{Name: "green", Value: &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "Nonymous", Value: []interface{}{ &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "S", Value: "verde0", NoIndex: false}, }, }, &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "S", Value: "verde1", NoIndex: false}, }, }, &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "S", Value: "verde2", NoIndex: false}, }, }, }, NoIndex: false}, Property{Name: "Other", Value: "", NoIndex: false}, Property{Name: "S", Value: "vert", NoIndex: false}, }, }, NoIndex: false}, Property{Name: "red", Value: &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "Nonymous", Value: []interface{}{ &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "S", Value: "rosso0", NoIndex: false}, }, }, &Entity{ Properties: []Property{ Property{Name: "I", Value: int64(0), NoIndex: false}, Property{Name: "S", Value: "rosso1", NoIndex: false}, }, }, }, NoIndex: false}, Property{Name: "Other", Value: "", NoIndex: false}, Property{Name: "S", Value: "rouge", NoIndex: false}, }, }, NoIndex: false}, }, "", "", }, { "nested entity with key", &WithNestedEntityWithKey{ N: EntityWithKey{ I: 12, S: "abcd", K: testKey0, }, }, &WithNestedEntityWithKey{ N: EntityWithKey{ I: 12, S: "abcd", K: testKey0, }, }, "", "", }, { "entity with key at top level", &EntityWithKey{ I: 12, S: "abc", K: testKey0, }, &EntityWithKey{ I: 12, S: "abc", K: testKey0, }, "", "", }, { "entity with key at top level (key is populated on load)", &EntityWithKey{ I: 12, S: "abc", }, &EntityWithKey{ I: 12, S: "abc", K: testKey0, }, "", "", }, { "__key__ field not a *Key", &NestedWithNonKeyField{ N: WithNonKeyField{ I: 12, K: "abcd", }, }, &NestedWithNonKeyField{ N: WithNonKeyField{ I: 12, K: "abcd", }, }, "datastore: __key__ field on struct datastore.WithNonKeyField is not a *datastore.Key", "", }, { "save struct with ptr to struct fields", &PtrToStructField{ &Basic{ A: "b", }, &Basic{ A: "c", }, &Basic{ A: "anon", }, []*Basic{ &Basic{ A: "slice0", }, &Basic{ A: "slice1", }, }, }, &PropertyList{ Property{Name: "A", Value: "anon", NoIndex: false}, Property{Name: "B", Value: &Entity{ Properties: []Property{ Property{Name: "A", Value: "b", NoIndex: false}, }, }}, Property{Name: "D", Value: []interface{}{ &Entity{ Properties: []Property{ Property{Name: "A", Value: "slice0", NoIndex: false}, }, }, &Entity{ Properties: []Property{ Property{Name: "A", Value: "slice1", NoIndex: false}, }, }, }, NoIndex: false}, Property{Name: "c", Value: &Entity{ Properties: []Property{ Property{Name: "A", Value: "c", NoIndex: true}, }, }, NoIndex: true}, }, "", "", }, { "save and load struct with ptr to struct fields", &PtrToStructField{ &Basic{ A: "b", }, &Basic{ A: "c", }, &Basic{ A: "anon", }, []*Basic{ &Basic{ A: "slice0", }, &Basic{ A: "slice1", }, }, }, &PtrToStructField{ &Basic{ A: "b", }, &Basic{ A: "c", }, &Basic{ A: "anon", }, []*Basic{ &Basic{ A: "slice0", }, &Basic{ A: "slice1", }, }, }, "", "", }, { "struct with nil ptr to struct fields", &PtrToStructField{ nil, nil, nil, nil, }, new(PropertyList), "", "", }, { "nested load entity with key", &WithNestedEntityWithKey{ N: EntityWithKey{ I: 12, S: "abcd", K: testKey0, }, }, &PropertyList{ Property{Name: "N", Value: &Entity{ Key: testKey0, Properties: []Property{ Property{Name: "I", Value: int64(12), NoIndex: false}, Property{Name: "S", Value: "abcd", NoIndex: false}, }, }, NoIndex: false}, }, "", "", }, { "nested save entity with key", &PropertyList{ Property{Name: "N", Value: &Entity{ Key: testKey0, Properties: []Property{ Property{Name: "I", Value: int64(12), NoIndex: false}, Property{Name: "S", Value: "abcd", NoIndex: false}, }, }, NoIndex: false}, }, &WithNestedEntityWithKey{ N: EntityWithKey{ I: 12, S: "abcd", K: testKey0, }, }, "", "", }, { "anonymous field with tag", &N3{ C3: C3{C: "s"}, }, &PropertyList{ Property{Name: "red", Value: &Entity{ Properties: []Property{ Property{Name: "C", Value: "s", NoIndex: false}, }, }, NoIndex: false}, }, "", "", }, { "unexported anonymous field", &N4{ c4: c4{C: "s"}, }, &PropertyList{ Property{Name: "C", Value: "s", NoIndex: false}, }, "", "", }, { "unexported anonymous field with tag", &N5{ c4: c4{C: "s"}, }, new(PropertyList), "", "", }, { "save props load structs with ragged fields", &PropertyList{ Property{Name: "red.S", Value: "rot", NoIndex: false}, Property{Name: "green.Nonymous.I", Value: []interface{}{int64(10), int64(11), int64(12), int64(13)}, NoIndex: false}, Property{Name: "Blue.Nonymous.I", Value: []interface{}{int64(20), int64(21)}, NoIndex: false}, Property{Name: "Blue.Nonymous.S", Value: []interface{}{"blau0", "blau1", "blau2"}, NoIndex: false}, }, &N2{ N1: N1{ X0: X0{S: "rot"}, }, Green: N1{ Nonymous: []X0{ {I: 10}, {I: 11}, {I: 12}, {I: 13}, }, }, Blue: N1{ Nonymous: []X0{ {S: "blau0", I: 20}, {S: "blau1", I: 21}, {S: "blau2"}, }, }, }, "", "", }, { "save structs with noindex tags", &struct { A struct { X string `datastore:",noindex"` Y string } `datastore:",noindex"` B struct { X string `datastore:",noindex"` Y string } }{}, &PropertyList{ Property{Name: "A", Value: &Entity{ Properties: []Property{ Property{Name: "X", Value: "", NoIndex: true}, Property{Name: "Y", Value: "", NoIndex: true}, }, }, NoIndex: true}, Property{Name: "B", Value: &Entity{ Properties: []Property{ Property{Name: "X", Value: "", NoIndex: true}, Property{Name: "Y", Value: "", NoIndex: false}, }, }, NoIndex: false}, }, "", "", }, { "embedded struct with name override", &struct { Inner1 `datastore:"foo"` }{}, &PropertyList{ Property{Name: "foo", Value: &Entity{ Properties: []Property{ Property{Name: "W", Value: int64(0), NoIndex: false}, Property{Name: "X", Value: "", NoIndex: false}, }, }, NoIndex: false}, }, "", "", }, { "slice of slices", &SliceOfSlices{}, nil, "flattening nested structs leads to a slice of slices", "", }, { "recursive struct", &Recursive{}, &Recursive{}, "", "", }, { "mutually recursive struct", &MutuallyRecursive0{}, &MutuallyRecursive0{}, "", "", }, { "non-exported struct fields", &struct { i, J int64 }{i: 1, J: 2}, &PropertyList{ Property{Name: "J", Value: int64(2), NoIndex: false}, }, "", "", }, { "json.RawMessage", &struct { J json.RawMessage }{ J: json.RawMessage("rawr"), }, &PropertyList{ Property{Name: "J", Value: []byte("rawr"), NoIndex: false}, }, "", "", }, { "json.RawMessage to myBlob", &struct { B json.RawMessage }{ B: json.RawMessage("rawr"), }, &B2{B: myBlob("rawr")}, "", "", }, { "repeated property names", &PropertyList{ Property{Name: "A", Value: ""}, Property{Name: "A", Value: ""}, }, nil, "duplicate Property", "", }, { "embedded time field", &SpecialTime{MyTime: EmbeddedTime{ts}}, &SpecialTime{MyTime: EmbeddedTime{ts}}, "", "", }, { "embedded time load", &PropertyList{ Property{Name: "MyTime.Time", Value: ts}, }, &SpecialTime{MyTime: EmbeddedTime{ts}}, "", "", }, { "pointer fields: nil", &Pointers{}, &Pointers{}, "", "", }, { "pointer fields: populated with zeroes", populatedPointers(), populatedPointers(), "", "", }, } // checkErr returns the empty string if either both want and err are zero, // or if want is a non-empty substring of err's string representation. func checkErr(want string, err error) string { if err != nil { got := err.Error() if want == "" || strings.Index(got, want) == -1 { return got } } else if want != "" { return fmt.Sprintf("want error %q", want) } return "" } func TestRoundTrip(t *testing.T) { for _, tc := range testCases { p, err := saveEntity(testKey0, tc.src) if s := checkErr(tc.putErr, err); s != "" { t.Errorf("%s: save: %s", tc.desc, s) continue } if p == nil { continue } var got interface{} if _, ok := tc.want.(*PropertyList); ok { got = new(PropertyList) } else { got = reflect.New(reflect.TypeOf(tc.want).Elem()).Interface() } err = loadEntityProto(got, p) if s := checkErr(tc.getErr, err); s != "" { t.Errorf("%s: load: %s", tc.desc, s) continue } if pl, ok := got.(*PropertyList); ok { // Sort by name to make sure we have a deterministic order. sortPL(*pl) } if !testutil.Equal(got, tc.want, cmp.AllowUnexported(X0{}, X2{})) { t.Errorf("%s: compare:\ngot: %+#v\nwant: %+#v", tc.desc, got, tc.want) continue } } } type aPtrPLS struct { Count int } func (pls *aPtrPLS) Load([]Property) error { pls.Count += 1 return nil } func (pls *aPtrPLS) Save() ([]Property, error) { return []Property{{Name: "Count", Value: 4}}, nil } type aValuePLS struct { Count int } func (pls aValuePLS) Load([]Property) error { pls.Count += 2 return nil } func (pls aValuePLS) Save() ([]Property, error) { return []Property{{Name: "Count", Value: 8}}, nil } type aValuePtrPLS struct { Count int } func (pls *aValuePtrPLS) Load([]Property) error { pls.Count = 11 return nil } func (pls *aValuePtrPLS) Save() ([]Property, error) { return []Property{{Name: "Count", Value: 12}}, nil } type aNotPLS struct { Count int } type plsString string func (s *plsString) Load([]Property) error { *s = "LOADED" return nil } func (s *plsString) Save() ([]Property, error) { return []Property{{Name: "SS", Value: "SAVED"}}, nil } func ptrToplsString(s string) *plsString { plsStr := plsString(s) return &plsStr } type aSubPLS struct { Foo string Bar *aPtrPLS Baz aValuePtrPLS S plsString } type aSubNotPLS struct { Foo string Bar *aNotPLS } type aSubPLSErr struct { Foo string Bar aValuePLS } type aSubPLSNoErr struct { Foo string Bar aPtrPLS } type GrandparentFlatten struct { Parent Parent `datastore:",flatten"` } type GrandparentOfPtrFlatten struct { Parent ParentOfPtr `datastore:",flatten"` } type GrandparentOfSlice struct { Parent ParentOfSlice } type GrandparentOfSlicePtrs struct { Parent ParentOfSlicePtrs } type GrandparentOfSliceFlatten struct { Parent ParentOfSlice `datastore:",flatten"` } type GrandparentOfSlicePtrsFlatten struct { Parent ParentOfSlicePtrs `datastore:",flatten"` } type Grandparent struct { Parent Parent } type Parent struct { Child Child String plsString } type ParentOfPtr struct { Child *Child String *plsString } type ParentOfSlice struct { Children []Child Strings []plsString } type ParentOfSlicePtrs struct { Children []*Child Strings []*plsString } type Child struct { I int Grandchild Grandchild } type Grandchild struct { S string } func (c *Child) Load(props []Property) error { for _, p := range props { if p.Name == "I" { c.I += 1 } else if p.Name == "Grandchild.S" { c.Grandchild.S = "grandchild loaded" } } return nil } func (c *Child) Save() ([]Property, error) { v := c.I + 1 return []Property{ {Name: "I", Value: v}, {Name: "Grandchild.S", Value: fmt.Sprintf("grandchild saved %d", v)}, }, nil } func TestLoadSavePLS(t *testing.T) { type testCase struct { desc string src interface{} wantSave *pb.Entity wantLoad interface{} saveErr string loadErr string } testCases := []testCase{ { desc: "non-struct implements PLS (top-level)", src: ptrToplsString("hello"), wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, wantLoad: ptrToplsString("LOADED"), }, { desc: "substructs do implement PLS", src: &aSubPLS{Foo: "foo", Bar: &aPtrPLS{Count: 2}, Baz: aValuePtrPLS{Count: 15}, S: "something"}, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Foo": {ValueType: &pb.Value_StringValue{StringValue: "foo"}}, "Bar": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 4}}, }, }, }}, "Baz": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 12}}, }, }, }}, "S": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, }}, }, }, wantLoad: &aSubPLS{Foo: "foo", Bar: &aPtrPLS{Count: 1}, Baz: aValuePtrPLS{Count: 11}, S: "LOADED"}, }, { desc: "substruct (ptr) does implement PLS, nil valued substruct", src: &aSubPLS{Foo: "foo", S: "something"}, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Foo": {ValueType: &pb.Value_StringValue{StringValue: "foo"}}, "Baz": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 12}}, }, }, }}, "S": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, }}, }, }, wantLoad: &aSubPLS{Foo: "foo", Baz: aValuePtrPLS{Count: 11}, S: "LOADED"}, }, { desc: "substruct (ptr) does not implement PLS", src: &aSubNotPLS{Foo: "foo", Bar: &aNotPLS{Count: 2}}, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Foo": {ValueType: &pb.Value_StringValue{StringValue: "foo"}}, "Bar": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}}, }, }, }}, }, }, wantLoad: &aSubNotPLS{Foo: "foo", Bar: &aNotPLS{Count: 2}}, }, { desc: "substruct (value) does implement PLS, error on save", src: &aSubPLSErr{Foo: "foo", Bar: aValuePLS{Count: 2}}, wantSave: (*pb.Entity)(nil), wantLoad: &aSubPLSErr{}, saveErr: "PropertyLoadSaver methods must be implemented on a pointer", }, { desc: "substruct (value) does implement PLS, error on load", src: &aSubPLSNoErr{Foo: "foo", Bar: aPtrPLS{Count: 2}}, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Foo": {ValueType: &pb.Value_StringValue{StringValue: "foo"}}, "Bar": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "Count": {ValueType: &pb.Value_IntegerValue{IntegerValue: 4}}, }, }, }}, }, }, wantLoad: &aSubPLSErr{}, loadErr: "PropertyLoadSaver methods must be implemented on a pointer", }, { desc: "parent does not have flatten option, child impl PLS", src: &Grandparent{ Parent: Parent{ Child: Child{ I: 9, Grandchild: Grandchild{ S: "BAD", }, }, String: plsString("something"), }, }, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Parent": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "Child": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 10}}, "Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}}, }, }, }}, "String": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, }}, }, }, }}, }, }, wantLoad: &Grandparent{ Parent: Parent{ Child: Child{ I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, String: "LOADED", }, }, }, { desc: "parent has flatten option enabled, child impl PLS", src: &GrandparentFlatten{ Parent: Parent{ Child: Child{ I: 7, Grandchild: Grandchild{ S: "BAD", }, }, String: plsString("something"), }, }, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Parent.Child.I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}}, "Parent.Child.Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}}, "Parent.String.SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, wantLoad: &GrandparentFlatten{ Parent: Parent{ Child: Child{ I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, String: "LOADED", }, }, }, { desc: "parent has flatten option enabled, child (ptr to) impl PLS", src: &GrandparentOfPtrFlatten{ Parent: ParentOfPtr{ Child: &Child{ I: 7, Grandchild: Grandchild{ S: "BAD", }, }, String: ptrToplsString("something"), }, }, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Parent.Child.I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}}, "Parent.Child.Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}}, "Parent.String.SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, wantLoad: &GrandparentOfPtrFlatten{ Parent: ParentOfPtr{ Child: &Child{ I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, String: ptrToplsString("LOADED"), }, }, }, { desc: "children (slice of) impl PLS", src: &GrandparentOfSlice{ Parent: ParentOfSlice{ Children: []Child{ { I: 7, Grandchild: Grandchild{ S: "BAD", }, }, { I: 9, Grandchild: Grandchild{ S: "BAD2", }, }, }, Strings: []plsString{ "something1", "something2", }, }, }, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Parent": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "Children": {ValueType: &pb.Value_ArrayValue{ ArrayValue: &pb.ArrayValue{Values: []*pb.Value{ {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}}, "Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}}, }, }, }}, {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 10}}, "Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}}, }, }, }}, }}, }}, "Strings": {ValueType: &pb.Value_ArrayValue{ ArrayValue: &pb.ArrayValue{Values: []*pb.Value{ {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, }}, {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, }}, }}, }}, }, }, }}, }, }, wantLoad: &GrandparentOfSlice{ Parent: ParentOfSlice{ Children: []Child{ { I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, { I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, }, Strings: []plsString{ "LOADED", "LOADED", }, }, }, }, { desc: "children (slice of ptrs) impl PLS", src: &GrandparentOfSlicePtrs{ Parent: ParentOfSlicePtrs{ Children: []*Child{ { I: 7, Grandchild: Grandchild{ S: "BAD", }, }, { I: 9, Grandchild: Grandchild{ S: "BAD2", }, }, }, Strings: []*plsString{ ptrToplsString("something1"), ptrToplsString("something2"), }, }, }, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Parent": {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "Children": {ValueType: &pb.Value_ArrayValue{ ArrayValue: &pb.ArrayValue{Values: []*pb.Value{ {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}}, "Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}}, }, }, }}, {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 10}}, "Grandchild.S": {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}}, }, }, }}, }}, }}, "Strings": {ValueType: &pb.Value_ArrayValue{ ArrayValue: &pb.ArrayValue{Values: []*pb.Value{ {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, }}, {ValueType: &pb.Value_EntityValue{ EntityValue: &pb.Entity{ Properties: map[string]*pb.Value{ "SS": {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, }}, }}, }}, }, }, }}, }, }, wantLoad: &GrandparentOfSlicePtrs{ Parent: ParentOfSlicePtrs{ Children: []*Child{ { I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, { I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, }, Strings: []*plsString{ ptrToplsString("LOADED"), ptrToplsString("LOADED"), }, }, }, }, { desc: "parent has flatten option, children (slice of) impl PLS", src: &GrandparentOfSliceFlatten{ Parent: ParentOfSlice{ Children: []Child{ { I: 7, Grandchild: Grandchild{ S: "BAD", }, }, { I: 9, Grandchild: Grandchild{ S: "BAD2", }, }, }, Strings: []plsString{ "something1", "something2", }, }, }, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Parent.Children.I": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{ Values: []*pb.Value{ {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}}, {ValueType: &pb.Value_IntegerValue{IntegerValue: 10}}, }, }, }}, "Parent.Children.Grandchild.S": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{ Values: []*pb.Value{ {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}}, {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}}, }, }, }}, "Parent.Strings.SS": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{ Values: []*pb.Value{ {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, }}, }, }, wantLoad: &GrandparentOfSliceFlatten{ Parent: ParentOfSlice{ Children: []Child{ { I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, { I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, }, Strings: []plsString{ "LOADED", "LOADED", }, }, }, }, { desc: "parent has flatten option, children (slice of ptrs) impl PLS", src: &GrandparentOfSlicePtrsFlatten{ Parent: ParentOfSlicePtrs{ Children: []*Child{ { I: 7, Grandchild: Grandchild{ S: "BAD", }, }, { I: 9, Grandchild: Grandchild{ S: "BAD2", }, }, }, Strings: []*plsString{ ptrToplsString("something1"), ptrToplsString("something1"), }, }, }, wantSave: &pb.Entity{ Key: keyToProto(testKey0), Properties: map[string]*pb.Value{ "Parent.Children.I": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{ Values: []*pb.Value{ {ValueType: &pb.Value_IntegerValue{IntegerValue: 8}}, {ValueType: &pb.Value_IntegerValue{IntegerValue: 10}}, }, }, }}, "Parent.Children.Grandchild.S": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{ Values: []*pb.Value{ {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 8"}}, {ValueType: &pb.Value_StringValue{StringValue: "grandchild saved 10"}}, }, }, }}, "Parent.Strings.SS": {ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{ Values: []*pb.Value{ {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, {ValueType: &pb.Value_StringValue{StringValue: "SAVED"}}, }, }, }}, }, }, wantLoad: &GrandparentOfSlicePtrsFlatten{ Parent: ParentOfSlicePtrs{ Children: []*Child{ { I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, { I: 1, Grandchild: Grandchild{ S: "grandchild loaded", }, }, }, Strings: []*plsString{ ptrToplsString("LOADED"), ptrToplsString("LOADED"), }, }, }, }, } for _, tc := range testCases { e, err := saveEntity(testKey0, tc.src) if tc.saveErr == "" { // Want no error. if err != nil { t.Errorf("%s: save: %v", tc.desc, err) continue } if !testutil.Equal(e, tc.wantSave) { t.Errorf("%s: save: \ngot: %+v\nwant: %+v", tc.desc, e, tc.wantSave) continue } } else { // Want error. if err == nil { t.Errorf("%s: save: want err", tc.desc) continue } if !strings.Contains(err.Error(), tc.saveErr) { t.Errorf("%s: save: \ngot err '%s'\nwant err '%s'", tc.desc, err.Error(), tc.saveErr) } continue } gota := reflect.New(reflect.TypeOf(tc.wantLoad).Elem()).Interface() err = loadEntityProto(gota, e) if tc.loadErr == "" { // Want no error. if err != nil { t.Errorf("%s: load: %v", tc.desc, err) continue } if !testutil.Equal(gota, tc.wantLoad) { t.Errorf("%s: load: \ngot: %+v\nwant: %+v", tc.desc, gota, tc.wantLoad) continue } } else { // Want error. if err == nil { t.Errorf("%s: load: want err", tc.desc) continue } if !strings.Contains(err.Error(), tc.loadErr) { t.Errorf("%s: load: \ngot err '%s'\nwant err '%s'", tc.desc, err.Error(), tc.loadErr) } } } } func TestQueryConstruction(t *testing.T) { tests := []struct { q, exp *Query err string }{ { q: NewQuery("Foo"), exp: &Query{ kind: "Foo", limit: -1, }, }, { // Regular filtered query with standard spacing. q: NewQuery("Foo").Filter("foo >", 7), exp: &Query{ kind: "Foo", filter: []filter{ { FieldName: "foo", Op: greaterThan, Value: 7, }, }, limit: -1, }, }, { // Filtered query with no spacing. q: NewQuery("Foo").Filter("foo=", 6), exp: &Query{ kind: "Foo", filter: []filter{ { FieldName: "foo", Op: equal, Value: 6, }, }, limit: -1, }, }, { // Filtered query with funky spacing. q: NewQuery("Foo").Filter(" foo< ", 8), exp: &Query{ kind: "Foo", filter: []filter{ { FieldName: "foo", Op: lessThan, Value: 8, }, }, limit: -1, }, }, { // Filtered query with multicharacter op. q: NewQuery("Foo").Filter("foo >=", 9), exp: &Query{ kind: "Foo", filter: []filter{ { FieldName: "foo", Op: greaterEq, Value: 9, }, }, limit: -1, }, }, { // Query with ordering. q: NewQuery("Foo").Order("bar"), exp: &Query{ kind: "Foo", order: []order{ { FieldName: "bar", Direction: ascending, }, }, limit: -1, }, }, { // Query with reverse ordering, and funky spacing. q: NewQuery("Foo").Order(" - bar"), exp: &Query{ kind: "Foo", order: []order{ { FieldName: "bar", Direction: descending, }, }, limit: -1, }, }, { // Query with an empty ordering. q: NewQuery("Foo").Order(""), err: "empty order", }, { // Query with a + ordering. q: NewQuery("Foo").Order("+bar"), err: "invalid order", }, } for i, test := range tests { if test.q.err != nil { got := test.q.err.Error() if !strings.Contains(got, test.err) { t.Errorf("%d: error mismatch: got %q want something containing %q", i, got, test.err) } continue } if !testutil.Equal(test.q, test.exp, cmp.AllowUnexported(Query{})) { t.Errorf("%d: mismatch: got %v want %v", i, test.q, test.exp) } } } func TestPutMultiTypes(t *testing.T) { ctx := context.Background() type S struct { A int B string } testCases := []struct { desc string src interface{} wantErr bool }{ // Test cases to check each of the valid input types for src. // Each case has the same elements. { desc: "type []struct", src: []S{ {1, "one"}, {2, "two"}, }, }, { desc: "type []*struct", src: []*S{ {1, "one"}, {2, "two"}, }, }, { desc: "type []interface{} with PLS elems", src: []interface{}{ &PropertyList{Property{Name: "A", Value: 1}, Property{Name: "B", Value: "one"}}, &PropertyList{Property{Name: "A", Value: 2}, Property{Name: "B", Value: "two"}}, }, }, { desc: "type []interface{} with struct ptr elems", src: []interface{}{ &S{1, "one"}, &S{2, "two"}, }, }, { desc: "type []PropertyLoadSaver{}", src: []PropertyLoadSaver{ &PropertyList{Property{Name: "A", Value: 1}, Property{Name: "B", Value: "one"}}, &PropertyList{Property{Name: "A", Value: 2}, Property{Name: "B", Value: "two"}}, }, }, { desc: "type []P (non-pointer, *P implements PropertyLoadSaver)", src: []PropertyList{ {Property{Name: "A", Value: 1}, Property{Name: "B", Value: "one"}}, {Property{Name: "A", Value: 2}, Property{Name: "B", Value: "two"}}, }, }, // Test some invalid cases. { desc: "type []interface{} with struct elems", src: []interface{}{ S{1, "one"}, S{2, "two"}, }, wantErr: true, }, { desc: "PropertyList", src: PropertyList{ Property{Name: "A", Value: 1}, Property{Name: "B", Value: "one"}, }, wantErr: true, }, { desc: "type []int", src: []int{1, 2}, wantErr: true, }, { desc: "not a slice", src: S{1, "one"}, wantErr: true, }, } // Use the same keys and expected entities for all tests. keys := []*Key{ NameKey("testKind", "first", nil), NameKey("testKind", "second", nil), } want := []*pb.Mutation{ {Operation: &pb.Mutation_Upsert{ Upsert: &pb.Entity{ Key: keyToProto(keys[0]), Properties: map[string]*pb.Value{ "A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 1}}, "B": {ValueType: &pb.Value_StringValue{StringValue: "one"}}, }, }}}, {Operation: &pb.Mutation_Upsert{ Upsert: &pb.Entity{ Key: keyToProto(keys[1]), Properties: map[string]*pb.Value{ "A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}}, "B": {ValueType: &pb.Value_StringValue{StringValue: "two"}}, }, }}}, } for _, tt := range testCases { // Set up a fake client which captures upserts. var got []*pb.Mutation client := &Client{ client: &fakeClient{ commitFn: func(req *pb.CommitRequest) (*pb.CommitResponse, error) { got = req.Mutations return &pb.CommitResponse{}, nil }, }, } _, err := client.PutMulti(ctx, keys, tt.src) if err != nil { if !tt.wantErr { t.Errorf("%s: error %v", tt.desc, err) } continue } if tt.wantErr { t.Errorf("%s: wanted error, but none returned", tt.desc) continue } if len(got) != len(want) { t.Errorf("%s: got %d entities, want %d", tt.desc, len(got), len(want)) continue } for i, e := range got { if !proto.Equal(e, want[i]) { t.Logf("%s: entity %d doesn't match\ngot: %v\nwant: %v", tt.desc, i, e, want[i]) } } } } func TestNoIndexOnSliceProperties(t *testing.T) { // Check that ExcludeFromIndexes is set on the inner elements, // rather than the top-level ArrayValue value. pl := PropertyList{ Property{ Name: "repeated", Value: []interface{}{ 123, false, "short", strings.Repeat("a", 1503), }, NoIndex: true, }, } key := NameKey("dummy", "dummy", nil) entity, err := saveEntity(key, &pl) if err != nil { t.Fatalf("saveEntity: %v", err) } want := &pb.Value{ ValueType: &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{Values: []*pb.Value{ {ValueType: &pb.Value_IntegerValue{IntegerValue: 123}, ExcludeFromIndexes: true}, {ValueType: &pb.Value_BooleanValue{BooleanValue: false}, ExcludeFromIndexes: true}, {ValueType: &pb.Value_StringValue{StringValue: "short"}, ExcludeFromIndexes: true}, {ValueType: &pb.Value_StringValue{StringValue: strings.Repeat("a", 1503)}, ExcludeFromIndexes: true}, }}}, } if got := entity.Properties["repeated"]; !proto.Equal(got, want) { t.Errorf("Entity proto differs\ngot: %v\nwant: %v", got, want) } } type byName PropertyList func (s byName) Len() int { return len(s) } func (s byName) Less(i, j int) bool { return s[i].Name < s[j].Name } func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // sortPL sorts the property list by property name, and // recursively sorts any nested property lists, or nested slices of // property lists. func sortPL(pl PropertyList) { sort.Stable(byName(pl)) for _, p := range pl { switch p.Value.(type) { case *Entity: sortPL(p.Value.(*Entity).Properties) case []interface{}: for _, p2 := range p.Value.([]interface{}) { if nent, ok := p2.(*Entity); ok { sortPL(nent.Properties) } } } } } func TestValidGeoPoint(t *testing.T) { testCases := []struct { desc string pt GeoPoint want bool }{ { "valid", GeoPoint{67.21, 13.37}, true, }, { "high lat", GeoPoint{-90.01, 13.37}, false, }, { "low lat", GeoPoint{90.01, 13.37}, false, }, { "high lng", GeoPoint{67.21, 182}, false, }, { "low lng", GeoPoint{67.21, -181}, false, }, } for _, tc := range testCases { if got := tc.pt.Valid(); got != tc.want { t.Errorf("%s: got %v, want %v", tc.desc, got, tc.want) } } } func TestPutInvalidEntity(t *testing.T) { // Test that trying to put an invalid entity always returns the correct error // type. // Fake client that can pretend to start a transaction. fakeClient := &fakeDatastoreClient{ beginTransaction: func(*pb.BeginTransactionRequest) (*pb.BeginTransactionResponse, error) { return &pb.BeginTransactionResponse{ Transaction: []byte("deadbeef"), }, nil }, } client := &Client{ client: fakeClient, } ctx := context.Background() key := IncompleteKey("kind", nil) _, err := client.Put(ctx, key, "invalid entity") if err != ErrInvalidEntityType { t.Errorf("client.Put returned err %v, want %v", err, ErrInvalidEntityType) } _, err = client.PutMulti(ctx, []*Key{key}, []interface{}{"invalid entity"}) if me, ok := err.(MultiError); !ok { t.Errorf("client.PutMulti returned err %v, want MultiError type", err) } else if len(me) != 1 || me[0] != ErrInvalidEntityType { t.Errorf("client.PutMulti returned err %v, want MulitError{ErrInvalidEntityType}", err) } client.RunInTransaction(ctx, func(tx *Transaction) error { _, err := tx.Put(key, "invalid entity") if err != ErrInvalidEntityType { t.Errorf("tx.Put returned err %v, want %v", err, ErrInvalidEntityType) } _, err = tx.PutMulti([]*Key{key}, []interface{}{"invalid entity"}) if me, ok := err.(MultiError); !ok { t.Errorf("tx.PutMulti returned err %v, want MultiError type", err) } else if len(me) != 1 || me[0] != ErrInvalidEntityType { t.Errorf("tx.PutMulti returned err %v, want MulitError{ErrInvalidEntityType}", err) } return errors.New("bang!") // Return error: we don't actually want to commit. }) } func TestDeferred(t *testing.T) { type Ent struct { A int B string } keys := []*Key{ NameKey("testKind", "first", nil), NameKey("testKind", "second", nil), } entity1 := &pb.Entity{ Key: keyToProto(keys[0]), Properties: map[string]*pb.Value{ "A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 1}}, "B": {ValueType: &pb.Value_StringValue{StringValue: "one"}}, }, } entity2 := &pb.Entity{ Key: keyToProto(keys[1]), Properties: map[string]*pb.Value{ "A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}}, "B": {ValueType: &pb.Value_StringValue{StringValue: "two"}}, }, } // count keeps track of the number of times fakeClient.lookup has been // called. var count int // Fake client that will return Deferred keys in resp on the first call. fakeClient := &fakeDatastoreClient{ lookup: func(*pb.LookupRequest) (*pb.LookupResponse, error) { count++ // On the first call, we return deferred keys. if count == 1 { return &pb.LookupResponse{ Found: []*pb.EntityResult{ { Entity: entity1, Version: 1, }, }, Deferred: []*pb.Key{ keyToProto(keys[1]), }, }, nil } // On the second call, we do not return any more deferred keys. return &pb.LookupResponse{ Found: []*pb.EntityResult{ { Entity: entity2, Version: 1, }, }, }, nil }, } client := &Client{ client: fakeClient, } ctx := context.Background() dst := make([]Ent, len(keys)) err := client.GetMulti(ctx, keys, dst) if err != nil { t.Fatalf("client.Get: %v", err) } if count != 2 { t.Fatalf("expected client.lookup to be called 2 times. Got %d", count) } if len(dst) != 2 { t.Fatalf("expected 2 entities returned, got %d", len(dst)) } for _, e := range dst { if e.A == 1 { if e.B != "one" { t.Fatalf("unexpected entity %+v", e) } } else if e.A == 2 { if e.B != "two" { t.Fatalf("unexpected entity %+v", e) } } else { t.Fatalf("unexpected entity %+v", e) } } } type KeyLoaderEnt struct { A int K *Key } func (e *KeyLoaderEnt) Load(p []Property) error { e.A = 2 return nil } func (e *KeyLoaderEnt) LoadKey(k *Key) error { e.K = k return nil } func (e *KeyLoaderEnt) Save() ([]Property, error) { return []Property{{Name: "A", Value: int64(3)}}, nil } func TestKeyLoaderEndToEnd(t *testing.T) { keys := []*Key{ NameKey("testKind", "first", nil), NameKey("testKind", "second", nil), } entity1 := &pb.Entity{ Key: keyToProto(keys[0]), Properties: map[string]*pb.Value{ "A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 1}}, "B": {ValueType: &pb.Value_StringValue{StringValue: "one"}}, }, } entity2 := &pb.Entity{ Key: keyToProto(keys[1]), Properties: map[string]*pb.Value{ "A": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}}, "B": {ValueType: &pb.Value_StringValue{StringValue: "two"}}, }, } fakeClient := &fakeDatastoreClient{ lookup: func(*pb.LookupRequest) (*pb.LookupResponse, error) { return &pb.LookupResponse{ Found: []*pb.EntityResult{ { Entity: entity1, Version: 1, }, { Entity: entity2, Version: 1, }, }, }, nil }, } client := &Client{ client: fakeClient, } ctx := context.Background() dst := make([]*KeyLoaderEnt, len(keys)) err := client.GetMulti(ctx, keys, dst) if err != nil { t.Fatalf("client.Get: %v", err) } for i := range dst { if !testutil.Equal(dst[i].K, keys[i]) { t.Fatalf("unexpected entity %d to have key %+v, got %+v", i, keys[i], dst[i].K) } } } func TestDeferredMissing(t *testing.T) { type Ent struct { A int B string } keys := []*Key{ NameKey("testKind", "first", nil), NameKey("testKind", "second", nil), } entity1 := &pb.Entity{ Key: keyToProto(keys[0]), } entity2 := &pb.Entity{ Key: keyToProto(keys[1]), } var count int fakeClient := &fakeDatastoreClient{ lookup: func(*pb.LookupRequest) (*pb.LookupResponse, error) { count++ if count == 1 { return &pb.LookupResponse{ Missing: []*pb.EntityResult{ { Entity: entity1, Version: 1, }, }, Deferred: []*pb.Key{ keyToProto(keys[1]), }, }, nil } return &pb.LookupResponse{ Missing: []*pb.EntityResult{ { Entity: entity2, Version: 1, }, }, }, nil }, } client := &Client{ client: fakeClient, } ctx := context.Background() dst := make([]Ent, len(keys)) err := client.GetMulti(ctx, keys, dst) errs, ok := err.(MultiError) if !ok { t.Fatalf("expected error returns to be MultiError; got %v", err) } if len(errs) != 2 { t.Fatalf("expected 2 errors returns, got %d", len(errs)) } if errs[0] != ErrNoSuchEntity { t.Fatalf("expected error to be ErrNoSuchEntity; got %v", errs[0]) } if errs[1] != ErrNoSuchEntity { t.Fatalf("expected error to be ErrNoSuchEntity; got %v", errs[1]) } if count != 2 { t.Fatalf("expected client.lookup to be called 2 times. Got %d", count) } if len(dst) != 2 { t.Fatalf("expected 2 entities returned, got %d", len(dst)) } for _, e := range dst { if e.A != 0 || e.B != "" { t.Fatalf("unexpected entity %+v", e) } } } type fakeDatastoreClient struct { pb.DatastoreClient // Optional handlers for the datastore methods. // Any handlers left undefined will return an error. lookup func(*pb.LookupRequest) (*pb.LookupResponse, error) runQuery func(*pb.RunQueryRequest) (*pb.RunQueryResponse, error) beginTransaction func(*pb.BeginTransactionRequest) (*pb.BeginTransactionResponse, error) commit func(*pb.CommitRequest) (*pb.CommitResponse, error) rollback func(*pb.RollbackRequest) (*pb.RollbackResponse, error) allocateIds func(*pb.AllocateIdsRequest) (*pb.AllocateIdsResponse, error) } func (c *fakeDatastoreClient) Lookup(ctx context.Context, in *pb.LookupRequest, opts ...grpc.CallOption) (*pb.LookupResponse, error) { if c.lookup == nil { return nil, errors.New("no lookup handler defined") } return c.lookup(in) } func (c *fakeDatastoreClient) RunQuery(ctx context.Context, in *pb.RunQueryRequest, opts ...grpc.CallOption) (*pb.RunQueryResponse, error) { if c.runQuery == nil { return nil, errors.New("no runQuery handler defined") } return c.runQuery(in) } func (c *fakeDatastoreClient) BeginTransaction(ctx context.Context, in *pb.BeginTransactionRequest, opts ...grpc.CallOption) (*pb.BeginTransactionResponse, error) { if c.beginTransaction == nil { return nil, errors.New("no beginTransaction handler defined") } return c.beginTransaction(in) } func (c *fakeDatastoreClient) Commit(ctx context.Context, in *pb.CommitRequest, opts ...grpc.CallOption) (*pb.CommitResponse, error) { if c.commit == nil { return nil, errors.New("no commit handler defined") } return c.commit(in) } func (c *fakeDatastoreClient) Rollback(ctx context.Context, in *pb.RollbackRequest, opts ...grpc.CallOption) (*pb.RollbackResponse, error) { if c.rollback == nil { return nil, errors.New("no rollback handler defined") } return c.rollback(in) } func (c *fakeDatastoreClient) AllocateIds(ctx context.Context, in *pb.AllocateIdsRequest, opts ...grpc.CallOption) (*pb.AllocateIdsResponse, error) { if c.allocateIds == nil { return nil, errors.New("no allocateIds handler defined") } return c.allocateIds(in) }