+118
@@ -0,0 +1,118 @@
|
||||
// Copyright 2017 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 (
|
||||
"fmt"
|
||||
|
||||
gax "github.com/googleapis/gax-go"
|
||||
|
||||
"cloud.google.com/go/internal"
|
||||
"cloud.google.com/go/internal/version"
|
||||
"golang.org/x/net/context"
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// datastoreClient is a wrapper for the pb.DatastoreClient that includes gRPC
|
||||
// metadata to be sent in each request for server-side traffic management.
|
||||
type datastoreClient struct {
|
||||
// Embed so we still implement the DatastoreClient interface,
|
||||
// if the interface adds more methods.
|
||||
pb.DatastoreClient
|
||||
|
||||
c pb.DatastoreClient
|
||||
md metadata.MD
|
||||
}
|
||||
|
||||
func newDatastoreClient(conn *grpc.ClientConn, projectID string) pb.DatastoreClient {
|
||||
return &datastoreClient{
|
||||
c: pb.NewDatastoreClient(conn),
|
||||
md: metadata.Pairs(
|
||||
resourcePrefixHeader, "projects/"+projectID,
|
||||
"x-goog-api-client", fmt.Sprintf("gl-go/%s gccl/%s grpc/", version.Go(), version.Repo)),
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) Lookup(ctx context.Context, in *pb.LookupRequest, opts ...grpc.CallOption) (res *pb.LookupResponse, err error) {
|
||||
err = dc.invoke(ctx, func(ctx context.Context) error {
|
||||
res, err = dc.c.Lookup(ctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) RunQuery(ctx context.Context, in *pb.RunQueryRequest, opts ...grpc.CallOption) (res *pb.RunQueryResponse, err error) {
|
||||
err = dc.invoke(ctx, func(ctx context.Context) error {
|
||||
res, err = dc.c.RunQuery(ctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) BeginTransaction(ctx context.Context, in *pb.BeginTransactionRequest, opts ...grpc.CallOption) (res *pb.BeginTransactionResponse, err error) {
|
||||
err = dc.invoke(ctx, func(ctx context.Context) error {
|
||||
res, err = dc.c.BeginTransaction(ctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) Commit(ctx context.Context, in *pb.CommitRequest, opts ...grpc.CallOption) (res *pb.CommitResponse, err error) {
|
||||
err = dc.invoke(ctx, func(ctx context.Context) error {
|
||||
res, err = dc.c.Commit(ctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) Rollback(ctx context.Context, in *pb.RollbackRequest, opts ...grpc.CallOption) (res *pb.RollbackResponse, err error) {
|
||||
err = dc.invoke(ctx, func(ctx context.Context) error {
|
||||
res, err = dc.c.Rollback(ctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) AllocateIds(ctx context.Context, in *pb.AllocateIdsRequest, opts ...grpc.CallOption) (res *pb.AllocateIdsResponse, err error) {
|
||||
err = dc.invoke(ctx, func(ctx context.Context) error {
|
||||
res, err = dc.c.AllocateIds(ctx, in, opts...)
|
||||
return err
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) invoke(ctx context.Context, f func(ctx context.Context) error) error {
|
||||
ctx = metadata.NewOutgoingContext(ctx, dc.md)
|
||||
return internal.Retry(ctx, gax.Backoff{}, func() (stop bool, err error) {
|
||||
err = f(ctx)
|
||||
return !shouldRetry(err), err
|
||||
})
|
||||
}
|
||||
|
||||
func shouldRetry(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
s, ok := status.FromError(err)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// See https://cloud.google.com/datastore/docs/concepts/errors.
|
||||
return s.Code() == codes.Unavailable || s.Code() == codes.DeadlineExceeded
|
||||
}
|
||||
+627
@@ -0,0 +1,627 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"cloud.google.com/go/internal/trace"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
gtransport "google.golang.org/api/transport/grpc"
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
prodAddr = "datastore.googleapis.com:443"
|
||||
userAgent = "gcloud-golang-datastore/20160401"
|
||||
)
|
||||
|
||||
// ScopeDatastore grants permissions to view and/or manage datastore entities
|
||||
const ScopeDatastore = "https://www.googleapis.com/auth/datastore"
|
||||
|
||||
// resourcePrefixHeader is the name of the metadata header used to indicate
|
||||
// the resource being operated on.
|
||||
const resourcePrefixHeader = "google-cloud-resource-prefix"
|
||||
|
||||
// Client is a client for reading and writing data in a datastore dataset.
|
||||
type Client struct {
|
||||
conn *grpc.ClientConn
|
||||
client pb.DatastoreClient
|
||||
endpoint string
|
||||
dataset string // Called dataset by the datastore API, synonym for project ID.
|
||||
}
|
||||
|
||||
// NewClient creates a new Client for a given dataset.
|
||||
// If the project ID is empty, it is derived from the DATASTORE_PROJECT_ID environment variable.
|
||||
// If the DATASTORE_EMULATOR_HOST environment variable is set, client will use its value
|
||||
// to connect to a locally-running datastore emulator.
|
||||
func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error) {
|
||||
var o []option.ClientOption
|
||||
// Environment variables for gcd emulator:
|
||||
// https://cloud.google.com/datastore/docs/tools/datastore-emulator
|
||||
// If the emulator is available, dial it directly (and don't pass any credentials).
|
||||
if addr := os.Getenv("DATASTORE_EMULATOR_HOST"); addr != "" {
|
||||
conn, err := grpc.Dial(addr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("grpc.Dial: %v", err)
|
||||
}
|
||||
o = []option.ClientOption{option.WithGRPCConn(conn)}
|
||||
} else {
|
||||
o = []option.ClientOption{
|
||||
option.WithEndpoint(prodAddr),
|
||||
option.WithScopes(ScopeDatastore),
|
||||
option.WithUserAgent(userAgent),
|
||||
}
|
||||
}
|
||||
// Warn if we see the legacy emulator environment variables.
|
||||
if os.Getenv("DATASTORE_HOST") != "" && os.Getenv("DATASTORE_EMULATOR_HOST") == "" {
|
||||
log.Print("WARNING: legacy environment variable DATASTORE_HOST is ignored. Use DATASTORE_EMULATOR_HOST instead.")
|
||||
}
|
||||
if os.Getenv("DATASTORE_DATASET") != "" && os.Getenv("DATASTORE_PROJECT_ID") == "" {
|
||||
log.Print("WARNING: legacy environment variable DATASTORE_DATASET is ignored. Use DATASTORE_PROJECT_ID instead.")
|
||||
}
|
||||
if projectID == "" {
|
||||
projectID = os.Getenv("DATASTORE_PROJECT_ID")
|
||||
}
|
||||
if projectID == "" {
|
||||
return nil, errors.New("datastore: missing project/dataset id")
|
||||
}
|
||||
o = append(o, opts...)
|
||||
conn, err := gtransport.Dial(ctx, o...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing: %v", err)
|
||||
}
|
||||
return &Client{
|
||||
conn: conn,
|
||||
client: newDatastoreClient(conn, projectID),
|
||||
dataset: projectID,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrInvalidEntityType is returned when functions like Get or Next are
|
||||
// passed a dst or src argument of invalid type.
|
||||
ErrInvalidEntityType = errors.New("datastore: invalid entity type")
|
||||
// ErrInvalidKey is returned when an invalid key is presented.
|
||||
ErrInvalidKey = errors.New("datastore: invalid key")
|
||||
// ErrNoSuchEntity is returned when no entity was found for a given key.
|
||||
ErrNoSuchEntity = errors.New("datastore: no such entity")
|
||||
)
|
||||
|
||||
type multiArgType int
|
||||
|
||||
const (
|
||||
multiArgTypeInvalid multiArgType = iota
|
||||
multiArgTypePropertyLoadSaver
|
||||
multiArgTypeStruct
|
||||
multiArgTypeStructPtr
|
||||
multiArgTypeInterface
|
||||
)
|
||||
|
||||
// ErrFieldMismatch is returned when a field is to be loaded into a different
|
||||
// type than the one it was stored from, or when a field is missing or
|
||||
// unexported in the destination struct.
|
||||
// StructType is the type of the struct pointed to by the destination argument
|
||||
// passed to Get or to Iterator.Next.
|
||||
type ErrFieldMismatch struct {
|
||||
StructType reflect.Type
|
||||
FieldName string
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e *ErrFieldMismatch) Error() string {
|
||||
return fmt.Sprintf("datastore: cannot load field %q into a %q: %s",
|
||||
e.FieldName, e.StructType, e.Reason)
|
||||
}
|
||||
|
||||
// GeoPoint represents a location as latitude/longitude in degrees.
|
||||
type GeoPoint struct {
|
||||
Lat, Lng float64
|
||||
}
|
||||
|
||||
// Valid returns whether a GeoPoint is within [-90, 90] latitude and [-180, 180] longitude.
|
||||
func (g GeoPoint) Valid() bool {
|
||||
return -90 <= g.Lat && g.Lat <= 90 && -180 <= g.Lng && g.Lng <= 180
|
||||
}
|
||||
|
||||
func keyToProto(k *Key) *pb.Key {
|
||||
if k == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var path []*pb.Key_PathElement
|
||||
for {
|
||||
el := &pb.Key_PathElement{Kind: k.Kind}
|
||||
if k.ID != 0 {
|
||||
el.IdType = &pb.Key_PathElement_Id{Id: k.ID}
|
||||
} else if k.Name != "" {
|
||||
el.IdType = &pb.Key_PathElement_Name{Name: k.Name}
|
||||
}
|
||||
path = append(path, el)
|
||||
if k.Parent == nil {
|
||||
break
|
||||
}
|
||||
k = k.Parent
|
||||
}
|
||||
|
||||
// The path should be in order [grandparent, parent, child]
|
||||
// We did it backward above, so reverse back.
|
||||
for i := 0; i < len(path)/2; i++ {
|
||||
path[i], path[len(path)-i-1] = path[len(path)-i-1], path[i]
|
||||
}
|
||||
|
||||
key := &pb.Key{Path: path}
|
||||
if k.Namespace != "" {
|
||||
key.PartitionId = &pb.PartitionId{
|
||||
NamespaceId: k.Namespace,
|
||||
}
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// protoToKey decodes a protocol buffer representation of a key into an
|
||||
// equivalent *Key object. If the key is invalid, protoToKey will return the
|
||||
// invalid key along with ErrInvalidKey.
|
||||
func protoToKey(p *pb.Key) (*Key, error) {
|
||||
var key *Key
|
||||
var namespace string
|
||||
if partition := p.PartitionId; partition != nil {
|
||||
namespace = partition.NamespaceId
|
||||
}
|
||||
for _, el := range p.Path {
|
||||
key = &Key{
|
||||
Namespace: namespace,
|
||||
Kind: el.Kind,
|
||||
ID: el.GetId(),
|
||||
Name: el.GetName(),
|
||||
Parent: key,
|
||||
}
|
||||
}
|
||||
if !key.valid() { // Also detects key == nil.
|
||||
return key, ErrInvalidKey
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// multiKeyToProto is a batch version of keyToProto.
|
||||
func multiKeyToProto(keys []*Key) []*pb.Key {
|
||||
ret := make([]*pb.Key, len(keys))
|
||||
for i, k := range keys {
|
||||
ret[i] = keyToProto(k)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// multiKeyToProto is a batch version of keyToProto.
|
||||
func multiProtoToKey(keys []*pb.Key) ([]*Key, error) {
|
||||
hasErr := false
|
||||
ret := make([]*Key, len(keys))
|
||||
err := make(MultiError, len(keys))
|
||||
for i, k := range keys {
|
||||
ret[i], err[i] = protoToKey(k)
|
||||
if err[i] != nil {
|
||||
hasErr = true
|
||||
}
|
||||
}
|
||||
if hasErr {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// multiValid is a batch version of Key.valid. It returns an error, not a
|
||||
// []bool.
|
||||
func multiValid(key []*Key) error {
|
||||
invalid := false
|
||||
for _, k := range key {
|
||||
if !k.valid() {
|
||||
invalid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !invalid {
|
||||
return nil
|
||||
}
|
||||
err := make(MultiError, len(key))
|
||||
for i, k := range key {
|
||||
if !k.valid() {
|
||||
err[i] = ErrInvalidKey
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// checkMultiArg checks that v has type []S, []*S, []I, or []P, for some struct
|
||||
// type S, for some interface type I, or some non-interface non-pointer type P
|
||||
// such that P or *P implements PropertyLoadSaver.
|
||||
//
|
||||
// It returns what category the slice's elements are, and the reflect.Type
|
||||
// that represents S, I or P.
|
||||
//
|
||||
// As a special case, PropertyList is an invalid type for v.
|
||||
//
|
||||
// TODO(djd): multiArg is very confusing. Fold this logic into the
|
||||
// relevant Put/Get methods to make the logic less opaque.
|
||||
func checkMultiArg(v reflect.Value) (m multiArgType, elemType reflect.Type) {
|
||||
if v.Kind() != reflect.Slice {
|
||||
return multiArgTypeInvalid, nil
|
||||
}
|
||||
if v.Type() == typeOfPropertyList {
|
||||
return multiArgTypeInvalid, nil
|
||||
}
|
||||
elemType = v.Type().Elem()
|
||||
if reflect.PtrTo(elemType).Implements(typeOfPropertyLoadSaver) {
|
||||
return multiArgTypePropertyLoadSaver, elemType
|
||||
}
|
||||
switch elemType.Kind() {
|
||||
case reflect.Struct:
|
||||
return multiArgTypeStruct, elemType
|
||||
case reflect.Interface:
|
||||
return multiArgTypeInterface, elemType
|
||||
case reflect.Ptr:
|
||||
elemType = elemType.Elem()
|
||||
if elemType.Kind() == reflect.Struct {
|
||||
return multiArgTypeStructPtr, elemType
|
||||
}
|
||||
}
|
||||
return multiArgTypeInvalid, nil
|
||||
}
|
||||
|
||||
// Close closes the Client.
|
||||
func (c *Client) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// Get loads the entity stored for key into dst, which must be a struct pointer
|
||||
// or implement PropertyLoadSaver. If there is no such entity for the key, Get
|
||||
// returns ErrNoSuchEntity.
|
||||
//
|
||||
// The values of dst's unmatched struct fields are not modified, and matching
|
||||
// slice-typed fields are not reset before appending to them. In particular, it
|
||||
// is recommended to pass a pointer to a zero valued struct on each Get call.
|
||||
//
|
||||
// ErrFieldMismatch is returned when a field is to be loaded into a different
|
||||
// type than the one it was stored from, or when a field is missing or
|
||||
// unexported in the destination struct. ErrFieldMismatch is only returned if
|
||||
// dst is a struct pointer.
|
||||
func (c *Client) Get(ctx context.Context, key *Key, dst interface{}) (err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.Get")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
if dst == nil { // get catches nil interfaces; we need to catch nil ptr here
|
||||
return ErrInvalidEntityType
|
||||
}
|
||||
err = c.get(ctx, []*Key{key}, []interface{}{dst}, nil)
|
||||
if me, ok := err.(MultiError); ok {
|
||||
return me[0]
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMulti is a batch version of Get.
|
||||
//
|
||||
// dst must be a []S, []*S, []I or []P, for some struct type S, some interface
|
||||
// type I, or some non-interface non-pointer type P such that P or *P
|
||||
// implements PropertyLoadSaver. If an []I, each element must be a valid dst
|
||||
// for Get: it must be a struct pointer or implement PropertyLoadSaver.
|
||||
//
|
||||
// As a special case, PropertyList is an invalid type for dst, even though a
|
||||
// PropertyList is a slice of structs. It is treated as invalid to avoid being
|
||||
// mistakenly passed when []PropertyList was intended.
|
||||
func (c *Client) GetMulti(ctx context.Context, keys []*Key, dst interface{}) (err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.GetMulti")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
return c.get(ctx, keys, dst, nil)
|
||||
}
|
||||
|
||||
func (c *Client) get(ctx context.Context, keys []*Key, dst interface{}, opts *pb.ReadOptions) error {
|
||||
v := reflect.ValueOf(dst)
|
||||
multiArgType, _ := checkMultiArg(v)
|
||||
|
||||
// Sanity checks
|
||||
if multiArgType == multiArgTypeInvalid {
|
||||
return errors.New("datastore: dst has invalid type")
|
||||
}
|
||||
if len(keys) != v.Len() {
|
||||
return errors.New("datastore: keys and dst slices have different length")
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Go through keys, validate them, serialize then, and create a dict mapping them to their indices.
|
||||
// Equal keys are deduped.
|
||||
multiErr, any := make(MultiError, len(keys)), false
|
||||
keyMap := make(map[string][]int, len(keys))
|
||||
pbKeys := make([]*pb.Key, 0, len(keys))
|
||||
for i, k := range keys {
|
||||
if !k.valid() {
|
||||
multiErr[i] = ErrInvalidKey
|
||||
any = true
|
||||
} else {
|
||||
ks := k.String()
|
||||
if _, ok := keyMap[ks]; !ok {
|
||||
pbKeys = append(pbKeys, keyToProto(k))
|
||||
}
|
||||
keyMap[ks] = append(keyMap[ks], i)
|
||||
}
|
||||
}
|
||||
if any {
|
||||
return multiErr
|
||||
}
|
||||
req := &pb.LookupRequest{
|
||||
ProjectId: c.dataset,
|
||||
Keys: pbKeys,
|
||||
ReadOptions: opts,
|
||||
}
|
||||
resp, err := c.client.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found := resp.Found
|
||||
missing := resp.Missing
|
||||
// Upper bound 100 iterations to prevent infinite loop.
|
||||
// We choose 100 iterations somewhat logically:
|
||||
// Max number of Entities you can request from Datastore is 1,000.
|
||||
// Max size for a Datastore Entity is 1 MiB.
|
||||
// Max request size is 10 MiB, so we assume max response size is also 10 MiB.
|
||||
// 1,000 / 10 = 100.
|
||||
// Note that if ctx has a deadline, the deadline will probably
|
||||
// be hit before we reach 100 iterations.
|
||||
for i := 0; len(resp.Deferred) > 0 && i < 100; i++ {
|
||||
req.Keys = resp.Deferred
|
||||
resp, err = c.client.Lookup(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
found = append(found, resp.Found...)
|
||||
missing = append(missing, resp.Missing...)
|
||||
}
|
||||
|
||||
filled := 0
|
||||
for _, e := range found {
|
||||
k, err := protoToKey(e.Entity.Key)
|
||||
if err != nil {
|
||||
return errors.New("datastore: internal error: server returned an invalid key")
|
||||
}
|
||||
filled += len(keyMap[k.String()])
|
||||
for _, index := range keyMap[k.String()] {
|
||||
elem := v.Index(index)
|
||||
if multiArgType == multiArgTypePropertyLoadSaver || multiArgType == multiArgTypeStruct {
|
||||
elem = elem.Addr()
|
||||
}
|
||||
if multiArgType == multiArgTypeStructPtr && elem.IsNil() {
|
||||
elem.Set(reflect.New(elem.Type().Elem()))
|
||||
}
|
||||
if err := loadEntityProto(elem.Interface(), e.Entity); err != nil {
|
||||
multiErr[index] = err
|
||||
any = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, e := range missing {
|
||||
k, err := protoToKey(e.Entity.Key)
|
||||
if err != nil {
|
||||
return errors.New("datastore: internal error: server returned an invalid key")
|
||||
}
|
||||
filled += len(keyMap[k.String()])
|
||||
for _, index := range keyMap[k.String()] {
|
||||
multiErr[index] = ErrNoSuchEntity
|
||||
}
|
||||
any = true
|
||||
}
|
||||
|
||||
if filled != len(keys) {
|
||||
return errors.New("datastore: internal error: server returned the wrong number of entities")
|
||||
}
|
||||
|
||||
if any {
|
||||
return multiErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put saves the entity src into the datastore with key k. src must be a struct
|
||||
// pointer or implement PropertyLoadSaver; if a struct pointer then any
|
||||
// unexported fields of that struct will be skipped. If k is an incomplete key,
|
||||
// the returned key will be a unique key generated by the datastore.
|
||||
func (c *Client) Put(ctx context.Context, key *Key, src interface{}) (*Key, error) {
|
||||
k, err := c.PutMulti(ctx, []*Key{key}, []interface{}{src})
|
||||
if err != nil {
|
||||
if me, ok := err.(MultiError); ok {
|
||||
return nil, me[0]
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return k[0], nil
|
||||
}
|
||||
|
||||
// PutMulti is a batch version of Put.
|
||||
//
|
||||
// src must satisfy the same conditions as the dst argument to GetMulti.
|
||||
// TODO(jba): rewrite in terms of Mutate.
|
||||
func (c *Client) PutMulti(ctx context.Context, keys []*Key, src interface{}) (ret []*Key, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.PutMulti")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
mutations, err := putMutations(keys, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make the request.
|
||||
req := &pb.CommitRequest{
|
||||
ProjectId: c.dataset,
|
||||
Mutations: mutations,
|
||||
Mode: pb.CommitRequest_NON_TRANSACTIONAL,
|
||||
}
|
||||
resp, err := c.client.Commit(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Copy any newly minted keys into the returned keys.
|
||||
ret = make([]*Key, len(keys))
|
||||
for i, key := range keys {
|
||||
if key.Incomplete() {
|
||||
// This key is in the mutation results.
|
||||
ret[i], err = protoToKey(resp.MutationResults[i].Key)
|
||||
if err != nil {
|
||||
return nil, errors.New("datastore: internal error: server returned an invalid key")
|
||||
}
|
||||
} else {
|
||||
ret[i] = key
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func putMutations(keys []*Key, src interface{}) ([]*pb.Mutation, error) {
|
||||
v := reflect.ValueOf(src)
|
||||
multiArgType, _ := checkMultiArg(v)
|
||||
if multiArgType == multiArgTypeInvalid {
|
||||
return nil, errors.New("datastore: src has invalid type")
|
||||
}
|
||||
if len(keys) != v.Len() {
|
||||
return nil, errors.New("datastore: key and src slices have different length")
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err := multiValid(keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mutations := make([]*pb.Mutation, 0, len(keys))
|
||||
multiErr := make(MultiError, len(keys))
|
||||
hasErr := false
|
||||
for i, k := range keys {
|
||||
elem := v.Index(i)
|
||||
// Two cases where we need to take the address:
|
||||
// 1) multiArgTypePropertyLoadSaver => &elem implements PLS
|
||||
// 2) multiArgTypeStruct => saveEntity needs *struct
|
||||
if multiArgType == multiArgTypePropertyLoadSaver || multiArgType == multiArgTypeStruct {
|
||||
elem = elem.Addr()
|
||||
}
|
||||
p, err := saveEntity(k, elem.Interface())
|
||||
if err != nil {
|
||||
multiErr[i] = err
|
||||
hasErr = true
|
||||
}
|
||||
var mut *pb.Mutation
|
||||
if k.Incomplete() {
|
||||
mut = &pb.Mutation{Operation: &pb.Mutation_Insert{Insert: p}}
|
||||
} else {
|
||||
mut = &pb.Mutation{Operation: &pb.Mutation_Upsert{Upsert: p}}
|
||||
}
|
||||
mutations = append(mutations, mut)
|
||||
}
|
||||
if hasErr {
|
||||
return nil, multiErr
|
||||
}
|
||||
return mutations, nil
|
||||
}
|
||||
|
||||
// Delete deletes the entity for the given key.
|
||||
func (c *Client) Delete(ctx context.Context, key *Key) error {
|
||||
err := c.DeleteMulti(ctx, []*Key{key})
|
||||
if me, ok := err.(MultiError); ok {
|
||||
return me[0]
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteMulti is a batch version of Delete.
|
||||
// TODO(jba): rewrite in terms of Mutate.
|
||||
func (c *Client) DeleteMulti(ctx context.Context, keys []*Key) (err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.DeleteMulti")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
mutations, err := deleteMutations(keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &pb.CommitRequest{
|
||||
ProjectId: c.dataset,
|
||||
Mutations: mutations,
|
||||
Mode: pb.CommitRequest_NON_TRANSACTIONAL,
|
||||
}
|
||||
_, err = c.client.Commit(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
func deleteMutations(keys []*Key) ([]*pb.Mutation, error) {
|
||||
mutations := make([]*pb.Mutation, 0, len(keys))
|
||||
set := make(map[string]bool, len(keys))
|
||||
for _, k := range keys {
|
||||
if k.Incomplete() {
|
||||
return nil, fmt.Errorf("datastore: can't delete the incomplete key: %v", k)
|
||||
}
|
||||
ks := k.String()
|
||||
if !set[ks] {
|
||||
mutations = append(mutations, &pb.Mutation{
|
||||
Operation: &pb.Mutation_Delete{Delete: keyToProto(k)},
|
||||
})
|
||||
}
|
||||
set[ks] = true
|
||||
}
|
||||
return mutations, nil
|
||||
}
|
||||
|
||||
// Mutate applies one or more mutations atomically.
|
||||
// It returns the keys of the argument Mutations, in the same order.
|
||||
//
|
||||
// If any of the mutations are invalid, Mutate returns a MultiError with the errors.
|
||||
// Mutate returns a MultiError in this case even if there is only one Mutation.
|
||||
func (c *Client) Mutate(ctx context.Context, muts ...*Mutation) (ret []*Key, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.Mutate")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
pmuts, err := mutationProtos(muts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &pb.CommitRequest{
|
||||
ProjectId: c.dataset,
|
||||
Mutations: pmuts,
|
||||
Mode: pb.CommitRequest_NON_TRANSACTIONAL,
|
||||
}
|
||||
resp, err := c.client.Commit(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Copy any newly minted keys into the returned keys.
|
||||
ret = make([]*Key, len(muts))
|
||||
for i, mut := range muts {
|
||||
if mut.key.Incomplete() {
|
||||
// This key is in the mutation results.
|
||||
ret[i], err = protoToKey(resp.MutationResults[i].Key)
|
||||
if err != nil {
|
||||
return nil, errors.New("datastore: internal error: server returned an invalid key")
|
||||
}
|
||||
} else {
|
||||
ret[i] = mut.key
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
+3493
File diff suppressed because it is too large
Load Diff
+491
@@ -0,0 +1,491 @@
|
||||
// Copyright 2016 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 provides a client for Google Cloud Datastore.
|
||||
|
||||
|
||||
Basic Operations
|
||||
|
||||
Entities are the unit of storage and are associated with a key. A key
|
||||
consists of an optional parent key, a string application ID, a string kind
|
||||
(also known as an entity type), and either a StringID or an IntID. A
|
||||
StringID is also known as an entity name or key name.
|
||||
|
||||
It is valid to create a key with a zero StringID and a zero IntID; this is
|
||||
called an incomplete key, and does not refer to any saved entity. Putting an
|
||||
entity into the datastore under an incomplete key will cause a unique key
|
||||
to be generated for that entity, with a non-zero IntID.
|
||||
|
||||
An entity's contents are a mapping from case-sensitive field names to values.
|
||||
Valid value types are:
|
||||
- signed integers (int, int8, int16, int32 and int64),
|
||||
- bool,
|
||||
- string,
|
||||
- float32 and float64,
|
||||
- []byte (up to 1 megabyte in length),
|
||||
- any type whose underlying type is one of the above predeclared types,
|
||||
- *Key,
|
||||
- GeoPoint,
|
||||
- time.Time (stored with microsecond precision),
|
||||
- structs whose fields are all valid value types,
|
||||
- pointers to structs whose fields are all valid value types,
|
||||
- slices of any of the above,
|
||||
- pointers to a signed integer, bool, string, float32, or float64.
|
||||
|
||||
Slices of structs are valid, as are structs that contain slices.
|
||||
|
||||
The Get and Put functions load and save an entity's contents. An entity's
|
||||
contents are typically represented by a struct pointer.
|
||||
|
||||
Example code:
|
||||
|
||||
type Entity struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a datastore client. In a typical application, you would create
|
||||
// a single client which is reused for every datastore operation.
|
||||
dsClient, err := datastore.NewClient(ctx, "my-project")
|
||||
if err != nil {
|
||||
// Handle error.
|
||||
}
|
||||
|
||||
k := datastore.NameKey("Entity", "stringID", nil)
|
||||
e := new(Entity)
|
||||
if err := dsClient.Get(ctx, k, e); err != nil {
|
||||
// Handle error.
|
||||
}
|
||||
|
||||
old := e.Value
|
||||
e.Value = "Hello World!"
|
||||
|
||||
if _, err := dsClient.Put(ctx, k, e); err != nil {
|
||||
// Handle error.
|
||||
}
|
||||
|
||||
fmt.Printf("Updated value from %q to %q\n", old, e.Value)
|
||||
}
|
||||
|
||||
GetMulti, PutMulti and DeleteMulti are batch versions of the Get, Put and
|
||||
Delete functions. They take a []*Key instead of a *Key, and may return a
|
||||
datastore.MultiError when encountering partial failure.
|
||||
|
||||
Mutate generalizes PutMulti and DeleteMulti to a sequence of any Datastore mutations.
|
||||
It takes a series of mutations created with NewInsert, NewUpdate, NewUpsert and
|
||||
NewDelete and applies them atomically.
|
||||
|
||||
|
||||
Properties
|
||||
|
||||
An entity's contents can be represented by a variety of types. These are
|
||||
typically struct pointers, but can also be any type that implements the
|
||||
PropertyLoadSaver interface. If using a struct pointer, you do not have to
|
||||
explicitly implement the PropertyLoadSaver interface; the datastore will
|
||||
automatically convert via reflection. If a struct pointer does implement that
|
||||
interface then those methods will be used in preference to the default
|
||||
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 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
|
||||
a struct pointer, an entity that cannot be completely represented (such as a
|
||||
missing field) will result in an ErrFieldMismatch error but it is up to the
|
||||
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.
|
||||
|
||||
The only valid options are "omitempty", "noindex" and "flatten".
|
||||
|
||||
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 pointer or
|
||||
interface value, and any array, slice, map, or string of length zero. Struct field
|
||||
values will never be empty, except for nil pointers.
|
||||
|
||||
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.
|
||||
|
||||
For a nested struct field, the options may also include "flatten". This indicates
|
||||
that the immediate fields and any nested substruct fields of the nested struct should be
|
||||
flattened. See below for examples.
|
||||
|
||||
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:
|
||||
|
||||
// A and B are renamed to a and b.
|
||||
// A, C and J are not indexed.
|
||||
// D's tag is equivalent to having no tag at all (E).
|
||||
// I is ignored entirely by the datastore.
|
||||
// J has tag information for both the datastore and json packages.
|
||||
type TaggedStruct 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"`
|
||||
}
|
||||
|
||||
|
||||
Slice Fields
|
||||
|
||||
A field of slice type corresponds to a Datastore array property, except for []byte, which corresponds
|
||||
to a Datastore blob.
|
||||
|
||||
Zero-length slice fields are not saved. Slice fields of length 1 or greater are saved
|
||||
as Datastore arrays. When a zero-length Datastore array is loaded into a slice field,
|
||||
the slice field remains unchanged.
|
||||
|
||||
If a non-array value is loaded into a slice field, the result will be a slice with
|
||||
one element, containing the value.
|
||||
|
||||
Loading Nulls
|
||||
|
||||
Loading a Datastore Null into a basic type (int, float, etc.) results in a zero value.
|
||||
Loading a Null into a slice of basic type results in a slice of size 1 containing the zero value.
|
||||
Loading a Null into a pointer field results in nil.
|
||||
Loading a Null into a field of struct type is an error.
|
||||
|
||||
Pointer Fields
|
||||
|
||||
A struct field can be a pointer to a signed integer, floating-point number, string or
|
||||
bool. Putting a non-nil pointer will store its dereferenced value. Putting a nil
|
||||
pointer will store a Datastore Null property, unless the field is marked omitempty,
|
||||
in which case no property will be stored.
|
||||
|
||||
Loading a Null into a pointer field sets the pointer to nil. Loading any other value
|
||||
allocates new storage with the value, and sets the field to point to it.
|
||||
|
||||
|
||||
Key Field
|
||||
|
||||
If the struct contains a *datastore.Key field tagged with the name "__key__",
|
||||
its value will be ignored on Put. When reading the Entity back into the Go struct,
|
||||
the field will be populated with the *datastore.Key value used to query for
|
||||
the Entity.
|
||||
|
||||
Example code:
|
||||
|
||||
type MyEntity struct {
|
||||
A int
|
||||
K *datastore.Key `datastore:"__key__"`
|
||||
}
|
||||
|
||||
k := datastore.NameKey("Entity", "stringID", nil)
|
||||
e := MyEntity{A: 12}
|
||||
k, err = dsClient.Put(ctx, k, e)
|
||||
if err != nil {
|
||||
// Handle error.
|
||||
}
|
||||
|
||||
var entities []MyEntity
|
||||
q := datastore.NewQuery("Entity").Filter("A =", 12).Limit(1)
|
||||
_, err := dsClient.GetAll(ctx, q, &entities)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
log.Println(entities[0])
|
||||
// Prints {12 /Entity,stringID}
|
||||
|
||||
|
||||
|
||||
Structured Properties
|
||||
|
||||
If the struct pointed to contains other structs, then the nested or embedded
|
||||
structs are themselves saved as Entity values. For example, given these definitions:
|
||||
|
||||
type Inner struct {
|
||||
W int32
|
||||
X string
|
||||
}
|
||||
|
||||
type Outer struct {
|
||||
I Inner
|
||||
}
|
||||
|
||||
then an Outer would have one property, Inner, encoded as an Entity value.
|
||||
|
||||
If an outer struct is tagged "noindex" then all of its implicit flattened
|
||||
fields are effectively "noindex".
|
||||
|
||||
If the Inner struct contains a *Key field with the name "__key__", like so:
|
||||
|
||||
type Inner struct {
|
||||
W int32
|
||||
X string
|
||||
K *datastore.Key `datastore:"__key__"`
|
||||
}
|
||||
|
||||
type Outer struct {
|
||||
I Inner
|
||||
}
|
||||
|
||||
then the value of K will be used as the Key for Inner, represented
|
||||
as an Entity value in datastore.
|
||||
|
||||
If any nested struct fields should be flattened, instead of encoded as
|
||||
Entity values, the nested struct field should be tagged with the "flatten"
|
||||
option. For example, given the following:
|
||||
|
||||
type Inner1 struct {
|
||||
W int32
|
||||
X string
|
||||
}
|
||||
|
||||
type Inner2 struct {
|
||||
Y float64
|
||||
}
|
||||
|
||||
type Inner3 struct {
|
||||
Z bool
|
||||
}
|
||||
|
||||
type Inner4 struct {
|
||||
WW int
|
||||
}
|
||||
|
||||
type Inner5 struct {
|
||||
X Inner4
|
||||
}
|
||||
|
||||
type Outer struct {
|
||||
A int16
|
||||
I []Inner1 `datastore:",flatten"`
|
||||
J Inner2 `datastore:",flatten"`
|
||||
K Inner5 `datastore:",flatten"`
|
||||
Inner3 `datastore:",flatten"`
|
||||
}
|
||||
|
||||
an Outer's properties would be equivalent to those of:
|
||||
|
||||
type OuterEquivalent struct {
|
||||
A int16
|
||||
IDotW []int32 `datastore:"I.W"`
|
||||
IDotX []string `datastore:"I.X"`
|
||||
JDotY float64 `datastore:"J.Y"`
|
||||
KDotXDotWW int `datastore:"K.X.WW"`
|
||||
Z bool
|
||||
}
|
||||
|
||||
Note that the "flatten" option cannot be used for Entity value fields.
|
||||
The server will reject any dotted field names for an Entity value.
|
||||
|
||||
|
||||
The PropertyLoadSaver Interface
|
||||
|
||||
An entity's contents can also be represented by any type that implements the
|
||||
PropertyLoadSaver interface. This type may be a struct pointer, but it does
|
||||
not have to be. The datastore package will call Load when getting the entity's
|
||||
contents, and Save when putting the entity's contents.
|
||||
Possible uses include deriving non-stored fields, verifying fields, or indexing
|
||||
a field only if its value is positive.
|
||||
|
||||
Example code:
|
||||
|
||||
type CustomPropsExample struct {
|
||||
I, J int
|
||||
// Sum is not stored, but should always be equal to I + J.
|
||||
Sum int `datastore:"-"`
|
||||
}
|
||||
|
||||
func (x *CustomPropsExample) Load(ps []datastore.Property) error {
|
||||
// Load I and J as usual.
|
||||
if err := datastore.LoadStruct(x, ps); err != nil {
|
||||
return err
|
||||
}
|
||||
// Derive the Sum field.
|
||||
x.Sum = x.I + x.J
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CustomPropsExample) Save() ([]datastore.Property, error) {
|
||||
// Validate the Sum field.
|
||||
if x.Sum != x.I + x.J {
|
||||
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
|
||||
// demonstration purposes.
|
||||
return []datastore.Property{
|
||||
{
|
||||
Name: "I",
|
||||
Value: int64(x.I),
|
||||
},
|
||||
{
|
||||
Name: "J",
|
||||
Value: int64(x.J),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
The *PropertyList type implements PropertyLoadSaver, and can therefore hold an
|
||||
arbitrary entity's contents.
|
||||
|
||||
The KeyLoader Interface
|
||||
|
||||
If a type implements the PropertyLoadSaver interface, it may
|
||||
also want to implement the KeyLoader interface.
|
||||
The KeyLoader interface exists to allow implementations of PropertyLoadSaver
|
||||
to also load an Entity's Key into the Go type. This type may be a struct
|
||||
pointer, but it does not have to be. The datastore package will call LoadKey
|
||||
when getting the entity's contents, after calling Load.
|
||||
|
||||
Example code:
|
||||
|
||||
type WithKeyExample struct {
|
||||
I int
|
||||
Key *datastore.Key
|
||||
}
|
||||
|
||||
func (x *WithKeyExample) LoadKey(k *datastore.Key) error {
|
||||
x.Key = k
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *WithKeyExample) Load(ps []datastore.Property) error {
|
||||
// Load I as usual.
|
||||
return datastore.LoadStruct(x, ps)
|
||||
}
|
||||
|
||||
func (x *WithKeyExample) Save() ([]datastore.Property, error) {
|
||||
// Save I as usual.
|
||||
return datastore.SaveStruct(x)
|
||||
}
|
||||
|
||||
To load a Key into a struct which does not implement the PropertyLoadSaver
|
||||
interface, see the "Key Field" section above.
|
||||
|
||||
|
||||
Queries
|
||||
|
||||
Queries retrieve entities based on their properties or key's ancestry. Running
|
||||
a query yields an iterator of results: either keys or (key, entity) pairs.
|
||||
Queries are re-usable and it is safe to call Query.Run from concurrent
|
||||
goroutines. Iterators are not safe for concurrent use.
|
||||
|
||||
Queries are immutable, and are either created by calling NewQuery, or derived
|
||||
from an existing query by calling a method like Filter or Order that returns a
|
||||
new query value. A query is typically constructed by calling NewQuery followed
|
||||
by a chain of zero or more such methods. These methods are:
|
||||
- Ancestor and Filter constrain the entities returned by running a query.
|
||||
- Order affects the order in which they are returned.
|
||||
- Project constrains the fields returned.
|
||||
- Distinct de-duplicates projected entities.
|
||||
- KeysOnly makes the iterator return only keys, not (key, entity) pairs.
|
||||
- Start, End, Offset and Limit define which sub-sequence of matching entities
|
||||
to return. Start and End take cursors, Offset and Limit take integers. Start
|
||||
and Offset affect the first result, End and Limit affect the last result.
|
||||
If both Start and Offset are set, then the offset is relative to Start.
|
||||
If both End and Limit are set, then the earliest constraint wins. Limit is
|
||||
relative to Start+Offset, not relative to End. As a special case, a
|
||||
negative limit means unlimited.
|
||||
|
||||
Example code:
|
||||
|
||||
type Widget struct {
|
||||
Description string
|
||||
Price int
|
||||
}
|
||||
|
||||
func printWidgets(ctx context.Context, client *datastore.Client) {
|
||||
q := datastore.NewQuery("Widget").
|
||||
Filter("Price <", 1000).
|
||||
Order("-Price")
|
||||
for t := client.Run(ctx, q); ; {
|
||||
var x Widget
|
||||
key, err := t.Next(&x)
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// Handle error.
|
||||
}
|
||||
fmt.Printf("Key=%v\nWidget=%#v\n\n", key, x)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Transactions
|
||||
|
||||
Client.RunInTransaction runs a function in a transaction.
|
||||
|
||||
Example code:
|
||||
|
||||
type Counter struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
func incCount(ctx context.Context, client *datastore.Client) {
|
||||
var count int
|
||||
key := datastore.NameKey("Counter", "singleton", nil)
|
||||
_, err := client.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
|
||||
var x Counter
|
||||
if err := tx.Get(key, &x); err != nil && err != datastore.ErrNoSuchEntity {
|
||||
return err
|
||||
}
|
||||
x.Count++
|
||||
if _, err := tx.Put(key, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
count = x.Count
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
// Handle error.
|
||||
}
|
||||
// The value of count is only valid once the transaction is successful
|
||||
// (RunInTransaction has returned nil).
|
||||
fmt.Printf("Count=%d\n", count)
|
||||
}
|
||||
|
||||
Pass the ReadOnly option to RunInTransaction if your transaction is used only for Get,
|
||||
GetMulti or queries. Read-only transactions are more efficient.
|
||||
|
||||
Google Cloud Datastore Emulator
|
||||
|
||||
This package supports the Cloud Datastore emulator, which is useful for testing and
|
||||
development. Environment variables are used to indicate that datastore traffic should be
|
||||
directed to the emulator instead of the production Datastore service.
|
||||
|
||||
To install and set up the emulator and its environment variables, see the documentation
|
||||
at https://cloud.google.com/datastore/docs/tools/datastore-emulator.
|
||||
|
||||
Authentication
|
||||
|
||||
See examples of authorization and authentication at
|
||||
https://godoc.org/cloud.google.com/go#pkg-examples.
|
||||
|
||||
*/
|
||||
package datastore // import "cloud.google.com/go/datastore"
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// 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.
|
||||
|
||||
// This file provides error functions for common API failure modes.
|
||||
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MultiError is returned by batch operations when there are errors with
|
||||
// particular elements. Errors will be in a one-to-one correspondence with
|
||||
// the input elements; successful elements will have a nil entry.
|
||||
type MultiError []error
|
||||
|
||||
func (m MultiError) Error() string {
|
||||
s, n := "", 0
|
||||
for _, e := range m {
|
||||
if e != nil {
|
||||
if n == 0 {
|
||||
s = e.Error()
|
||||
}
|
||||
n++
|
||||
}
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return "(0 errors)"
|
||||
case 1:
|
||||
return s
|
||||
case 2:
|
||||
return s + " (and 1 other error)"
|
||||
}
|
||||
return fmt.Sprintf("%s (and %d other errors)", s, n-1)
|
||||
}
|
||||
+567
@@ -0,0 +1,567 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/datastore"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
func ExampleNewClient() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_ = client // TODO: Use client.
|
||||
}
|
||||
|
||||
func ExampleClient_Get() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
type Article struct {
|
||||
Title string
|
||||
Description string
|
||||
Body string `datastore:",noindex"`
|
||||
Author *datastore.Key
|
||||
PublishedAt time.Time
|
||||
}
|
||||
key := datastore.NameKey("Article", "articled1", nil)
|
||||
article := &Article{}
|
||||
if err := client.Get(ctx, key, article); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_Put() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
type Article struct {
|
||||
Title string
|
||||
Description string
|
||||
Body string `datastore:",noindex"`
|
||||
Author *datastore.Key
|
||||
PublishedAt time.Time
|
||||
}
|
||||
newKey := datastore.IncompleteKey("Article", nil)
|
||||
_, err = client.Put(ctx, newKey, &Article{
|
||||
Title: "The title of the article",
|
||||
Description: "The description of the article...",
|
||||
Body: "...",
|
||||
Author: datastore.NameKey("Author", "jbd", nil),
|
||||
PublishedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_Put_flatten() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
type Animal struct {
|
||||
Name string
|
||||
Type string
|
||||
Breed string
|
||||
}
|
||||
|
||||
type Human struct {
|
||||
Name string
|
||||
Height int
|
||||
Pet Animal `datastore:",flatten"`
|
||||
}
|
||||
|
||||
newKey := datastore.IncompleteKey("Human", nil)
|
||||
_, err = client.Put(ctx, newKey, &Human{
|
||||
Name: "Susan",
|
||||
Height: 67,
|
||||
Pet: Animal{
|
||||
Name: "Fluffy",
|
||||
Type: "Cat",
|
||||
Breed: "Sphynx",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_Delete() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
key := datastore.NameKey("Article", "articled1", nil)
|
||||
if err := client.Delete(ctx, key); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_DeleteMulti() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
var keys []*datastore.Key
|
||||
for i := 1; i <= 10; i++ {
|
||||
keys = append(keys, datastore.IDKey("Article", int64(i), nil))
|
||||
}
|
||||
if err := client.DeleteMulti(ctx, keys); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
Title string
|
||||
PublishedAt time.Time
|
||||
Comments int
|
||||
}
|
||||
|
||||
func ExampleClient_GetMulti() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
keys := []*datastore.Key{
|
||||
datastore.NameKey("Post", "post1", nil),
|
||||
datastore.NameKey("Post", "post2", nil),
|
||||
datastore.NameKey("Post", "post3", nil),
|
||||
}
|
||||
posts := make([]Post, 3)
|
||||
if err := client.GetMulti(ctx, keys, posts); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_PutMulti_slice() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
keys := []*datastore.Key{
|
||||
datastore.NameKey("Post", "post1", nil),
|
||||
datastore.NameKey("Post", "post2", nil),
|
||||
}
|
||||
|
||||
// PutMulti with a Post slice.
|
||||
posts := []*Post{
|
||||
{Title: "Post 1", PublishedAt: time.Now()},
|
||||
{Title: "Post 2", PublishedAt: time.Now()},
|
||||
}
|
||||
if _, err := client.PutMulti(ctx, keys, posts); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_PutMulti_interfaceSlice() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
keys := []*datastore.Key{
|
||||
datastore.NameKey("Post", "post1", nil),
|
||||
datastore.NameKey("Post", "post2", nil),
|
||||
}
|
||||
|
||||
// PutMulti with an empty interface slice.
|
||||
posts := []interface{}{
|
||||
&Post{Title: "Post 1", PublishedAt: time.Now()},
|
||||
&Post{Title: "Post 2", PublishedAt: time.Now()},
|
||||
}
|
||||
if _, err := client.PutMulti(ctx, keys, posts); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleNewQuery() {
|
||||
// Query for Post entities.
|
||||
q := datastore.NewQuery("Post")
|
||||
_ = q // TODO: Use the query with Client.Run.
|
||||
}
|
||||
|
||||
func ExampleNewQuery_options() {
|
||||
// Query to order the posts by the number of comments they have recieved.
|
||||
q := datastore.NewQuery("Post").Order("-Comments")
|
||||
// Start listing from an offset and limit the results.
|
||||
q = q.Offset(20).Limit(10)
|
||||
_ = q // TODO: Use the query.
|
||||
}
|
||||
|
||||
func ExampleClient_Count() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// Count the number of the post entities.
|
||||
q := datastore.NewQuery("Post")
|
||||
n, err := client.Count(ctx, q)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Printf("There are %d posts.", n)
|
||||
}
|
||||
|
||||
func ExampleClient_Run() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// List the posts published since yesterday.
|
||||
yesterday := time.Now().Add(-24 * time.Hour)
|
||||
q := datastore.NewQuery("Post").Filter("PublishedAt >", yesterday)
|
||||
it := client.Run(ctx, q)
|
||||
_ = it // TODO: iterate using Next.
|
||||
}
|
||||
|
||||
func ExampleClient_NewTransaction() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
const retries = 3
|
||||
|
||||
// Increment a counter.
|
||||
// See https://cloud.google.com/appengine/articles/sharding_counters for
|
||||
// a more scalable solution.
|
||||
type Counter struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
key := datastore.NameKey("counter", "CounterA", nil)
|
||||
var tx *datastore.Transaction
|
||||
for i := 0; i < retries; i++ {
|
||||
tx, err = client.NewTransaction(ctx)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var c Counter
|
||||
if err = tx.Get(key, &c); err != nil && err != datastore.ErrNoSuchEntity {
|
||||
break
|
||||
}
|
||||
c.Count++
|
||||
if _, err = tx.Put(key, &c); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Attempt to commit the transaction. If there's a conflict, try again.
|
||||
if _, err = tx.Commit(); err != datastore.ErrConcurrentTransaction {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_RunInTransaction() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
// Increment a counter.
|
||||
// See https://cloud.google.com/appengine/articles/sharding_counters for
|
||||
// a more scalable solution.
|
||||
type Counter struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
var count int
|
||||
key := datastore.NameKey("Counter", "singleton", nil)
|
||||
_, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
|
||||
var x Counter
|
||||
if err := tx.Get(key, &x); err != nil && err != datastore.ErrNoSuchEntity {
|
||||
return err
|
||||
}
|
||||
x.Count++
|
||||
if _, err := tx.Put(key, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
count = x.Count
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// The value of count is only valid once the transaction is successful
|
||||
// (RunInTransaction has returned nil).
|
||||
fmt.Printf("Count=%d\n", count)
|
||||
}
|
||||
|
||||
func ExampleClient_AllocateIDs() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
var keys []*datastore.Key
|
||||
for i := 0; i < 10; i++ {
|
||||
keys = append(keys, datastore.IncompleteKey("Article", nil))
|
||||
}
|
||||
keys, err = client.AllocateIDs(ctx, keys)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_ = keys // TODO: Use keys.
|
||||
}
|
||||
|
||||
func ExampleKey_Encode() {
|
||||
key := datastore.IDKey("Article", 1, nil)
|
||||
encoded := key.Encode()
|
||||
fmt.Println(encoded)
|
||||
// Output: EgsKB0FydGljbGUQAQ
|
||||
}
|
||||
|
||||
func ExampleDecodeKey() {
|
||||
const encoded = "EgsKB0FydGljbGUQAQ"
|
||||
key, err := datastore.DecodeKey(encoded)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(key)
|
||||
// Output: /Article,1
|
||||
}
|
||||
|
||||
func ExampleIDKey() {
|
||||
// Key with numeric ID.
|
||||
k := datastore.IDKey("Article", 1, nil)
|
||||
_ = k // TODO: Use key.
|
||||
}
|
||||
|
||||
func ExampleNameKey() {
|
||||
// Key with string ID.
|
||||
k := datastore.NameKey("Article", "article8", nil)
|
||||
_ = k // TODO: Use key.
|
||||
}
|
||||
|
||||
func ExampleIncompleteKey() {
|
||||
k := datastore.IncompleteKey("Article", nil)
|
||||
_ = k // TODO: Use incomplete key.
|
||||
}
|
||||
|
||||
func ExampleClient_GetAll() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
var posts []*Post
|
||||
keys, err := client.GetAll(ctx, datastore.NewQuery("Post"), &posts)
|
||||
for i, key := range keys {
|
||||
fmt.Println(key)
|
||||
fmt.Println(posts[i])
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_Mutate() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
key1 := datastore.NameKey("Post", "post1", nil)
|
||||
key2 := datastore.NameKey("Post", "post2", nil)
|
||||
key3 := datastore.NameKey("Post", "post3", nil)
|
||||
key4 := datastore.NameKey("Post", "post4", nil)
|
||||
|
||||
_, err = client.Mutate(ctx,
|
||||
datastore.NewInsert(key1, Post{Title: "Post 1"}),
|
||||
datastore.NewUpsert(key2, Post{Title: "Post 2"}),
|
||||
datastore.NewUpdate(key3, Post{Title: "Post 3"}),
|
||||
datastore.NewDelete(key4))
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleCommit_Key() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
var pk1, pk2 *datastore.PendingKey
|
||||
// Create two posts in a single transaction.
|
||||
commit, err := client.RunInTransaction(ctx, func(tx *datastore.Transaction) error {
|
||||
var err error
|
||||
pk1, err = tx.Put(datastore.IncompleteKey("Post", nil), &Post{Title: "Post 1", PublishedAt: time.Now()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pk2, err = tx.Put(datastore.IncompleteKey("Post", nil), &Post{Title: "Post 2", PublishedAt: time.Now()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// Now pk1, pk2 are valid PendingKeys. Let's convert them into real keys
|
||||
// using the Commit object.
|
||||
k1 := commit.Key(pk1)
|
||||
k2 := commit.Key(pk2)
|
||||
fmt.Println(k1, k2)
|
||||
}
|
||||
|
||||
func ExampleIterator_Next() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
it := client.Run(ctx, datastore.NewQuery("Post"))
|
||||
for {
|
||||
var p Post
|
||||
key, err := it.Next(&p)
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(key, p)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleIterator_Cursor() {
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
it := client.Run(ctx, datastore.NewQuery("Post"))
|
||||
for {
|
||||
var p Post
|
||||
_, err := it.Next(&p)
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(p)
|
||||
cursor, err := it.Cursor()
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// When printed, a cursor will display as a string that can be passed
|
||||
// to datastore.NewCursor.
|
||||
fmt.Printf("to resume with this post, use cursor %s\n", cursor)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleDecodeCursor() {
|
||||
// See Query.Start for a fuller example of DecodeCursor.
|
||||
// getCursor represents a function that returns a cursor from a previous
|
||||
// iteration in string form.
|
||||
cursorString := getCursor()
|
||||
cursor, err := datastore.DecodeCursor(cursorString)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_ = cursor // TODO: Use the cursor with Query.Start or Query.End.
|
||||
}
|
||||
|
||||
func getCursor() string { return "" }
|
||||
|
||||
func ExampleQuery_Start() {
|
||||
// This example demonstrates how to use cursors and Query.Start
|
||||
// to resume an iteration.
|
||||
ctx := context.Background()
|
||||
client, err := datastore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// getCursor represents a function that returns a cursor from a previous
|
||||
// iteration in string form.
|
||||
cursorString := getCursor()
|
||||
cursor, err := datastore.DecodeCursor(cursorString)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
it := client.Run(ctx, datastore.NewQuery("Post").Start(cursor))
|
||||
_ = it // TODO: Use iterator.
|
||||
}
|
||||
|
||||
func ExampleLoadStruct() {
|
||||
type Player struct {
|
||||
User string
|
||||
Score int
|
||||
}
|
||||
// Normally LoadStruct would only be used inside a custom implementation of
|
||||
// PropertyLoadSaver; this is for illustrative purposes only.
|
||||
props := []datastore.Property{
|
||||
{Name: "User", Value: "Alice"},
|
||||
{Name: "Score", Value: int64(97)},
|
||||
}
|
||||
|
||||
var p Player
|
||||
if err := datastore.LoadStruct(&p, props); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(p)
|
||||
// Output: {Alice 97}
|
||||
}
|
||||
|
||||
func ExampleSaveStruct() {
|
||||
type Player struct {
|
||||
User string
|
||||
Score int
|
||||
}
|
||||
|
||||
p := &Player{
|
||||
User: "Alice",
|
||||
Score: 97,
|
||||
}
|
||||
props, err := datastore.SaveStruct(p)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(props)
|
||||
// TODO(jba): make this output stable: Output: [{User Alice false} {Score 97 false}]
|
||||
}
|
||||
+1277
File diff suppressed because it is too large
Load Diff
+280
@@ -0,0 +1,280 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"golang.org/x/net/context"
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
// Key represents the datastore key for a stored entity.
|
||||
type Key struct {
|
||||
// Kind cannot be empty.
|
||||
Kind string
|
||||
// Either ID or Name must be zero for the Key to be valid.
|
||||
// If both are zero, the Key is incomplete.
|
||||
ID int64
|
||||
Name string
|
||||
// Parent must either be a complete Key or nil.
|
||||
Parent *Key
|
||||
|
||||
// Namespace provides the ability to partition your data for multiple
|
||||
// tenants. In most cases, it is not necessary to specify a namespace.
|
||||
// See docs on datastore multitenancy for details:
|
||||
// https://cloud.google.com/datastore/docs/concepts/multitenancy
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// Incomplete reports whether the key does not refer to a stored entity.
|
||||
func (k *Key) Incomplete() bool {
|
||||
return k.Name == "" && k.ID == 0
|
||||
}
|
||||
|
||||
// valid returns whether the key is valid.
|
||||
func (k *Key) valid() bool {
|
||||
if k == nil {
|
||||
return false
|
||||
}
|
||||
for ; k != nil; k = k.Parent {
|
||||
if k.Kind == "" {
|
||||
return false
|
||||
}
|
||||
if k.Name != "" && k.ID != 0 {
|
||||
return false
|
||||
}
|
||||
if k.Parent != nil {
|
||||
if k.Parent.Incomplete() {
|
||||
return false
|
||||
}
|
||||
if k.Parent.Namespace != k.Namespace {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal reports whether two keys are equal. Two keys are equal if they are
|
||||
// both nil, or if their kinds, IDs, names, namespaces and parents are equal.
|
||||
func (k *Key) Equal(o *Key) bool {
|
||||
for {
|
||||
if k == nil || o == nil {
|
||||
return k == o // if either is nil, both must be nil
|
||||
}
|
||||
if k.Namespace != o.Namespace || k.Name != o.Name || k.ID != o.ID || k.Kind != o.Kind {
|
||||
return false
|
||||
}
|
||||
if k.Parent == nil && o.Parent == nil {
|
||||
return true
|
||||
}
|
||||
k = k.Parent
|
||||
o = o.Parent
|
||||
}
|
||||
}
|
||||
|
||||
// marshal marshals the key's string representation to the buffer.
|
||||
func (k *Key) marshal(b *bytes.Buffer) {
|
||||
if k.Parent != nil {
|
||||
k.Parent.marshal(b)
|
||||
}
|
||||
b.WriteByte('/')
|
||||
b.WriteString(k.Kind)
|
||||
b.WriteByte(',')
|
||||
if k.Name != "" {
|
||||
b.WriteString(k.Name)
|
||||
} else {
|
||||
b.WriteString(strconv.FormatInt(k.ID, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of the key.
|
||||
func (k *Key) String() string {
|
||||
if k == nil {
|
||||
return ""
|
||||
}
|
||||
b := bytes.NewBuffer(make([]byte, 0, 512))
|
||||
k.marshal(b)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Note: Fields not renamed compared to appengine gobKey struct
|
||||
// This ensures gobs created by appengine can be read here, and vice/versa
|
||||
type gobKey struct {
|
||||
Kind string
|
||||
StringID string
|
||||
IntID int64
|
||||
Parent *gobKey
|
||||
AppID string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func keyToGobKey(k *Key) *gobKey {
|
||||
if k == nil {
|
||||
return nil
|
||||
}
|
||||
return &gobKey{
|
||||
Kind: k.Kind,
|
||||
StringID: k.Name,
|
||||
IntID: k.ID,
|
||||
Parent: keyToGobKey(k.Parent),
|
||||
Namespace: k.Namespace,
|
||||
}
|
||||
}
|
||||
|
||||
func gobKeyToKey(gk *gobKey) *Key {
|
||||
if gk == nil {
|
||||
return nil
|
||||
}
|
||||
return &Key{
|
||||
Kind: gk.Kind,
|
||||
Name: gk.StringID,
|
||||
ID: gk.IntID,
|
||||
Parent: gobKeyToKey(gk.Parent),
|
||||
Namespace: gk.Namespace,
|
||||
}
|
||||
}
|
||||
|
||||
// GobEncode marshals the key into a sequence of bytes
|
||||
// using an encoding/gob.Encoder.
|
||||
func (k *Key) GobEncode() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := gob.NewEncoder(buf).Encode(keyToGobKey(k)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode unmarshals a sequence of bytes using an encoding/gob.Decoder.
|
||||
func (k *Key) GobDecode(buf []byte) error {
|
||||
gk := new(gobKey)
|
||||
if err := gob.NewDecoder(bytes.NewBuffer(buf)).Decode(gk); err != nil {
|
||||
return err
|
||||
}
|
||||
*k = *gobKeyToKey(gk)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON marshals the key into JSON.
|
||||
func (k *Key) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + k.Encode() + `"`), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals a key JSON object into a Key.
|
||||
func (k *Key) UnmarshalJSON(buf []byte) error {
|
||||
if len(buf) < 2 || buf[0] != '"' || buf[len(buf)-1] != '"' {
|
||||
return errors.New("datastore: bad JSON key")
|
||||
}
|
||||
k2, err := DecodeKey(string(buf[1 : len(buf)-1]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*k = *k2
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode returns an opaque representation of the key
|
||||
// suitable for use in HTML and URLs.
|
||||
// This is compatible with the Python and Java runtimes.
|
||||
func (k *Key) Encode() string {
|
||||
pKey := keyToProto(k)
|
||||
|
||||
b, err := proto.Marshal(pKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Trailing padding is stripped.
|
||||
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
|
||||
}
|
||||
|
||||
// DecodeKey decodes a key from the opaque representation returned by Encode.
|
||||
func DecodeKey(encoded string) (*Key, error) {
|
||||
// Re-add padding.
|
||||
if m := len(encoded) % 4; m != 0 {
|
||||
encoded += strings.Repeat("=", 4-m)
|
||||
}
|
||||
|
||||
b, err := base64.URLEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pKey := new(pb.Key)
|
||||
if err := proto.Unmarshal(b, pKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protoToKey(pKey)
|
||||
}
|
||||
|
||||
// AllocateIDs accepts a slice of incomplete keys and returns a
|
||||
// slice of complete keys that are guaranteed to be valid in the datastore.
|
||||
func (c *Client) AllocateIDs(ctx context.Context, keys []*Key) ([]*Key, error) {
|
||||
if keys == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
req := &pb.AllocateIdsRequest{
|
||||
ProjectId: c.dataset,
|
||||
Keys: multiKeyToProto(keys),
|
||||
}
|
||||
resp, err := c.client.AllocateIds(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return multiProtoToKey(resp.Keys)
|
||||
}
|
||||
|
||||
// IncompleteKey creates a new incomplete key.
|
||||
// The supplied kind cannot be empty.
|
||||
// The namespace of the new key is empty.
|
||||
func IncompleteKey(kind string, parent *Key) *Key {
|
||||
return &Key{
|
||||
Kind: kind,
|
||||
Parent: parent,
|
||||
}
|
||||
}
|
||||
|
||||
// NameKey creates a new key with a name.
|
||||
// The supplied kind cannot be empty.
|
||||
// The supplied parent must either be a complete key or nil.
|
||||
// The namespace of the new key is empty.
|
||||
func NameKey(kind, name string, parent *Key) *Key {
|
||||
return &Key{
|
||||
Kind: kind,
|
||||
Name: name,
|
||||
Parent: parent,
|
||||
}
|
||||
}
|
||||
|
||||
// IDKey creates a new key with an ID.
|
||||
// The supplied kind cannot be empty.
|
||||
// The supplied parent must either be a complete key or nil.
|
||||
// The namespace of the new key is empty.
|
||||
func IDKey(kind string, id int64, parent *Key) *Key {
|
||||
return &Key{
|
||||
Kind: kind,
|
||||
ID: id,
|
||||
Parent: parent,
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEqual(t *testing.T) {
|
||||
testCases := []struct {
|
||||
x, y *Key
|
||||
equal bool
|
||||
}{
|
||||
{
|
||||
x: nil,
|
||||
y: nil,
|
||||
equal: true,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA"},
|
||||
y: &Key{Kind: "kindA"},
|
||||
equal: true,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA", Name: "nameA"},
|
||||
y: &Key{Kind: "kindA", Name: "nameA"},
|
||||
equal: true,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA", Name: "nameA", Namespace: "gopherspace"},
|
||||
y: &Key{Kind: "kindA", Name: "nameA", Namespace: "gopherspace"},
|
||||
equal: true,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA", ID: 1337, Parent: &Key{Kind: "kindX", Name: "nameX"}},
|
||||
y: &Key{Kind: "kindA", ID: 1337, Parent: &Key{Kind: "kindX", Name: "nameX"}},
|
||||
equal: true,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA", Name: "nameA"},
|
||||
y: &Key{Kind: "kindB", Name: "nameA"},
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA", Name: "nameA"},
|
||||
y: &Key{Kind: "kindA", Name: "nameB"},
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA", Name: "nameA"},
|
||||
y: &Key{Kind: "kindA", ID: 1337},
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA", Name: "nameA"},
|
||||
y: &Key{Kind: "kindA", Name: "nameA", Namespace: "gopherspace"},
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA", ID: 1337, Parent: &Key{Kind: "kindX", Name: "nameX"}},
|
||||
y: &Key{Kind: "kindA", ID: 1337, Parent: &Key{Kind: "kindY", Name: "nameX"}},
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
x: &Key{Kind: "kindA", ID: 1337, Parent: &Key{Kind: "kindX", Name: "nameX"}},
|
||||
y: &Key{Kind: "kindA", ID: 1337},
|
||||
equal: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
if got := tt.x.Equal(tt.y); got != tt.equal {
|
||||
t.Errorf("Equal(%v, %v) = %t; want %t", tt.x, tt.y, got, tt.equal)
|
||||
}
|
||||
if got := tt.y.Equal(tt.x); got != tt.equal {
|
||||
t.Errorf("Equal(%v, %v) = %t; want %t", tt.y, tt.x, got, tt.equal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoding(t *testing.T) {
|
||||
testCases := []struct {
|
||||
k *Key
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
k: nil,
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
k: &Key{},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
k: &Key{Kind: "kindA"},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
k: &Key{Kind: "kindA", Namespace: "gopherspace"},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
k: &Key{Kind: "kindA", Name: "nameA"},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
k: &Key{Kind: "kindA", ID: 1337},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
k: &Key{Kind: "kindA", Name: "nameA", ID: 1337},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
k: &Key{Kind: "kindA", Parent: &Key{Kind: "kindB", Name: "nameB"}},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
k: &Key{Kind: "kindA", Parent: &Key{Kind: "kindB"}},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
k: &Key{Kind: "kindA", Parent: &Key{Kind: "kindB", Name: "nameB", Namespace: "gopherspace"}},
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
if got := tt.k.valid(); got != tt.valid {
|
||||
t.Errorf("valid(%v) = %t; want %t", tt.k, got, tt.valid)
|
||||
}
|
||||
|
||||
// Check encoding/decoding for valid keys.
|
||||
if !tt.valid {
|
||||
continue
|
||||
}
|
||||
enc := tt.k.Encode()
|
||||
dec, err := DecodeKey(enc)
|
||||
if err != nil {
|
||||
t.Errorf("DecodeKey(%q) from %v: %v", enc, tt.k, err)
|
||||
continue
|
||||
}
|
||||
if !tt.k.Equal(dec) {
|
||||
t.Logf("Proto: %s", keyToProto(tt.k))
|
||||
t.Errorf("Decoded key %v not equal to %v", dec, tt.k)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(tt.k)
|
||||
if err != nil {
|
||||
t.Errorf("json.Marshal(%v): %v", tt.k, err)
|
||||
continue
|
||||
}
|
||||
key := &Key{}
|
||||
if err := json.Unmarshal(b, key); err != nil {
|
||||
t.Errorf("json.Unmarshal(%s) for key %v: %v", b, tt.k, err)
|
||||
continue
|
||||
}
|
||||
if !tt.k.Equal(key) {
|
||||
t.Errorf("JSON decoded key %v not equal to %v", dec, tt.k)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
gobEnc := gob.NewEncoder(buf)
|
||||
if err := gobEnc.Encode(tt.k); err != nil {
|
||||
t.Errorf("gobEnc.Encode(%v): %v", tt.k, err)
|
||||
continue
|
||||
}
|
||||
gobDec := gob.NewDecoder(buf)
|
||||
key = &Key{}
|
||||
if err := gobDec.Decode(key); err != nil {
|
||||
t.Errorf("gobDec.Decode() for key %v: %v", tt.k, err)
|
||||
}
|
||||
if !tt.k.Equal(key) {
|
||||
t.Errorf("gob decoded key %v not equal to %v", dec, tt.k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidKeyDecode(t *testing.T) {
|
||||
// Check that decoding an invalid key returns an err and doesn't panic.
|
||||
enc := NameKey("Kind", "Foo", nil).Encode()
|
||||
|
||||
invalid := []string{
|
||||
"",
|
||||
"Laboratorio",
|
||||
enc + "Junk",
|
||||
enc[:len(enc)-4],
|
||||
}
|
||||
for _, enc := range invalid {
|
||||
key, err := DecodeKey(enc)
|
||||
if err == nil || key != nil {
|
||||
t.Errorf("DecodeKey(%q) = %v, %v; want nil, error", enc, key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/fields"
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
typeOfByteSlice = reflect.TypeOf([]byte(nil))
|
||||
typeOfTime = reflect.TypeOf(time.Time{})
|
||||
typeOfGeoPoint = reflect.TypeOf(GeoPoint{})
|
||||
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 {
|
||||
entityType := "empty"
|
||||
switch p.Value.(type) {
|
||||
case int64:
|
||||
entityType = "int"
|
||||
case bool:
|
||||
entityType = "bool"
|
||||
case string:
|
||||
entityType = "string"
|
||||
case float64:
|
||||
entityType = "float"
|
||||
case *Key:
|
||||
entityType = "*datastore.Key"
|
||||
case *Entity:
|
||||
entityType = "*datastore.Entity"
|
||||
case GeoPoint:
|
||||
entityType = "GeoPoint"
|
||||
case time.Time:
|
||||
entityType = "time.Time"
|
||||
case []byte:
|
||||
entityType = "[]byte"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("type mismatch: %s versus %v", entityType, v.Type())
|
||||
}
|
||||
|
||||
func overflowReason(x interface{}, v reflect.Value) string {
|
||||
return fmt.Sprintf("value %v overflows struct field of type %v", x, v.Type())
|
||||
}
|
||||
|
||||
type propertyLoader struct {
|
||||
// m holds the number of times a substruct field like "Foo.Bar.Baz" has
|
||||
// been seen so far. The map is constructed lazily.
|
||||
m map[string]int
|
||||
}
|
||||
|
||||
func (l *propertyLoader) load(codec fields.List, structValue reflect.Value, p Property, prev map[string]struct{}) string {
|
||||
sl, ok := p.Value.([]interface{})
|
||||
if !ok {
|
||||
return l.loadOneElement(codec, structValue, p, prev)
|
||||
}
|
||||
for _, val := range sl {
|
||||
p.Value = val
|
||||
if errStr := l.loadOneElement(codec, structValue, p, prev); errStr != "" {
|
||||
return errStr
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// loadOneElement loads the value of Property p into structValue based on the provided
|
||||
// codec. codec is used to find the field in structValue into which p should be loaded.
|
||||
// prev is the set of property names already seen for structValue.
|
||||
func (l *propertyLoader) loadOneElement(codec fields.List, structValue reflect.Value, p Property, prev map[string]struct{}) string {
|
||||
var sliceOk bool
|
||||
var sliceIndex int
|
||||
var v reflect.Value
|
||||
|
||||
name := p.Name
|
||||
fieldNames := strings.Split(name, ".")
|
||||
|
||||
for len(fieldNames) > 0 {
|
||||
var field *fields.Field
|
||||
|
||||
// Start by trying to find a field with name. If none found,
|
||||
// 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(fieldNames); i > 0; i-- {
|
||||
parent := strings.Join(fieldNames[:i], ".")
|
||||
field = codec.Match(parent)
|
||||
if field != nil {
|
||||
fieldNames = fieldNames[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we never found a matching field in the codec, return
|
||||
// error message.
|
||||
if field == nil {
|
||||
return "no such struct field"
|
||||
}
|
||||
|
||||
v = initField(structValue, field.Index)
|
||||
if !v.IsValid() {
|
||||
return "no such struct field"
|
||||
}
|
||||
if !v.CanSet() {
|
||||
return "cannot set struct field"
|
||||
}
|
||||
|
||||
// If field implements PLS, we delegate loading to the PLS's Load early,
|
||||
// and stop iterating through fields.
|
||||
ok, err := plsFieldLoad(v, p, fieldNames)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
codec, err = structCache.Fields(field.Type)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
structValue = v
|
||||
}
|
||||
|
||||
// If the element is a slice, we need to accommodate it.
|
||||
if v.Kind() == reflect.Slice && v.Type() != typeOfByteSlice {
|
||||
if l.m == nil {
|
||||
l.m = make(map[string]int)
|
||||
}
|
||||
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(sliceIndex)
|
||||
|
||||
// If structValue implements PLS, we delegate loading to the PLS's
|
||||
// Load early, and stop iterating through fields.
|
||||
ok, err := plsFieldLoad(structValue, p, fieldNames)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
if structValue.Type().Kind() == reflect.Struct {
|
||||
codec, err = structCache.Fields(structValue.Type())
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
}
|
||||
sliceOk = true
|
||||
}
|
||||
}
|
||||
|
||||
var slice reflect.Value
|
||||
if v.Kind() == reflect.Slice && v.Type().Elem().Kind() != reflect.Uint8 {
|
||||
slice = v
|
||||
v = reflect.New(v.Type().Elem()).Elem()
|
||||
} else if _, ok := prev[p.Name]; ok && !sliceOk {
|
||||
// Zero the field back out that was set previously, turns out
|
||||
// it's a slice and we don't know what to do with it
|
||||
v.Set(reflect.Zero(v.Type()))
|
||||
return "multiple-valued property requires a slice field type"
|
||||
}
|
||||
|
||||
prev[p.Name] = struct{}{}
|
||||
|
||||
if errReason := setVal(v, p); 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 ""
|
||||
}
|
||||
|
||||
// plsFieldLoad first tries to converts v's value to a PLS, then v's addressed
|
||||
// value to a PLS. If neither succeeds, plsFieldLoad returns false for first return
|
||||
// value. Otherwise, the first return value will be true.
|
||||
// If v is successfully converted to a PLS, plsFieldLoad will then try to Load
|
||||
// the property p into v (by way of the PLS's Load method).
|
||||
//
|
||||
// If the field v has been flattened, the Property's name must be altered
|
||||
// before calling Load to reflect the field v.
|
||||
// For example, if our original field name was "A.B.C.D",
|
||||
// and at this point in iteration we had initialized the field
|
||||
// corresponding to "A" and have moved into the struct, so that now
|
||||
// v corresponds to the field named "B", then we want to let the
|
||||
// PLS handle this field (B)'s subfields ("C", "D"),
|
||||
// so we send the property to the PLS's Load, renamed to "C.D".
|
||||
//
|
||||
// If subfields are present, the field v has been flattened.
|
||||
func plsFieldLoad(v reflect.Value, p Property, subfields []string) (ok bool, err error) {
|
||||
vpls, err := plsForLoad(v)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if vpls == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// If Entity, load properties as well as key.
|
||||
if e, ok := p.Value.(*Entity); ok {
|
||||
err = loadEntity(vpls, e)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// If flattened, we must alter the property's name to reflect
|
||||
// the field v.
|
||||
if len(subfields) > 0 {
|
||||
p.Name = strings.Join(subfields, ".")
|
||||
}
|
||||
|
||||
return true, vpls.Load([]Property{p})
|
||||
}
|
||||
|
||||
// setVal sets 'v' to the value of the Property 'p'.
|
||||
func setVal(v reflect.Value, p Property) (s string) {
|
||||
pValue := p.Value
|
||||
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)
|
||||
}
|
||||
if v.OverflowInt(x) {
|
||||
return overflowReason(x, v)
|
||||
}
|
||||
v.SetInt(x)
|
||||
case reflect.Bool:
|
||||
x, ok := pValue.(bool)
|
||||
if !ok && pValue != nil {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
v.SetBool(x)
|
||||
case reflect.String:
|
||||
x, ok := pValue.(string)
|
||||
if !ok && pValue != nil {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
v.SetString(x)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
x, ok := pValue.(float64)
|
||||
if !ok && pValue != nil {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
if v.OverflowFloat(x) {
|
||||
return overflowReason(x, v)
|
||||
}
|
||||
v.SetFloat(x)
|
||||
case reflect.Ptr:
|
||||
// v must be a pointer to either a Key, an Entity, or one of the supported basic types.
|
||||
if v.Type() != typeOfKeyPtr && v.Type().Elem().Kind() != reflect.Struct && !isValidPointerType(v.Type().Elem()) {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
|
||||
if pValue == nil {
|
||||
// If v is populated already, set it to nil.
|
||||
if !v.IsNil() {
|
||||
v.Set(reflect.New(v.Type()).Elem())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
if x, ok := p.Value.(*Key); ok {
|
||||
if _, ok := v.Interface().(*Key); !ok {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
v.Set(reflect.ValueOf(x))
|
||||
return ""
|
||||
}
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
switch x := pValue.(type) {
|
||||
case *Entity:
|
||||
err := loadEntity(v.Interface(), x)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
case int64:
|
||||
if v.Elem().OverflowInt(x) {
|
||||
return overflowReason(x, v.Elem())
|
||||
}
|
||||
v.Elem().SetInt(x)
|
||||
case float64:
|
||||
if v.Elem().OverflowFloat(x) {
|
||||
return overflowReason(x, v.Elem())
|
||||
}
|
||||
v.Elem().SetFloat(x)
|
||||
case bool:
|
||||
v.Elem().SetBool(x)
|
||||
case string:
|
||||
v.Elem().SetString(x)
|
||||
case GeoPoint, time.Time:
|
||||
v.Elem().Set(reflect.ValueOf(x))
|
||||
default:
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
case reflect.Struct:
|
||||
switch v.Type() {
|
||||
case typeOfTime:
|
||||
x, ok := pValue.(time.Time)
|
||||
if !ok && pValue != nil {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
v.Set(reflect.ValueOf(x))
|
||||
case typeOfGeoPoint:
|
||||
x, ok := pValue.(GeoPoint)
|
||||
if !ok && pValue != nil {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
v.Set(reflect.ValueOf(x))
|
||||
default:
|
||||
ent, ok := pValue.(*Entity)
|
||||
if !ok {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
err := loadEntity(v.Addr().Interface(), ent)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
x, ok := pValue.([]byte)
|
||||
if !ok && pValue != nil {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
if v.Type().Elem().Kind() != reflect.Uint8 {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
v.SetBytes(x)
|
||||
default:
|
||||
return typeMismatchReason(p, 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])
|
||||
}
|
||||
|
||||
// loadEntityProto loads an EntityProto into PropertyLoadSaver or struct pointer.
|
||||
func loadEntityProto(dst interface{}, src *pb.Entity) error {
|
||||
ent, err := protoToEntity(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return loadEntity(dst, ent)
|
||||
}
|
||||
|
||||
func loadEntity(dst interface{}, ent *Entity) error {
|
||||
if pls, ok := dst.(PropertyLoadSaver); ok {
|
||||
err := pls.Load(ent.Properties)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e, ok := dst.(KeyLoader); ok {
|
||||
err = e.LoadKey(ent.Key)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return loadEntityToStruct(dst, ent)
|
||||
}
|
||||
|
||||
func loadEntityToStruct(dst interface{}, ent *Entity) error {
|
||||
pls, err := newStructPLS(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Load properties.
|
||||
err = pls.Load(ent.Properties)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Load key.
|
||||
keyField := pls.codec.Match(keyFieldName)
|
||||
if keyField != nil && ent.Key != nil {
|
||||
pls.v.FieldByIndex(keyField.Index).Set(reflect.ValueOf(ent.Key))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s structPLS) Load(props []Property) error {
|
||||
var fieldName, errReason string
|
||||
var l propertyLoader
|
||||
|
||||
prev := make(map[string]struct{})
|
||||
for _, p := range props {
|
||||
if errStr := l.load(s.codec, s.v, p, prev); errStr != "" {
|
||||
// We don't return early, as we try to load as many properties as possible.
|
||||
// It is valid to load an entity into a struct that cannot fully represent it.
|
||||
// That case returns an error, but the caller is free to ignore it.
|
||||
fieldName, errReason = p.Name, errStr
|
||||
}
|
||||
}
|
||||
if errReason != "" {
|
||||
return &ErrFieldMismatch{
|
||||
StructType: s.v.Type(),
|
||||
FieldName: fieldName,
|
||||
Reason: errReason,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func protoToEntity(src *pb.Entity) (*Entity, error) {
|
||||
props := make([]Property, 0, len(src.Properties))
|
||||
for name, val := range src.Properties {
|
||||
v, err := propToValue(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
props = append(props, Property{
|
||||
Name: name,
|
||||
Value: v,
|
||||
NoIndex: val.ExcludeFromIndexes,
|
||||
})
|
||||
}
|
||||
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, props}, nil
|
||||
}
|
||||
|
||||
// propToValue returns a Go value that represents the PropertyValue. For
|
||||
// example, a TimestampValue becomes a time.Time.
|
||||
func propToValue(v *pb.Value) (interface{}, error) {
|
||||
switch v := v.ValueType.(type) {
|
||||
case *pb.Value_NullValue:
|
||||
return nil, nil
|
||||
case *pb.Value_BooleanValue:
|
||||
return v.BooleanValue, nil
|
||||
case *pb.Value_IntegerValue:
|
||||
return v.IntegerValue, nil
|
||||
case *pb.Value_DoubleValue:
|
||||
return v.DoubleValue, nil
|
||||
case *pb.Value_TimestampValue:
|
||||
return time.Unix(v.TimestampValue.Seconds, int64(v.TimestampValue.Nanos)), nil
|
||||
case *pb.Value_KeyValue:
|
||||
return protoToKey(v.KeyValue)
|
||||
case *pb.Value_StringValue:
|
||||
return v.StringValue, nil
|
||||
case *pb.Value_BlobValue:
|
||||
return []byte(v.BlobValue), nil
|
||||
case *pb.Value_GeoPointValue:
|
||||
return GeoPoint{Lat: v.GeoPointValue.Latitude, Lng: v.GeoPointValue.Longitude}, nil
|
||||
case *pb.Value_EntityValue:
|
||||
return protoToEntity(v.EntityValue)
|
||||
case *pb.Value_ArrayValue:
|
||||
arr := make([]interface{}, 0, len(v.ArrayValue.Values))
|
||||
for _, v := range v.ArrayValue.Values {
|
||||
vv, err := propToValue(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, vv)
|
||||
}
|
||||
return arr, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
+886
@@ -0,0 +1,886 @@
|
||||
// Copyright 2016 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
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 int
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func TestLoadEntityNestedLegacy(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
src *pb.Entity
|
||||
want interface{}
|
||||
}{
|
||||
{
|
||||
desc: "nested",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
||||
"A.I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
||||
},
|
||||
},
|
||||
want: &NestedSimple1{
|
||||
A: Simple{I: 2},
|
||||
X: "two",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested with tag",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"AA.II": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
||||
},
|
||||
},
|
||||
want: &NestedSimpleWithTag{
|
||||
A: SimpleWithTag{I: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested with anonymous struct field",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
||||
},
|
||||
},
|
||||
want: &NestedSimpleAnonymous{
|
||||
Simple: Simple{I: 2},
|
||||
X: "two",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested with dotted field tag",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"A.B.B": {ValueType: &pb.Value_StringValue{StringValue: "bb"}},
|
||||
},
|
||||
},
|
||||
want: &ABDotB{
|
||||
A: BDotB{
|
||||
B: "bb",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested with multiple anonymous fields",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 3}},
|
||||
"S": {ValueType: &pb.Value_StringValue{StringValue: "S"}},
|
||||
"SS": {ValueType: &pb.Value_StringValue{StringValue: "s"}},
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "s"}},
|
||||
},
|
||||
},
|
||||
want: &MultiAnonymous{
|
||||
Simple: Simple{I: 3},
|
||||
SimpleTwoFields: SimpleTwoFields{S: "S", SS: "s"},
|
||||
X: "s",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
dst := reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
|
||||
err := loadEntityProto(dst, tc.src)
|
||||
if err != nil {
|
||||
t.Errorf("loadEntityProto: %s: %v", tc.desc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !testutil.Equal(tc.want, dst) {
|
||||
t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, dst, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WithKey struct {
|
||||
X string
|
||||
I int
|
||||
K *Key `datastore:"__key__"`
|
||||
}
|
||||
|
||||
type NestedWithKey struct {
|
||||
Y string
|
||||
N WithKey
|
||||
}
|
||||
|
||||
var (
|
||||
incompleteKey = newKey("", nil)
|
||||
invalidKey = newKey("s", incompleteKey)
|
||||
)
|
||||
|
||||
func TestLoadEntityNested(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
src *pb.Entity
|
||||
want interface{}
|
||||
}{
|
||||
{
|
||||
desc: "nested basic",
|
||||
src: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 3}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 10}},
|
||||
},
|
||||
},
|
||||
want: &NestedSimple{
|
||||
A: Simple{I: 3},
|
||||
I: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested with struct tags",
|
||||
src: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"AA": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"II": {ValueType: &pb.Value_IntegerValue{IntegerValue: 1}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
want: &NestedSimpleWithTag{
|
||||
A: SimpleWithTag{I: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested 2x",
|
||||
src: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"AA": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 3}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 1}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"A": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"S": {ValueType: &pb.Value_StringValue{StringValue: "S"}},
|
||||
"SS": {ValueType: &pb.Value_StringValue{StringValue: "s"}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"S": {ValueType: &pb.Value_StringValue{StringValue: "SS"}},
|
||||
},
|
||||
},
|
||||
want: &NestedSimple2X{
|
||||
AA: NestedSimple{
|
||||
A: Simple{I: 3},
|
||||
I: 1,
|
||||
},
|
||||
A: SimpleTwoFields{S: "S", SS: "s"},
|
||||
S: "SS",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested anonymous",
|
||||
src: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 3}},
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "SomeX"}},
|
||||
},
|
||||
},
|
||||
want: &NestedSimpleAnonymous{
|
||||
Simple: Simple{I: 3},
|
||||
X: "SomeX",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested simple with slice",
|
||||
src: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {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: 3}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 4}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
|
||||
want: &NestedSliceOfSimple{
|
||||
A: []Simple{Simple{I: 3}, Simple{I: 4}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested with multiple anonymous fields",
|
||||
src: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 3}},
|
||||
"S": {ValueType: &pb.Value_StringValue{StringValue: "S"}},
|
||||
"SS": {ValueType: &pb.Value_StringValue{StringValue: "s"}},
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "ss"}},
|
||||
},
|
||||
},
|
||||
want: &MultiAnonymous{
|
||||
Simple: Simple{I: 3},
|
||||
SimpleTwoFields: SimpleTwoFields{S: "S", SS: "s"},
|
||||
X: "ss",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested with dotted field tag",
|
||||
src: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"B.B": {ValueType: &pb.Value_StringValue{StringValue: "bb"}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
want: &ABDotB{
|
||||
A: BDotB{
|
||||
B: "bb",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested entity with key",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Key: keyToProto(testKey1a),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
want: &NestedWithKey{
|
||||
Y: "yyy",
|
||||
N: WithKey{
|
||||
X: "two",
|
||||
I: 2,
|
||||
K: testKey1a,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested entity with invalid key",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Key: keyToProto(invalidKey),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
want: &NestedWithKey{
|
||||
Y: "yyy",
|
||||
N: WithKey{
|
||||
X: "two",
|
||||
I: 2,
|
||||
K: invalidKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
dst := reflect.New(reflect.TypeOf(tc.want).Elem()).Interface()
|
||||
err := loadEntityProto(dst, tc.src)
|
||||
if err != nil {
|
||||
t.Errorf("loadEntityProto: %s: %v", tc.desc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !testutil.Equal(tc.want, dst) {
|
||||
t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, dst, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type NestedStructPtrs struct {
|
||||
*SimpleTwoFields
|
||||
Nest *SimpleTwoFields
|
||||
TwiceNest *NestedSimple2
|
||||
I int
|
||||
}
|
||||
|
||||
type NestedSimple2 struct {
|
||||
A *Simple
|
||||
I int
|
||||
}
|
||||
|
||||
func TestAlreadyPopulatedDst(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
src *pb.Entity
|
||||
dst interface{}
|
||||
want interface{}
|
||||
}{
|
||||
{
|
||||
desc: "simple already populated, nil properties",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_NullValue{}},
|
||||
},
|
||||
},
|
||||
dst: &Simple{
|
||||
I: 12,
|
||||
},
|
||||
want: &Simple{},
|
||||
},
|
||||
{
|
||||
desc: "nested structs already populated",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"SS": {ValueType: &pb.Value_StringValue{StringValue: "world"}},
|
||||
},
|
||||
},
|
||||
dst: &SimpleTwoFields{S: "hello" /* SS: "" */},
|
||||
want: &SimpleTwoFields{S: "hello", SS: "world"},
|
||||
},
|
||||
{
|
||||
desc: "nested structs already populated, pValues nil",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"S": {ValueType: &pb.Value_NullValue{}},
|
||||
"SS": {ValueType: &pb.Value_StringValue{StringValue: "ss hello"}},
|
||||
"Nest": {ValueType: &pb.Value_NullValue{}},
|
||||
"TwiceNest": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_NullValue{}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 5}},
|
||||
},
|
||||
},
|
||||
dst: &NestedStructPtrs{
|
||||
&SimpleTwoFields{S: "hello" /* SS: "" */},
|
||||
&SimpleTwoFields{ /* S: "" */ SS: "twice hello"},
|
||||
&NestedSimple2{
|
||||
A: &Simple{I: 2},
|
||||
/* I: 0 */
|
||||
},
|
||||
0,
|
||||
},
|
||||
want: &NestedStructPtrs{
|
||||
&SimpleTwoFields{ /* S: "" */ SS: "ss hello"},
|
||||
nil,
|
||||
&NestedSimple2{
|
||||
/* A: nil, */
|
||||
I: 2,
|
||||
},
|
||||
5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
err := loadEntityProto(tc.dst, tc.src)
|
||||
if err != nil {
|
||||
t.Errorf("loadEntityProto: %s: %v", tc.desc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !testutil.Equal(tc.want, tc.dst) {
|
||||
t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, tc.dst, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PLS0 struct {
|
||||
A string
|
||||
}
|
||||
|
||||
func (p *PLS0) Load(props []Property) error {
|
||||
for _, pp := range props {
|
||||
if pp.Name == "A" {
|
||||
p.A = pp.Value.(string)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PLS0) Save() (props []Property, err error) {
|
||||
return []Property{{Name: "A", Value: p.A}}, nil
|
||||
}
|
||||
|
||||
type KeyLoader1 struct {
|
||||
A string
|
||||
K *Key
|
||||
}
|
||||
|
||||
func (kl *KeyLoader1) Load(props []Property) error {
|
||||
for _, pp := range props {
|
||||
if pp.Name == "A" {
|
||||
kl.A = pp.Value.(string)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kl *KeyLoader1) Save() (props []Property, err error) {
|
||||
return []Property{{Name: "A", Value: kl.A}}, nil
|
||||
}
|
||||
|
||||
func (kl *KeyLoader1) LoadKey(k *Key) error {
|
||||
kl.K = k
|
||||
return nil
|
||||
}
|
||||
|
||||
type KeyLoader2 struct {
|
||||
B int
|
||||
Key *Key
|
||||
}
|
||||
|
||||
func (kl *KeyLoader2) Load(props []Property) error {
|
||||
for _, pp := range props {
|
||||
if pp.Name == "B" {
|
||||
kl.B = int(pp.Value.(int64))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kl *KeyLoader2) Save() (props []Property, err error) {
|
||||
return []Property{{Name: "B", Value: int64(kl.B)}}, nil
|
||||
}
|
||||
|
||||
func (kl *KeyLoader2) LoadKey(k *Key) error {
|
||||
kl.Key = k
|
||||
return nil
|
||||
}
|
||||
|
||||
type KeyLoader3 struct {
|
||||
C bool
|
||||
K *Key
|
||||
}
|
||||
|
||||
func (kl *KeyLoader3) Load(props []Property) error {
|
||||
for _, pp := range props {
|
||||
if pp.Name == "C" {
|
||||
kl.C = pp.Value.(bool)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kl *KeyLoader3) Save() (props []Property, err error) {
|
||||
return []Property{{Name: "C", Value: kl.C}}, nil
|
||||
}
|
||||
|
||||
func (kl *KeyLoader3) LoadKey(k *Key) error {
|
||||
kl.K = k
|
||||
return nil
|
||||
}
|
||||
|
||||
type KeyLoader4 struct {
|
||||
PLS0
|
||||
K *Key
|
||||
}
|
||||
|
||||
func (kl *KeyLoader4) LoadKey(k *Key) error {
|
||||
kl.K = k
|
||||
return nil
|
||||
}
|
||||
|
||||
type NotKeyLoader struct {
|
||||
A string
|
||||
K *Key
|
||||
}
|
||||
|
||||
func (p *NotKeyLoader) Load(props []Property) error {
|
||||
for _, pp := range props {
|
||||
if pp.Name == "A" {
|
||||
p.A = pp.Value.(string)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NotKeyLoader) Save() (props []Property, err error) {
|
||||
return []Property{{Name: "A", Value: p.A}}, nil
|
||||
}
|
||||
|
||||
type NestedKeyLoaders struct {
|
||||
Two *KeyLoader2
|
||||
Three []*KeyLoader3
|
||||
Four *KeyLoader4
|
||||
PLS *NotKeyLoader
|
||||
}
|
||||
|
||||
func TestKeyLoader(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
src *pb.Entity
|
||||
dst interface{}
|
||||
want interface{}
|
||||
}{
|
||||
{
|
||||
desc: "simple key loader",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_StringValue{StringValue: "hello"}},
|
||||
},
|
||||
},
|
||||
dst: &KeyLoader1{},
|
||||
want: &KeyLoader1{
|
||||
A: "hello",
|
||||
K: testKey0,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "embedded PLS key loader",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_StringValue{StringValue: "hello"}},
|
||||
},
|
||||
},
|
||||
dst: &KeyLoader4{},
|
||||
want: &KeyLoader4{
|
||||
PLS0: PLS0{A: "hello"},
|
||||
K: testKey0,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested key loaders",
|
||||
src: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Two": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"B": {ValueType: &pb.Value_IntegerValue{IntegerValue: 12}},
|
||||
},
|
||||
Key: keyToProto(testKey1a),
|
||||
},
|
||||
}},
|
||||
"Three": {ValueType: &pb.Value_ArrayValue{
|
||||
ArrayValue: &pb.ArrayValue{
|
||||
Values: []*pb.Value{
|
||||
{ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"C": {ValueType: &pb.Value_BooleanValue{BooleanValue: true}},
|
||||
},
|
||||
Key: keyToProto(testKey1b),
|
||||
},
|
||||
}},
|
||||
{ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"C": {ValueType: &pb.Value_BooleanValue{BooleanValue: false}},
|
||||
},
|
||||
Key: keyToProto(testKey0),
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"Four": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_StringValue{StringValue: "testing"}},
|
||||
},
|
||||
Key: keyToProto(testKey2a),
|
||||
},
|
||||
}},
|
||||
"PLS": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_StringValue{StringValue: "something"}},
|
||||
},
|
||||
|
||||
Key: keyToProto(testKey1a),
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
dst: &NestedKeyLoaders{},
|
||||
want: &NestedKeyLoaders{
|
||||
Two: &KeyLoader2{B: 12, Key: testKey1a},
|
||||
Three: []*KeyLoader3{
|
||||
{
|
||||
C: true,
|
||||
K: testKey1b,
|
||||
},
|
||||
{
|
||||
C: false,
|
||||
K: testKey0,
|
||||
},
|
||||
},
|
||||
Four: &KeyLoader4{
|
||||
PLS0: PLS0{A: "testing"},
|
||||
K: testKey2a,
|
||||
},
|
||||
PLS: &NotKeyLoader{A: "something"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
err := loadEntityProto(tc.dst, tc.src)
|
||||
if err != nil {
|
||||
t.Errorf("loadEntityProto: %s: %v", tc.desc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !testutil.Equal(tc.want, tc.dst) {
|
||||
t.Errorf("%s: compare:\ngot: %+v\nwant: %+v", tc.desc, tc.dst, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPointers(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
in []Property
|
||||
want Pointers
|
||||
}{
|
||||
{
|
||||
desc: "nil properties load as nil pointers",
|
||||
in: []Property{
|
||||
Property{Name: "Pi", Value: nil},
|
||||
Property{Name: "Ps", Value: nil},
|
||||
Property{Name: "Pb", Value: nil},
|
||||
Property{Name: "Pf", Value: nil},
|
||||
Property{Name: "Pg", Value: nil},
|
||||
Property{Name: "Pt", Value: nil},
|
||||
},
|
||||
want: Pointers{},
|
||||
},
|
||||
{
|
||||
desc: "missing properties load as nil pointers",
|
||||
in: []Property(nil),
|
||||
want: Pointers{},
|
||||
},
|
||||
{
|
||||
desc: "non-nil properties load as the appropriate values",
|
||||
in: []Property{
|
||||
Property{Name: "Pi", Value: int64(1)},
|
||||
Property{Name: "Ps", Value: "x"},
|
||||
Property{Name: "Pb", Value: true},
|
||||
Property{Name: "Pf", Value: 3.14},
|
||||
Property{Name: "Pg", Value: GeoPoint{Lat: 1, Lng: 2}},
|
||||
Property{Name: "Pt", Value: time.Unix(100, 0)},
|
||||
},
|
||||
want: func() Pointers {
|
||||
p := populatedPointers()
|
||||
*p.Pi = 1
|
||||
*p.Ps = "x"
|
||||
*p.Pb = true
|
||||
*p.Pf = 3.14
|
||||
*p.Pg = GeoPoint{Lat: 1, Lng: 2}
|
||||
*p.Pt = time.Unix(100, 0)
|
||||
return *p
|
||||
}(),
|
||||
},
|
||||
} {
|
||||
var got Pointers
|
||||
if err := LoadStruct(&got, test.in); err != nil {
|
||||
t.Fatalf("%s: %v", test.desc, err)
|
||||
}
|
||||
if !testutil.Equal(got, test.want) {
|
||||
t.Errorf("%s:\ngot %+v\nwant %+v", test.desc, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNonArrayIntoSlice(t *testing.T) {
|
||||
// Loading a non-array value into a slice field results in a slice of size 1.
|
||||
var got struct{ S []string }
|
||||
if err := LoadStruct(&got, []Property{{Name: "S", Value: "x"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := []string{"x"}; !testutil.Equal(got.S, want) {
|
||||
t.Errorf("got %#v, want %#v", got.S, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEmptyArrayIntoSlice(t *testing.T) {
|
||||
// Loading an empty array into a slice field is a no-op.
|
||||
var got = struct{ S []string }{[]string{"x"}}
|
||||
if err := LoadStruct(&got, []Property{{Name: "S", Value: []interface{}{}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := []string{"x"}; !testutil.Equal(got.S, want) {
|
||||
t.Errorf("got %#v, want %#v", got.S, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNull(t *testing.T) {
|
||||
// Loading a Datastore Null into a basic type (int, float, etc.) results in a zero value.
|
||||
// Loading a Null into a slice of basic type results in a slice of size 1 containing the zero value.
|
||||
// (As expected from the behavior of slices and nulls with basic types.)
|
||||
type S struct {
|
||||
I int64
|
||||
F float64
|
||||
S string
|
||||
B bool
|
||||
A []string
|
||||
}
|
||||
got := S{
|
||||
I: 1,
|
||||
F: 1.0,
|
||||
S: "1",
|
||||
B: true,
|
||||
A: []string{"X"},
|
||||
}
|
||||
want := S{A: []string{""}}
|
||||
props := []Property{{Name: "I"}, {Name: "F"}, {Name: "S"}, {Name: "B"}, {Name: "A"}}
|
||||
if err := LoadStruct(&got, props); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Errorf("got %+v, want %+v", got, want)
|
||||
}
|
||||
|
||||
// Loading a Null into a pointer to struct field results in a nil field.
|
||||
got2 := struct{ X *S }{X: &S{}}
|
||||
if err := LoadStruct(&got2, []Property{{Name: "X"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got2.X != nil {
|
||||
t.Errorf("got %v, want nil", got2.X)
|
||||
}
|
||||
|
||||
// Loading a Null into a struct field is an error.
|
||||
got3 := struct{ X S }{}
|
||||
err := LoadStruct(&got3, []Property{{Name: "X"}})
|
||||
if err == nil {
|
||||
t.Error("got nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
// var got2 struct{ S []Pet }
|
||||
// if err := LoadStruct(&got2, []Property{{Name: "S", Value: nil}}); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
|
||||
// }
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
// Copyright 2018 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 (
|
||||
"fmt"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
// A Mutation represents a change to a Datastore entity.
|
||||
type Mutation struct {
|
||||
key *Key // needed for transaction PendingKeys and to dedup deletions
|
||||
mut *pb.Mutation
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *Mutation) isDelete() bool {
|
||||
_, ok := m.mut.Operation.(*pb.Mutation_Delete)
|
||||
return ok
|
||||
}
|
||||
|
||||
// NewInsert creates a mutation that will save the entity src into the datastore with
|
||||
// key k, returning an error if k already exists.
|
||||
// See Client.Put for valid values of src.
|
||||
func NewInsert(k *Key, src interface{}) *Mutation {
|
||||
if !k.valid() {
|
||||
return &Mutation{err: ErrInvalidKey}
|
||||
}
|
||||
p, err := saveEntity(k, src)
|
||||
if err != nil {
|
||||
return &Mutation{err: err}
|
||||
}
|
||||
return &Mutation{
|
||||
key: k,
|
||||
mut: &pb.Mutation{Operation: &pb.Mutation_Insert{Insert: p}},
|
||||
}
|
||||
}
|
||||
|
||||
// NewUpsert creates a mutation that saves the entity src into the datastore with key
|
||||
// k, whether or not k exists. See Client.Put for valid values of src.
|
||||
func NewUpsert(k *Key, src interface{}) *Mutation {
|
||||
if !k.valid() {
|
||||
return &Mutation{err: ErrInvalidKey}
|
||||
}
|
||||
p, err := saveEntity(k, src)
|
||||
if err != nil {
|
||||
return &Mutation{err: err}
|
||||
}
|
||||
return &Mutation{
|
||||
key: k,
|
||||
mut: &pb.Mutation{Operation: &pb.Mutation_Upsert{Upsert: p}},
|
||||
}
|
||||
}
|
||||
|
||||
// NewUpdate creates a mutation that replaces the entity in the datastore with key k,
|
||||
// returning an error if k does not exist. See Client.Put for valid values of src.
|
||||
func NewUpdate(k *Key, src interface{}) *Mutation {
|
||||
if !k.valid() {
|
||||
return &Mutation{err: ErrInvalidKey}
|
||||
}
|
||||
if k.Incomplete() {
|
||||
return &Mutation{err: fmt.Errorf("datastore: can't update the incomplete key: %v", k)}
|
||||
}
|
||||
p, err := saveEntity(k, src)
|
||||
if err != nil {
|
||||
return &Mutation{err: err}
|
||||
}
|
||||
return &Mutation{
|
||||
key: k,
|
||||
mut: &pb.Mutation{Operation: &pb.Mutation_Update{Update: p}},
|
||||
}
|
||||
}
|
||||
|
||||
// NewDelete creates a mutation that deletes the entity with key k.
|
||||
func NewDelete(k *Key) *Mutation {
|
||||
if !k.valid() {
|
||||
return &Mutation{err: ErrInvalidKey}
|
||||
}
|
||||
if k.Incomplete() {
|
||||
return &Mutation{err: fmt.Errorf("datastore: can't delete the incomplete key: %v", k)}
|
||||
}
|
||||
return &Mutation{
|
||||
key: k,
|
||||
mut: &pb.Mutation{Operation: &pb.Mutation_Delete{Delete: keyToProto(k)}},
|
||||
}
|
||||
}
|
||||
|
||||
func mutationProtos(muts []*Mutation) ([]*pb.Mutation, error) {
|
||||
// If any of the mutations have errors, collect and return them.
|
||||
var merr MultiError
|
||||
for i, m := range muts {
|
||||
if m.err != nil {
|
||||
if merr == nil {
|
||||
merr = make(MultiError, len(muts))
|
||||
}
|
||||
merr[i] = m.err
|
||||
}
|
||||
}
|
||||
if merr != nil {
|
||||
return nil, merr
|
||||
}
|
||||
var protos []*pb.Mutation
|
||||
// Collect protos. Remove duplicate deletions (see deleteMutations).
|
||||
seen := map[string]bool{}
|
||||
for _, m := range muts {
|
||||
if m.isDelete() {
|
||||
ks := m.key.String()
|
||||
if seen[ks] {
|
||||
continue
|
||||
}
|
||||
seen[ks] = true
|
||||
}
|
||||
protos = append(protos, m.mut)
|
||||
}
|
||||
return protos, nil
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
// Copyright 2018 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 (
|
||||
"testing"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
func TestMutationProtos(t *testing.T) {
|
||||
var keys []*Key
|
||||
for i := 1; i <= 4; i++ {
|
||||
k := IDKey("kind", int64(i), nil)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
entity := &PropertyList{{Name: "n", Value: "v"}}
|
||||
entityForKey := func(k *Key) *pb.Entity {
|
||||
return &pb.Entity{
|
||||
Key: keyToProto(k),
|
||||
Properties: map[string]*pb.Value{
|
||||
"n": &pb.Value{ValueType: &pb.Value_StringValue{StringValue: "v"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
in []*Mutation
|
||||
want []*pb.Mutation
|
||||
}{
|
||||
{
|
||||
desc: "nil",
|
||||
in: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: []*Mutation{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
desc: "various",
|
||||
in: []*Mutation{
|
||||
NewInsert(keys[0], entity),
|
||||
NewUpsert(keys[1], entity),
|
||||
NewUpdate(keys[2], entity),
|
||||
NewDelete(keys[3]),
|
||||
},
|
||||
want: []*pb.Mutation{
|
||||
&pb.Mutation{Operation: &pb.Mutation_Insert{Insert: entityForKey(keys[0])}},
|
||||
&pb.Mutation{Operation: &pb.Mutation_Upsert{Upsert: entityForKey(keys[1])}},
|
||||
&pb.Mutation{Operation: &pb.Mutation_Update{Update: entityForKey(keys[2])}},
|
||||
&pb.Mutation{Operation: &pb.Mutation_Delete{Delete: keyToProto(keys[3])}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "duplicate deletes",
|
||||
in: []*Mutation{
|
||||
NewDelete(keys[0]),
|
||||
NewInsert(keys[1], entity),
|
||||
NewDelete(keys[0]),
|
||||
NewDelete(keys[2]),
|
||||
NewDelete(keys[0]),
|
||||
},
|
||||
want: []*pb.Mutation{
|
||||
&pb.Mutation{Operation: &pb.Mutation_Delete{Delete: keyToProto(keys[0])}},
|
||||
&pb.Mutation{Operation: &pb.Mutation_Insert{Insert: entityForKey(keys[1])}},
|
||||
&pb.Mutation{Operation: &pb.Mutation_Delete{Delete: keyToProto(keys[2])}},
|
||||
},
|
||||
},
|
||||
} {
|
||||
got, err := mutationProtos(test.in)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", test.desc, err)
|
||||
continue
|
||||
}
|
||||
if diff := testutil.Diff(got, test.want); diff != "" {
|
||||
t.Errorf("%s: %s", test.desc, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMutationProtosErrors(t *testing.T) {
|
||||
entity := &PropertyList{{Name: "n", Value: "v"}}
|
||||
k := IDKey("kind", 1, nil)
|
||||
ik := IncompleteKey("kind", nil)
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
in []*Mutation
|
||||
want []int // non-nil indexes of MultiError
|
||||
}{
|
||||
{
|
||||
desc: "invalid key",
|
||||
in: []*Mutation{
|
||||
NewInsert(nil, entity),
|
||||
NewUpdate(nil, entity),
|
||||
NewUpsert(nil, entity),
|
||||
NewDelete(nil),
|
||||
},
|
||||
want: []int{0, 1, 2, 3},
|
||||
},
|
||||
{
|
||||
desc: "incomplete key",
|
||||
in: []*Mutation{
|
||||
NewInsert(ik, entity),
|
||||
NewUpdate(ik, entity),
|
||||
NewUpsert(ik, entity),
|
||||
NewDelete(ik),
|
||||
},
|
||||
want: []int{1, 3},
|
||||
},
|
||||
{
|
||||
desc: "bad entity",
|
||||
in: []*Mutation{
|
||||
NewInsert(k, 1),
|
||||
NewUpdate(k, 2),
|
||||
NewUpsert(k, 3),
|
||||
},
|
||||
want: []int{0, 1, 2},
|
||||
},
|
||||
} {
|
||||
_, err := mutationProtos(test.in)
|
||||
if err == nil {
|
||||
t.Errorf("%s: got nil, want error", test.desc)
|
||||
continue
|
||||
}
|
||||
var got []int
|
||||
for i, err := range err.(MultiError) {
|
||||
if err != nil {
|
||||
got = append(got, i)
|
||||
}
|
||||
}
|
||||
if !testutil.Equal(got, test.want) {
|
||||
t.Errorf("%s: got errors at %v, want at %v", test.desc, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright 2018 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.
|
||||
|
||||
// +build go1.8
|
||||
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestOCTracing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := newTestClient(ctx, t)
|
||||
defer client.Close()
|
||||
|
||||
te := testutil.NewTestExporter()
|
||||
defer te.Unregister()
|
||||
|
||||
type SomeValue struct {
|
||||
S string
|
||||
}
|
||||
_, err := client.Put(ctx, IncompleteKey("SomeKey", nil), &SomeValue{"foo"})
|
||||
if err != nil {
|
||||
t.Fatalf("client.Put: %v", err)
|
||||
}
|
||||
|
||||
if len(te.Spans) == 0 {
|
||||
t.Fatalf("Expected some span to be created, but got %d", 0)
|
||||
}
|
||||
}
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"cloud.google.com/go/internal/fields"
|
||||
)
|
||||
|
||||
// Entities with more than this many indexed properties will not be saved.
|
||||
const maxIndexedProperties = 20000
|
||||
|
||||
// []byte fields more than 1 megabyte long will not be loaded or saved.
|
||||
const maxBlobLen = 1 << 20
|
||||
|
||||
// Property is a name/value pair plus some metadata. A datastore entity's
|
||||
// contents are loaded and saved as a sequence of Properties. Each property
|
||||
// name must be unique within an entity.
|
||||
type Property struct {
|
||||
// Name is the property name.
|
||||
Name string
|
||||
// Value is the property value. The valid types are:
|
||||
// - int64
|
||||
// - bool
|
||||
// - string
|
||||
// - float64
|
||||
// - *Key
|
||||
// - time.Time
|
||||
// - GeoPoint
|
||||
// - []byte (up to 1 megabyte in length)
|
||||
// - *Entity (representing a nested struct)
|
||||
// Value can also be:
|
||||
// - []interface{} where each element is one of the above types
|
||||
// This set is smaller than the set of valid struct field types that the
|
||||
// datastore can load and save. A Value's type must be explicitly on
|
||||
// the list above; it is not sufficient for the underlying type to be
|
||||
// on that list. For example, a Value of "type myInt64 int64" is
|
||||
// invalid. Smaller-width integers and floats are also invalid. Again,
|
||||
// this is more restrictive than the set of valid struct field types.
|
||||
//
|
||||
// A Value will have an opaque type when loading entities from an index,
|
||||
// such as via a projection query. Load entities into a struct instead
|
||||
// of a PropertyLoadSaver when using a projection query.
|
||||
//
|
||||
// A Value may also be the nil interface value; this is equivalent to
|
||||
// Python's None but not directly representable by a Go struct. Loading
|
||||
// a nil-valued property into a struct will set that field to the zero
|
||||
// value.
|
||||
Value interface{}
|
||||
// NoIndex is whether the datastore cannot index this property.
|
||||
// If NoIndex is set to false, []byte and string values are limited to
|
||||
// 1500 bytes.
|
||||
NoIndex 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
|
||||
}
|
||||
|
||||
// PropertyLoadSaver can be converted from and to a slice of Properties.
|
||||
type PropertyLoadSaver interface {
|
||||
Load([]Property) error
|
||||
Save() ([]Property, error)
|
||||
}
|
||||
|
||||
// KeyLoader can store a Key.
|
||||
type KeyLoader interface {
|
||||
// PropertyLoadSaver is embedded because a KeyLoader
|
||||
// must also always implement PropertyLoadSaver.
|
||||
PropertyLoadSaver
|
||||
LoadKey(k *Key) error
|
||||
}
|
||||
|
||||
// PropertyList converts a []Property to implement PropertyLoadSaver.
|
||||
type PropertyList []Property
|
||||
|
||||
var (
|
||||
typeOfPropertyLoadSaver = reflect.TypeOf((*PropertyLoadSaver)(nil)).Elem()
|
||||
typeOfPropertyList = reflect.TypeOf(PropertyList(nil))
|
||||
)
|
||||
|
||||
// Load loads all of the provided properties into l.
|
||||
// It does not first reset *l to an empty slice.
|
||||
func (l *PropertyList) Load(p []Property) error {
|
||||
*l = append(*l, p...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves all of l's properties as a slice of Properties.
|
||||
func (l *PropertyList) Save() ([]Property, error) {
|
||||
return *l, nil
|
||||
}
|
||||
|
||||
// validPropertyName returns whether name consists of one or more valid Go
|
||||
// identifiers joined by ".".
|
||||
func validPropertyName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
for _, s := range strings.Split(name, ".") {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
first := true
|
||||
for _, c := range s {
|
||||
if first {
|
||||
first = false
|
||||
if c != '_' && !unicode.IsLetter(c) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if c != '_' && !unicode.IsLetter(c) && !unicode.IsDigit(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// parseTag interprets datastore struct field tags
|
||||
func parseTag(t reflect.StructTag) (name string, keep bool, other interface{}, err error) {
|
||||
s := t.Get("datastore")
|
||||
parts := strings.Split(s, ",")
|
||||
if parts[0] == "-" && len(parts) == 1 {
|
||||
return "", false, nil, nil
|
||||
}
|
||||
if parts[0] != "" && !validPropertyName(parts[0]) {
|
||||
err = fmt.Errorf("datastore: struct tag has invalid property name: %q", parts[0])
|
||||
return "", false, nil, err
|
||||
}
|
||||
|
||||
var opts saveOpts
|
||||
if len(parts) > 1 {
|
||||
for _, p := range parts[1:] {
|
||||
switch p {
|
||||
case "flatten":
|
||||
opts.flatten = true
|
||||
case "omitempty":
|
||||
opts.omitEmpty = true
|
||||
case "noindex":
|
||||
opts.noIndex = true
|
||||
default:
|
||||
err = fmt.Errorf("datastore: struct tag has invalid option: %q", p)
|
||||
return "", false, nil, err
|
||||
}
|
||||
}
|
||||
other = opts
|
||||
}
|
||||
return parts[0], true, other, nil
|
||||
}
|
||||
|
||||
func validateType(t reflect.Type) error {
|
||||
if t.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("datastore: validate called with non-struct type %s", t)
|
||||
}
|
||||
|
||||
return validateChildType(t, "", false, false, map[reflect.Type]bool{})
|
||||
}
|
||||
|
||||
// validateChildType is a recursion helper func for validateType
|
||||
func validateChildType(t reflect.Type, fieldName string, flatten, prevSlice bool, prevTypes map[reflect.Type]bool) error {
|
||||
if prevTypes[t] {
|
||||
return nil
|
||||
}
|
||||
prevTypes[t] = true
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Slice:
|
||||
if flatten && prevSlice {
|
||||
return fmt.Errorf("datastore: flattening nested structs leads to a slice of slices: field %q", fieldName)
|
||||
}
|
||||
return validateChildType(t.Elem(), fieldName, flatten, true, prevTypes)
|
||||
case reflect.Struct:
|
||||
if t == typeOfTime || t == typeOfGeoPoint {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
|
||||
// If a named field is unexported, ignore it. An anonymous
|
||||
// unexported field is processed, because it may contain
|
||||
// exported fields, which are visible.
|
||||
exported := (f.PkgPath == "")
|
||||
if !exported && !f.Anonymous {
|
||||
continue
|
||||
}
|
||||
|
||||
_, keep, other, err := parseTag(f.Tag)
|
||||
// Handle error from parseTag now instead of later (in cache.Fields call).
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
if other != nil {
|
||||
opts := other.(saveOpts)
|
||||
flatten = flatten || opts.flatten
|
||||
}
|
||||
if err := validateChildType(f.Type, f.Name, flatten, prevSlice, prevTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case reflect.Ptr:
|
||||
if t == typeOfKeyPtr {
|
||||
return nil
|
||||
}
|
||||
return validateChildType(t.Elem(), fieldName, flatten, prevSlice, prevTypes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isLeafType determines whether or not a type is a 'leaf type'
|
||||
// and should not be recursed into, but considered one field.
|
||||
func isLeafType(t reflect.Type) bool {
|
||||
return t == typeOfTime || t == typeOfGeoPoint
|
||||
}
|
||||
|
||||
// structCache collects the structs whose fields have already been calculated.
|
||||
var structCache = fields.NewCache(parseTag, validateType, isLeafType)
|
||||
|
||||
// structPLS adapts a struct to be a PropertyLoadSaver.
|
||||
type structPLS struct {
|
||||
v reflect.Value
|
||||
codec fields.List
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
v = v.Elem()
|
||||
f, err := structCache.Fields(v.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &structPLS{v, f}, nil
|
||||
}
|
||||
|
||||
// LoadStruct loads the properties from p to dst.
|
||||
// dst must be a struct pointer.
|
||||
//
|
||||
// The values of dst's unmatched struct fields are not modified,
|
||||
// and matching slice-typed fields are not reset before appending to
|
||||
// them. In particular, it is recommended to pass a pointer to a zero
|
||||
// valued struct on each LoadStruct call.
|
||||
func LoadStruct(dst interface{}, p []Property) error {
|
||||
x, err := newStructPLS(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return x.Load(p)
|
||||
}
|
||||
|
||||
// SaveStruct returns the properties from src as a slice of Properties.
|
||||
// src must be a struct pointer.
|
||||
func SaveStruct(src interface{}) ([]Property, error) {
|
||||
x, err := newStructPLS(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x.Save()
|
||||
}
|
||||
|
||||
// plsForLoad tries to convert v to a PropertyLoadSaver.
|
||||
// If successful, plsForLoad returns a settable v as a PropertyLoadSaver.
|
||||
//
|
||||
// plsForLoad is intended to be used with nested struct fields which
|
||||
// may implement PropertyLoadSaver.
|
||||
//
|
||||
// v must be settable.
|
||||
func plsForLoad(v reflect.Value) (PropertyLoadSaver, error) {
|
||||
var nilPtr bool
|
||||
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||
nilPtr = true
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
|
||||
vpls, err := pls(v)
|
||||
if nilPtr && (vpls == nil || err != nil) {
|
||||
// unset v
|
||||
v.Set(reflect.Zero(v.Type()))
|
||||
}
|
||||
|
||||
return vpls, err
|
||||
}
|
||||
|
||||
// plsForSave tries to convert v to a PropertyLoadSaver.
|
||||
// If successful, plsForSave returns v as a PropertyLoadSaver.
|
||||
//
|
||||
// plsForSave is intended to be used with nested struct fields which
|
||||
// may implement PropertyLoadSaver.
|
||||
//
|
||||
// v must be settable.
|
||||
func plsForSave(v reflect.Value) (PropertyLoadSaver, error) {
|
||||
switch v.Kind() {
|
||||
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface, reflect.Chan, reflect.Func:
|
||||
// If v is nil, return early. v contains no data to save.
|
||||
if v.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
return pls(v)
|
||||
}
|
||||
|
||||
func pls(v reflect.Value) (PropertyLoadSaver, error) {
|
||||
if v.Kind() != reflect.Ptr {
|
||||
if _, ok := v.Interface().(PropertyLoadSaver); ok {
|
||||
return nil, fmt.Errorf("datastore: PropertyLoadSaver methods must be implemented on a pointer to %T.", v.Interface())
|
||||
}
|
||||
|
||||
v = v.Addr()
|
||||
}
|
||||
|
||||
vpls, _ := v.Interface().(PropertyLoadSaver)
|
||||
return vpls, nil
|
||||
}
|
||||
+784
@@ -0,0 +1,784 @@
|
||||
// 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/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cloud.google.com/go/internal/trace"
|
||||
wrapperspb "github.com/golang/protobuf/ptypes/wrappers"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
type operator int
|
||||
|
||||
const (
|
||||
lessThan operator = iota + 1
|
||||
lessEq
|
||||
equal
|
||||
greaterEq
|
||||
greaterThan
|
||||
|
||||
keyFieldName = "__key__"
|
||||
)
|
||||
|
||||
var operatorToProto = map[operator]pb.PropertyFilter_Operator{
|
||||
lessThan: pb.PropertyFilter_LESS_THAN,
|
||||
lessEq: pb.PropertyFilter_LESS_THAN_OR_EQUAL,
|
||||
equal: pb.PropertyFilter_EQUAL,
|
||||
greaterEq: pb.PropertyFilter_GREATER_THAN_OR_EQUAL,
|
||||
greaterThan: pb.PropertyFilter_GREATER_THAN,
|
||||
}
|
||||
|
||||
// filter is a conditional filter on query results.
|
||||
type filter struct {
|
||||
FieldName string
|
||||
Op operator
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
type sortDirection bool
|
||||
|
||||
const (
|
||||
ascending sortDirection = false
|
||||
descending sortDirection = true
|
||||
)
|
||||
|
||||
var sortDirectionToProto = map[sortDirection]pb.PropertyOrder_Direction{
|
||||
ascending: pb.PropertyOrder_ASCENDING,
|
||||
descending: pb.PropertyOrder_DESCENDING,
|
||||
}
|
||||
|
||||
// order is a sort order on query results.
|
||||
type order struct {
|
||||
FieldName string
|
||||
Direction sortDirection
|
||||
}
|
||||
|
||||
// NewQuery creates a new Query for a specific entity kind.
|
||||
//
|
||||
// An empty kind means to return all entities, including entities created and
|
||||
// managed by other App Engine features, and is called a kindless query.
|
||||
// Kindless queries cannot include filters or sort orders on property values.
|
||||
func NewQuery(kind string) *Query {
|
||||
return &Query{
|
||||
kind: kind,
|
||||
limit: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// Query represents a datastore query.
|
||||
type Query struct {
|
||||
kind string
|
||||
ancestor *Key
|
||||
filter []filter
|
||||
order []order
|
||||
projection []string
|
||||
|
||||
distinct bool
|
||||
distinctOn []string
|
||||
keysOnly bool
|
||||
eventual bool
|
||||
limit int32
|
||||
offset int32
|
||||
start []byte
|
||||
end []byte
|
||||
|
||||
namespace string
|
||||
|
||||
trans *Transaction
|
||||
|
||||
err error
|
||||
}
|
||||
|
||||
func (q *Query) clone() *Query {
|
||||
x := *q
|
||||
// Copy the contents of the slice-typed fields to a new backing store.
|
||||
if len(q.filter) > 0 {
|
||||
x.filter = make([]filter, len(q.filter))
|
||||
copy(x.filter, q.filter)
|
||||
}
|
||||
if len(q.order) > 0 {
|
||||
x.order = make([]order, len(q.order))
|
||||
copy(x.order, q.order)
|
||||
}
|
||||
return &x
|
||||
}
|
||||
|
||||
// Ancestor returns a derivative query with an ancestor filter.
|
||||
// The ancestor should not be nil.
|
||||
func (q *Query) Ancestor(ancestor *Key) *Query {
|
||||
q = q.clone()
|
||||
if ancestor == nil {
|
||||
q.err = errors.New("datastore: nil query ancestor")
|
||||
return q
|
||||
}
|
||||
q.ancestor = ancestor
|
||||
return q
|
||||
}
|
||||
|
||||
// EventualConsistency returns a derivative query that returns eventually
|
||||
// consistent results.
|
||||
// It only has an effect on ancestor queries.
|
||||
func (q *Query) EventualConsistency() *Query {
|
||||
q = q.clone()
|
||||
q.eventual = true
|
||||
return q
|
||||
}
|
||||
|
||||
// Namespace returns a derivative query that is associated with the given
|
||||
// namespace.
|
||||
//
|
||||
// A namespace may be used to partition data for multi-tenant applications.
|
||||
// For details, see https://cloud.google.com/datastore/docs/concepts/multitenancy.
|
||||
func (q *Query) Namespace(ns string) *Query {
|
||||
q = q.clone()
|
||||
q.namespace = ns
|
||||
return q
|
||||
}
|
||||
|
||||
// Transaction returns a derivative query that is associated with the given
|
||||
// transaction.
|
||||
//
|
||||
// All reads performed as part of the transaction will come from a single
|
||||
// consistent snapshot. Furthermore, if the transaction is set to a
|
||||
// serializable isolation level, another transaction cannot concurrently modify
|
||||
// the data that is read or modified by this transaction.
|
||||
func (q *Query) Transaction(t *Transaction) *Query {
|
||||
q = q.clone()
|
||||
q.trans = t
|
||||
return q
|
||||
}
|
||||
|
||||
// Filter returns a derivative query with a field-based filter.
|
||||
// The filterStr argument must be a field name followed by optional space,
|
||||
// followed by an operator, one of ">", "<", ">=", "<=", or "=".
|
||||
// Fields are compared against the provided value using the operator.
|
||||
// Multiple filters are AND'ed together.
|
||||
// Field names which contain spaces, quote marks, or operator characters
|
||||
// should be passed as quoted Go string literals as returned by strconv.Quote
|
||||
// or the fmt package's %q verb.
|
||||
func (q *Query) Filter(filterStr string, value interface{}) *Query {
|
||||
q = q.clone()
|
||||
filterStr = strings.TrimSpace(filterStr)
|
||||
if filterStr == "" {
|
||||
q.err = fmt.Errorf("datastore: invalid filter %q", filterStr)
|
||||
return q
|
||||
}
|
||||
f := filter{
|
||||
FieldName: strings.TrimRight(filterStr, " ><=!"),
|
||||
Value: value,
|
||||
}
|
||||
switch op := strings.TrimSpace(filterStr[len(f.FieldName):]); op {
|
||||
case "<=":
|
||||
f.Op = lessEq
|
||||
case ">=":
|
||||
f.Op = greaterEq
|
||||
case "<":
|
||||
f.Op = lessThan
|
||||
case ">":
|
||||
f.Op = greaterThan
|
||||
case "=":
|
||||
f.Op = equal
|
||||
default:
|
||||
q.err = fmt.Errorf("datastore: invalid operator %q in filter %q", op, filterStr)
|
||||
return q
|
||||
}
|
||||
var err error
|
||||
f.FieldName, err = unquote(f.FieldName)
|
||||
if err != nil {
|
||||
q.err = fmt.Errorf("datastore: invalid syntax for quoted field name %q", f.FieldName)
|
||||
return q
|
||||
}
|
||||
q.filter = append(q.filter, f)
|
||||
return q
|
||||
}
|
||||
|
||||
// Order returns a derivative query with a field-based sort order. Orders are
|
||||
// applied in the order they are added. The default order is ascending; to sort
|
||||
// in descending order prefix the fieldName with a minus sign (-).
|
||||
// Field names which contain spaces, quote marks, or the minus sign
|
||||
// should be passed as quoted Go string literals as returned by strconv.Quote
|
||||
// or the fmt package's %q verb.
|
||||
func (q *Query) Order(fieldName string) *Query {
|
||||
q = q.clone()
|
||||
fieldName, dir := strings.TrimSpace(fieldName), ascending
|
||||
if strings.HasPrefix(fieldName, "-") {
|
||||
fieldName, dir = strings.TrimSpace(fieldName[1:]), descending
|
||||
} else if strings.HasPrefix(fieldName, "+") {
|
||||
q.err = fmt.Errorf("datastore: invalid order: %q", fieldName)
|
||||
return q
|
||||
}
|
||||
fieldName, err := unquote(fieldName)
|
||||
if err != nil {
|
||||
q.err = fmt.Errorf("datastore: invalid syntax for quoted field name %q", fieldName)
|
||||
return q
|
||||
}
|
||||
if fieldName == "" {
|
||||
q.err = errors.New("datastore: empty order")
|
||||
return q
|
||||
}
|
||||
q.order = append(q.order, order{
|
||||
Direction: dir,
|
||||
FieldName: fieldName,
|
||||
})
|
||||
return q
|
||||
}
|
||||
|
||||
// unquote optionally interprets s as a double-quoted or backquoted Go
|
||||
// string literal if it begins with the relevant character.
|
||||
func unquote(s string) (string, error) {
|
||||
if s == "" || (s[0] != '`' && s[0] != '"') {
|
||||
return s, nil
|
||||
}
|
||||
return strconv.Unquote(s)
|
||||
}
|
||||
|
||||
// Project returns a derivative query that yields only the given fields. It
|
||||
// cannot be used with KeysOnly.
|
||||
func (q *Query) Project(fieldNames ...string) *Query {
|
||||
q = q.clone()
|
||||
q.projection = append([]string(nil), fieldNames...)
|
||||
return q
|
||||
}
|
||||
|
||||
// Distinct returns a derivative query that yields de-duplicated entities with
|
||||
// respect to the set of projected fields. It is only used for projection
|
||||
// queries. Distinct cannot be used with DistinctOn.
|
||||
func (q *Query) Distinct() *Query {
|
||||
q = q.clone()
|
||||
q.distinct = true
|
||||
return q
|
||||
}
|
||||
|
||||
// DistinctOn returns a derivative query that yields de-duplicated entities with
|
||||
// respect to the set of the specified fields. It is only used for projection
|
||||
// queries. The field list should be a subset of the projected field list.
|
||||
// DistinctOn cannot be used with Distinct.
|
||||
func (q *Query) DistinctOn(fieldNames ...string) *Query {
|
||||
q = q.clone()
|
||||
q.distinctOn = fieldNames
|
||||
return q
|
||||
}
|
||||
|
||||
// KeysOnly returns a derivative query that yields only keys, not keys and
|
||||
// entities. It cannot be used with projection queries.
|
||||
func (q *Query) KeysOnly() *Query {
|
||||
q = q.clone()
|
||||
q.keysOnly = true
|
||||
return q
|
||||
}
|
||||
|
||||
// Limit returns a derivative query that has a limit on the number of results
|
||||
// returned. A negative value means unlimited.
|
||||
func (q *Query) Limit(limit int) *Query {
|
||||
q = q.clone()
|
||||
if limit < math.MinInt32 || limit > math.MaxInt32 {
|
||||
q.err = errors.New("datastore: query limit overflow")
|
||||
return q
|
||||
}
|
||||
q.limit = int32(limit)
|
||||
return q
|
||||
}
|
||||
|
||||
// Offset returns a derivative query that has an offset of how many keys to
|
||||
// skip over before returning results. A negative value is invalid.
|
||||
func (q *Query) Offset(offset int) *Query {
|
||||
q = q.clone()
|
||||
if offset < 0 {
|
||||
q.err = errors.New("datastore: negative query offset")
|
||||
return q
|
||||
}
|
||||
if offset > math.MaxInt32 {
|
||||
q.err = errors.New("datastore: query offset overflow")
|
||||
return q
|
||||
}
|
||||
q.offset = int32(offset)
|
||||
return q
|
||||
}
|
||||
|
||||
// Start returns a derivative query with the given start point.
|
||||
func (q *Query) Start(c Cursor) *Query {
|
||||
q = q.clone()
|
||||
q.start = c.cc
|
||||
return q
|
||||
}
|
||||
|
||||
// End returns a derivative query with the given end point.
|
||||
func (q *Query) End(c Cursor) *Query {
|
||||
q = q.clone()
|
||||
q.end = c.cc
|
||||
return q
|
||||
}
|
||||
|
||||
// toProto converts the query to a protocol buffer.
|
||||
func (q *Query) toProto(req *pb.RunQueryRequest) error {
|
||||
if len(q.projection) != 0 && q.keysOnly {
|
||||
return errors.New("datastore: query cannot both project and be keys-only")
|
||||
}
|
||||
if len(q.distinctOn) != 0 && q.distinct {
|
||||
return errors.New("datastore: query cannot be both distinct and distinct-on")
|
||||
}
|
||||
dst := &pb.Query{}
|
||||
if q.kind != "" {
|
||||
dst.Kind = []*pb.KindExpression{{Name: q.kind}}
|
||||
}
|
||||
if q.projection != nil {
|
||||
for _, propertyName := range q.projection {
|
||||
dst.Projection = append(dst.Projection, &pb.Projection{Property: &pb.PropertyReference{Name: propertyName}})
|
||||
}
|
||||
|
||||
for _, propertyName := range q.distinctOn {
|
||||
dst.DistinctOn = append(dst.DistinctOn, &pb.PropertyReference{Name: propertyName})
|
||||
}
|
||||
|
||||
if q.distinct {
|
||||
for _, propertyName := range q.projection {
|
||||
dst.DistinctOn = append(dst.DistinctOn, &pb.PropertyReference{Name: propertyName})
|
||||
}
|
||||
}
|
||||
}
|
||||
if q.keysOnly {
|
||||
dst.Projection = []*pb.Projection{{Property: &pb.PropertyReference{Name: keyFieldName}}}
|
||||
}
|
||||
|
||||
var filters []*pb.Filter
|
||||
for _, qf := range q.filter {
|
||||
if qf.FieldName == "" {
|
||||
return errors.New("datastore: empty query filter field name")
|
||||
}
|
||||
v, err := interfaceToProto(reflect.ValueOf(qf.Value).Interface(), false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("datastore: bad query filter value type: %v", err)
|
||||
}
|
||||
op, ok := operatorToProto[qf.Op]
|
||||
if !ok {
|
||||
return errors.New("datastore: unknown query filter operator")
|
||||
}
|
||||
xf := &pb.PropertyFilter{
|
||||
Op: op,
|
||||
Property: &pb.PropertyReference{Name: qf.FieldName},
|
||||
Value: v,
|
||||
}
|
||||
filters = append(filters, &pb.Filter{
|
||||
FilterType: &pb.Filter_PropertyFilter{PropertyFilter: xf},
|
||||
})
|
||||
}
|
||||
|
||||
if q.ancestor != nil {
|
||||
filters = append(filters, &pb.Filter{
|
||||
FilterType: &pb.Filter_PropertyFilter{PropertyFilter: &pb.PropertyFilter{
|
||||
Property: &pb.PropertyReference{Name: keyFieldName},
|
||||
Op: pb.PropertyFilter_HAS_ANCESTOR,
|
||||
Value: &pb.Value{ValueType: &pb.Value_KeyValue{KeyValue: keyToProto(q.ancestor)}},
|
||||
}}})
|
||||
}
|
||||
|
||||
if len(filters) == 1 {
|
||||
dst.Filter = filters[0]
|
||||
} else if len(filters) > 1 {
|
||||
dst.Filter = &pb.Filter{FilterType: &pb.Filter_CompositeFilter{CompositeFilter: &pb.CompositeFilter{
|
||||
Op: pb.CompositeFilter_AND,
|
||||
Filters: filters,
|
||||
}}}
|
||||
}
|
||||
|
||||
for _, qo := range q.order {
|
||||
if qo.FieldName == "" {
|
||||
return errors.New("datastore: empty query order field name")
|
||||
}
|
||||
xo := &pb.PropertyOrder{
|
||||
Property: &pb.PropertyReference{Name: qo.FieldName},
|
||||
Direction: sortDirectionToProto[qo.Direction],
|
||||
}
|
||||
dst.Order = append(dst.Order, xo)
|
||||
}
|
||||
if q.limit >= 0 {
|
||||
dst.Limit = &wrapperspb.Int32Value{Value: q.limit}
|
||||
}
|
||||
dst.Offset = q.offset
|
||||
dst.StartCursor = q.start
|
||||
dst.EndCursor = q.end
|
||||
|
||||
if t := q.trans; t != nil {
|
||||
if t.id == nil {
|
||||
return errExpiredTransaction
|
||||
}
|
||||
if q.eventual {
|
||||
return errors.New("datastore: cannot use EventualConsistency query in a transaction")
|
||||
}
|
||||
req.ReadOptions = &pb.ReadOptions{
|
||||
ConsistencyType: &pb.ReadOptions_Transaction{Transaction: t.id},
|
||||
}
|
||||
}
|
||||
|
||||
if q.eventual {
|
||||
req.ReadOptions = &pb.ReadOptions{ConsistencyType: &pb.ReadOptions_ReadConsistency_{ReadConsistency: pb.ReadOptions_EVENTUAL}}
|
||||
}
|
||||
|
||||
req.QueryType = &pb.RunQueryRequest_Query{Query: dst}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count returns the number of results for the given query.
|
||||
//
|
||||
// The running time and number of API calls made by Count scale linearly with
|
||||
// with 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 Count will
|
||||
// continue until it finishes counting or the provided context expires.
|
||||
func (c *Client) Count(ctx context.Context, q *Query) (n int, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.Query.Count")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
// Check that the query is well-formed.
|
||||
if q.err != nil {
|
||||
return 0, q.err
|
||||
}
|
||||
|
||||
// Create a copy of the query, with keysOnly true (if we're not a projection,
|
||||
// since the two are incompatible).
|
||||
newQ := q.clone()
|
||||
newQ.keysOnly = len(newQ.projection) == 0
|
||||
|
||||
// Create an iterator and use it to walk through the batches of results
|
||||
// directly.
|
||||
it := c.Run(ctx, newQ)
|
||||
for {
|
||||
err := it.nextBatch()
|
||||
if err == iterator.Done {
|
||||
return n, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n += len(it.results)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll runs the provided query in the given context and returns all keys
|
||||
// that match that query, as well as appending the values to dst.
|
||||
//
|
||||
// dst must have type *[]S or *[]*S or *[]P, for some struct type S or some non-
|
||||
// interface, non-pointer type P such that P or *P implements PropertyLoadSaver.
|
||||
//
|
||||
// As a special case, *PropertyList is an invalid type for dst, even though a
|
||||
// PropertyList is a slice of structs. It is treated as invalid to avoid being
|
||||
// mistakenly passed when *[]PropertyList was intended.
|
||||
//
|
||||
// The keys returned by GetAll will be in a 1-1 correspondence with the entities
|
||||
// added to dst.
|
||||
//
|
||||
// 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
|
||||
// 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.
|
||||
func (c *Client) GetAll(ctx context.Context, q *Query, dst interface{}) (keys []*Key, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.Query.GetAll")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
var (
|
||||
dv reflect.Value
|
||||
mat multiArgType
|
||||
elemType reflect.Type
|
||||
errFieldMismatch error
|
||||
)
|
||||
if !q.keysOnly {
|
||||
dv = reflect.ValueOf(dst)
|
||||
if dv.Kind() != reflect.Ptr || dv.IsNil() {
|
||||
return nil, ErrInvalidEntityType
|
||||
}
|
||||
dv = dv.Elem()
|
||||
mat, elemType = checkMultiArg(dv)
|
||||
if mat == multiArgTypeInvalid || mat == multiArgTypeInterface {
|
||||
return nil, ErrInvalidEntityType
|
||||
}
|
||||
}
|
||||
|
||||
for t := c.Run(ctx, q); ; {
|
||||
k, e, err := t.next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return keys, err
|
||||
}
|
||||
if !q.keysOnly {
|
||||
ev := reflect.New(elemType)
|
||||
if elemType.Kind() == reflect.Map {
|
||||
// This is a special case. The zero values of a map type are
|
||||
// not immediately useful; they have to be make'd.
|
||||
//
|
||||
// Funcs and channels are similar, in that a zero value is not useful,
|
||||
// but even a freshly make'd channel isn't useful: there's no fixed
|
||||
// channel buffer size that is always going to be large enough, and
|
||||
// there's no goroutine to drain the other end. Theoretically, these
|
||||
// types could be supported, for example by sniffing for a constructor
|
||||
// method or requiring prior registration, but for now it's not a
|
||||
// frequent enough concern to be worth it. Programmers can work around
|
||||
// it by explicitly using Iterator.Next instead of the Query.GetAll
|
||||
// convenience method.
|
||||
x := reflect.MakeMap(elemType)
|
||||
ev.Elem().Set(x)
|
||||
}
|
||||
if err = loadEntityProto(ev.Interface(), e); err != nil {
|
||||
if _, ok := err.(*ErrFieldMismatch); ok {
|
||||
// We continue loading entities even in the face of field mismatch errors.
|
||||
// If we encounter any other error, that other error is returned. Otherwise,
|
||||
// an ErrFieldMismatch is returned.
|
||||
errFieldMismatch = err
|
||||
} else {
|
||||
return keys, err
|
||||
}
|
||||
}
|
||||
if mat != multiArgTypeStructPtr {
|
||||
ev = ev.Elem()
|
||||
}
|
||||
dv.Set(reflect.Append(dv, ev))
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys, errFieldMismatch
|
||||
}
|
||||
|
||||
// Run runs the given query in the given context.
|
||||
func (c *Client) Run(ctx context.Context, q *Query) *Iterator {
|
||||
if q.err != nil {
|
||||
return &Iterator{err: q.err}
|
||||
}
|
||||
t := &Iterator{
|
||||
ctx: ctx,
|
||||
client: c,
|
||||
limit: q.limit,
|
||||
offset: q.offset,
|
||||
keysOnly: q.keysOnly,
|
||||
pageCursor: q.start,
|
||||
entityCursor: q.start,
|
||||
req: &pb.RunQueryRequest{
|
||||
ProjectId: c.dataset,
|
||||
},
|
||||
}
|
||||
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.Query.Run")
|
||||
defer func() { trace.EndSpan(ctx, t.err) }()
|
||||
if q.namespace != "" {
|
||||
t.req.PartitionId = &pb.PartitionId{
|
||||
NamespaceId: q.namespace,
|
||||
}
|
||||
}
|
||||
|
||||
if err := q.toProto(t.req); err != nil {
|
||||
t.err = err
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Iterator is the result of running a query.
|
||||
type Iterator struct {
|
||||
ctx context.Context
|
||||
client *Client
|
||||
err error
|
||||
|
||||
// results is the list of EntityResults still to be iterated over from the
|
||||
// most recent API call. It will be nil if no requests have yet been issued.
|
||||
results []*pb.EntityResult
|
||||
// req is the request to send. It may be modified and used multiple times.
|
||||
req *pb.RunQueryRequest
|
||||
|
||||
// limit is the limit on the number of results this iterator should return.
|
||||
// The zero value is used to prevent further fetches from the server.
|
||||
// A negative value means unlimited.
|
||||
limit int32
|
||||
// offset is the number of results that still need to be skipped.
|
||||
offset int32
|
||||
// keysOnly records whether the query was keys-only (skip entity loading).
|
||||
keysOnly bool
|
||||
|
||||
// pageCursor is the compiled cursor for the next batch/page of result.
|
||||
// TODO(djd): Can we delete this in favour of paging with the last
|
||||
// entityCursor from each batch?
|
||||
pageCursor []byte
|
||||
// entityCursor is the compiled cursor of the next result.
|
||||
entityCursor []byte
|
||||
}
|
||||
|
||||
// Next returns the key of the next result. When there are no more results,
|
||||
// iterator.Done is returned as the error.
|
||||
//
|
||||
// If the query is not keys only and dst is non-nil, it also loads the entity
|
||||
// stored for that key into the struct pointer or PropertyLoadSaver dst, with
|
||||
// the same semantics and possible errors as for the Get function.
|
||||
func (t *Iterator) Next(dst interface{}) (k *Key, err error) {
|
||||
k, e, err := t.next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dst != nil && !t.keysOnly {
|
||||
err = loadEntityProto(dst, e)
|
||||
}
|
||||
return k, err
|
||||
}
|
||||
|
||||
func (t *Iterator) next() (*Key, *pb.Entity, error) {
|
||||
// Fetch additional batches while there are no more results.
|
||||
for t.err == nil && len(t.results) == 0 {
|
||||
t.err = t.nextBatch()
|
||||
}
|
||||
if t.err != nil {
|
||||
return nil, nil, t.err
|
||||
}
|
||||
|
||||
// Extract the next result, update cursors, and parse the entity's key.
|
||||
e := t.results[0]
|
||||
t.results = t.results[1:]
|
||||
t.entityCursor = e.Cursor
|
||||
if len(t.results) == 0 {
|
||||
t.entityCursor = t.pageCursor // At the end of the batch.
|
||||
}
|
||||
if e.Entity.Key == nil {
|
||||
return nil, nil, errors.New("datastore: internal error: server did not return a key")
|
||||
}
|
||||
k, err := protoToKey(e.Entity.Key)
|
||||
if err != nil || k.Incomplete() {
|
||||
return nil, nil, errors.New("datastore: internal error: server returned an invalid key")
|
||||
}
|
||||
|
||||
return k, e.Entity, nil
|
||||
}
|
||||
|
||||
// nextBatch makes a single call to the server for a batch of results.
|
||||
func (t *Iterator) nextBatch() error {
|
||||
if t.limit == 0 {
|
||||
return iterator.Done // Short-circuits the zero-item response.
|
||||
}
|
||||
|
||||
// Adjust the query with the latest start cursor, limit and offset.
|
||||
q := t.req.GetQuery()
|
||||
q.StartCursor = t.pageCursor
|
||||
q.Offset = t.offset
|
||||
if t.limit >= 0 {
|
||||
q.Limit = &wrapperspb.Int32Value{Value: t.limit}
|
||||
} else {
|
||||
q.Limit = nil
|
||||
}
|
||||
|
||||
// Run the query.
|
||||
resp, err := t.client.client.RunQuery(t.ctx, t.req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Adjust any offset from skipped results.
|
||||
skip := resp.Batch.SkippedResults
|
||||
if skip < 0 {
|
||||
return errors.New("datastore: internal error: negative number of skipped_results")
|
||||
}
|
||||
t.offset -= skip
|
||||
if t.offset < 0 {
|
||||
return errors.New("datastore: internal error: query skipped too many results")
|
||||
}
|
||||
if t.offset > 0 && len(resp.Batch.EntityResults) > 0 {
|
||||
return errors.New("datastore: internal error: query returned results before requested offset")
|
||||
}
|
||||
|
||||
// Adjust the limit.
|
||||
if t.limit >= 0 {
|
||||
t.limit -= int32(len(resp.Batch.EntityResults))
|
||||
if t.limit < 0 {
|
||||
return errors.New("datastore: internal error: query returned more results than the limit")
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no more results available, set limit to zero to prevent
|
||||
// further fetches. Otherwise, check that there is a next page cursor available.
|
||||
if resp.Batch.MoreResults != pb.QueryResultBatch_NOT_FINISHED {
|
||||
t.limit = 0
|
||||
} else if resp.Batch.EndCursor == nil {
|
||||
return errors.New("datastore: internal error: server did not return a cursor")
|
||||
}
|
||||
|
||||
// Update cursors.
|
||||
// If any results were skipped, use the SkippedCursor as the next entity cursor.
|
||||
if skip > 0 {
|
||||
t.entityCursor = resp.Batch.SkippedCursor
|
||||
} else {
|
||||
t.entityCursor = q.StartCursor
|
||||
}
|
||||
t.pageCursor = resp.Batch.EndCursor
|
||||
|
||||
t.results = resp.Batch.EntityResults
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cursor returns a cursor for the iterator's current location.
|
||||
func (t *Iterator) Cursor() (c Cursor, err error) {
|
||||
t.ctx = trace.StartSpan(t.ctx, "cloud.google.com/go/datastore.Query.Cursor")
|
||||
defer func() { trace.EndSpan(t.ctx, err) }()
|
||||
|
||||
// If there is still an offset, we need to the skip those results first.
|
||||
for t.err == nil && t.offset > 0 {
|
||||
t.err = t.nextBatch()
|
||||
}
|
||||
|
||||
if t.err != nil && t.err != iterator.Done {
|
||||
return Cursor{}, t.err
|
||||
}
|
||||
|
||||
return Cursor{t.entityCursor}, nil
|
||||
}
|
||||
|
||||
// Cursor is an iterator's position. It can be converted to and from an opaque
|
||||
// string. A cursor can be used from different HTTP requests, but only with a
|
||||
// query with the same kind, ancestor, filter and order constraints.
|
||||
//
|
||||
// The zero Cursor can be used to indicate that there is no start and/or end
|
||||
// constraint for a query.
|
||||
type Cursor struct {
|
||||
cc []byte
|
||||
}
|
||||
|
||||
// String returns a base-64 string representation of a cursor.
|
||||
func (c Cursor) String() string {
|
||||
if c.cc == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimRight(base64.URLEncoding.EncodeToString(c.cc), "=")
|
||||
}
|
||||
|
||||
// Decode decodes a cursor from its base-64 string representation.
|
||||
func DecodeCursor(s string) (Cursor, error) {
|
||||
if s == "" {
|
||||
return Cursor{}, nil
|
||||
}
|
||||
if n := len(s) % 4; n != 0 {
|
||||
s += strings.Repeat("=", 4-n)
|
||||
}
|
||||
b, err := base64.URLEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return Cursor{}, err
|
||||
}
|
||||
return Cursor{b}, nil
|
||||
}
|
||||
+547
@@ -0,0 +1,547 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
key1 = &pb.Key{
|
||||
Path: []*pb.Key_PathElement{
|
||||
{
|
||||
Kind: "Gopher",
|
||||
IdType: &pb.Key_PathElement_Id{Id: 6},
|
||||
},
|
||||
},
|
||||
}
|
||||
key2 = &pb.Key{
|
||||
Path: []*pb.Key_PathElement{
|
||||
{
|
||||
Kind: "Gopher",
|
||||
IdType: &pb.Key_PathElement_Id{Id: 6},
|
||||
},
|
||||
{
|
||||
Kind: "Gopher",
|
||||
IdType: &pb.Key_PathElement_Id{Id: 8},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
pb.DatastoreClient
|
||||
queryFn func(*pb.RunQueryRequest) (*pb.RunQueryResponse, error)
|
||||
commitFn func(*pb.CommitRequest) (*pb.CommitResponse, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) RunQuery(_ context.Context, req *pb.RunQueryRequest, _ ...grpc.CallOption) (*pb.RunQueryResponse, error) {
|
||||
return c.queryFn(req)
|
||||
}
|
||||
|
||||
func (c *fakeClient) Commit(_ context.Context, req *pb.CommitRequest, _ ...grpc.CallOption) (*pb.CommitResponse, error) {
|
||||
return c.commitFn(req)
|
||||
}
|
||||
|
||||
func fakeRunQuery(in *pb.RunQueryRequest) (*pb.RunQueryResponse, error) {
|
||||
expectedIn := &pb.RunQueryRequest{
|
||||
QueryType: &pb.RunQueryRequest_Query{Query: &pb.Query{
|
||||
Kind: []*pb.KindExpression{{Name: "Gopher"}},
|
||||
}},
|
||||
}
|
||||
if !proto.Equal(in, expectedIn) {
|
||||
return nil, fmt.Errorf("unsupported argument: got %v want %v", in, expectedIn)
|
||||
}
|
||||
return &pb.RunQueryResponse{
|
||||
Batch: &pb.QueryResultBatch{
|
||||
MoreResults: pb.QueryResultBatch_NO_MORE_RESULTS,
|
||||
EntityResultType: pb.EntityResult_FULL,
|
||||
EntityResults: []*pb.EntityResult{
|
||||
{
|
||||
Entity: &pb.Entity{
|
||||
Key: key1,
|
||||
Properties: map[string]*pb.Value{
|
||||
"Name": {ValueType: &pb.Value_StringValue{StringValue: "George"}},
|
||||
"Height": {ValueType: &pb.Value_IntegerValue{IntegerValue: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Entity: &pb.Entity{
|
||||
Key: key2,
|
||||
Properties: map[string]*pb.Value{
|
||||
"Name": {ValueType: &pb.Value_StringValue{StringValue: "Rufus"}},
|
||||
// No height for Rufus.
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type StructThatImplementsPLS struct{}
|
||||
|
||||
func (StructThatImplementsPLS) Load(p []Property) error { return nil }
|
||||
func (StructThatImplementsPLS) Save() ([]Property, error) { return nil, nil }
|
||||
|
||||
var _ PropertyLoadSaver = StructThatImplementsPLS{}
|
||||
|
||||
type StructPtrThatImplementsPLS struct{}
|
||||
|
||||
func (*StructPtrThatImplementsPLS) Load(p []Property) error { return nil }
|
||||
func (*StructPtrThatImplementsPLS) Save() ([]Property, error) { return nil, nil }
|
||||
|
||||
var _ PropertyLoadSaver = &StructPtrThatImplementsPLS{}
|
||||
|
||||
type PropertyMap map[string]Property
|
||||
|
||||
func (m PropertyMap) Load(props []Property) error {
|
||||
for _, p := range props {
|
||||
m[p.Name] = p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m PropertyMap) Save() ([]Property, error) {
|
||||
props := make([]Property, 0, len(m))
|
||||
for _, p := range m {
|
||||
props = append(props, p)
|
||||
}
|
||||
return props, nil
|
||||
}
|
||||
|
||||
var _ PropertyLoadSaver = PropertyMap{}
|
||||
|
||||
type Gopher struct {
|
||||
Name string
|
||||
Height int
|
||||
}
|
||||
|
||||
// typeOfEmptyInterface is the type of interface{}, but we can't use
|
||||
// reflect.TypeOf((interface{})(nil)) directly because TypeOf takes an
|
||||
// interface{}.
|
||||
var typeOfEmptyInterface = reflect.TypeOf((*interface{})(nil)).Elem()
|
||||
|
||||
func TestCheckMultiArg(t *testing.T) {
|
||||
testCases := []struct {
|
||||
v interface{}
|
||||
mat multiArgType
|
||||
elemType reflect.Type
|
||||
}{
|
||||
// Invalid cases.
|
||||
{nil, multiArgTypeInvalid, nil},
|
||||
{Gopher{}, multiArgTypeInvalid, nil},
|
||||
{&Gopher{}, multiArgTypeInvalid, nil},
|
||||
{PropertyList{}, multiArgTypeInvalid, nil}, // This is a special case.
|
||||
{PropertyMap{}, multiArgTypeInvalid, nil},
|
||||
{[]*PropertyList(nil), multiArgTypeInvalid, nil},
|
||||
{[]*PropertyMap(nil), multiArgTypeInvalid, nil},
|
||||
{[]**Gopher(nil), multiArgTypeInvalid, nil},
|
||||
{[]*interface{}(nil), multiArgTypeInvalid, nil},
|
||||
// Valid cases.
|
||||
{
|
||||
[]PropertyList(nil),
|
||||
multiArgTypePropertyLoadSaver,
|
||||
reflect.TypeOf(PropertyList{}),
|
||||
},
|
||||
{
|
||||
[]PropertyMap(nil),
|
||||
multiArgTypePropertyLoadSaver,
|
||||
reflect.TypeOf(PropertyMap{}),
|
||||
},
|
||||
{
|
||||
[]StructThatImplementsPLS(nil),
|
||||
multiArgTypePropertyLoadSaver,
|
||||
reflect.TypeOf(StructThatImplementsPLS{}),
|
||||
},
|
||||
{
|
||||
[]StructPtrThatImplementsPLS(nil),
|
||||
multiArgTypePropertyLoadSaver,
|
||||
reflect.TypeOf(StructPtrThatImplementsPLS{}),
|
||||
},
|
||||
{
|
||||
[]Gopher(nil),
|
||||
multiArgTypeStruct,
|
||||
reflect.TypeOf(Gopher{}),
|
||||
},
|
||||
{
|
||||
[]*Gopher(nil),
|
||||
multiArgTypeStructPtr,
|
||||
reflect.TypeOf(Gopher{}),
|
||||
},
|
||||
{
|
||||
[]interface{}(nil),
|
||||
multiArgTypeInterface,
|
||||
typeOfEmptyInterface,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
mat, elemType := checkMultiArg(reflect.ValueOf(tc.v))
|
||||
if mat != tc.mat || elemType != tc.elemType {
|
||||
t.Errorf("checkMultiArg(%T): got %v, %v want %v, %v",
|
||||
tc.v, mat, elemType, tc.mat, tc.elemType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleQuery(t *testing.T) {
|
||||
struct1 := Gopher{Name: "George", Height: 32}
|
||||
struct2 := Gopher{Name: "Rufus"}
|
||||
pList1 := PropertyList{
|
||||
{
|
||||
Name: "Height",
|
||||
Value: int64(32),
|
||||
},
|
||||
{
|
||||
Name: "Name",
|
||||
Value: "George",
|
||||
},
|
||||
}
|
||||
pList2 := PropertyList{
|
||||
{
|
||||
Name: "Name",
|
||||
Value: "Rufus",
|
||||
},
|
||||
}
|
||||
pMap1 := PropertyMap{
|
||||
"Name": Property{
|
||||
Name: "Name",
|
||||
Value: "George",
|
||||
},
|
||||
"Height": Property{
|
||||
Name: "Height",
|
||||
Value: int64(32),
|
||||
},
|
||||
}
|
||||
pMap2 := PropertyMap{
|
||||
"Name": Property{
|
||||
Name: "Name",
|
||||
Value: "Rufus",
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
dst interface{}
|
||||
want interface{}
|
||||
}{
|
||||
// The destination must have type *[]P, *[]S or *[]*S, for some non-interface
|
||||
// type P such that *P implements PropertyLoadSaver, or for some struct type S.
|
||||
{new([]Gopher), &[]Gopher{struct1, struct2}},
|
||||
{new([]*Gopher), &[]*Gopher{&struct1, &struct2}},
|
||||
{new([]PropertyList), &[]PropertyList{pList1, pList2}},
|
||||
{new([]PropertyMap), &[]PropertyMap{pMap1, pMap2}},
|
||||
|
||||
// Any other destination type is invalid.
|
||||
{0, nil},
|
||||
{Gopher{}, nil},
|
||||
{PropertyList{}, nil},
|
||||
{PropertyMap{}, nil},
|
||||
{[]int{}, nil},
|
||||
{[]Gopher{}, nil},
|
||||
{[]PropertyList{}, nil},
|
||||
{new(int), nil},
|
||||
{new(Gopher), nil},
|
||||
{new(PropertyList), nil}, // This is a special case.
|
||||
{new(PropertyMap), nil},
|
||||
{new([]int), nil},
|
||||
{new([]map[int]int), nil},
|
||||
{new([]map[string]Property), nil},
|
||||
{new([]map[string]interface{}), nil},
|
||||
{new([]*int), nil},
|
||||
{new([]*map[int]int), nil},
|
||||
{new([]*map[string]Property), nil},
|
||||
{new([]*map[string]interface{}), nil},
|
||||
{new([]**Gopher), nil},
|
||||
{new([]*PropertyList), nil},
|
||||
{new([]*PropertyMap), nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
nCall := 0
|
||||
client := &Client{
|
||||
client: &fakeClient{
|
||||
queryFn: func(req *pb.RunQueryRequest) (*pb.RunQueryResponse, error) {
|
||||
nCall++
|
||||
return fakeRunQuery(req)
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
var (
|
||||
expectedErr error
|
||||
expectedNCall int
|
||||
)
|
||||
if tc.want == nil {
|
||||
expectedErr = ErrInvalidEntityType
|
||||
} else {
|
||||
expectedNCall = 1
|
||||
}
|
||||
keys, err := client.GetAll(ctx, NewQuery("Gopher"), tc.dst)
|
||||
if err != expectedErr {
|
||||
t.Errorf("dst type %T: got error %v, want %v", tc.dst, err, expectedErr)
|
||||
continue
|
||||
}
|
||||
if nCall != expectedNCall {
|
||||
t.Errorf("dst type %T: Context.Call was called an incorrect number of times: got %d want %d", tc.dst, nCall, expectedNCall)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
key1 := IDKey("Gopher", 6, nil)
|
||||
expectedKeys := []*Key{
|
||||
key1,
|
||||
IDKey("Gopher", 8, key1),
|
||||
}
|
||||
if l1, l2 := len(keys), len(expectedKeys); l1 != l2 {
|
||||
t.Errorf("dst type %T: got %d keys, want %d keys", tc.dst, l1, l2)
|
||||
continue
|
||||
}
|
||||
for i, key := range keys {
|
||||
if !keysEqual(key, expectedKeys[i]) {
|
||||
t.Errorf("dst type %T: got key #%d %v, want %v", tc.dst, i, key, expectedKeys[i])
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we sort any PropertyList items (the order is not deterministic).
|
||||
if pLists, ok := tc.dst.(*[]PropertyList); ok {
|
||||
for _, p := range *pLists {
|
||||
sort.Sort(byName(p))
|
||||
}
|
||||
}
|
||||
|
||||
if !testutil.Equal(tc.dst, tc.want) {
|
||||
t.Errorf("dst type %T: Entities\ngot %+v\nwant %+v", tc.dst, tc.dst, tc.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keysEqual is like (*Key).Equal, but ignores the App ID.
|
||||
func keysEqual(a, b *Key) bool {
|
||||
for a != nil && b != nil {
|
||||
if a.Kind != b.Kind || a.Name != b.Name || a.ID != b.ID {
|
||||
return false
|
||||
}
|
||||
a, b = a.Parent, b.Parent
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
func TestQueriesAreImmutable(t *testing.T) {
|
||||
// Test that deriving q2 from q1 does not modify q1.
|
||||
q0 := NewQuery("foo")
|
||||
q1 := NewQuery("foo")
|
||||
q2 := q1.Offset(2)
|
||||
if !testutil.Equal(q0, q1, cmp.AllowUnexported(Query{})) {
|
||||
t.Errorf("q0 and q1 were not equal")
|
||||
}
|
||||
if testutil.Equal(q1, q2, cmp.AllowUnexported(Query{})) {
|
||||
t.Errorf("q1 and q2 were equal")
|
||||
}
|
||||
|
||||
// Test that deriving from q4 twice does not conflict, even though
|
||||
// q4 has a long list of order clauses. This tests that the arrays
|
||||
// backed by a query's slice of orders are not shared.
|
||||
f := func() *Query {
|
||||
q := NewQuery("bar")
|
||||
// 47 is an ugly number that is unlikely to be near a re-allocation
|
||||
// point in repeated append calls. For example, it's not near a power
|
||||
// of 2 or a multiple of 10.
|
||||
for i := 0; i < 47; i++ {
|
||||
q = q.Order(fmt.Sprintf("x%d", i))
|
||||
}
|
||||
return q
|
||||
}
|
||||
q3 := f().Order("y")
|
||||
q4 := f()
|
||||
q5 := q4.Order("y")
|
||||
q6 := q4.Order("z")
|
||||
if !testutil.Equal(q3, q5, cmp.AllowUnexported(Query{})) {
|
||||
t.Errorf("q3 and q5 were not equal")
|
||||
}
|
||||
if testutil.Equal(q5, q6, cmp.AllowUnexported(Query{})) {
|
||||
t.Errorf("q5 and q6 were equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterParser(t *testing.T) {
|
||||
testCases := []struct {
|
||||
filterStr string
|
||||
wantOK bool
|
||||
wantFieldName string
|
||||
wantOp operator
|
||||
}{
|
||||
// Supported ops.
|
||||
{"x<", true, "x", lessThan},
|
||||
{"x <", true, "x", lessThan},
|
||||
{"x <", true, "x", lessThan},
|
||||
{" x < ", true, "x", lessThan},
|
||||
{"x <=", true, "x", lessEq},
|
||||
{"x =", true, "x", equal},
|
||||
{"x >=", true, "x", greaterEq},
|
||||
{"x >", true, "x", greaterThan},
|
||||
{"in >", true, "in", greaterThan},
|
||||
{"in>", true, "in", greaterThan},
|
||||
// Valid but (currently) unsupported ops.
|
||||
{"x!=", false, "", 0},
|
||||
{"x !=", false, "", 0},
|
||||
{" x != ", false, "", 0},
|
||||
{"x IN", false, "", 0},
|
||||
{"x in", false, "", 0},
|
||||
// Invalid ops.
|
||||
{"x EQ", false, "", 0},
|
||||
{"x lt", false, "", 0},
|
||||
{"x <>", false, "", 0},
|
||||
{"x >>", false, "", 0},
|
||||
{"x ==", false, "", 0},
|
||||
{"x =<", false, "", 0},
|
||||
{"x =>", false, "", 0},
|
||||
{"x !", false, "", 0},
|
||||
{"x ", false, "", 0},
|
||||
{"x", false, "", 0},
|
||||
// Quoted and interesting field names.
|
||||
{"x > y =", true, "x > y", equal},
|
||||
{"` x ` =", true, " x ", equal},
|
||||
{`" x " =`, true, " x ", equal},
|
||||
{`" \"x " =`, true, ` "x `, equal},
|
||||
{`" x =`, false, "", 0},
|
||||
{`" x ="`, false, "", 0},
|
||||
{"` x \" =", false, "", 0},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
q := NewQuery("foo").Filter(tc.filterStr, 42)
|
||||
if ok := q.err == nil; ok != tc.wantOK {
|
||||
t.Errorf("%q: ok=%t, want %t", tc.filterStr, ok, tc.wantOK)
|
||||
continue
|
||||
}
|
||||
if !tc.wantOK {
|
||||
continue
|
||||
}
|
||||
if len(q.filter) != 1 {
|
||||
t.Errorf("%q: len=%d, want %d", tc.filterStr, len(q.filter), 1)
|
||||
continue
|
||||
}
|
||||
got, want := q.filter[0], filter{tc.wantFieldName, tc.wantOp, 42}
|
||||
if got != want {
|
||||
t.Errorf("%q: got %v, want %v", tc.filterStr, got, want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceQuery(t *testing.T) {
|
||||
gotNamespace := make(chan string, 1)
|
||||
ctx := context.Background()
|
||||
client := &Client{
|
||||
client: &fakeClient{
|
||||
queryFn: func(req *pb.RunQueryRequest) (*pb.RunQueryResponse, error) {
|
||||
if part := req.PartitionId; part != nil {
|
||||
gotNamespace <- part.NamespaceId
|
||||
} else {
|
||||
gotNamespace <- ""
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var gs []Gopher
|
||||
|
||||
client.GetAll(ctx, NewQuery("gopher"), &gs)
|
||||
if got, want := <-gotNamespace, ""; got != want {
|
||||
t.Errorf("GetAll: got namespace %q, want %q", got, want)
|
||||
}
|
||||
client.Count(ctx, NewQuery("gopher"))
|
||||
if got, want := <-gotNamespace, ""; got != want {
|
||||
t.Errorf("Count: got namespace %q, want %q", got, want)
|
||||
}
|
||||
|
||||
const ns = "not_default"
|
||||
client.GetAll(ctx, NewQuery("gopher").Namespace(ns), &gs)
|
||||
if got, want := <-gotNamespace, ns; got != want {
|
||||
t.Errorf("GetAll: got namespace %q, want %q", got, want)
|
||||
}
|
||||
client.Count(ctx, NewQuery("gopher").Namespace(ns))
|
||||
if got, want := <-gotNamespace, ns; got != want {
|
||||
t.Errorf("Count: got namespace %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOptions(t *testing.T) {
|
||||
tid := []byte{1}
|
||||
for _, test := range []struct {
|
||||
q *Query
|
||||
want *pb.ReadOptions
|
||||
}{
|
||||
{
|
||||
q: NewQuery(""),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
q: NewQuery("").Transaction(nil),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
q: NewQuery("").Transaction(&Transaction{id: tid}),
|
||||
want: &pb.ReadOptions{
|
||||
ConsistencyType: &pb.ReadOptions_Transaction{
|
||||
Transaction: tid,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
q: NewQuery("").EventualConsistency(),
|
||||
want: &pb.ReadOptions{
|
||||
ConsistencyType: &pb.ReadOptions_ReadConsistency_{
|
||||
ReadConsistency: pb.ReadOptions_EVENTUAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
req := &pb.RunQueryRequest{}
|
||||
if err := test.q.toProto(req); err != nil {
|
||||
t.Fatalf("%+v: got %v, want no error", test.q, err)
|
||||
}
|
||||
if got := req.ReadOptions; !proto.Equal(got, test.want) {
|
||||
t.Errorf("%+v:\ngot %+v\nwant %+v", test.q, got, test.want)
|
||||
}
|
||||
}
|
||||
// Test errors.
|
||||
for _, q := range []*Query{
|
||||
NewQuery("").Transaction(&Transaction{id: nil}),
|
||||
NewQuery("").Transaction(&Transaction{id: tid}).EventualConsistency(),
|
||||
} {
|
||||
req := &pb.RunQueryRequest{}
|
||||
if err := q.toProto(req); err == nil {
|
||||
t.Errorf("%+v: got nil, wanted error", q)
|
||||
}
|
||||
}
|
||||
}
|
||||
+462
@@ -0,0 +1,462 @@
|
||||
// Copyright 4 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
timepb "github.com/golang/protobuf/ptypes/timestamp"
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
llpb "google.golang.org/genproto/googleapis/type/latlng"
|
||||
)
|
||||
|
||||
type saveOpts struct {
|
||||
noIndex bool
|
||||
flatten bool
|
||||
omitEmpty bool
|
||||
}
|
||||
|
||||
// saveEntity saves an EntityProto into a PropertyLoadSaver or struct pointer.
|
||||
func saveEntity(key *Key, src interface{}) (*pb.Entity, error) {
|
||||
var err error
|
||||
var props []Property
|
||||
if e, ok := src.(PropertyLoadSaver); ok {
|
||||
props, err = e.Save()
|
||||
} else {
|
||||
props, err = SaveStruct(src)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return propertiesToProto(key, props)
|
||||
}
|
||||
|
||||
// TODO(djd): Convert this and below to return ([]Property, error).
|
||||
func saveStructProperty(props *[]Property, name string, opts saveOpts, v reflect.Value) error {
|
||||
p := Property{
|
||||
Name: name,
|
||||
NoIndex: opts.noIndex,
|
||||
}
|
||||
|
||||
if opts.omitEmpty && isEmptyValue(v) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// First check if field type implements PLS. If so, use PLS to
|
||||
// save.
|
||||
ok, err := plsFieldSave(props, p, name, opts, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch x := v.Interface().(type) {
|
||||
case *Key, time.Time, GeoPoint:
|
||||
p.Value = x
|
||||
default:
|
||||
switch v.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
p.Value = v.Int()
|
||||
case reflect.Bool:
|
||||
p.Value = v.Bool()
|
||||
case reflect.String:
|
||||
p.Value = v.String()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
p.Value = v.Float()
|
||||
case reflect.Slice:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 {
|
||||
p.Value = v.Bytes()
|
||||
} else {
|
||||
return saveSliceProperty(props, name, opts, v)
|
||||
}
|
||||
case reflect.Ptr:
|
||||
if isValidPointerType(v.Type().Elem()) {
|
||||
if v.IsNil() {
|
||||
// Nil pointer becomes a nil property value (unless omitempty, handled above).
|
||||
p.Value = nil
|
||||
*props = append(*props, p)
|
||||
return nil
|
||||
}
|
||||
return saveStructProperty(props, name, opts, v.Elem())
|
||||
}
|
||||
if v.Type().Elem().Kind() != reflect.Struct {
|
||||
return fmt.Errorf("datastore: unsupported struct field type: %s", v.Type())
|
||||
}
|
||||
// Pointer to struct is a special case.
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
fallthrough
|
||||
case reflect.Struct:
|
||||
if !v.CanAddr() {
|
||||
return fmt.Errorf("datastore: unsupported struct field: value is unaddressable")
|
||||
}
|
||||
vi := v.Addr().Interface()
|
||||
|
||||
sub, err := newStructPLS(vi)
|
||||
if err != nil {
|
||||
return fmt.Errorf("datastore: unsupported struct field: %v", err)
|
||||
}
|
||||
|
||||
if opts.flatten {
|
||||
return sub.save(props, opts, name+".")
|
||||
}
|
||||
|
||||
var subProps []Property
|
||||
err = sub.save(&subProps, opts, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
subKey, err := sub.key(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Value = &Entity{
|
||||
Key: subKey,
|
||||
Properties: subProps,
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.Value == nil {
|
||||
return fmt.Errorf("datastore: unsupported struct field type: %v", v.Type())
|
||||
}
|
||||
*props = append(*props, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// plsFieldSave first tries to converts v's value to a PLS, then v's addressed
|
||||
// value to a PLS. If neither succeeds, plsFieldSave returns false for first return
|
||||
// value.
|
||||
// If v is successfully converted to a PLS, plsFieldSave will then add the
|
||||
// Value to property p by way of the PLS's Save method, and append it to props.
|
||||
//
|
||||
// If the flatten option is present in opts, name must be prepended to each property's
|
||||
// name before it is appended to props. Eg. if name were "A" and a subproperty's name
|
||||
// were "B", the resultant name of the property to be appended to props would be "A.B".
|
||||
func plsFieldSave(props *[]Property, p Property, name string, opts saveOpts, v reflect.Value) (ok bool, err error) {
|
||||
vpls, err := plsForSave(v)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if vpls == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
subProps, err := vpls.Save()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if opts.flatten {
|
||||
for _, subp := range subProps {
|
||||
subp.Name = name + "." + subp.Name
|
||||
*props = append(*props, subp)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
p.Value = &Entity{Properties: subProps}
|
||||
*props = append(*props, p)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// key extracts the *Key struct field from struct v based on the structCodec of s.
|
||||
func (s structPLS) key(v reflect.Value) (*Key, error) {
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil, errors.New("datastore: cannot save key of non-struct type")
|
||||
}
|
||||
|
||||
keyField := s.codec.Match(keyFieldName)
|
||||
|
||||
if keyField == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
f := v.FieldByIndex(keyField.Index)
|
||||
k, ok := f.Interface().(*Key)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("datastore: %s field on struct %T is not a *datastore.Key", keyFieldName, v.Interface())
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func saveSliceProperty(props *[]Property, name string, opts saveOpts, v reflect.Value) error {
|
||||
// Easy case: if the slice is empty, we're done.
|
||||
if v.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
// Work out the properties generated by the first element in the slice. This will
|
||||
// usually be a single property, but will be more if this is a slice of structs.
|
||||
var headProps []Property
|
||||
if err := saveStructProperty(&headProps, name, opts, v.Index(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert the first element's properties into slice properties, and
|
||||
// keep track of the values in a map.
|
||||
values := make(map[string][]interface{}, len(headProps))
|
||||
for _, p := range headProps {
|
||||
values[p.Name] = append(make([]interface{}, 0, v.Len()), p.Value)
|
||||
}
|
||||
|
||||
// Find the elements for the subsequent elements.
|
||||
for i := 1; i < v.Len(); i++ {
|
||||
elemProps := make([]Property, 0, len(headProps))
|
||||
if err := saveStructProperty(&elemProps, name, opts, v.Index(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range elemProps {
|
||||
v, ok := values[p.Name]
|
||||
if !ok {
|
||||
return fmt.Errorf("datastore: unexpected property %q in elem %d of slice", p.Name, i)
|
||||
}
|
||||
values[p.Name] = append(v, p.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to the final properties.
|
||||
for _, p := range headProps {
|
||||
p.Value = values[p.Name]
|
||||
*props = append(*props, p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s structPLS) Save() ([]Property, error) {
|
||||
var props []Property
|
||||
if err := s.save(&props, saveOpts{}, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return props, nil
|
||||
}
|
||||
|
||||
func (s structPLS) save(props *[]Property, opts saveOpts, prefix string) error {
|
||||
for _, f := range s.codec {
|
||||
name := prefix + f.Name
|
||||
v := getField(s.v, f.Index)
|
||||
if !v.IsValid() || !v.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
var tagOpts saveOpts
|
||||
if f.ParsedTag != nil {
|
||||
tagOpts = f.ParsedTag.(saveOpts)
|
||||
}
|
||||
|
||||
var opts1 saveOpts
|
||||
opts1.noIndex = opts.noIndex || tagOpts.noIndex
|
||||
opts1.flatten = opts.flatten || tagOpts.flatten
|
||||
opts1.omitEmpty = tagOpts.omitEmpty // don't propagate
|
||||
if err := saveStructProperty(props, name, opts1, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getField returns the field from v at the given index path.
|
||||
// If it encounters a nil-valued field in the path, getField
|
||||
// stops and returns a zero-valued reflect.Value, preventing the
|
||||
// panic that would have been caused by reflect's FieldByIndex.
|
||||
func getField(v reflect.Value, index []int) reflect.Value {
|
||||
var zero reflect.Value
|
||||
if v.Type().Kind() != reflect.Struct {
|
||||
return zero
|
||||
}
|
||||
|
||||
for _, i := range index {
|
||||
if v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct {
|
||||
if v.IsNil() {
|
||||
return zero
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
v = v.Field(i)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func propertiesToProto(key *Key, props []Property) (*pb.Entity, error) {
|
||||
e := &pb.Entity{
|
||||
Key: keyToProto(key),
|
||||
Properties: map[string]*pb.Value{},
|
||||
}
|
||||
indexedProps := 0
|
||||
for _, p := range props {
|
||||
// Do not send a Key value a a field to datastore.
|
||||
if p.Name == keyFieldName {
|
||||
continue
|
||||
}
|
||||
|
||||
val, err := interfaceToProto(p.Value, p.NoIndex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("datastore: %v for a Property with Name %q", err, p.Name)
|
||||
}
|
||||
if !p.NoIndex {
|
||||
rVal := reflect.ValueOf(p.Value)
|
||||
if rVal.Kind() == reflect.Slice && rVal.Type().Elem().Kind() != reflect.Uint8 {
|
||||
indexedProps += rVal.Len()
|
||||
} else {
|
||||
indexedProps++
|
||||
}
|
||||
}
|
||||
if indexedProps > maxIndexedProperties {
|
||||
return nil, errors.New("datastore: too many indexed properties")
|
||||
}
|
||||
|
||||
if _, ok := e.Properties[p.Name]; ok {
|
||||
return nil, fmt.Errorf("datastore: duplicate Property with Name %q", p.Name)
|
||||
}
|
||||
e.Properties[p.Name] = val
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func interfaceToProto(iv interface{}, noIndex bool) (*pb.Value, error) {
|
||||
val := &pb.Value{ExcludeFromIndexes: noIndex}
|
||||
switch v := iv.(type) {
|
||||
case int:
|
||||
val.ValueType = &pb.Value_IntegerValue{IntegerValue: int64(v)}
|
||||
case int32:
|
||||
val.ValueType = &pb.Value_IntegerValue{IntegerValue: int64(v)}
|
||||
case int64:
|
||||
val.ValueType = &pb.Value_IntegerValue{IntegerValue: v}
|
||||
case bool:
|
||||
val.ValueType = &pb.Value_BooleanValue{BooleanValue: v}
|
||||
case string:
|
||||
if len(v) > 1500 && !noIndex {
|
||||
return nil, errors.New("string property too long to index")
|
||||
}
|
||||
if !utf8.ValidString(v) {
|
||||
return nil, fmt.Errorf("string is not valid utf8: %q", v)
|
||||
}
|
||||
val.ValueType = &pb.Value_StringValue{StringValue: v}
|
||||
case float32:
|
||||
val.ValueType = &pb.Value_DoubleValue{DoubleValue: float64(v)}
|
||||
case float64:
|
||||
val.ValueType = &pb.Value_DoubleValue{DoubleValue: v}
|
||||
case *Key:
|
||||
if v == nil {
|
||||
val.ValueType = &pb.Value_NullValue{}
|
||||
} else {
|
||||
val.ValueType = &pb.Value_KeyValue{KeyValue: keyToProto(v)}
|
||||
}
|
||||
case GeoPoint:
|
||||
if !v.Valid() {
|
||||
return nil, errors.New("invalid GeoPoint value")
|
||||
}
|
||||
val.ValueType = &pb.Value_GeoPointValue{GeoPointValue: &llpb.LatLng{
|
||||
Latitude: v.Lat,
|
||||
Longitude: v.Lng,
|
||||
}}
|
||||
case time.Time:
|
||||
if v.Before(minTime) || v.After(maxTime) {
|
||||
return nil, errors.New("time value out of range")
|
||||
}
|
||||
val.ValueType = &pb.Value_TimestampValue{TimestampValue: &timepb.Timestamp{
|
||||
Seconds: v.Unix(),
|
||||
Nanos: int32(v.Nanosecond()),
|
||||
}}
|
||||
case []byte:
|
||||
if len(v) > 1500 && !noIndex {
|
||||
return nil, errors.New("[]byte property too long to index")
|
||||
}
|
||||
val.ValueType = &pb.Value_BlobValue{BlobValue: v}
|
||||
case *Entity:
|
||||
e, err := propertiesToProto(v.Key, v.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val.ValueType = &pb.Value_EntityValue{EntityValue: e}
|
||||
case []interface{}:
|
||||
arr := make([]*pb.Value, 0, len(v))
|
||||
for i, v := range v {
|
||||
elem, err := interfaceToProto(v, noIndex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v at index %d", err, i)
|
||||
}
|
||||
arr = append(arr, elem)
|
||||
}
|
||||
val.ValueType = &pb.Value_ArrayValue{ArrayValue: &pb.ArrayValue{Values: arr}}
|
||||
// ArrayValues have ExcludeFromIndexes set on the individual items, rather
|
||||
// than the top-level value.
|
||||
val.ExcludeFromIndexes = false
|
||||
default:
|
||||
rv := reflect.ValueOf(iv)
|
||||
if !rv.IsValid() {
|
||||
val.ValueType = &pb.Value_NullValue{}
|
||||
} else if rv.Kind() == reflect.Ptr { // non-nil pointer: dereference
|
||||
if rv.IsNil() {
|
||||
val.ValueType = &pb.Value_NullValue{}
|
||||
return val, nil
|
||||
}
|
||||
return interfaceToProto(rv.Elem().Interface(), noIndex)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid Value type %T", iv)
|
||||
}
|
||||
}
|
||||
// TODO(jbd): Support EntityValue.
|
||||
return val, 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:
|
||||
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:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Interface, reflect.Ptr:
|
||||
return v.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isValidPointerType reports whether a struct field can be a pointer to type t
|
||||
// for the purposes of saving and loading.
|
||||
func isValidPointerType(t reflect.Type) bool {
|
||||
if t == typeOfTime || t == typeOfGeoPoint {
|
||||
return true
|
||||
}
|
||||
switch t.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return true
|
||||
case reflect.Bool:
|
||||
return true
|
||||
case reflect.String:
|
||||
return true
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
// Copyright 2016 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 (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
func TestInterfaceToProtoNil(t *testing.T) {
|
||||
// A nil *Key, or a nil value of any other pointer type, should convert to a NullValue.
|
||||
for _, in := range []interface{}{
|
||||
(*Key)(nil),
|
||||
(*int)(nil),
|
||||
(*string)(nil),
|
||||
(*bool)(nil),
|
||||
(*float64)(nil),
|
||||
(*GeoPoint)(nil),
|
||||
(*time.Time)(nil),
|
||||
} {
|
||||
got, err := interfaceToProto(in, false)
|
||||
if err != nil {
|
||||
t.Fatalf("%T: %v", in, err)
|
||||
}
|
||||
_, ok := got.ValueType.(*pb.Value_NullValue)
|
||||
if !ok {
|
||||
t.Errorf("%T: got: %T\nwant: %T", in, got.ValueType, &pb.Value_NullValue{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveEntityNested(t *testing.T) {
|
||||
type WithKey struct {
|
||||
X string
|
||||
I int
|
||||
K *Key `datastore:"__key__"`
|
||||
}
|
||||
|
||||
type NestedWithKey struct {
|
||||
Y string
|
||||
N WithKey
|
||||
}
|
||||
|
||||
type WithoutKey struct {
|
||||
X string
|
||||
I int
|
||||
}
|
||||
|
||||
type NestedWithoutKey struct {
|
||||
Y string
|
||||
N WithoutKey
|
||||
}
|
||||
|
||||
type a struct {
|
||||
S string
|
||||
}
|
||||
|
||||
type UnexpAnonym struct {
|
||||
a
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
src interface{}
|
||||
key *Key
|
||||
want *pb.Entity
|
||||
}{
|
||||
{
|
||||
desc: "nested entity with key",
|
||||
src: &NestedWithKey{
|
||||
Y: "yyy",
|
||||
N: WithKey{
|
||||
X: "two",
|
||||
I: 2,
|
||||
K: testKey1a,
|
||||
},
|
||||
},
|
||||
key: testKey0,
|
||||
want: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Key: keyToProto(testKey1a),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested entity with incomplete key",
|
||||
src: &NestedWithKey{
|
||||
Y: "yyy",
|
||||
N: WithKey{
|
||||
X: "two",
|
||||
I: 2,
|
||||
K: incompleteKey,
|
||||
},
|
||||
},
|
||||
key: testKey0,
|
||||
want: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Key: keyToProto(incompleteKey),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested entity without key",
|
||||
src: &NestedWithoutKey{
|
||||
Y: "yyy",
|
||||
N: WithoutKey{
|
||||
X: "two",
|
||||
I: 2,
|
||||
},
|
||||
},
|
||||
key: testKey0,
|
||||
want: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{StringValue: "yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
EntityValue: &pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "key at top level",
|
||||
src: &WithKey{
|
||||
X: "three",
|
||||
I: 3,
|
||||
K: testKey0,
|
||||
},
|
||||
key: testKey0,
|
||||
want: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{StringValue: "three"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{IntegerValue: 3}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested unexported anonymous struct field",
|
||||
src: &UnexpAnonym{
|
||||
a{S: "hello"},
|
||||
},
|
||||
key: testKey0,
|
||||
want: &pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"S": {ValueType: &pb.Value_StringValue{StringValue: "hello"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
got, err := saveEntity(tc.key, tc.src)
|
||||
if err != nil {
|
||||
t.Errorf("saveEntity: %s: %v", tc.desc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !testutil.Equal(tc.want, got) {
|
||||
t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavePointers(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
in interface{}
|
||||
want []Property
|
||||
}{
|
||||
{
|
||||
desc: "nil pointers save as nil-valued properties",
|
||||
in: &Pointers{},
|
||||
want: []Property{
|
||||
Property{Name: "Pi", Value: nil},
|
||||
Property{Name: "Ps", Value: nil},
|
||||
Property{Name: "Pb", Value: nil},
|
||||
Property{Name: "Pf", Value: nil},
|
||||
Property{Name: "Pg", Value: nil},
|
||||
Property{Name: "Pt", Value: nil},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nil omitempty pointers not saved",
|
||||
in: &PointersOmitEmpty{},
|
||||
want: []Property(nil),
|
||||
},
|
||||
{
|
||||
desc: "non-nil zero-valued pointers save as zero values",
|
||||
in: populatedPointers(),
|
||||
want: []Property{
|
||||
Property{Name: "Pi", Value: int64(0)},
|
||||
Property{Name: "Ps", Value: ""},
|
||||
Property{Name: "Pb", Value: false},
|
||||
Property{Name: "Pf", Value: 0.0},
|
||||
Property{Name: "Pg", Value: GeoPoint{}},
|
||||
Property{Name: "Pt", Value: time.Time{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "non-nil non-zero-valued pointers save as the appropriate values",
|
||||
in: func() *Pointers {
|
||||
p := populatedPointers()
|
||||
*p.Pi = 1
|
||||
*p.Ps = "x"
|
||||
*p.Pb = true
|
||||
*p.Pf = 3.14
|
||||
*p.Pg = GeoPoint{Lat: 1, Lng: 2}
|
||||
*p.Pt = time.Unix(100, 0)
|
||||
return p
|
||||
}(),
|
||||
want: []Property{
|
||||
Property{Name: "Pi", Value: int64(1)},
|
||||
Property{Name: "Ps", Value: "x"},
|
||||
Property{Name: "Pb", Value: true},
|
||||
Property{Name: "Pf", Value: 3.14},
|
||||
Property{Name: "Pg", Value: GeoPoint{Lat: 1, Lng: 2}},
|
||||
Property{Name: "Pt", Value: time.Unix(100, 0)},
|
||||
},
|
||||
},
|
||||
} {
|
||||
got, err := SaveStruct(test.in)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", test.desc, err)
|
||||
}
|
||||
if !testutil.Equal(got, test.want) {
|
||||
t.Errorf("%s\ngot %#v\nwant %#v\n", test.desc, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveEmptySlice(t *testing.T) {
|
||||
// Zero-length slice fields are not saved.
|
||||
for _, slice := range [][]string{nil, {}} {
|
||||
got, err := SaveStruct(&struct{ S []string }{S: slice})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Errorf("%#v: got %d properties, wanted zero", slice, len(got))
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
indexes:
|
||||
|
||||
- kind: SQChild
|
||||
ancestor: yes
|
||||
properties:
|
||||
- name: T
|
||||
- name: I
|
||||
|
||||
- kind: SQChild
|
||||
ancestor: yes
|
||||
properties:
|
||||
- name: T
|
||||
- name: I
|
||||
direction: desc
|
||||
|
||||
- kind: SQChild
|
||||
ancestor: yes
|
||||
properties:
|
||||
- name: I
|
||||
- name: T
|
||||
- name: U
|
||||
|
||||
- kind: SQChild
|
||||
ancestor: yes
|
||||
properties:
|
||||
- name: I
|
||||
- name: T
|
||||
- name: U
|
||||
|
||||
- kind: SQChild
|
||||
ancestor: yes
|
||||
properties:
|
||||
- name: T
|
||||
- name: J
|
||||
|
||||
- kind: SQChild
|
||||
ancestor: yes
|
||||
properties:
|
||||
- name: T
|
||||
- name: J
|
||||
- name: U
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// 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 (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
minTime = time.Unix(int64(math.MinInt64)/1e6, (int64(math.MinInt64)%1e6)*1e3)
|
||||
maxTime = time.Unix(int64(math.MaxInt64)/1e6, (int64(math.MaxInt64)%1e6)*1e3)
|
||||
)
|
||||
|
||||
func toUnixMicro(t time.Time) int64 {
|
||||
// We cannot use t.UnixNano() / 1e3 because we want to handle times more than
|
||||
// 2^63 nanoseconds (which is about 292 years) away from 1970, and those cannot
|
||||
// be represented in the numerator of a single int64 divide.
|
||||
return t.Unix()*1e6 + int64(t.Nanosecond()/1e3)
|
||||
}
|
||||
|
||||
func fromUnixMicro(t int64) time.Time {
|
||||
return time.Unix(t/1e6, (t%1e6)*1e3)
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUnixMicro(t *testing.T) {
|
||||
// Test that all these time.Time values survive a round trip to unix micros.
|
||||
testCases := []time.Time{
|
||||
{},
|
||||
time.Date(2, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(23, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(234, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(1000, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(1600, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(1700, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(1800, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Unix(-1e6, -1000),
|
||||
time.Unix(-1e6, 0),
|
||||
time.Unix(-1e6, +1000),
|
||||
time.Unix(-60, -1000),
|
||||
time.Unix(-60, 0),
|
||||
time.Unix(-60, +1000),
|
||||
time.Unix(-1, -1000),
|
||||
time.Unix(-1, 0),
|
||||
time.Unix(-1, +1000),
|
||||
time.Unix(0, -3000),
|
||||
time.Unix(0, -2000),
|
||||
time.Unix(0, -1000),
|
||||
time.Unix(0, 0),
|
||||
time.Unix(0, +1000),
|
||||
time.Unix(0, +2000),
|
||||
time.Unix(+60, -1000),
|
||||
time.Unix(+60, 0),
|
||||
time.Unix(+60, +1000),
|
||||
time.Unix(+1e6, -1000),
|
||||
time.Unix(+1e6, 0),
|
||||
time.Unix(+1e6, +1000),
|
||||
time.Date(1999, 12, 31, 23, 59, 59, 999000, time.UTC),
|
||||
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2006, 1, 2, 15, 4, 5, 678000, time.UTC),
|
||||
time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC),
|
||||
time.Date(3456, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
got := fromUnixMicro(toUnixMicro(tc))
|
||||
if !got.Equal(tc) {
|
||||
t.Errorf("got %q, want %q", got, tc)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that a time.Time that isn't an integral number of microseconds
|
||||
// is not perfectly reconstructed after a round trip.
|
||||
t0 := time.Unix(0, 123)
|
||||
t1 := fromUnixMicro(toUnixMicro(t0))
|
||||
if t1.Nanosecond()%1000 != 0 || t0.Nanosecond()%1000 == 0 {
|
||||
t.Errorf("quantization to µs: got %q with %d ns, started with %d ns", t1, t1.Nanosecond(), t0.Nanosecond())
|
||||
}
|
||||
}
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
|
||||
"cloud.google.com/go/internal/trace"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
// ErrConcurrentTransaction is returned when a transaction is rolled back due
|
||||
// to a conflict with a concurrent transaction.
|
||||
var ErrConcurrentTransaction = errors.New("datastore: concurrent transaction")
|
||||
|
||||
var errExpiredTransaction = errors.New("datastore: transaction expired")
|
||||
|
||||
type transactionSettings struct {
|
||||
attempts int
|
||||
readOnly bool
|
||||
prevID []byte // ID of the transaction to retry
|
||||
}
|
||||
|
||||
// newTransactionSettings creates a transactionSettings with a given TransactionOption slice.
|
||||
// Unconfigured options will be set to default values.
|
||||
func newTransactionSettings(opts []TransactionOption) *transactionSettings {
|
||||
s := &transactionSettings{attempts: 3}
|
||||
for _, o := range opts {
|
||||
o.apply(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// TransactionOption configures the way a transaction is executed.
|
||||
type TransactionOption interface {
|
||||
apply(*transactionSettings)
|
||||
}
|
||||
|
||||
// MaxAttempts returns a TransactionOption that overrides the default 3 attempt times.
|
||||
func MaxAttempts(attempts int) TransactionOption {
|
||||
return maxAttempts(attempts)
|
||||
}
|
||||
|
||||
type maxAttempts int
|
||||
|
||||
func (w maxAttempts) apply(s *transactionSettings) {
|
||||
if w > 0 {
|
||||
s.attempts = int(w)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadOnly is a TransactionOption that marks the transaction as read-only.
|
||||
var ReadOnly TransactionOption
|
||||
|
||||
func init() {
|
||||
ReadOnly = readOnly{}
|
||||
}
|
||||
|
||||
type readOnly struct{}
|
||||
|
||||
func (readOnly) apply(s *transactionSettings) {
|
||||
s.readOnly = true
|
||||
}
|
||||
|
||||
// Transaction represents a set of datastore operations to be committed atomically.
|
||||
//
|
||||
// Operations are enqueued by calling the Put and Delete methods on Transaction
|
||||
// (or their Multi-equivalents). These operations are only committed when the
|
||||
// Commit method is invoked. To ensure consistency, reads must be performed by
|
||||
// using Transaction's Get method or by using the Transaction method when
|
||||
// building a query.
|
||||
//
|
||||
// A Transaction must be committed or rolled back exactly once.
|
||||
type Transaction struct {
|
||||
id []byte
|
||||
client *Client
|
||||
ctx context.Context
|
||||
mutations []*pb.Mutation // The mutations to apply.
|
||||
pending map[int]*PendingKey // Map from mutation index to incomplete keys pending transaction completion.
|
||||
}
|
||||
|
||||
// NewTransaction starts a new transaction.
|
||||
func (c *Client) NewTransaction(ctx context.Context, opts ...TransactionOption) (t *Transaction, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.NewTransaction")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(maxAttempts); ok {
|
||||
return nil, errors.New("datastore: NewTransaction does not accept MaxAttempts option")
|
||||
}
|
||||
}
|
||||
return c.newTransaction(ctx, newTransactionSettings(opts))
|
||||
}
|
||||
|
||||
func (c *Client) newTransaction(ctx context.Context, s *transactionSettings) (*Transaction, error) {
|
||||
req := &pb.BeginTransactionRequest{ProjectId: c.dataset}
|
||||
if s.readOnly {
|
||||
req.TransactionOptions = &pb.TransactionOptions{
|
||||
Mode: &pb.TransactionOptions_ReadOnly_{ReadOnly: &pb.TransactionOptions_ReadOnly{}},
|
||||
}
|
||||
} else if s.prevID != nil {
|
||||
req.TransactionOptions = &pb.TransactionOptions{
|
||||
Mode: &pb.TransactionOptions_ReadWrite_{ReadWrite: &pb.TransactionOptions_ReadWrite{
|
||||
PreviousTransaction: s.prevID,
|
||||
}},
|
||||
}
|
||||
}
|
||||
resp, err := c.client.BeginTransaction(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Transaction{
|
||||
id: resp.Transaction,
|
||||
ctx: ctx,
|
||||
client: c,
|
||||
mutations: nil,
|
||||
pending: make(map[int]*PendingKey),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RunInTransaction runs f in a transaction. f is invoked with a Transaction
|
||||
// that f should use for all the transaction's datastore operations.
|
||||
//
|
||||
// f must not call Commit or Rollback on the provided Transaction.
|
||||
//
|
||||
// If f returns nil, RunInTransaction commits the transaction,
|
||||
// returning the Commit and a nil error if it succeeds. If the commit fails due
|
||||
// to a conflicting transaction, RunInTransaction retries f with a new
|
||||
// Transaction. It gives up and returns ErrConcurrentTransaction after three
|
||||
// failed attempts (or as configured with MaxAttempts).
|
||||
//
|
||||
// If f returns non-nil, then the transaction will be rolled back and
|
||||
// RunInTransaction will return the same error. The function f is not retried.
|
||||
//
|
||||
// Note that when f returns, the transaction is not committed. Calling code
|
||||
// must not assume that any of f's changes have been committed until
|
||||
// RunInTransaction returns nil.
|
||||
//
|
||||
// Since f may be called multiple times, f should usually be idempotent – that
|
||||
// is, it should have the same result when called multiple times. Note that
|
||||
// Transaction.Get will append when unmarshalling slice fields, so it is not
|
||||
// necessarily idempotent.
|
||||
func (c *Client) RunInTransaction(ctx context.Context, f func(tx *Transaction) error, opts ...TransactionOption) (cmt *Commit, err error) {
|
||||
ctx = trace.StartSpan(ctx, "cloud.google.com/go/datastore.RunInTransaction")
|
||||
defer func() { trace.EndSpan(ctx, err) }()
|
||||
|
||||
settings := newTransactionSettings(opts)
|
||||
for n := 0; n < settings.attempts; n++ {
|
||||
tx, err := c.newTransaction(ctx, settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
if cmt, err := tx.Commit(); err != ErrConcurrentTransaction {
|
||||
return cmt, err
|
||||
}
|
||||
// Pass this transaction's ID to the retry transaction to preserve
|
||||
// transaction priority.
|
||||
if !settings.readOnly {
|
||||
settings.prevID = tx.id
|
||||
}
|
||||
}
|
||||
return nil, ErrConcurrentTransaction
|
||||
}
|
||||
|
||||
// Commit applies the enqueued operations atomically.
|
||||
func (t *Transaction) Commit() (c *Commit, err error) {
|
||||
t.ctx = trace.StartSpan(t.ctx, "cloud.google.com/go/datastore.Transaction.Commit")
|
||||
defer func() { trace.EndSpan(t.ctx, err) }()
|
||||
|
||||
if t.id == nil {
|
||||
return nil, errExpiredTransaction
|
||||
}
|
||||
req := &pb.CommitRequest{
|
||||
ProjectId: t.client.dataset,
|
||||
TransactionSelector: &pb.CommitRequest_Transaction{Transaction: t.id},
|
||||
Mutations: t.mutations,
|
||||
Mode: pb.CommitRequest_TRANSACTIONAL,
|
||||
}
|
||||
t.id = nil
|
||||
resp, err := t.client.client.Commit(t.ctx, req)
|
||||
if err != nil {
|
||||
if grpc.Code(err) == codes.Aborted {
|
||||
return nil, ErrConcurrentTransaction
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Copy any newly minted keys into the returned keys.
|
||||
for i, p := range t.pending {
|
||||
if i >= len(resp.MutationResults) || resp.MutationResults[i].Key == nil {
|
||||
return nil, errors.New("datastore: internal error: server returned the wrong mutation results")
|
||||
}
|
||||
key, err := protoToKey(resp.MutationResults[i].Key)
|
||||
if err != nil {
|
||||
return nil, errors.New("datastore: internal error: server returned an invalid key")
|
||||
}
|
||||
p.key = key
|
||||
p.commit = c
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Rollback abandons a pending transaction.
|
||||
func (t *Transaction) Rollback() (err error) {
|
||||
t.ctx = trace.StartSpan(t.ctx, "cloud.google.com/go/datastore.Transaction.Rollback")
|
||||
defer func() { trace.EndSpan(t.ctx, err) }()
|
||||
|
||||
if t.id == nil {
|
||||
return errExpiredTransaction
|
||||
}
|
||||
id := t.id
|
||||
t.id = nil
|
||||
_, err = t.client.client.Rollback(t.ctx, &pb.RollbackRequest{
|
||||
ProjectId: t.client.dataset,
|
||||
Transaction: id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Get is the transaction-specific version of the package function Get.
|
||||
// All reads performed during the transaction will come from a single consistent
|
||||
// snapshot. Furthermore, if the transaction is set to a serializable isolation
|
||||
// level, another transaction cannot concurrently modify the data that is read
|
||||
// or modified by this transaction.
|
||||
func (t *Transaction) Get(key *Key, dst interface{}) (err error) {
|
||||
t.ctx = trace.StartSpan(t.ctx, "cloud.google.com/go/datastore.Transaction.Get")
|
||||
defer func() { trace.EndSpan(t.ctx, err) }()
|
||||
|
||||
opts := &pb.ReadOptions{
|
||||
ConsistencyType: &pb.ReadOptions_Transaction{Transaction: t.id},
|
||||
}
|
||||
err = t.client.get(t.ctx, []*Key{key}, []interface{}{dst}, opts)
|
||||
if me, ok := err.(MultiError); ok {
|
||||
return me[0]
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMulti is a batch version of Get.
|
||||
func (t *Transaction) GetMulti(keys []*Key, dst interface{}) (err error) {
|
||||
t.ctx = trace.StartSpan(t.ctx, "cloud.google.com/go/datastore.Transaction.GetMulti")
|
||||
defer func() { trace.EndSpan(t.ctx, err) }()
|
||||
|
||||
if t.id == nil {
|
||||
return errExpiredTransaction
|
||||
}
|
||||
opts := &pb.ReadOptions{
|
||||
ConsistencyType: &pb.ReadOptions_Transaction{Transaction: t.id},
|
||||
}
|
||||
return t.client.get(t.ctx, keys, dst, opts)
|
||||
}
|
||||
|
||||
// Put is the transaction-specific version of the package function Put.
|
||||
//
|
||||
// Put returns a PendingKey which can be resolved into a Key using the
|
||||
// return value from a successful Commit. If key is an incomplete key, the
|
||||
// returned pending key will resolve to a unique key generated by the
|
||||
// datastore.
|
||||
func (t *Transaction) Put(key *Key, src interface{}) (*PendingKey, error) {
|
||||
h, err := t.PutMulti([]*Key{key}, []interface{}{src})
|
||||
if err != nil {
|
||||
if me, ok := err.(MultiError); ok {
|
||||
return nil, me[0]
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return h[0], nil
|
||||
}
|
||||
|
||||
// PutMulti is a batch version of Put. One PendingKey is returned for each
|
||||
// element of src in the same order.
|
||||
// TODO(jba): rewrite in terms of Mutate.
|
||||
func (t *Transaction) PutMulti(keys []*Key, src interface{}) (ret []*PendingKey, err error) {
|
||||
t.ctx = trace.StartSpan(t.ctx, "cloud.google.com/go/datastore.Transaction.PutMulti")
|
||||
defer func() { trace.EndSpan(t.ctx, err) }()
|
||||
|
||||
if t.id == nil {
|
||||
return nil, errExpiredTransaction
|
||||
}
|
||||
mutations, err := putMutations(keys, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
origin := len(t.mutations)
|
||||
t.mutations = append(t.mutations, mutations...)
|
||||
|
||||
// Prepare the returned handles, pre-populating where possible.
|
||||
ret = make([]*PendingKey, len(keys))
|
||||
for i, key := range keys {
|
||||
p := &PendingKey{}
|
||||
if key.Incomplete() {
|
||||
// This key will be in the final commit result.
|
||||
t.pending[origin+i] = p
|
||||
} else {
|
||||
p.key = key
|
||||
}
|
||||
ret[i] = p
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Delete is the transaction-specific version of the package function Delete.
|
||||
// Delete enqueues the deletion of the entity for the given key, to be
|
||||
// committed atomically upon calling Commit.
|
||||
func (t *Transaction) Delete(key *Key) error {
|
||||
err := t.DeleteMulti([]*Key{key})
|
||||
if me, ok := err.(MultiError); ok {
|
||||
return me[0]
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteMulti is a batch version of Delete.
|
||||
// TODO(jba): rewrite in terms of Mutate.
|
||||
func (t *Transaction) DeleteMulti(keys []*Key) (err error) {
|
||||
t.ctx = trace.StartSpan(t.ctx, "cloud.google.com/go/datastore.Transaction.DeleteMulti")
|
||||
defer func() { trace.EndSpan(t.ctx, err) }()
|
||||
|
||||
if t.id == nil {
|
||||
return errExpiredTransaction
|
||||
}
|
||||
mutations, err := deleteMutations(keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.mutations = append(t.mutations, mutations...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mutate adds the mutations to the transaction. They will all be applied atomically
|
||||
// upon calling Commit. Mutate returns a PendingKey for each Mutation in the argument
|
||||
// list, in the same order. PendingKeys for Delete mutations are always nil.
|
||||
//
|
||||
// If any of the mutations are invalid, Mutate returns a MultiError with the errors.
|
||||
// Mutate returns a MultiError in this case even if there is only one Mutation.
|
||||
//
|
||||
// For an example, see Client.Mutate.
|
||||
func (t *Transaction) Mutate(muts ...*Mutation) ([]*PendingKey, error) {
|
||||
if t.id == nil {
|
||||
return nil, errExpiredTransaction
|
||||
}
|
||||
pmuts, err := mutationProtos(muts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
origin := len(t.mutations)
|
||||
t.mutations = append(t.mutations, pmuts...)
|
||||
// Prepare the returned handles, pre-populating where possible.
|
||||
ret := make([]*PendingKey, len(muts))
|
||||
for i, mut := range muts {
|
||||
if mut.isDelete() {
|
||||
continue
|
||||
}
|
||||
p := &PendingKey{}
|
||||
if mut.key.Incomplete() {
|
||||
// This key will be in the final commit result.
|
||||
t.pending[origin+i] = p
|
||||
} else {
|
||||
p.key = mut.key
|
||||
}
|
||||
ret[i] = p
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Commit represents the result of a committed transaction.
|
||||
type Commit struct{}
|
||||
|
||||
// Key resolves a pending key handle into a final key.
|
||||
func (c *Commit) Key(p *PendingKey) *Key {
|
||||
if p == nil { // if called on a *PendingKey from a Delete mutation
|
||||
return nil
|
||||
}
|
||||
// If p.commit is nil, the PendingKey did not come from an incomplete key,
|
||||
// so p.key is valid.
|
||||
if p.commit != nil && c != p.commit {
|
||||
panic("PendingKey was not created by corresponding transaction")
|
||||
}
|
||||
return p.key
|
||||
}
|
||||
|
||||
// PendingKey represents the key for newly-inserted entity. It can be
|
||||
// resolved into a Key by calling the Key method of Commit.
|
||||
type PendingKey struct {
|
||||
key *Key
|
||||
commit *Commit
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// Copyright 2017 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
func TestNewTransaction(t *testing.T) {
|
||||
var got *pb.BeginTransactionRequest
|
||||
client := &Client{
|
||||
dataset: "project",
|
||||
client: &fakeDatastoreClient{
|
||||
beginTransaction: func(req *pb.BeginTransactionRequest) (*pb.BeginTransactionResponse, error) {
|
||||
got = req
|
||||
return &pb.BeginTransactionResponse{
|
||||
Transaction: []byte("tid"),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
for _, test := range []struct {
|
||||
settings *transactionSettings
|
||||
want *pb.BeginTransactionRequest
|
||||
}{
|
||||
{
|
||||
&transactionSettings{},
|
||||
&pb.BeginTransactionRequest{ProjectId: "project"},
|
||||
},
|
||||
{
|
||||
&transactionSettings{readOnly: true},
|
||||
&pb.BeginTransactionRequest{
|
||||
ProjectId: "project",
|
||||
TransactionOptions: &pb.TransactionOptions{
|
||||
Mode: &pb.TransactionOptions_ReadOnly_{ReadOnly: &pb.TransactionOptions_ReadOnly{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
&transactionSettings{prevID: []byte("tid")},
|
||||
&pb.BeginTransactionRequest{
|
||||
ProjectId: "project",
|
||||
TransactionOptions: &pb.TransactionOptions{
|
||||
Mode: &pb.TransactionOptions_ReadWrite_{ReadWrite: &pb.TransactionOptions_ReadWrite{
|
||||
PreviousTransaction: []byte("tid"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
_, err := client.newTransaction(ctx, test.settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !proto.Equal(got, test.want) {
|
||||
t.Errorf("%+v:\ngot %+v\nwant %+v", test.settings, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user