1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-16 18:01:37 +00:00

Update vendor dependencies

This commit is contained in:
Frédéric Guillot 2018-07-06 21:18:14 -07:00
parent 34a3fe426b
commit 459bb4531f
747 changed files with 89857 additions and 39711 deletions

View file

@ -43,7 +43,8 @@ func (e *ErrFieldMismatch) Error() string {
e.FieldName, e.StructType, e.Reason)
}
// protoToKey converts a Reference proto to a *Key.
// protoToKey converts a Reference proto to a *Key. If the key is invalid,
// protoToKey will return the invalid key along with ErrInvalidKey.
func protoToKey(r *pb.Reference) (k *Key, err error) {
appID := r.GetApp()
namespace := r.GetNameSpace()
@ -57,7 +58,7 @@ func protoToKey(r *pb.Reference) (k *Key, err error) {
namespace: namespace,
}
if !k.valid() {
return nil, ErrInvalidKey
return k, ErrInvalidKey
}
}
return

View file

@ -10,6 +10,7 @@ import (
"fmt"
"os"
"reflect"
"sort"
"strings"
"testing"
"time"
@ -70,6 +71,8 @@ var (
testGeoPt0 = appengine.GeoPoint{Lat: 1.2, Lng: 3.4}
testGeoPt1 = appengine.GeoPoint{Lat: 5, Lng: 10}
testBadGeoPt = appengine.GeoPoint{Lat: 1000, Lng: 34}
now = time.Unix(1e9, 0).UTC()
)
type B0 struct {
@ -133,6 +136,39 @@ 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"`
D time.Time `datastore:",omitempty"`
F []int `datastore:",omitempty"`
}
type Omit struct {
A string `datastore:",omitempty"`
B int `datastore:"Bb,omitempty"`
C bool `datastore:",omitempty,noindex"`
D time.Time `datastore:",omitempty"`
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
@ -306,10 +342,27 @@ type Doubler struct {
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)
}
type EmbeddedTime struct {
time.Time
}
type SpecialTime struct {
MyTime EmbeddedTime
}
func (d *Doubler) Save() ([]Property, error) {
// Save the default Property slice to an in-memory buffer (a PropertyList).
props, err := SaveStruct(d)
@ -475,6 +528,81 @@ var testCases = []testCase{
"",
"",
},
{
"omit empty, all",
&OmitAll{},
new(PropertyList),
"",
"",
},
{
"omit empty",
&Omit{},
&PropertyList{
Property{Name: "St", Value: "", NoIndex: false, Multiple: false},
},
"",
"",
},
{
"omit empty, fields populated",
&Omit{
A: "a",
B: 10,
C: true,
D: now,
F: []int{11},
},
&PropertyList{
Property{Name: "A", Value: "a", NoIndex: false, Multiple: false},
Property{Name: "Bb", Value: int64(10), NoIndex: false, Multiple: false},
Property{Name: "C", Value: true, NoIndex: true, Multiple: false},
Property{Name: "D", Value: now, NoIndex: false, Multiple: false},
Property{Name: "F", Value: int64(11), NoIndex: false, Multiple: true},
Property{Name: "St", Value: "", NoIndex: false, Multiple: false},
},
"",
"",
},
{
"omit empty, fields populated",
&Omit{
A: "a",
B: 10,
C: true,
D: now,
F: []int{11},
S: S{St: "string"},
},
&PropertyList{
Property{Name: "A", Value: "a", NoIndex: false, Multiple: false},
Property{Name: "Bb", Value: int64(10), NoIndex: false, Multiple: false},
Property{Name: "C", Value: true, NoIndex: true, Multiple: false},
Property{Name: "D", Value: now, NoIndex: false, Multiple: false},
Property{Name: "F", Value: int64(11), NoIndex: false, Multiple: true},
Property{Name: "St", Value: "string", NoIndex: false, Multiple: false},
},
"",
"",
},
{
"omit empty does not propagate",
&NoOmits{
No: []NoOmit{
NoOmit{},
},
S: S{},
Ss: S{},
},
&PropertyList{
Property{Name: "No.A", Value: "", NoIndex: false, Multiple: true},
Property{Name: "No.Bb", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "No.C", Value: false, NoIndex: true, Multiple: true},
Property{Name: "Ss.St", Value: "", NoIndex: false, Multiple: false},
Property{Name: "St", Value: "", NoIndex: false, Multiple: false}},
"",
"",
},
{
"key",
&K0{K: testKey1a},
@ -624,6 +752,35 @@ var testCases = []testCase{
"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)},
@ -727,16 +884,16 @@ var testCases = []testCase{
// A and B are renamed to a and b; A and C are noindex, I is ignored.
// Indexed properties are loaded before raw properties. Thus, the
// result is: b, b, b, D, E, a, c.
Property{Name: "C", Value: int64(3), NoIndex: true, Multiple: false},
Property{Name: "D", Value: int64(4), NoIndex: false, Multiple: false},
Property{Name: "E", Value: int64(5), NoIndex: false, Multiple: false},
Property{Name: "F", Value: int64(6), NoIndex: true, Multiple: false},
Property{Name: "G", Value: int64(7), NoIndex: false, Multiple: false},
Property{Name: "J", Value: int64(9), NoIndex: true, Multiple: false},
Property{Name: "a", Value: int64(1), NoIndex: true, Multiple: false},
Property{Name: "b", Value: int64(21), NoIndex: false, Multiple: true},
Property{Name: "b", Value: int64(22), NoIndex: false, Multiple: true},
Property{Name: "b", Value: int64(23), NoIndex: false, Multiple: true},
Property{Name: "D", Value: int64(4), NoIndex: false, Multiple: false},
Property{Name: "E", Value: int64(5), NoIndex: false, Multiple: false},
Property{Name: "G", Value: int64(7), NoIndex: false, Multiple: false},
Property{Name: "a", Value: int64(1), NoIndex: true, Multiple: false},
Property{Name: "C", Value: int64(3), NoIndex: true, Multiple: false},
Property{Name: "F", Value: int64(6), NoIndex: true, Multiple: false},
Property{Name: "J", Value: int64(9), NoIndex: true, Multiple: false},
},
"",
"",
@ -783,8 +940,8 @@ var testCases = []testCase{
"save struct load props",
&X0{S: "s", I: 1},
&PropertyList{
Property{Name: "S", Value: "s", NoIndex: false, Multiple: false},
Property{Name: "I", Value: int64(1), NoIndex: false, Multiple: false},
Property{Name: "S", Value: "s", NoIndex: false, Multiple: false},
},
"",
"",
@ -845,10 +1002,10 @@ var testCases = []testCase{
&PropertyList{
Property{Name: "A", Value: int64(1), NoIndex: false, Multiple: false},
Property{Name: "I.W", Value: int64(10), NoIndex: false, Multiple: true},
Property{Name: "I.X", Value: "ten", NoIndex: false, Multiple: true},
Property{Name: "I.W", Value: int64(20), NoIndex: false, Multiple: true},
Property{Name: "I.X", Value: "twenty", NoIndex: false, Multiple: true},
Property{Name: "I.W", Value: int64(30), NoIndex: false, Multiple: true},
Property{Name: "I.X", Value: "ten", NoIndex: false, Multiple: true},
Property{Name: "I.X", Value: "twenty", NoIndex: false, Multiple: true},
Property{Name: "I.X", Value: "thirty", NoIndex: false, Multiple: true},
Property{Name: "J.Y", Value: float64(3.14), NoIndex: false, Multiple: false},
Property{Name: "Z", Value: true, NoIndex: false, Multiple: false},
@ -1115,33 +1272,33 @@ var testCases = []testCase{
},
},
&PropertyList{
Property{Name: "red.S", Value: "rouge", NoIndex: false, Multiple: false},
Property{Name: "red.I", Value: int64(0), NoIndex: false, Multiple: false},
Property{Name: "red.Nonymous.S", Value: "rosso0", NoIndex: false, Multiple: true},
Property{Name: "red.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "red.Nonymous.S", Value: "rosso1", NoIndex: false, Multiple: true},
Property{Name: "red.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "red.Other", Value: "", NoIndex: false, Multiple: false},
Property{Name: "green.S", Value: "vert", NoIndex: false, Multiple: false},
Property{Name: "green.I", Value: int64(0), NoIndex: false, Multiple: false},
Property{Name: "green.Nonymous.S", Value: "verde0", NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.S", Value: "verde1", NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.S", Value: "verde2", NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "green.Other", Value: "", NoIndex: false, Multiple: false},
Property{Name: "Blue.S", Value: "bleu", NoIndex: false, Multiple: false},
Property{Name: "Blue.I", Value: int64(0), NoIndex: false, Multiple: false},
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.S", Value: "blu0", NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.S", Value: "blu1", NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.S", Value: "blu2", NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.S", Value: "blu3", NoIndex: false, Multiple: true},
Property{Name: "Blue.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "Blue.Other", Value: "", NoIndex: false, Multiple: false},
Property{Name: "Blue.S", Value: "bleu", NoIndex: false, Multiple: false},
Property{Name: "green.I", Value: int64(0), NoIndex: false, Multiple: false},
Property{Name: "green.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.S", Value: "verde0", NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.S", Value: "verde1", NoIndex: false, Multiple: true},
Property{Name: "green.Nonymous.S", Value: "verde2", NoIndex: false, Multiple: true},
Property{Name: "green.Other", Value: "", NoIndex: false, Multiple: false},
Property{Name: "green.S", Value: "vert", NoIndex: false, Multiple: false},
Property{Name: "red.I", Value: int64(0), NoIndex: false, Multiple: false},
Property{Name: "red.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "red.Nonymous.I", Value: int64(0), NoIndex: false, Multiple: true},
Property{Name: "red.Nonymous.S", Value: "rosso0", NoIndex: false, Multiple: true},
Property{Name: "red.Nonymous.S", Value: "rosso1", NoIndex: false, Multiple: true},
Property{Name: "red.Other", Value: "", NoIndex: false, Multiple: false},
Property{Name: "red.S", Value: "rouge", NoIndex: false, Multiple: false},
},
"",
"",
@ -1196,10 +1353,10 @@ var testCases = []testCase{
}
}{},
&PropertyList{
Property{Name: "B.Y", Value: "", NoIndex: false, Multiple: false},
Property{Name: "A.X", Value: "", NoIndex: true, Multiple: false},
Property{Name: "A.Y", Value: "", NoIndex: true, Multiple: false},
Property{Name: "B.X", Value: "", NoIndex: true, Multiple: false},
Property{Name: "B.Y", Value: "", NoIndex: false, Multiple: false},
},
"",
"",
@ -1272,6 +1429,22 @@ var testCases = []testCase{
"",
"",
},
{
"embedded time field",
&SpecialTime{MyTime: EmbeddedTime{now}},
&SpecialTime{MyTime: EmbeddedTime{now}},
"",
"",
},
{
"embedded time load",
&PropertyList{
Property{Name: "MyTime.", Value: now, NoIndex: false, Multiple: false},
},
&SpecialTime{MyTime: EmbeddedTime{now}},
"",
"",
},
}
// checkErr returns the empty string if either both want and err are zero,
@ -1309,6 +1482,10 @@ func TestRoundTrip(t *testing.T) {
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.
sort.Stable(byName(*pl))
}
equal := false
if gotT, ok := got.(*T); ok {
// Round tripping a time.Time can result in a different time.Location: Local instead of UTC.
@ -1324,6 +1501,12 @@ func TestRoundTrip(t *testing.T) {
}
}
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] }
func TestQueryConstruction(t *testing.T) {
tests := []struct {
q, exp *Query

View file

@ -87,7 +87,7 @@ behavior for struct pointers. Struct pointers are more strongly typed and are
easier to use; PropertyLoadSavers are more flexible.
The actual types passed do not have to match between Get and Put calls or even
across different App Engine requests. It is valid to put a *PropertyList and
across different calls to datastore. It is valid to put a *PropertyList and
get that same entity as a *myStruct, or put a *myStruct0 and get a *myStruct1.
Conceptually, any entity is saved as a sequence of properties, and is loaded
into the destination value on a property-by-property basis. When loading into
@ -97,18 +97,28 @@ caller whether this error is fatal, recoverable or ignorable.
By default, for struct pointers, all properties are potentially indexed, and
the property name is the same as the field name (and hence must start with an
upper case letter). Fields may have a `datastore:"name,options"` tag. The tag
name is the property name, which must be one or more valid Go identifiers
joined by ".", but may start with a lower case letter. An empty tag name means
to just use the field name. A "-" tag name means that the datastore will
ignore that field. If options is "noindex" then the field will not be indexed.
If the options is "" then the comma may be omitted. There are no other
recognized options.
upper case letter).
Fields (except for []byte) are indexed by default. Strings longer than 1500
bytes cannot be indexed; fields used to store long strings should be
tagged with "noindex". Similarly, ByteStrings longer than 1500 bytes cannot be
indexed.
Fields may have a `datastore:"name,options"` tag. The tag name is the
property name, which must be one or more valid Go identifiers joined by ".",
but may start with a lower case letter. An empty tag name means to just use the
field name. A "-" tag name means that the datastore will ignore that field.
The only valid options are "omitempty" and "noindex".
If the options include "omitempty" and the value of the field is empty, then the field will be omitted on Save.
The empty values are false, 0, any nil interface value, and any array, slice, map, or string of length zero.
Struct field values will never be empty.
If options include "noindex" then the field will not be indexed. All fields are indexed
by default. Strings or byte slices longer than 1500 bytes cannot be indexed;
fields used to store long strings and byte slices must be tagged with "noindex"
or they will cause Put operations to fail.
To use multiple options together, separate them by a comma.
The order does not matter.
If the options is "" then the comma may be omitted.
Example code:
@ -200,7 +210,7 @@ Example code:
func (x *CustomPropsExample) Save() ([]datastore.Property, error) {
// Validate the Sum field.
if x.Sum != x.I + x.J {
return errors.New("CustomPropsExample has inconsistent sum")
return nil, errors.New("CustomPropsExample has inconsistent sum")
}
// Save I and J as usual. The code below is equivalent to calling
// "return datastore.SaveStruct(x)", but is done manually for
@ -214,7 +224,7 @@ Example code:
Name: "J",
Value: int64(x.J),
},
}
}, nil
}
The *PropertyList type implements PropertyLoadSaver, and can therefore hold an
@ -343,7 +353,7 @@ Example code:
continue
}
for p, rep := range props {
fmt.Fprintf(w, "\t-%s (%s)\n", p, strings.Join(", ", rep))
fmt.Fprintf(w, "\t-%s (%s)\n", p, strings.Join(rep, ", "))
}
}
}

View file

@ -20,6 +20,26 @@ import (
pb "google.golang.org/appengine/internal/datastore"
)
type KeyRangeCollisionError struct {
start int64
end int64
}
func (e *KeyRangeCollisionError) Error() string {
return fmt.Sprintf("datastore: Collision when attempting to allocate range [%d, %d]",
e.start, e.end)
}
type KeyRangeContentionError struct {
start int64
end int64
}
func (e *KeyRangeContentionError) Error() string {
return fmt.Sprintf("datastore: Contention when attempting to allocate range [%d, %d]",
e.start, e.end)
}
// Key represents the datastore key for a stored entity, and is immutable.
type Key struct {
kind string
@ -307,3 +327,70 @@ func AllocateIDs(c context.Context, kind string, parent *Key, n int) (low, high
}
return low, high, nil
}
// AllocateIDRange allocates a range of IDs with specific endpoints.
// The range is inclusive at both the low and high end. Once these IDs have been
// allocated, you can manually assign them to newly created entities.
//
// The Datastore's automatic ID allocator never assigns a key that has already
// been allocated (either through automatic ID allocation or through an explicit
// AllocateIDs call). As a result, entities written to the given key range will
// never be overwritten. However, writing entities with manually assigned keys in
// this range may overwrite existing entities (or new entities written by a separate
// request), depending on the error returned.
//
// Use this only if you have an existing numeric ID range that you want to reserve
// (for example, bulk loading entities that already have IDs). If you don't care
// about which IDs you receive, use AllocateIDs instead.
//
// AllocateIDRange returns nil if the range is successfully allocated. If one or more
// entities with an ID in the given range already exist, it returns a KeyRangeCollisionError.
// If the Datastore has already cached IDs in this range (e.g. from a previous call to
// AllocateIDRange), it returns a KeyRangeContentionError. Errors of other types indicate
// problems with arguments or an error returned directly from the Datastore.
func AllocateIDRange(c context.Context, kind string, parent *Key, start, end int64) (err error) {
if kind == "" {
return errors.New("datastore: AllocateIDRange given an empty kind")
}
if start < 1 || end < 1 {
return errors.New("datastore: AllocateIDRange start and end must both be greater than 0")
}
if start > end {
return errors.New("datastore: AllocateIDRange start must be before end")
}
req := &pb.AllocateIdsRequest{
ModelKey: keyToProto("", NewIncompleteKey(c, kind, parent)),
Max: proto.Int64(end),
}
res := &pb.AllocateIdsResponse{}
if err := internal.Call(c, "datastore_v3", "AllocateIds", req, res); err != nil {
return err
}
// Check for collisions, i.e. existing entities with IDs in this range.
// We could do this before the allocation, but we'd still have to do it
// afterward as well to catch the race condition where an entity is inserted
// after that initial check but before the allocation. Skip the up-front check
// and just do it once.
q := NewQuery(kind).Filter("__key__ >=", NewKey(c, kind, "", start, parent)).
Filter("__key__ <=", NewKey(c, kind, "", end, parent)).KeysOnly().Limit(1)
keys, err := q.GetAll(c, nil)
if err != nil {
return err
}
if len(keys) != 0 {
return &KeyRangeCollisionError{start: start, end: end}
}
// Check for a race condition, i.e. cases where the datastore may have
// cached ID batches that contain IDs in this range.
if start < res.GetStart() {
return &KeyRangeContentionError{start: start, end: end}
}
return nil
}

View file

@ -7,8 +7,10 @@ package datastore
import (
"fmt"
"reflect"
"strings"
"time"
"github.com/golang/protobuf/proto"
"google.golang.org/appengine"
pb "google.golang.org/appengine/internal/datastore"
)
@ -19,13 +21,15 @@ var (
typeOfByteString = reflect.TypeOf(ByteString(nil))
typeOfGeoPoint = reflect.TypeOf(appengine.GeoPoint{})
typeOfTime = reflect.TypeOf(time.Time{})
typeOfKeyPtr = reflect.TypeOf(&Key{})
typeOfEntityPtr = reflect.TypeOf(&Entity{})
)
// typeMismatchReason returns a string explaining why the property p could not
// be stored in an entity field of type v.Type().
func typeMismatchReason(p Property, v reflect.Value) string {
func typeMismatchReason(pValue interface{}, v reflect.Value) string {
entityType := "empty"
switch p.Value.(type) {
switch pValue.(type) {
case int64:
entityType = "int"
case bool:
@ -58,13 +62,41 @@ type propertyLoader struct {
func (l *propertyLoader) load(codec *structCodec, structValue reflect.Value, p Property, requireSlice bool) string {
var v reflect.Value
// Traverse a struct's struct-typed fields.
for name := p.Name; ; {
decoder, ok := codec.byName[name]
var sliceIndex int
name := p.Name
// If name ends with a '.', the last field is anonymous.
// In this case, strings.Split will give us "" as the
// last element of our fields slice, which will match the ""
// field name in the substruct codec.
fields := strings.Split(name, ".")
for len(fields) > 0 {
var decoder fieldCodec
var ok bool
// Cut off the last field (delimited by ".") and find its parent
// in the codec.
// eg. for name "A.B.C.D", split off "A.B.C" and try to
// find a field in the codec with this name.
// Loop again with "A.B", etc.
for i := len(fields); i > 0; i-- {
parent := strings.Join(fields[:i], ".")
decoder, ok = codec.fields[parent]
if ok {
fields = fields[i:]
break
}
}
// If we never found a matching field in the codec, return
// error message.
if !ok {
return "no such struct field"
}
v = structValue.Field(decoder.index)
v = initField(structValue, decoder.path)
if !v.IsValid() {
return "no such struct field"
}
@ -72,27 +104,23 @@ func (l *propertyLoader) load(codec *structCodec, structValue reflect.Value, p P
return "cannot set struct field"
}
if decoder.substructCodec == nil {
break
if decoder.structCodec != nil {
codec = decoder.structCodec
structValue = v
}
if v.Kind() == reflect.Slice {
if v.Kind() == reflect.Slice && v.Type() != typeOfByteSlice {
if l.m == nil {
l.m = make(map[string]int)
}
index := l.m[p.Name]
l.m[p.Name] = index + 1
for v.Len() <= index {
sliceIndex = l.m[p.Name]
l.m[p.Name] = sliceIndex + 1
for v.Len() <= sliceIndex {
v.Set(reflect.Append(v, reflect.New(v.Type().Elem()).Elem()))
}
structValue = v.Index(index)
structValue = v.Index(sliceIndex)
requireSlice = false
} else {
structValue = v
}
// Strip the "I." from "I.X".
name = name[len(codec.byIndex[decoder.index].name):]
codec = decoder.substructCodec
}
var slice reflect.Value
@ -119,6 +147,8 @@ func (l *propertyLoader) load(codec *structCodec, structValue reflect.Value, p P
meaning = pb.Property_GEORSS_POINT
case typeOfTime:
meaning = pb.Property_GD_WHEN
case typeOfEntityPtr:
meaning = pb.Property_ENTITY_PROTO
}
var err error
pValue, err = propValue(iv.value, meaning)
@ -127,11 +157,28 @@ func (l *propertyLoader) load(codec *structCodec, structValue reflect.Value, p P
}
}
if errReason := setVal(v, pValue); errReason != "" {
// Set the slice back to its zero value.
if slice.IsValid() {
slice.Set(reflect.Zero(slice.Type()))
}
return errReason
}
if slice.IsValid() {
slice.Index(sliceIndex).Set(v)
}
return ""
}
// setVal sets v to the value pValue.
func setVal(v reflect.Value, pValue interface{}) string {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x, ok := pValue.(int64)
if !ok && pValue != nil {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
if v.OverflowInt(x) {
return fmt.Sprintf("value %v overflows struct field of type %v", x, v.Type())
@ -140,7 +187,7 @@ func (l *propertyLoader) load(codec *structCodec, structValue reflect.Value, p P
case reflect.Bool:
x, ok := pValue.(bool)
if !ok && pValue != nil {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
v.SetBool(x)
case reflect.String:
@ -153,13 +200,13 @@ func (l *propertyLoader) load(codec *structCodec, structValue reflect.Value, p P
v.SetString(x)
default:
if pValue != nil {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
}
case reflect.Float32, reflect.Float64:
x, ok := pValue.(float64)
if !ok && pValue != nil {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
if v.OverflowFloat(x) {
return fmt.Sprintf("value %v overflows struct field of type %v", x, v.Type())
@ -168,10 +215,10 @@ func (l *propertyLoader) load(codec *structCodec, structValue reflect.Value, p P
case reflect.Ptr:
x, ok := pValue.(*Key)
if !ok && pValue != nil {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
if _, ok := v.Interface().(*Key); !ok {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
v.Set(reflect.ValueOf(x))
case reflect.Struct:
@ -179,17 +226,38 @@ func (l *propertyLoader) load(codec *structCodec, structValue reflect.Value, p P
case typeOfTime:
x, ok := pValue.(time.Time)
if !ok && pValue != nil {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
v.Set(reflect.ValueOf(x))
case typeOfGeoPoint:
x, ok := pValue.(appengine.GeoPoint)
if !ok && pValue != nil {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
v.Set(reflect.ValueOf(x))
default:
return typeMismatchReason(p, v)
ent, ok := pValue.(*Entity)
if !ok {
return typeMismatchReason(pValue, v)
}
// Recursively load nested struct
pls, err := newStructPLS(v.Addr().Interface())
if err != nil {
return err.Error()
}
// if ent has a Key value and our struct has a Key field,
// load the Entity's Key value into the Key field on the struct.
if ent.Key != nil && pls.codec.keyField != -1 {
pls.v.Field(pls.codec.keyField).Set(reflect.ValueOf(ent.Key))
}
err = pls.Load(ent.Properties)
if err != nil {
return err.Error()
}
}
case reflect.Slice:
x, ok := pValue.([]byte)
@ -199,31 +267,44 @@ func (l *propertyLoader) load(codec *structCodec, structValue reflect.Value, p P
}
}
if !ok && pValue != nil {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
if v.Type().Elem().Kind() != reflect.Uint8 {
return typeMismatchReason(p, v)
return typeMismatchReason(pValue, v)
}
v.SetBytes(x)
default:
return typeMismatchReason(p, v)
}
if slice.IsValid() {
slice.Set(reflect.Append(slice, v))
return typeMismatchReason(pValue, v)
}
return ""
}
// initField is similar to reflect's Value.FieldByIndex, in that it
// returns the nested struct field corresponding to index, but it
// initialises any nil pointers encountered when traversing the structure.
func initField(val reflect.Value, index []int) reflect.Value {
for _, i := range index[:len(index)-1] {
val = val.Field(i)
if val.Kind() == reflect.Ptr {
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
val = val.Elem()
}
}
return val.Field(index[len(index)-1])
}
// loadEntity loads an EntityProto into PropertyLoadSaver or struct pointer.
func loadEntity(dst interface{}, src *pb.EntityProto) (err error) {
props, err := protoToProperties(src)
ent, err := protoToEntity(src)
if err != nil {
return err
}
if e, ok := dst.(PropertyLoadSaver); ok {
return e.Load(props)
return e.Load(ent.Properties)
}
return LoadStruct(dst, props)
return LoadStruct(dst, ent.Properties)
}
func (s structPLS) Load(props []Property) error {
@ -247,9 +328,9 @@ func (s structPLS) Load(props []Property) error {
return nil
}
func protoToProperties(src *pb.EntityProto) ([]Property, error) {
func protoToEntity(src *pb.EntityProto) (*Entity, error) {
props, rawProps := src.Property, src.RawProperty
out := make([]Property, 0, len(props)+len(rawProps))
outProps := make([]Property, 0, len(props)+len(rawProps))
for {
var (
x *pb.Property
@ -274,14 +355,21 @@ func protoToProperties(src *pb.EntityProto) ([]Property, error) {
return nil, err
}
}
out = append(out, Property{
outProps = append(outProps, Property{
Name: x.GetName(),
Value: value,
NoIndex: noIndex,
Multiple: x.GetMultiple(),
})
}
return out, nil
var key *Key
if src.Key != nil {
// Ignore any error, since nested entity values
// are allowed to have an invalid key.
key, _ = protoToKey(src.Key)
}
return &Entity{key, outProps}, nil
}
// propValue returns a Go value that combines the raw PropertyValue with a
@ -303,6 +391,13 @@ func propValue(v *pb.PropertyValue, m pb.Property_Meaning) (interface{}, error)
return appengine.BlobKey(*v.StringValue), nil
} else if m == pb.Property_BYTESTRING {
return ByteString(*v.StringValue), nil
} else if m == pb.Property_ENTITY_PROTO {
var ent pb.EntityProto
err := proto.Unmarshal([]byte(*v.StringValue), &ent)
if err != nil {
return nil, err
}
return protoToEntity(&ent)
} else {
return *v.StringValue, nil
}

View file

@ -0,0 +1,656 @@
// Copyright 2016 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 (
"reflect"
"testing"
proto "github.com/golang/protobuf/proto"
pb "google.golang.org/appengine/internal/datastore"
)
type Simple struct {
I int64
}
type SimpleWithTag struct {
I int64 `datastore:"II"`
}
type NestedSimpleWithTag struct {
A SimpleWithTag `datastore:"AA"`
}
type NestedSliceOfSimple struct {
A []Simple
}
type SimpleTwoFields struct {
S string
SS string
}
type NestedSimpleAnonymous struct {
Simple
X string
}
type NestedSimple struct {
A Simple
I int64
}
type NestedSimple1 struct {
A Simple
X string
}
type NestedSimple2X struct {
AA NestedSimple
A SimpleTwoFields
S string
}
type BDotB struct {
B string `datastore:"B.B"`
}
type ABDotB struct {
A BDotB
}
type MultiAnonymous struct {
Simple
SimpleTwoFields
X string
}
var (
// these values need to be addressable
testString2 = "two"
testString3 = "three"
testInt64 = int64(2)
fieldNameI = "I"
fieldNameX = "X"
fieldNameS = "S"
fieldNameSS = "SS"
fieldNameADotI = "A.I"
fieldNameAADotII = "AA.II"
fieldNameADotBDotB = "A.B.B"
)
func TestLoadEntityNestedLegacy(t *testing.T) {
testCases := []struct {
desc string
src *pb.EntityProto
want interface{}
}{
{
"nested",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameX,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
},
&pb.Property{
Name: &fieldNameADotI,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
},
},
},
&NestedSimple1{
A: Simple{I: testInt64},
X: testString2,
},
},
{
"nested with tag",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameAADotII,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
},
},
},
&NestedSimpleWithTag{
A: SimpleWithTag{I: testInt64},
},
},
{
"nested with anonymous struct field",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameX,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
},
&pb.Property{
Name: &fieldNameI,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
},
},
},
&NestedSimpleAnonymous{
Simple: Simple{I: testInt64},
X: testString2,
},
},
{
"nested with dotted field tag",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameADotBDotB,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
},
},
},
&ABDotB{
A: BDotB{
B: testString2,
},
},
},
{
"nested with dotted field tag",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameI,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
},
&pb.Property{
Name: &fieldNameS,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
},
&pb.Property{
Name: &fieldNameSS,
Value: &pb.PropertyValue{
StringValue: &testString3,
},
},
&pb.Property{
Name: &fieldNameX,
Value: &pb.PropertyValue{
StringValue: &testString3,
},
},
},
},
&MultiAnonymous{
Simple: Simple{I: testInt64},
SimpleTwoFields: SimpleTwoFields{S: "two", SS: "three"},
X: "three",
},
},
}
for _, tc := range testCases {
dst := reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
err := loadEntity(dst, tc.src)
if err != nil {
t.Errorf("loadEntity: %s: %v", tc.desc, err)
continue
}
if !reflect.DeepEqual(tc.want, dst) {
t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, dst, tc.want)
}
}
}
type WithKey struct {
X string
I int64
K *Key `datastore:"__key__"`
}
type NestedWithKey struct {
N WithKey
Y string
}
var (
incompleteKey = newKey("", nil)
invalidKey = newKey("s", incompleteKey)
// these values need to be addressable
fieldNameA = "A"
fieldNameK = "K"
fieldNameN = "N"
fieldNameY = "Y"
fieldNameAA = "AA"
fieldNameII = "II"
fieldNameBDotB = "B.B"
entityProtoMeaning = pb.Property_ENTITY_PROTO
TRUE = true
FALSE = false
)
var (
simpleEntityProto, nestedSimpleEntityProto,
simpleTwoFieldsEntityProto, simpleWithTagEntityProto,
bDotBEntityProto, withKeyEntityProto string
)
func init() {
// simpleEntityProto corresponds to:
// Simple{I: testInt64}
simpleEntityProtob, err := proto.Marshal(&pb.EntityProto{
Key: keyToProto("", incompleteKey),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameI,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
Multiple: &FALSE,
},
},
EntityGroup: &pb.Path{},
})
if err != nil {
panic(err)
}
simpleEntityProto = string(simpleEntityProtob)
// nestedSimpleEntityProto corresponds to:
// NestedSimple{
// A: Simple{I: testInt64},
// I: testInt64,
// }
nestedSimpleEntityProtob, err := proto.Marshal(&pb.EntityProto{
Key: keyToProto("", incompleteKey),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameA,
Meaning: &entityProtoMeaning,
Value: &pb.PropertyValue{
StringValue: &simpleEntityProto,
},
Multiple: &FALSE,
},
&pb.Property{
Name: &fieldNameI,
Meaning: &entityProtoMeaning,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
Multiple: &FALSE,
},
},
EntityGroup: &pb.Path{},
})
if err != nil {
panic(err)
}
nestedSimpleEntityProto = string(nestedSimpleEntityProtob)
// simpleTwoFieldsEntityProto corresponds to:
// SimpleTwoFields{S: testString2, SS: testString3}
simpleTwoFieldsEntityProtob, err := proto.Marshal(&pb.EntityProto{
Key: keyToProto("", incompleteKey),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameS,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
Multiple: &FALSE,
},
&pb.Property{
Name: &fieldNameSS,
Value: &pb.PropertyValue{
StringValue: &testString3,
},
Multiple: &FALSE,
},
},
EntityGroup: &pb.Path{},
})
if err != nil {
panic(err)
}
simpleTwoFieldsEntityProto = string(simpleTwoFieldsEntityProtob)
// simpleWithTagEntityProto corresponds to:
// SimpleWithTag{I: testInt64}
simpleWithTagEntityProtob, err := proto.Marshal(&pb.EntityProto{
Key: keyToProto("", incompleteKey),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameII,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
Multiple: &FALSE,
},
},
EntityGroup: &pb.Path{},
})
if err != nil {
panic(err)
}
simpleWithTagEntityProto = string(simpleWithTagEntityProtob)
// bDotBEntityProto corresponds to:
// BDotB{
// B: testString2,
// }
bDotBEntityProtob, err := proto.Marshal(&pb.EntityProto{
Key: keyToProto("", incompleteKey),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameBDotB,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
Multiple: &FALSE,
},
},
EntityGroup: &pb.Path{},
})
if err != nil {
panic(err)
}
bDotBEntityProto = string(bDotBEntityProtob)
// withKeyEntityProto corresponds to:
// WithKey{
// X: testString3,
// I: testInt64,
// K: testKey1a,
// }
withKeyEntityProtob, err := proto.Marshal(&pb.EntityProto{
Key: keyToProto("", testKey1a),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameX,
Value: &pb.PropertyValue{
StringValue: &testString3,
},
Multiple: &FALSE,
},
&pb.Property{
Name: &fieldNameI,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
Multiple: &FALSE,
},
},
EntityGroup: &pb.Path{},
})
if err != nil {
panic(err)
}
withKeyEntityProto = string(withKeyEntityProtob)
}
func TestLoadEntityNested(t *testing.T) {
testCases := []struct {
desc string
src *pb.EntityProto
want interface{}
}{
{
"nested basic",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameA,
Meaning: &entityProtoMeaning,
Value: &pb.PropertyValue{
StringValue: &simpleEntityProto,
},
},
&pb.Property{
Name: &fieldNameI,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
},
},
},
&NestedSimple{
A: Simple{I: 2},
I: 2,
},
},
{
"nested with struct tags",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameAA,
Meaning: &entityProtoMeaning,
Value: &pb.PropertyValue{
StringValue: &simpleWithTagEntityProto,
},
},
},
},
&NestedSimpleWithTag{
A: SimpleWithTag{I: testInt64},
},
},
{
"nested 2x",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameAA,
Meaning: &entityProtoMeaning,
Value: &pb.PropertyValue{
StringValue: &nestedSimpleEntityProto,
},
},
&pb.Property{
Name: &fieldNameA,
Meaning: &entityProtoMeaning,
Value: &pb.PropertyValue{
StringValue: &simpleTwoFieldsEntityProto,
},
},
&pb.Property{
Name: &fieldNameS,
Value: &pb.PropertyValue{
StringValue: &testString3,
},
},
},
},
&NestedSimple2X{
AA: NestedSimple{
A: Simple{I: testInt64},
I: testInt64,
},
A: SimpleTwoFields{S: testString2, SS: testString3},
S: testString3,
},
},
{
"nested anonymous",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameI,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
},
&pb.Property{
Name: &fieldNameX,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
},
},
},
&NestedSimpleAnonymous{
Simple: Simple{I: testInt64},
X: testString2,
},
},
{
"nested simple with slice",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameA,
Meaning: &entityProtoMeaning,
Multiple: &TRUE,
Value: &pb.PropertyValue{
StringValue: &simpleEntityProto,
},
},
&pb.Property{
Name: &fieldNameA,
Meaning: &entityProtoMeaning,
Multiple: &TRUE,
Value: &pb.PropertyValue{
StringValue: &simpleEntityProto,
},
},
},
},
&NestedSliceOfSimple{
A: []Simple{Simple{I: testInt64}, Simple{I: testInt64}},
},
},
{
"nested with multiple anonymous fields",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameI,
Value: &pb.PropertyValue{
Int64Value: &testInt64,
},
},
&pb.Property{
Name: &fieldNameS,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
},
&pb.Property{
Name: &fieldNameSS,
Value: &pb.PropertyValue{
StringValue: &testString3,
},
},
&pb.Property{
Name: &fieldNameX,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
},
},
},
&MultiAnonymous{
Simple: Simple{I: testInt64},
SimpleTwoFields: SimpleTwoFields{S: testString2, SS: testString3},
X: testString2,
},
},
{
"nested with dotted field tag",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameA,
Meaning: &entityProtoMeaning,
Value: &pb.PropertyValue{
StringValue: &bDotBEntityProto,
},
},
},
},
&ABDotB{
A: BDotB{
B: testString2,
},
},
},
{
"nested entity with key",
&pb.EntityProto{
Key: keyToProto("some-app-id", testKey0),
Property: []*pb.Property{
&pb.Property{
Name: &fieldNameY,
Value: &pb.PropertyValue{
StringValue: &testString2,
},
},
&pb.Property{
Name: &fieldNameN,
Meaning: &entityProtoMeaning,
Value: &pb.PropertyValue{
StringValue: &withKeyEntityProto,
},
},
},
},
&NestedWithKey{
Y: testString2,
N: WithKey{
X: testString3,
I: testInt64,
K: testKey1a,
},
},
},
}
for _, tc := range testCases {
dst := reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
err := loadEntity(dst, tc.src)
if err != nil {
t.Errorf("loadEntity: %s: %v", tc.desc, err)
continue
}
if !reflect.DeepEqual(tc.want, dst) {
t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, dst, tc.want)
}
}
}

View file

@ -64,7 +64,7 @@ func KindProperties(ctx context.Context, kind string) (map[string][]string, erro
propMap := map[string][]string{}
props := []struct {
Repr []string `datastore:property_representation`
Repr []string `datastore:"property_representation"`
}{}
keys, err := q.GetAll(ctx, &props)

View file

@ -36,6 +36,7 @@ type Property struct {
// - appengine.BlobKey
// - appengine.GeoPoint
// - []byte (up to 1 megabyte in length)
// - *Entity (representing a nested struct)
// This set is smaller than the set of valid struct field types that the
// datastore can load and save. A Property Value cannot be a slice (apart
// from []byte); use multiple Properties instead. Also, a Value's type
@ -63,6 +64,13 @@ type Property struct {
Multiple bool
}
// An Entity is the value type for a nested struct.
// This type is only used for a Property's Value.
type Entity struct {
Key *Key
Properties []Property
}
// ByteString is a short byte slice (up to 1500 bytes) that can be indexed.
type ByteString []byte
@ -119,25 +127,18 @@ func validPropertyName(name string) bool {
return true
}
// structTag is the parsed `datastore:"name,options"` tag of a struct field.
// If a field has no tag, or the tag has an empty name, then the structTag's
// name is just the field name. A "-" name means that the datastore ignores
// that field.
type structTag struct {
name string
noIndex bool
}
// structCodec describes how to convert a struct to and from a sequence of
// properties.
type structCodec struct {
// byIndex gives the structTag for the i'th field.
byIndex []structTag
// byName gives the field codec for the structTag with the given name.
byName map[string]fieldCodec
// fields gives the field codec for the structTag with the given name.
fields map[string]fieldCodec
// hasSlice is whether a struct or any of its nested or embedded structs
// has a slice-typed field (other than []byte).
hasSlice bool
// keyField is the index of a *Key field with structTag __key__.
// This field is not relevant for the top level struct, only for
// nested structs.
keyField int
// complete is whether the structCodec is complete. An incomplete
// structCodec may be encountered when walking a recursive struct.
complete bool
@ -146,8 +147,15 @@ type structCodec struct {
// fieldCodec is a struct field's index and, if that struct field's type is
// itself a struct, that substruct's structCodec.
type fieldCodec struct {
index int
substructCodec *structCodec
// path is the index path to the field
path []int
noIndex bool
// omitEmpty indicates that the field should be omitted on save
// if empty.
omitEmpty bool
// structCodec is the codec fot the struct field at index 'path',
// or nil if the field is not a struct.
structCodec *structCodec
}
// structCodecs collects the structCodecs that have already been calculated.
@ -171,8 +179,10 @@ func getStructCodecLocked(t reflect.Type) (ret *structCodec, retErr error) {
return c, nil
}
c = &structCodec{
byIndex: make([]structTag, t.NumField()),
byName: make(map[string]fieldCodec),
fields: make(map[string]fieldCodec),
// We initialize keyField to -1 so that the zero-value is not
// misinterpreted as index 0.
keyField: -1,
}
// Add c to the structCodecs map before we are sure it is good. If t is
@ -185,22 +195,34 @@ func getStructCodecLocked(t reflect.Type) (ret *structCodec, retErr error) {
}
}()
for i := range c.byIndex {
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
// Skip unexported fields.
// Note that if f is an anonymous, unexported struct field,
// we will promote its fields.
if f.PkgPath != "" && !f.Anonymous {
continue
}
tags := strings.Split(f.Tag.Get("datastore"), ",")
name := tags[0]
opts := make(map[string]bool)
for _, t := range tags[1:] {
opts[t] = true
}
if name == "" {
switch {
case name == "":
if !f.Anonymous {
name = f.Name
}
} else if name == "-" {
c.byIndex[i] = structTag{name: name}
case name == "-":
continue
} else if !validPropertyName(name) {
case name == "__key__":
if f.Type != typeOfKeyPtr {
return nil, fmt.Errorf("datastore: __key__ field on struct %v is not a *datastore.Key", t)
}
c.keyField = i
case !validPropertyName(name):
return nil, fmt.Errorf("datastore: struct tag has invalid property name: %q", name)
}
@ -216,11 +238,10 @@ func getStructCodecLocked(t reflect.Type) (ret *structCodec, retErr error) {
c.hasSlice = c.hasSlice || fIsSlice
}
var sub *structCodec
if substructType != nil && substructType != typeOfTime && substructType != typeOfGeoPoint {
if name != "" {
name = name + "."
}
sub, err := getStructCodecLocked(substructType)
var err error
sub, err = getStructCodecLocked(substructType)
if err != nil {
return nil, err
}
@ -232,23 +253,35 @@ func getStructCodecLocked(t reflect.Type) (ret *structCodec, retErr error) {
"datastore: flattening nested structs leads to a slice of slices: field %q", f.Name)
}
c.hasSlice = c.hasSlice || sub.hasSlice
for relName := range sub.byName {
absName := name + relName
if _, ok := c.byName[absName]; ok {
return nil, fmt.Errorf("datastore: struct tag has repeated property name: %q", absName)
// If f is an anonymous struct field, we promote the substruct's fields up to this level
// in the linked list of struct codecs.
if f.Anonymous {
for subname, subfield := range sub.fields {
if name != "" {
subname = name + "." + subname
}
if _, ok := c.fields[subname]; ok {
return nil, fmt.Errorf("datastore: struct tag has repeated property name: %q", subname)
}
c.fields[subname] = fieldCodec{
path: append([]int{i}, subfield.path...),
noIndex: subfield.noIndex || opts["noindex"],
omitEmpty: subfield.omitEmpty,
structCodec: subfield.structCodec,
}
}
c.byName[absName] = fieldCodec{index: i, substructCodec: sub}
continue
}
} else {
if _, ok := c.byName[name]; ok {
return nil, fmt.Errorf("datastore: struct tag has repeated property name: %q", name)
}
c.byName[name] = fieldCodec{index: i}
}
c.byIndex[i] = structTag{
name: name,
noIndex: opts["noindex"],
if _, ok := c.fields[name]; ok {
return nil, fmt.Errorf("datastore: struct tag has repeated property name: %q", name)
}
c.fields[name] = fieldCodec{
path: []int{i},
noIndex: opts["noindex"],
omitEmpty: opts["omitempty"],
structCodec: sub,
}
}
c.complete = true
@ -261,8 +294,9 @@ type structPLS struct {
codec *structCodec
}
// newStructPLS returns a PropertyLoadSaver for the struct pointer p.
func newStructPLS(p interface{}) (PropertyLoadSaver, error) {
// newStructPLS returns a structPLS, which implements the
// PropertyLoadSaver interface, for the struct pointer p.
func newStructPLS(p interface{}) (*structPLS, error) {
v := reflect.ValueOf(p)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return nil, ErrInvalidEntityType
@ -272,7 +306,7 @@ func newStructPLS(p interface{}) (PropertyLoadSaver, error) {
if err != nil {
return nil, err
}
return structPLS{v, codec}, nil
return &structPLS{v, codec}, nil
}
// LoadStruct loads the properties from p to dst.

View file

@ -6,6 +6,7 @@ package datastore
import (
"reflect"
"sort"
"testing"
"time"
@ -80,58 +81,36 @@ func TestStructCodec(t *testing.T) {
V string `datastore:",noindex"`
}
oStructCodec := &structCodec{
byIndex: []structTag{
{name: "O"},
},
byName: map[string]fieldCodec{
"O": {index: 0},
fields: map[string]fieldCodec{
"O": {path: []int{0}},
},
complete: true,
}
pStructCodec := &structCodec{
byIndex: []structTag{
{name: "P"},
{name: "Q"},
},
byName: map[string]fieldCodec{
"P": {index: 0},
"Q": {index: 1},
fields: map[string]fieldCodec{
"P": {path: []int{0}},
"Q": {path: []int{1}},
},
complete: true,
}
rStructCodec := &structCodec{
byIndex: []structTag{
{name: "R"},
{name: "S."},
{name: "T."},
{name: ""},
},
byName: map[string]fieldCodec{
"R": {index: 0},
"S.P": {index: 1, substructCodec: pStructCodec},
"S.Q": {index: 1, substructCodec: pStructCodec},
"T.O": {index: 2, substructCodec: oStructCodec},
"O": {index: 3, substructCodec: oStructCodec},
fields: map[string]fieldCodec{
"R": {path: []int{0}},
"S": {path: []int{1}, structCodec: pStructCodec},
"T": {path: []int{2}, structCodec: oStructCodec},
"O": {path: []int{3, 0}},
},
complete: true,
}
uStructCodec := &structCodec{
byIndex: []structTag{
{name: "U"},
{name: "v"},
},
byName: map[string]fieldCodec{
"U": {index: 0},
"v": {index: 1},
fields: map[string]fieldCodec{
"U": {path: []int{0}},
},
complete: true,
}
vStructCodec := &structCodec{
byIndex: []structTag{
{name: "V", noIndex: true},
},
byName: map[string]fieldCodec{
"V": {index: 0},
fields: map[string]fieldCodec{
"V": {path: []int{0}, noIndex: true},
},
complete: true,
}
@ -169,15 +148,10 @@ func TestStructCodec(t *testing.T) {
T time.Time
}{},
&structCodec{
byIndex: []structTag{
{name: "B"},
{name: "K"},
{name: "T"},
},
byName: map[string]fieldCodec{
"B": {index: 0},
"K": {index: 1},
"T": {index: 2},
fields: map[string]fieldCodec{
"B": {path: []int{0}},
"K": {path: []int{1}},
"T": {path: []int{2}},
},
complete: true,
},
@ -195,23 +169,13 @@ func TestStructCodec(t *testing.T) {
oStruct `datastore:"-"`
}{},
&structCodec{
byIndex: []structTag{
{name: "a", noIndex: true},
{name: "b", noIndex: false},
{name: "C", noIndex: true},
{name: "D", noIndex: false},
{name: "E", noIndex: false},
{name: "-", noIndex: false},
{name: "J", noIndex: true},
{name: "-", noIndex: false},
},
byName: map[string]fieldCodec{
"a": {index: 0},
"b": {index: 1},
"C": {index: 2},
"D": {index: 3},
"E": {index: 4},
"J": {index: 6},
fields: map[string]fieldCodec{
"a": {path: []int{0}, noIndex: true},
"b": {path: []int{1}},
"C": {path: []int{2}, noIndex: true},
"D": {path: []int{3}},
"E": {path: []int{4}},
"J": {path: []int{6}, noIndex: true},
},
complete: true,
},
@ -225,17 +189,9 @@ func TestStructCodec(t *testing.T) {
d int `datastore:"Y"`
}{},
&structCodec{
byIndex: []structTag{
{name: "A"},
{name: "b"},
{name: "x"},
{name: "Y"},
},
byName: map[string]fieldCodec{
"A": {index: 0},
"b": {index: 1},
"x": {index: 2},
"Y": {index: 3},
fields: map[string]fieldCodec{
"A": {path: []int{0}},
"x": {path: []int{2}},
},
complete: true,
},
@ -250,23 +206,12 @@ func TestStructCodec(t *testing.T) {
oStruct
}{},
&structCodec{
byIndex: []structTag{
{name: "A"},
{name: "B"},
{name: "CC."},
{name: "DDD."},
{name: ""},
},
byName: map[string]fieldCodec{
"A": {index: 0},
"B": {index: 1},
"CC.O": {index: 2, substructCodec: oStructCodec},
"DDD.R": {index: 3, substructCodec: rStructCodec},
"DDD.S.P": {index: 3, substructCodec: rStructCodec},
"DDD.S.Q": {index: 3, substructCodec: rStructCodec},
"DDD.T.O": {index: 3, substructCodec: rStructCodec},
"DDD.O": {index: 3, substructCodec: rStructCodec},
"O": {index: 4, substructCodec: oStructCodec},
fields: map[string]fieldCodec{
"A": {path: []int{0}},
"B": {path: []int{1}},
"CC": {path: []int{2}, structCodec: oStructCodec},
"DDD": {path: []int{3}, structCodec: rStructCodec},
"O": {path: []int{4, 0}},
},
complete: true,
},
@ -281,22 +226,11 @@ func TestStructCodec(t *testing.T) {
oStruct `datastore:"z"`
}{},
&structCodec{
byIndex: []structTag{
{name: "-"},
{name: "w"},
{name: "xx."},
{name: "y."},
{name: "z."},
},
byName: map[string]fieldCodec{
"w": {index: 1},
"xx.O": {index: 2, substructCodec: oStructCodec},
"y.R": {index: 3, substructCodec: rStructCodec},
"y.S.P": {index: 3, substructCodec: rStructCodec},
"y.S.Q": {index: 3, substructCodec: rStructCodec},
"y.T.O": {index: 3, substructCodec: rStructCodec},
"y.O": {index: 3, substructCodec: rStructCodec},
"z.O": {index: 4, substructCodec: oStructCodec},
fields: map[string]fieldCodec{
"w": {path: []int{1}},
"xx": {path: []int{2}, structCodec: oStructCodec},
"y": {path: []int{3}, structCodec: rStructCodec},
"z.O": {path: []int{4, 0}},
},
complete: true,
},
@ -311,22 +245,10 @@ func TestStructCodec(t *testing.T) {
uStruct
}{},
&structCodec{
byIndex: []structTag{
{name: "a"},
{name: "B"},
{name: "c."},
{name: "D."},
{name: ""},
},
byName: map[string]fieldCodec{
"a": {index: 0},
"B": {index: 1},
"c.U": {index: 2, substructCodec: uStructCodec},
"c.v": {index: 2, substructCodec: uStructCodec},
"D.U": {index: 3, substructCodec: uStructCodec},
"D.v": {index: 3, substructCodec: uStructCodec},
"U": {index: 4, substructCodec: uStructCodec},
"v": {index: 4, substructCodec: uStructCodec},
fields: map[string]fieldCodec{
"B": {path: []int{1}},
"D": {path: []int{3}, structCodec: uStructCodec},
"U": {path: []int{4, 0}},
},
complete: true,
},
@ -337,11 +259,8 @@ func TestStructCodec(t *testing.T) {
A oStruct `datastore:",noindex"`
}{},
&structCodec{
byIndex: []structTag{
{name: "A.", noIndex: true},
},
byName: map[string]fieldCodec{
"A.O": {index: 0, substructCodec: oStructCodec},
fields: map[string]fieldCodec{
"A": {path: []int{0}, structCodec: oStructCodec, noIndex: true},
},
complete: true,
},
@ -352,11 +271,8 @@ func TestStructCodec(t *testing.T) {
A []string `datastore:",noindex"`
}{},
&structCodec{
byIndex: []structTag{
{name: "A", noIndex: true},
},
byName: map[string]fieldCodec{
"A": {index: 0},
fields: map[string]fieldCodec{
"A": {path: []int{0}, noIndex: true},
},
hasSlice: true,
complete: true,
@ -369,11 +285,8 @@ func TestStructCodec(t *testing.T) {
A []vStruct `datastore:",noindex"`
}{},
&structCodec{
byIndex: []structTag{
{name: "A.", noIndex: true},
},
byName: map[string]fieldCodec{
"A.V": {index: 0, substructCodec: vStructCodec},
fields: map[string]fieldCodec{
"A": {path: []int{0}, structCodec: vStructCodec, noIndex: true},
},
hasSlice: true,
complete: true,
@ -387,13 +300,44 @@ func TestStructCodec(t *testing.T) {
t.Errorf("%s: getStructCodec: %v", tc.desc, err)
continue
}
if !reflect.DeepEqual(got, tc.want) {
// can't reflect.DeepEqual b/c element order in fields map may differ
if !isEqualStructCodec(got, tc.want) {
t.Errorf("%s\ngot %+v\nwant %+v\n", tc.desc, got, tc.want)
continue
}
}
}
func isEqualStructCodec(got, want *structCodec) bool {
if got.complete != want.complete {
return false
}
if got.hasSlice != want.hasSlice {
return false
}
if len(got.fields) != len(want.fields) {
return false
}
for name, wantF := range want.fields {
gotF := got.fields[name]
if !reflect.DeepEqual(wantF.path, gotF.path) {
return false
}
if wantF.noIndex != gotF.noIndex {
return false
}
if wantF.structCodec != nil {
if gotF.structCodec == nil {
return false
}
if !isEqualStructCodec(gotF.structCodec, wantF.structCodec) {
return false
}
}
}
return true
}
func TestRepeatedPropertyName(t *testing.T) {
good := []interface{}{
struct {
@ -453,7 +397,7 @@ func TestRepeatedPropertyName(t *testing.T) {
}
func TestFlatteningNestedStructs(t *testing.T) {
type deepGood struct {
type DeepGood struct {
A struct {
B []struct {
C struct {
@ -462,7 +406,7 @@ func TestFlatteningNestedStructs(t *testing.T) {
}
}
}
type deepBad struct {
type DeepBad struct {
A struct {
B []struct {
C struct {
@ -471,16 +415,16 @@ func TestFlatteningNestedStructs(t *testing.T) {
}
}
}
type iSay struct {
type ISay struct {
Tomato int
}
type youSay struct {
type YouSay struct {
Tomato int
}
type tweedledee struct {
type Tweedledee struct {
Dee int `datastore:"D"`
}
type tweedledum struct {
type Tweedledum struct {
Dum int `datastore:"D"`
}
@ -515,18 +459,18 @@ func TestFlatteningNestedStructs(t *testing.T) {
Q []int
}{},
struct {
deepGood
DeepGood
}{},
struct {
DG deepGood
DG DeepGood
}{},
struct {
Foo struct {
Z int `datastore:"X"`
Z int
} `datastore:"A"`
Bar struct {
Z int `datastore:"Y"`
} `datastore:"A"`
Z int
} `datastore:"B"`
}{},
}
bad := []interface{}{
@ -541,18 +485,18 @@ func TestFlatteningNestedStructs(t *testing.T) {
}
}{},
struct {
deepBad
DeepBad
}{},
struct {
DB deepBad
DB DeepBad
}{},
struct {
iSay
youSay
ISay
YouSay
}{},
struct {
tweedledee
tweedledum
Tweedledee
Tweedledum
}{},
struct {
Foo struct {
@ -602,3 +546,127 @@ func TestNilKeyIsStored(t *testing.T) {
t.Errorf("I field was not zero")
}
}
func TestSaveStructOmitEmpty(t *testing.T) {
// Expected props names are sorted alphabetically
expectedPropNamesForSingles := []string{"EmptyValue", "NonEmptyValue", "OmitEmptyWithValue"}
expectedPropNamesForSlices := []string{"NonEmptyValue", "NonEmptyValue", "OmitEmptyWithValue", "OmitEmptyWithValue"}
testOmitted := func(expectedPropNames []string, src interface{}) {
// t.Helper() - this is available from Go version 1.9, but we also support Go versions 1.6, 1.7, 1.8
if props, err := SaveStruct(src); err != nil {
t.Fatal(err)
} else {
// Collect names for reporting if diffs from expected and for easier sorting
actualPropNames := make([]string, len(props))
for i := range props {
actualPropNames[i] = props[i].Name
}
// Sort actuals for comparing with already sorted expected names
sort.Sort(sort.StringSlice(actualPropNames))
if !reflect.DeepEqual(actualPropNames, expectedPropNames) {
t.Errorf("Expected this properties: %v, got: %v", expectedPropNames, actualPropNames)
}
}
}
testOmitted(expectedPropNamesForSingles, &struct {
EmptyValue int
NonEmptyValue int
OmitEmptyNoValue int `datastore:",omitempty"`
OmitEmptyWithValue int `datastore:",omitempty"`
}{
NonEmptyValue: 1,
OmitEmptyWithValue: 2,
})
testOmitted(expectedPropNamesForSlices, &struct {
EmptyValue []int
NonEmptyValue []int
OmitEmptyNoValue []int `datastore:",omitempty"`
OmitEmptyWithValue []int `datastore:",omitempty"`
}{
NonEmptyValue: []int{1, 2},
OmitEmptyWithValue: []int{3, 4},
})
testOmitted(expectedPropNamesForSingles, &struct {
EmptyValue bool
NonEmptyValue bool
OmitEmptyNoValue bool `datastore:",omitempty"`
OmitEmptyWithValue bool `datastore:",omitempty"`
}{
NonEmptyValue: true,
OmitEmptyWithValue: true,
})
testOmitted(expectedPropNamesForSlices, &struct {
EmptyValue []bool
NonEmptyValue []bool
OmitEmptyNoValue []bool `datastore:",omitempty"`
OmitEmptyWithValue []bool `datastore:",omitempty"`
}{
NonEmptyValue: []bool{true, true},
OmitEmptyWithValue: []bool{true, true},
})
testOmitted(expectedPropNamesForSingles, &struct {
EmptyValue string
NonEmptyValue string
OmitEmptyNoValue string `datastore:",omitempty"`
OmitEmptyWithValue string `datastore:",omitempty"`
}{
NonEmptyValue: "s",
OmitEmptyWithValue: "s",
})
testOmitted(expectedPropNamesForSlices, &struct {
EmptyValue []string
NonEmptyValue []string
OmitEmptyNoValue []string `datastore:",omitempty"`
OmitEmptyWithValue []string `datastore:",omitempty"`
}{
NonEmptyValue: []string{"s1", "s2"},
OmitEmptyWithValue: []string{"s3", "s4"},
})
testOmitted(expectedPropNamesForSingles, &struct {
EmptyValue float32
NonEmptyValue float32
OmitEmptyNoValue float32 `datastore:",omitempty"`
OmitEmptyWithValue float32 `datastore:",omitempty"`
}{
NonEmptyValue: 1.1,
OmitEmptyWithValue: 1.2,
})
testOmitted(expectedPropNamesForSlices, &struct {
EmptyValue []float32
NonEmptyValue []float32
OmitEmptyNoValue []float32 `datastore:",omitempty"`
OmitEmptyWithValue []float32 `datastore:",omitempty"`
}{
NonEmptyValue: []float32{1.1, 2.2},
OmitEmptyWithValue: []float32{3.3, 4.4},
})
testOmitted(expectedPropNamesForSingles, &struct {
EmptyValue time.Time
NonEmptyValue time.Time
OmitEmptyNoValue time.Time `datastore:",omitempty"`
OmitEmptyWithValue time.Time `datastore:",omitempty"`
}{
NonEmptyValue: now,
OmitEmptyWithValue: now,
})
testOmitted(expectedPropNamesForSlices, &struct {
EmptyValue []time.Time
NonEmptyValue []time.Time
OmitEmptyNoValue []time.Time `datastore:",omitempty"`
OmitEmptyWithValue []time.Time `datastore:",omitempty"`
}{
NonEmptyValue: []time.Time{now, now},
OmitEmptyWithValue: []time.Time{now, now},
})
}

View file

@ -87,6 +87,7 @@ type Query struct {
eventual bool
limit int32
offset int32
count int32
start *pb.CompiledCursor
end *pb.CompiledCursor
@ -241,6 +242,19 @@ func (q *Query) Offset(offset int) *Query {
return q
}
// BatchSize returns a derivative query to fetch the supplied number of results
// at once. This value should be greater than zero, and equal to or less than
// the Limit.
func (q *Query) BatchSize(size int) *Query {
q = q.clone()
if size <= 0 || size > math.MaxInt32 {
q.err = errors.New("datastore: query batch size overflow")
return q
}
q.count = int32(size)
return q
}
// Start returns a derivative query with the given start point.
func (q *Query) Start(c Cursor) *Query {
q = q.clone()
@ -325,6 +339,9 @@ func (q *Query) toProto(dst *pb.Query, appID string) error {
if q.offset != 0 {
dst.Offset = proto.Int32(q.offset)
}
if q.count != 0 {
dst.Count = proto.Int32(q.count)
}
dst.CompiledCursor = q.start
dst.EndCompiledCursor = q.end
dst.Compile = proto.Bool(true)
@ -394,7 +411,7 @@ func (q *Query) Count(c context.Context) (int, error) {
if !res.GetMoreResults() {
break
}
if err := callNext(c, res, newQ.offset-n, 0); err != nil {
if err := callNext(c, res, newQ.offset-n, q.count); err != nil {
return 0, err
}
}
@ -409,15 +426,15 @@ func (q *Query) Count(c context.Context) (int, error) {
// callNext issues a datastore_v3/Next RPC to advance a cursor, such as that
// returned by a query with more results.
func callNext(c context.Context, res *pb.QueryResult, offset, limit int32) error {
func callNext(c context.Context, res *pb.QueryResult, offset, count int32) error {
if res.Cursor == nil {
return errors.New("datastore: internal error: server did not return a cursor")
}
req := &pb.NextRequest{
Cursor: res.Cursor,
}
if limit >= 0 {
req.Count = proto.Int32(limit)
if count >= 0 {
req.Count = proto.Int32(count)
}
if offset != 0 {
req.Offset = proto.Int32(offset)
@ -445,7 +462,7 @@ func callNext(c context.Context, res *pb.QueryResult, offset, limit int32) error
// If q is a ``keys-only'' query, GetAll ignores dst and only returns the keys.
//
// The running time and number of API calls made by GetAll scale linearly with
// with the sum of the query's offset and limit. Unless the result count is
// the sum of the query's offset and limit. Unless the result count is
// expected to be small, it is best to specify a limit; otherwise GetAll will
// continue until it finishes collecting results or the provided context
// expires.
@ -523,6 +540,7 @@ func (q *Query) Run(c context.Context) *Iterator {
t := &Iterator{
c: c,
limit: q.limit,
count: q.count,
q: q,
prevCC: q.start,
}
@ -536,9 +554,15 @@ func (q *Query) Run(c context.Context) *Iterator {
return t
}
offset := q.offset - t.res.GetSkippedResults()
var count int32
if t.count > 0 && (t.limit < 0 || t.count < t.limit) {
count = t.count
} else {
count = t.limit
}
for offset > 0 && t.res.GetMoreResults() {
t.prevCC = t.res.CompiledCursor
if err := callNext(t.c, &t.res, offset, t.limit); err != nil {
if err := callNext(t.c, &t.res, offset, count); err != nil {
t.err = err
break
}
@ -566,6 +590,9 @@ type Iterator struct {
// limit is the limit on the number of results this iterator should return.
// A negative value means unlimited.
limit int32
// count is the number of results this iterator should fetch at once. This
// should be equal to or greater than zero.
count int32
// q is the original query which yielded this iterator.
q *Query
// prevCC is the compiled cursor that marks the end of the previous batch
@ -605,7 +632,13 @@ func (t *Iterator) next() (*Key, *pb.EntityProto, error) {
return nil, nil, t.err
}
t.prevCC = t.res.CompiledCursor
if err := callNext(t.c, &t.res, 0, t.limit); err != nil {
var count int32
if t.count > 0 && (t.limit < 0 || t.count < t.limit) {
count = t.count
} else {
count = t.limit
}
if err := callNext(t.c, &t.res, 0, count); err != nil {
t.err = err
return nil, nil, t.err
}

View file

@ -464,7 +464,7 @@ func TestQueryToProto(t *testing.T) {
},
{
desc: "standard query",
query: NewQuery("kind").Order("-I").Filter("I >", 17).Filter("U =", "Dave").Limit(7).Offset(42),
query: NewQuery("kind").Order("-I").Filter("I >", 17).Filter("U =", "Dave").Limit(7).Offset(42).BatchSize(5),
want: &pb.Query{
Kind: proto.String("kind"),
Filter: []*pb.Query_Filter{
@ -497,6 +497,7 @@ func TestQueryToProto(t *testing.T) {
},
Limit: proto.Int32(7),
Offset: proto.Int32(42),
Count: proto.Int32(5),
},
},
{

View file

@ -111,6 +111,12 @@ func valueToProto(defaultAppID, name string, v reflect.Value, multiple bool) (p
return p, ""
}
type saveOpts struct {
noIndex bool
multiple bool
omitEmpty bool
}
// saveEntity saves an EntityProto into a PropertyLoadSaver or struct pointer.
func saveEntity(defaultAppID string, key *Key, src interface{}) (*pb.EntityProto, error) {
var err error
@ -126,11 +132,14 @@ func saveEntity(defaultAppID string, key *Key, src interface{}) (*pb.EntityProto
return propertiesToProto(defaultAppID, key, props)
}
func saveStructProperty(props *[]Property, name string, noIndex, multiple bool, v reflect.Value) error {
func saveStructProperty(props *[]Property, name string, opts saveOpts, v reflect.Value) error {
if opts.omitEmpty && isEmptyValue(v) {
return nil
}
p := Property{
Name: name,
NoIndex: noIndex,
Multiple: multiple,
NoIndex: opts.noIndex,
Multiple: opts.multiple,
}
switch x := v.Interface().(type) {
case *Key:
@ -166,7 +175,7 @@ func saveStructProperty(props *[]Property, name string, noIndex, multiple bool,
if err != nil {
return fmt.Errorf("datastore: unsupported struct field: %v", err)
}
return sub.(structPLS).save(props, name, noIndex, multiple)
return sub.save(props, name+".", opts)
}
}
if p.Value == nil {
@ -178,37 +187,35 @@ func saveStructProperty(props *[]Property, name string, noIndex, multiple bool,
func (s structPLS) Save() ([]Property, error) {
var props []Property
if err := s.save(&props, "", false, false); err != nil {
if err := s.save(&props, "", saveOpts{}); err != nil {
return nil, err
}
return props, nil
}
func (s structPLS) save(props *[]Property, prefix string, noIndex, multiple bool) error {
for i, t := range s.codec.byIndex {
if t.name == "-" {
continue
}
name := t.name
if prefix != "" {
name = prefix + name
}
v := s.v.Field(i)
func (s structPLS) save(props *[]Property, prefix string, opts saveOpts) error {
for name, f := range s.codec.fields {
name = prefix + name
v := s.v.FieldByIndex(f.path)
if !v.IsValid() || !v.CanSet() {
continue
}
noIndex1 := noIndex || t.noIndex
var opts1 saveOpts
opts1.noIndex = opts.noIndex || f.noIndex
opts1.multiple = opts.multiple
opts1.omitEmpty = f.omitEmpty // don't propagate
// For slice fields that aren't []byte, save each element.
if v.Kind() == reflect.Slice && v.Type().Elem().Kind() != reflect.Uint8 {
opts1.multiple = true
for j := 0; j < v.Len(); j++ {
if err := saveStructProperty(props, name, noIndex1, true, v.Index(j)); err != nil {
if err := saveStructProperty(props, name, opts1, v.Index(j)); err != nil {
return err
}
}
continue
}
// Otherwise, save the field itself.
if err := saveStructProperty(props, name, noIndex1, multiple, v); err != nil {
if err := saveStructProperty(props, name, opts1, v); err != nil {
return err
}
}
@ -298,3 +305,29 @@ func propertiesToProto(defaultAppID string, key *Key, props []Property) (*pb.Ent
}
return e, nil
}
// isEmptyValue is taken from the encoding/json package in the standard library.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
// TODO(perfomance): Only reflect.String needed, other property types are not supported (copy/paste from json package)
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
// TODO(perfomance): Uint* are unsupported property types - should be removed (copy/paste from json package)
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
case reflect.Struct:
switch x := v.Interface().(type) {
case time.Time:
return x.IsZero()
}
}
return false
}

View file

@ -58,12 +58,18 @@ func RunInTransaction(c context.Context, f func(tc context.Context) error, opts
if opts != nil {
xg = opts.XG
}
readOnly := false
if opts != nil {
readOnly = opts.ReadOnly
}
attempts := 3
if opts != nil && opts.Attempts > 0 {
attempts = opts.Attempts
}
var t *pb.Transaction
var err error
for i := 0; i < attempts; i++ {
if err := internal.RunTransactionOnce(c, f, xg); err != internal.ErrConcurrentTransaction {
if t, err = internal.RunTransactionOnce(c, f, xg, readOnly, t); err != internal.ErrConcurrentTransaction {
return err
}
}
@ -84,4 +90,7 @@ type TransactionOptions struct {
// Attempts controls the number of retries to perform when commits fail
// due to a conflicting transaction. If omitted, it defaults to 3.
Attempts int
// ReadOnly controls whether the transaction is a read only transaction.
// Read only transactions are potentially more efficient.
ReadOnly bool
}