Switch to using the dep tool and update all the dependencies
This commit is contained in:
+600
@@ -0,0 +1,600 @@
|
||||
// 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/version"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/transport"
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
// protoClient is an interface for *transport.ProtoClient to support injecting
|
||||
// fake clients in tests.
|
||||
type protoClient interface {
|
||||
Call(context.Context, string, proto.Message, proto.Message) error
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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) (*pb.LookupResponse, error) {
|
||||
return dc.c.Lookup(metadata.NewContext(ctx, dc.md), in, opts...)
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) RunQuery(ctx context.Context, in *pb.RunQueryRequest, opts ...grpc.CallOption) (*pb.RunQueryResponse, error) {
|
||||
return dc.c.RunQuery(metadata.NewContext(ctx, dc.md), in, opts...)
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) BeginTransaction(ctx context.Context, in *pb.BeginTransactionRequest, opts ...grpc.CallOption) (*pb.BeginTransactionResponse, error) {
|
||||
return dc.c.BeginTransaction(metadata.NewContext(ctx, dc.md), in, opts...)
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) Commit(ctx context.Context, in *pb.CommitRequest, opts ...grpc.CallOption) (*pb.CommitResponse, error) {
|
||||
return dc.c.Commit(metadata.NewContext(ctx, dc.md), in, opts...)
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) Rollback(ctx context.Context, in *pb.RollbackRequest, opts ...grpc.CallOption) (*pb.RollbackResponse, error) {
|
||||
return dc.c.Rollback(metadata.NewContext(ctx, dc.md), in, opts...)
|
||||
}
|
||||
|
||||
func (dc *datastoreClient) AllocateIds(ctx context.Context, in *pb.AllocateIdsRequest, opts ...grpc.CallOption) (*pb.AllocateIdsResponse, error) {
|
||||
return dc.c.AllocateIds(metadata.NewContext(ctx, dc.md), in, opts...)
|
||||
}
|
||||
|
||||
// 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 := transport.DialGRPC(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
|
||||
}
|
||||
|
||||
// TODO(jbd): Eliminate unrequired allocations.
|
||||
var path []*pb.Key_PathElement
|
||||
for {
|
||||
el := &pb.Key_PathElement{Kind: k.Kind}
|
||||
if k.ID != 0 {
|
||||
el.IdType = &pb.Key_PathElement_Id{k.ID}
|
||||
} else if k.Name != "" {
|
||||
el.IdType = &pb.Key_PathElement_Name{k.Name}
|
||||
}
|
||||
path = append([]*pb.Key_PathElement{el}, path...)
|
||||
if k.Parent == nil {
|
||||
break
|
||||
}
|
||||
k = k.Parent
|
||||
}
|
||||
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{}) error {
|
||||
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{}) error {
|
||||
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 index
|
||||
multiErr, any := make(MultiError, len(keys)), false
|
||||
keyMap := make(map[string]int)
|
||||
pbKeys := make([]*pb.Key, len(keys))
|
||||
for i, k := range keys {
|
||||
if !k.valid() {
|
||||
multiErr[i] = ErrInvalidKey
|
||||
any = true
|
||||
} else {
|
||||
keyMap[k.String()] = i
|
||||
pbKeys[i] = keyToProto(k)
|
||||
}
|
||||
}
|
||||
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...)
|
||||
}
|
||||
if len(keys) != len(found)+len(missing) {
|
||||
return errors.New("datastore: internal error: server returned the wrong number of entities")
|
||||
}
|
||||
for _, e := range found {
|
||||
k, err := protoToKey(e.Entity.Key)
|
||||
if err != nil {
|
||||
return errors.New("datastore: internal error: server returned an invalid key")
|
||||
}
|
||||
index := 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")
|
||||
}
|
||||
multiErr[keyMap[k.String()]] = ErrNoSuchEntity
|
||||
any = true
|
||||
}
|
||||
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.
|
||||
func (c *Client) PutMulti(ctx context.Context, keys []*Key, src interface{}) ([]*Key, error) {
|
||||
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{p}}
|
||||
} else {
|
||||
mut = &pb.Mutation{Operation: &pb.Mutation_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.
|
||||
func (c *Client) DeleteMulti(ctx context.Context, keys []*Key) error {
|
||||
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))
|
||||
for _, k := range keys {
|
||||
if k.Incomplete() {
|
||||
return nil, fmt.Errorf("datastore: can't delete the incomplete key: %v", k)
|
||||
}
|
||||
mutations = append(mutations, &pb.Mutation{
|
||||
Operation: &pb.Mutation_Delete{keyToProto(k)},
|
||||
})
|
||||
}
|
||||
return mutations, nil
|
||||
}
|
||||
+2776
File diff suppressed because it is too large
Load Diff
+420
@@ -0,0 +1,420 @@
|
||||
// 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.
|
||||
|
||||
Note: This package is in beta. Some backwards-incompatible changes may occur.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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 interface value, and any array, slice, map, or string of length zero.
|
||||
Struct field values will never be empty.
|
||||
|
||||
If options include "noindex" then the field will not be indexed. All fields are indexed
|
||||
by default. Strings or byte slices longer than 1500 bytes cannot be indexed;
|
||||
fields used to store long strings and byte slices must be tagged with "noindex"
|
||||
or they will cause Put operations to fail.
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
+545
@@ -0,0 +1,545 @@
|
||||
// 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 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}]
|
||||
}
|
||||
+1040
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
// 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())
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
var err error
|
||||
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.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 ""
|
||||
}
|
||||
|
||||
// setVal sets 'v' to the value of the Property 'p'.
|
||||
func setVal(v reflect.Value, p Property) 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 fmt.Sprintf("value %v overflows struct field of type %v", x, v.Type())
|
||||
}
|
||||
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 fmt.Sprintf("value %v overflows struct field of type %v", x, v.Type())
|
||||
}
|
||||
v.SetFloat(x)
|
||||
case reflect.Ptr:
|
||||
// v must be either a pointer to a Key or Entity.
|
||||
if v.Type() != typeOfKeyPtr && v.Type().Elem().Kind() != reflect.Struct {
|
||||
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 ""
|
||||
}
|
||||
|
||||
switch x := pValue.(type) {
|
||||
case *Key:
|
||||
if _, ok := v.Interface().(*Key); !ok {
|
||||
return typeMismatchReason(p, v)
|
||||
}
|
||||
v.Set(reflect.ValueOf(x))
|
||||
case *Entity:
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
err := loadEntity(v.Interface(), x)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Check if v implements PropertyLoadSaver.
|
||||
if _, ok := v.Interface().(PropertyLoadSaver); ok {
|
||||
return fmt.Sprintf("datastore: PropertyLoadSaver methods must be implemented on a pointer to %T.", v.Interface())
|
||||
}
|
||||
|
||||
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 {
|
||||
return pls.Load(ent.Properties)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
// 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"
|
||||
|
||||
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{}
|
||||
}{
|
||||
{
|
||||
"nested",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{"two"}},
|
||||
"A.I": {ValueType: &pb.Value_IntegerValue{2}},
|
||||
},
|
||||
},
|
||||
&NestedSimple1{
|
||||
A: Simple{I: 2},
|
||||
X: "two",
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested with tag",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"AA.II": {ValueType: &pb.Value_IntegerValue{2}},
|
||||
},
|
||||
},
|
||||
&NestedSimpleWithTag{
|
||||
A: SimpleWithTag{I: 2},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested with anonymous struct field",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{"two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{2}},
|
||||
},
|
||||
},
|
||||
&NestedSimpleAnonymous{
|
||||
Simple: Simple{I: 2},
|
||||
X: "two",
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested with dotted field tag",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"A.B.B": {ValueType: &pb.Value_StringValue{"bb"}},
|
||||
},
|
||||
},
|
||||
&ABDotB{
|
||||
A: BDotB{
|
||||
B: "bb",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested with multiple anonymous fields",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{3}},
|
||||
"S": {ValueType: &pb.Value_StringValue{"S"}},
|
||||
"SS": {ValueType: &pb.Value_StringValue{"s"}},
|
||||
"X": {ValueType: &pb.Value_StringValue{"s"}},
|
||||
},
|
||||
},
|
||||
&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 !reflect.DeepEqual(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{}
|
||||
}{
|
||||
{
|
||||
"nested basic",
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{3}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{10}},
|
||||
},
|
||||
},
|
||||
&NestedSimple{
|
||||
A: Simple{I: 3},
|
||||
I: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested with struct tags",
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"AA": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"II": {ValueType: &pb.Value_IntegerValue{1}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
&NestedSimpleWithTag{
|
||||
A: SimpleWithTag{I: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested 2x",
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"AA": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{3}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{1}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"A": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"S": {ValueType: &pb.Value_StringValue{"S"}},
|
||||
"SS": {ValueType: &pb.Value_StringValue{"s"}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"S": {ValueType: &pb.Value_StringValue{"SS"}},
|
||||
},
|
||||
},
|
||||
&NestedSimple2X{
|
||||
AA: NestedSimple{
|
||||
A: Simple{I: 3},
|
||||
I: 1,
|
||||
},
|
||||
A: SimpleTwoFields{S: "S", SS: "s"},
|
||||
S: "SS",
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested anonymous",
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{3}},
|
||||
"X": {ValueType: &pb.Value_StringValue{"SomeX"}},
|
||||
},
|
||||
},
|
||||
&NestedSimpleAnonymous{
|
||||
Simple: Simple{I: 3},
|
||||
X: "SomeX",
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested simple with slice",
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_ArrayValue{
|
||||
&pb.ArrayValue{
|
||||
[]*pb.Value{
|
||||
{ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{3}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
{ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{4}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
|
||||
&NestedSliceOfSimple{
|
||||
A: []Simple{Simple{I: 3}, Simple{I: 4}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested with multiple anonymous fields",
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_IntegerValue{3}},
|
||||
"S": {ValueType: &pb.Value_StringValue{"S"}},
|
||||
"SS": {ValueType: &pb.Value_StringValue{"s"}},
|
||||
"X": {ValueType: &pb.Value_StringValue{"ss"}},
|
||||
},
|
||||
},
|
||||
&MultiAnonymous{
|
||||
Simple: Simple{I: 3},
|
||||
SimpleTwoFields: SimpleTwoFields{S: "S", SS: "s"},
|
||||
X: "ss",
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested with dotted field tag",
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"B.B": {ValueType: &pb.Value_StringValue{"bb"}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
&ABDotB{
|
||||
A: BDotB{
|
||||
B: "bb",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested entity with key",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{"yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey1a),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{"two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
&NestedWithKey{
|
||||
Y: "yyy",
|
||||
N: WithKey{
|
||||
X: "two",
|
||||
I: 2,
|
||||
K: testKey1a,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested entity with invalid key",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{"yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Key: keyToProto(invalidKey),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{"two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
&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 !reflect.DeepEqual(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{}
|
||||
}{
|
||||
{
|
||||
"simple already populated, nil properties",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"I": {ValueType: &pb.Value_NullValue{}},
|
||||
},
|
||||
},
|
||||
&Simple{
|
||||
I: 12,
|
||||
},
|
||||
&Simple{},
|
||||
},
|
||||
{
|
||||
"nested structs already populated",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"SS": {ValueType: &pb.Value_StringValue{"world"}},
|
||||
},
|
||||
},
|
||||
&SimpleTwoFields{S: "hello" /* SS: "" */},
|
||||
&SimpleTwoFields{S: "hello", SS: "world"},
|
||||
},
|
||||
{
|
||||
"nested structs already populated, pValues nil",
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"S": {ValueType: &pb.Value_NullValue{}},
|
||||
"SS": {ValueType: &pb.Value_StringValue{"ss hello"}},
|
||||
"Nest": {ValueType: &pb.Value_NullValue{}},
|
||||
"TwiceNest": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"A": {ValueType: &pb.Value_NullValue{}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{5}},
|
||||
},
|
||||
},
|
||||
&NestedStructPtrs{
|
||||
&SimpleTwoFields{S: "hello" /* SS: "" */},
|
||||
&SimpleTwoFields{ /* S: "" */ SS: "twice hello"},
|
||||
&NestedSimple2{
|
||||
A: &Simple{I: 2},
|
||||
/* I: 0 */
|
||||
},
|
||||
0,
|
||||
},
|
||||
&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 !reflect.DeepEqual(tc.want, tc.dst) {
|
||||
t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, tc.dst, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
+773
@@ -0,0 +1,773 @@
|
||||
// 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"
|
||||
|
||||
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) (int, error) {
|
||||
// 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)
|
||||
n := 0
|
||||
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{}) ([]*Key, error) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
var keys []*Key
|
||||
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,
|
||||
},
|
||||
}
|
||||
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{}) (*Key, 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() (Cursor, error) {
|
||||
// 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
|
||||
}
|
||||
+536
@@ -0,0 +1,536 @@
|
||||
// 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"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"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{6},
|
||||
},
|
||||
},
|
||||
}
|
||||
key2 = &pb.Key{
|
||||
Path: []*pb.Key_PathElement{
|
||||
{
|
||||
Kind: "Gopher",
|
||||
IdType: &pb.Key_PathElement_Id{6},
|
||||
},
|
||||
{
|
||||
Kind: "Gopher",
|
||||
IdType: &pb.Key_PathElement_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{&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{"George"}},
|
||||
"Height": {ValueType: &pb.Value_IntegerValue{32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Entity: &pb.Entity{
|
||||
Key: key2,
|
||||
Properties: map[string]*pb.Value{
|
||||
"Name": {ValueType: &pb.Value_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 !reflect.DeepEqual(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 !reflect.DeepEqual(q0, q1) {
|
||||
t.Errorf("q0 and q1 were not equal")
|
||||
}
|
||||
if reflect.DeepEqual(q1, q2) {
|
||||
t.Errorf("q1 and q2 were equal")
|
||||
}
|
||||
|
||||
// Test that deriving from q4 twice does not conflict, even though
|
||||
// q4 has a long list of order clauses. This tests that the arrays
|
||||
// backed by a query's slice of orders are not shared.
|
||||
f := func() *Query {
|
||||
q := NewQuery("bar")
|
||||
// 47 is an ugly number that is unlikely to be near a re-allocation
|
||||
// point in repeated append calls. For example, it's not near a power
|
||||
// of 2 or a multiple of 10.
|
||||
for i := 0; i < 47; i++ {
|
||||
q = q.Order(fmt.Sprintf("x%d", i))
|
||||
}
|
||||
return q
|
||||
}
|
||||
q3 := f().Order("y")
|
||||
q4 := f()
|
||||
q5 := q4.Order("y")
|
||||
q6 := q4.Order("z")
|
||||
if !reflect.DeepEqual(q3, q5) {
|
||||
t.Errorf("q3 and q5 were not equal")
|
||||
}
|
||||
if reflect.DeepEqual(q5, q6) {
|
||||
t.Errorf("q5 and q6 were equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterParser(t *testing.T) {
|
||||
testCases := []struct {
|
||||
filterStr string
|
||||
wantOK bool
|
||||
wantFieldName string
|
||||
wantOp operator
|
||||
}{
|
||||
// Supported ops.
|
||||
{"x<", true, "x", lessThan},
|
||||
{"x <", true, "x", lessThan},
|
||||
{"x <", true, "x", lessThan},
|
||||
{" x < ", true, "x", lessThan},
|
||||
{"x <=", true, "x", lessEq},
|
||||
{"x =", true, "x", equal},
|
||||
{"x >=", true, "x", greaterEq},
|
||||
{"x >", true, "x", greaterThan},
|
||||
{"in >", true, "in", greaterThan},
|
||||
{"in>", true, "in", greaterThan},
|
||||
// Valid but (currently) unsupported ops.
|
||||
{"x!=", false, "", 0},
|
||||
{"x !=", false, "", 0},
|
||||
{" x != ", false, "", 0},
|
||||
{"x IN", false, "", 0},
|
||||
{"x in", false, "", 0},
|
||||
// Invalid ops.
|
||||
{"x EQ", false, "", 0},
|
||||
{"x lt", false, "", 0},
|
||||
{"x <>", false, "", 0},
|
||||
{"x >>", false, "", 0},
|
||||
{"x ==", false, "", 0},
|
||||
{"x =<", false, "", 0},
|
||||
{"x =>", false, "", 0},
|
||||
{"x !", false, "", 0},
|
||||
{"x ", false, "", 0},
|
||||
{"x", false, "", 0},
|
||||
// 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{&pb.ReadOptions_Transaction{tid}},
|
||||
},
|
||||
{
|
||||
q: NewQuery("").EventualConsistency(),
|
||||
want: &pb.ReadOptions{&pb.ReadOptions_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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
// 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"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Check if v implements PropertyLoadSaver.
|
||||
pls, isPLS := v.Interface().(PropertyLoadSaver)
|
||||
|
||||
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 v.Type().Elem().Kind() != reflect.Struct {
|
||||
return fmt.Errorf("datastore: unsupported struct field type: %s", v.Type())
|
||||
}
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
fallthrough
|
||||
case reflect.Struct:
|
||||
if isPLS {
|
||||
subProps, err := pls.Save()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Value = &Entity{Properties: subProps}
|
||||
break
|
||||
}
|
||||
|
||||
if !v.CanAddr() {
|
||||
return fmt.Errorf("datastore: unsupported struct field: value is unaddressable")
|
||||
}
|
||||
sub, err := newStructPLS(v.Addr().Interface())
|
||||
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
|
||||
}
|
||||
|
||||
// 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{int64(v)}
|
||||
case int32:
|
||||
val.ValueType = &pb.Value_IntegerValue{int64(v)}
|
||||
case int64:
|
||||
val.ValueType = &pb.Value_IntegerValue{v}
|
||||
case bool:
|
||||
val.ValueType = &pb.Value_BooleanValue{v}
|
||||
case string:
|
||||
if len(v) > 1500 && !noIndex {
|
||||
return nil, errors.New("string property too long to index")
|
||||
}
|
||||
val.ValueType = &pb.Value_StringValue{v}
|
||||
case float32:
|
||||
val.ValueType = &pb.Value_DoubleValue{float64(v)}
|
||||
case float64:
|
||||
val.ValueType = &pb.Value_DoubleValue{v}
|
||||
case *Key:
|
||||
if v == nil {
|
||||
val.ValueType = &pb.Value_NullValue{}
|
||||
} else {
|
||||
val.ValueType = &pb.Value_KeyValue{keyToProto(v)}
|
||||
}
|
||||
case GeoPoint:
|
||||
if !v.Valid() {
|
||||
return nil, errors.New("invalid GeoPoint value")
|
||||
}
|
||||
val.ValueType = &pb.Value_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{&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{v}
|
||||
case *Entity:
|
||||
e, err := propertiesToProto(v.Key, v.Properties)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val.ValueType = &pb.Value_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{&pb.ArrayValue{arr}}
|
||||
// ArrayValues have ExcludeFromIndexes set on the individual items, rather
|
||||
// than the top-level value.
|
||||
val.ExcludeFromIndexes = false
|
||||
default:
|
||||
if iv != nil {
|
||||
return nil, fmt.Errorf("invalid Value type %t", iv)
|
||||
}
|
||||
val.ValueType = &pb.Value_NullValue{}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
// 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"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/datastore/v1"
|
||||
)
|
||||
|
||||
func TestInterfaceToProtoNilKey(t *testing.T) {
|
||||
var iv *Key
|
||||
pv, err := interfaceToProto(iv, false)
|
||||
if err != nil {
|
||||
t.Fatalf("nil key: interfaceToProto: %v", err)
|
||||
}
|
||||
|
||||
_, ok := pv.ValueType.(*pb.Value_NullValue)
|
||||
if !ok {
|
||||
t.Errorf("nil key: type:\ngot: %T\nwant: %T", pv.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
|
||||
}{
|
||||
{
|
||||
"nested entity with key",
|
||||
&NestedWithKey{
|
||||
Y: "yyy",
|
||||
N: WithKey{
|
||||
X: "two",
|
||||
I: 2,
|
||||
K: testKey1a,
|
||||
},
|
||||
},
|
||||
testKey0,
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{"yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey1a),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{"two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested entity with incomplete key",
|
||||
&NestedWithKey{
|
||||
Y: "yyy",
|
||||
N: WithKey{
|
||||
X: "two",
|
||||
I: 2,
|
||||
K: incompleteKey,
|
||||
},
|
||||
},
|
||||
testKey0,
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{"yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Key: keyToProto(incompleteKey),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{"two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested entity without key",
|
||||
&NestedWithoutKey{
|
||||
Y: "yyy",
|
||||
N: WithoutKey{
|
||||
X: "two",
|
||||
I: 2,
|
||||
},
|
||||
},
|
||||
testKey0,
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"Y": {ValueType: &pb.Value_StringValue{"yyy"}},
|
||||
"N": {ValueType: &pb.Value_EntityValue{
|
||||
&pb.Entity{
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{"two"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{2}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"key at top level",
|
||||
&WithKey{
|
||||
X: "three",
|
||||
I: 3,
|
||||
K: testKey0,
|
||||
},
|
||||
testKey0,
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"X": {ValueType: &pb.Value_StringValue{"three"}},
|
||||
"I": {ValueType: &pb.Value_IntegerValue{3}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested unexported anonymous struct field",
|
||||
&UnexpAnonym{
|
||||
a{S: "hello"},
|
||||
},
|
||||
testKey0,
|
||||
&pb.Entity{
|
||||
Key: keyToProto(testKey0),
|
||||
Properties: map[string]*pb.Value{
|
||||
"S": {ValueType: &pb.Value_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 !reflect.DeepEqual(tc.want, got) {
|
||||
t.Errorf("%s: compare:\ngot: %#v\nwant: %#v", tc.desc, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+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())
|
||||
}
|
||||
}
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
// 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"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) (*Transaction, error) {
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(maxAttempts); ok {
|
||||
return nil, errors.New("datastore: NewTransaction does not accept MaxAttempts option")
|
||||
}
|
||||
}
|
||||
req := &pb.BeginTransactionRequest{
|
||||
ProjectId: c.dataset,
|
||||
}
|
||||
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) (*Commit, error) {
|
||||
settings := newTransactionSettings(opts)
|
||||
for n := 0; n < settings.attempts; n++ {
|
||||
tx, err := c.NewTransaction(ctx)
|
||||
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
|
||||
}
|
||||
}
|
||||
return nil, ErrConcurrentTransaction
|
||||
}
|
||||
|
||||
// Commit applies the enqueued operations atomically.
|
||||
func (t *Transaction) Commit() (*Commit, error) {
|
||||
if t.id == nil {
|
||||
return nil, errExpiredTransaction
|
||||
}
|
||||
req := &pb.CommitRequest{
|
||||
ProjectId: t.client.dataset,
|
||||
TransactionSelector: &pb.CommitRequest_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.
|
||||
commit := &Commit{}
|
||||
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 = commit
|
||||
}
|
||||
|
||||
return commit, nil
|
||||
}
|
||||
|
||||
// Rollback abandons a pending transaction.
|
||||
func (t *Transaction) Rollback() error {
|
||||
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{}) error {
|
||||
opts := &pb.ReadOptions{
|
||||
ConsistencyType: &pb.ReadOptions_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{}) error {
|
||||
if t.id == nil {
|
||||
return errExpiredTransaction
|
||||
}
|
||||
opts := &pb.ReadOptions{
|
||||
ConsistencyType: &pb.ReadOptions_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.
|
||||
func (t *Transaction) PutMulti(keys []*Key, src interface{}) ([]*PendingKey, error) {
|
||||
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.
|
||||
func (t *Transaction) DeleteMulti(keys []*Key) error {
|
||||
if t.id == nil {
|
||||
return errExpiredTransaction
|
||||
}
|
||||
mutations, err := deleteMutations(keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.mutations = append(t.mutations, mutations...)
|
||||
return 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 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
|
||||
}
|
||||
Reference in New Issue
Block a user