+13
@@ -0,0 +1,13 @@
|
||||
# Copy textproto files in this directory from the source of truth.
|
||||
|
||||
SRC=$(GOPATH)/src/github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore
|
||||
|
||||
.PHONY: refresh-tests
|
||||
|
||||
refresh-tests:
|
||||
-rm genproto/*.pb.go
|
||||
cp $(SRC)/genproto/*.pb.go genproto
|
||||
-rm testdata/*.textproto
|
||||
cp $(SRC)/testdata/*.textproto testdata
|
||||
openssl dgst -sha1 $(SRC)/testdata/test-suite.binproto > testdata/VERSION
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
// Package firestore is an auto-generated package for the
|
||||
// Google Cloud Firestore API.
|
||||
//
|
||||
// NOTE: This package is in beta. It is not stable, and may be subject to changes.
|
||||
//
|
||||
//
|
||||
// Use the client at cloud.google.com/go/firestore in preference to this.
|
||||
package firestore // import "cloud.google.com/go/firestore/apiv1beta1"
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func insertMetadata(ctx context.Context, mds ...metadata.MD) context.Context {
|
||||
out, _ := metadata.FromOutgoingContext(ctx)
|
||||
out = out.Copy()
|
||||
for _, md := range mds {
|
||||
for k, v := range md {
|
||||
out[k] = append(out[k], v...)
|
||||
}
|
||||
}
|
||||
return metadata.NewOutgoingContext(ctx, out)
|
||||
}
|
||||
|
||||
// DefaultAuthScopes reports the default set of authentication scopes to use with this package.
|
||||
func DefaultAuthScopes() []string {
|
||||
return []string{
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/datastore",
|
||||
}
|
||||
}
|
||||
+497
@@ -0,0 +1,497 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/version"
|
||||
gax "github.com/googleapis/gax-go"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/transport"
|
||||
firestorepb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// CallOptions contains the retry settings for each method of Client.
|
||||
type CallOptions struct {
|
||||
GetDocument []gax.CallOption
|
||||
ListDocuments []gax.CallOption
|
||||
CreateDocument []gax.CallOption
|
||||
UpdateDocument []gax.CallOption
|
||||
DeleteDocument []gax.CallOption
|
||||
BatchGetDocuments []gax.CallOption
|
||||
BeginTransaction []gax.CallOption
|
||||
Commit []gax.CallOption
|
||||
Rollback []gax.CallOption
|
||||
RunQuery []gax.CallOption
|
||||
Write []gax.CallOption
|
||||
Listen []gax.CallOption
|
||||
ListCollectionIds []gax.CallOption
|
||||
}
|
||||
|
||||
func defaultClientOptions() []option.ClientOption {
|
||||
return []option.ClientOption{
|
||||
option.WithEndpoint("firestore.googleapis.com:443"),
|
||||
option.WithScopes(DefaultAuthScopes()...),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultCallOptions() *CallOptions {
|
||||
retry := map[[2]string][]gax.CallOption{
|
||||
{"default", "idempotent"}: {
|
||||
gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.DeadlineExceeded,
|
||||
codes.Unavailable,
|
||||
}, gax.Backoff{
|
||||
Initial: 100 * time.Millisecond,
|
||||
Max: 60000 * time.Millisecond,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
}),
|
||||
},
|
||||
{"streaming", "idempotent"}: {
|
||||
gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.DeadlineExceeded,
|
||||
codes.Unavailable,
|
||||
}, gax.Backoff{
|
||||
Initial: 100 * time.Millisecond,
|
||||
Max: 60000 * time.Millisecond,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
}),
|
||||
},
|
||||
}
|
||||
return &CallOptions{
|
||||
GetDocument: retry[[2]string{"default", "idempotent"}],
|
||||
ListDocuments: retry[[2]string{"default", "idempotent"}],
|
||||
CreateDocument: retry[[2]string{"default", "non_idempotent"}],
|
||||
UpdateDocument: retry[[2]string{"default", "non_idempotent"}],
|
||||
DeleteDocument: retry[[2]string{"default", "idempotent"}],
|
||||
BatchGetDocuments: retry[[2]string{"streaming", "idempotent"}],
|
||||
BeginTransaction: retry[[2]string{"default", "idempotent"}],
|
||||
Commit: retry[[2]string{"default", "non_idempotent"}],
|
||||
Rollback: retry[[2]string{"default", "idempotent"}],
|
||||
RunQuery: retry[[2]string{"default", "idempotent"}],
|
||||
Write: retry[[2]string{"streaming", "non_idempotent"}],
|
||||
Listen: retry[[2]string{"streaming", "idempotent"}],
|
||||
ListCollectionIds: retry[[2]string{"default", "idempotent"}],
|
||||
}
|
||||
}
|
||||
|
||||
// Client is a client for interacting with Google Cloud Firestore API.
|
||||
type Client struct {
|
||||
// The connection to the service.
|
||||
conn *grpc.ClientConn
|
||||
|
||||
// The gRPC API client.
|
||||
client firestorepb.FirestoreClient
|
||||
|
||||
// The call options for this service.
|
||||
CallOptions *CallOptions
|
||||
|
||||
// The x-goog-* metadata to be sent with each request.
|
||||
xGoogMetadata metadata.MD
|
||||
}
|
||||
|
||||
// NewClient creates a new firestore client.
|
||||
//
|
||||
// The Cloud Firestore service.
|
||||
//
|
||||
// This service exposes several types of comparable timestamps:
|
||||
//
|
||||
// create_time - The time at which a document was created. Changes only
|
||||
// when a document is deleted, then re-created. Increases in a strict
|
||||
// monotonic fashion.
|
||||
//
|
||||
// update_time - The time at which a document was last updated. Changes
|
||||
// every time a document is modified. Does not change when a write results
|
||||
// in no modifications. Increases in a strict monotonic fashion.
|
||||
//
|
||||
// read_time - The time at which a particular state was observed. Used
|
||||
// to denote a consistent snapshot of the database or the time at which a
|
||||
// Document was observed to not exist.
|
||||
//
|
||||
// commit_time - The time at which the writes in a transaction were
|
||||
// committed. Any read with an equal or greater read_time is guaranteed
|
||||
// to see the effects of the transaction.
|
||||
func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error) {
|
||||
conn, err := transport.DialGRPC(ctx, append(defaultClientOptions(), opts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &Client{
|
||||
conn: conn,
|
||||
CallOptions: defaultCallOptions(),
|
||||
|
||||
client: firestorepb.NewFirestoreClient(conn),
|
||||
}
|
||||
c.SetGoogleClientInfo()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Connection returns the client's connection to the API service.
|
||||
func (c *Client) Connection() *grpc.ClientConn {
|
||||
return c.conn
|
||||
}
|
||||
|
||||
// Close closes the connection to the API service. The user should invoke this when
|
||||
// the client is no longer required.
|
||||
func (c *Client) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// SetGoogleClientInfo sets the name and version of the application in
|
||||
// the `x-goog-api-client` header passed on each request. Intended for
|
||||
// use by Google-written clients.
|
||||
func (c *Client) SetGoogleClientInfo(keyval ...string) {
|
||||
kv := append([]string{"gl-go", version.Go()}, keyval...)
|
||||
kv = append(kv, "gapic", version.Repo, "gax", gax.Version, "grpc", grpc.Version)
|
||||
c.xGoogMetadata = metadata.Pairs("x-goog-api-client", gax.XGoogHeader(kv...))
|
||||
}
|
||||
|
||||
// GetDocument gets a single document.
|
||||
func (c *Client) GetDocument(ctx context.Context, req *firestorepb.GetDocumentRequest, opts ...gax.CallOption) (*firestorepb.Document, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.GetDocument[0:len(c.CallOptions.GetDocument):len(c.CallOptions.GetDocument)], opts...)
|
||||
var resp *firestorepb.Document
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.GetDocument(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListDocuments lists documents.
|
||||
func (c *Client) ListDocuments(ctx context.Context, req *firestorepb.ListDocumentsRequest, opts ...gax.CallOption) *DocumentIterator {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.ListDocuments[0:len(c.CallOptions.ListDocuments):len(c.CallOptions.ListDocuments)], opts...)
|
||||
it := &DocumentIterator{}
|
||||
it.InternalFetch = func(pageSize int, pageToken string) ([]*firestorepb.Document, string, error) {
|
||||
var resp *firestorepb.ListDocumentsResponse
|
||||
req.PageToken = pageToken
|
||||
if pageSize > math.MaxInt32 {
|
||||
req.PageSize = math.MaxInt32
|
||||
} else {
|
||||
req.PageSize = int32(pageSize)
|
||||
}
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.ListDocuments(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return resp.Documents, resp.NextPageToken, nil
|
||||
}
|
||||
fetch := func(pageSize int, pageToken string) (string, error) {
|
||||
items, nextPageToken, err := it.InternalFetch(pageSize, pageToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
it.items = append(it.items, items...)
|
||||
return nextPageToken, nil
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf)
|
||||
return it
|
||||
}
|
||||
|
||||
// CreateDocument creates a new document.
|
||||
func (c *Client) CreateDocument(ctx context.Context, req *firestorepb.CreateDocumentRequest, opts ...gax.CallOption) (*firestorepb.Document, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.CreateDocument[0:len(c.CallOptions.CreateDocument):len(c.CallOptions.CreateDocument)], opts...)
|
||||
var resp *firestorepb.Document
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.CreateDocument(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UpdateDocument updates or inserts a document.
|
||||
func (c *Client) UpdateDocument(ctx context.Context, req *firestorepb.UpdateDocumentRequest, opts ...gax.CallOption) (*firestorepb.Document, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.UpdateDocument[0:len(c.CallOptions.UpdateDocument):len(c.CallOptions.UpdateDocument)], opts...)
|
||||
var resp *firestorepb.Document
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.UpdateDocument(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteDocument deletes a document.
|
||||
func (c *Client) DeleteDocument(ctx context.Context, req *firestorepb.DeleteDocumentRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.DeleteDocument[0:len(c.CallOptions.DeleteDocument):len(c.CallOptions.DeleteDocument)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.client.DeleteDocument(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
// BatchGetDocuments gets multiple documents.
|
||||
//
|
||||
// Documents returned by this method are not guaranteed to be returned in the
|
||||
// same order that they were requested.
|
||||
func (c *Client) BatchGetDocuments(ctx context.Context, req *firestorepb.BatchGetDocumentsRequest, opts ...gax.CallOption) (firestorepb.Firestore_BatchGetDocumentsClient, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.BatchGetDocuments[0:len(c.CallOptions.BatchGetDocuments):len(c.CallOptions.BatchGetDocuments)], opts...)
|
||||
var resp firestorepb.Firestore_BatchGetDocumentsClient
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.BatchGetDocuments(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// BeginTransaction starts a new transaction.
|
||||
func (c *Client) BeginTransaction(ctx context.Context, req *firestorepb.BeginTransactionRequest, opts ...gax.CallOption) (*firestorepb.BeginTransactionResponse, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.BeginTransaction[0:len(c.CallOptions.BeginTransaction):len(c.CallOptions.BeginTransaction)], opts...)
|
||||
var resp *firestorepb.BeginTransactionResponse
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.BeginTransaction(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Commit commits a transaction, while optionally updating documents.
|
||||
func (c *Client) Commit(ctx context.Context, req *firestorepb.CommitRequest, opts ...gax.CallOption) (*firestorepb.CommitResponse, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.Commit[0:len(c.CallOptions.Commit):len(c.CallOptions.Commit)], opts...)
|
||||
var resp *firestorepb.CommitResponse
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.Commit(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Rollback rolls back a transaction.
|
||||
func (c *Client) Rollback(ctx context.Context, req *firestorepb.RollbackRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.Rollback[0:len(c.CallOptions.Rollback):len(c.CallOptions.Rollback)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.client.Rollback(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
// RunQuery runs a query.
|
||||
func (c *Client) RunQuery(ctx context.Context, req *firestorepb.RunQueryRequest, opts ...gax.CallOption) (firestorepb.Firestore_RunQueryClient, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.RunQuery[0:len(c.CallOptions.RunQuery):len(c.CallOptions.RunQuery)], opts...)
|
||||
var resp firestorepb.Firestore_RunQueryClient
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.RunQuery(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Write streams batches of document updates and deletes, in order.
|
||||
func (c *Client) Write(ctx context.Context, opts ...gax.CallOption) (firestorepb.Firestore_WriteClient, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.Write[0:len(c.CallOptions.Write):len(c.CallOptions.Write)], opts...)
|
||||
var resp firestorepb.Firestore_WriteClient
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.Write(ctx, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Listen listens to changes.
|
||||
func (c *Client) Listen(ctx context.Context, opts ...gax.CallOption) (firestorepb.Firestore_ListenClient, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.Listen[0:len(c.CallOptions.Listen):len(c.CallOptions.Listen)], opts...)
|
||||
var resp firestorepb.Firestore_ListenClient
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.Listen(ctx, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListCollectionIds lists all the collection IDs underneath a document.
|
||||
func (c *Client) ListCollectionIds(ctx context.Context, req *firestorepb.ListCollectionIdsRequest, opts ...gax.CallOption) *StringIterator {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.ListCollectionIds[0:len(c.CallOptions.ListCollectionIds):len(c.CallOptions.ListCollectionIds)], opts...)
|
||||
it := &StringIterator{}
|
||||
it.InternalFetch = func(pageSize int, pageToken string) ([]string, string, error) {
|
||||
var resp *firestorepb.ListCollectionIdsResponse
|
||||
req.PageToken = pageToken
|
||||
if pageSize > math.MaxInt32 {
|
||||
req.PageSize = math.MaxInt32
|
||||
} else {
|
||||
req.PageSize = int32(pageSize)
|
||||
}
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.client.ListCollectionIds(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return resp.CollectionIds, resp.NextPageToken, nil
|
||||
}
|
||||
fetch := func(pageSize int, pageToken string) (string, error) {
|
||||
items, nextPageToken, err := it.InternalFetch(pageSize, pageToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
it.items = append(it.items, items...)
|
||||
return nextPageToken, nil
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf)
|
||||
return it
|
||||
}
|
||||
|
||||
// DocumentIterator manages a stream of *firestorepb.Document.
|
||||
type DocumentIterator struct {
|
||||
items []*firestorepb.Document
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []*firestorepb.Document, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||||
func (it *DocumentIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *DocumentIterator) Next() (*firestorepb.Document, error) {
|
||||
var item *firestorepb.Document
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *DocumentIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *DocumentIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
|
||||
// StringIterator manages a stream of string.
|
||||
type StringIterator struct {
|
||||
items []string
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []string, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||||
func (it *StringIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *StringIterator) Next() (string, error) {
|
||||
var item string
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *StringIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *StringIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package firestore_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"cloud.google.com/go/firestore/apiv1beta1"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
firestorepb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
)
|
||||
|
||||
func ExampleNewClient() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use client.
|
||||
_ = c
|
||||
}
|
||||
|
||||
func ExampleClient_GetDocument() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.GetDocumentRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.GetDocument(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_ListDocuments() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.ListDocumentsRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
it := c.ListDocuments(ctx, req)
|
||||
for {
|
||||
resp, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_CreateDocument() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.CreateDocumentRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.CreateDocument(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_UpdateDocument() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.UpdateDocumentRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.UpdateDocument(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_DeleteDocument() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.DeleteDocumentRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.DeleteDocument(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_BatchGetDocuments() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.BatchGetDocumentsRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
stream, err := c.BatchGetDocuments(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_BeginTransaction() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.BeginTransactionRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.BeginTransaction(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_Commit() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.CommitRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.Commit(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleClient_Rollback() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.RollbackRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.Rollback(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_RunQuery() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.RunQueryRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
stream, err := c.RunQuery(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_Write() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
stream, err := c.Write(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
go func() {
|
||||
reqs := []*firestorepb.WriteRequest{
|
||||
// TODO: Create requests.
|
||||
}
|
||||
for _, req := range reqs {
|
||||
if err := stream.Send(req); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
stream.CloseSend()
|
||||
}()
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_Listen() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
stream, err := c.Listen(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
go func() {
|
||||
reqs := []*firestorepb.ListenRequest{
|
||||
// TODO: Create requests.
|
||||
}
|
||||
for _, req := range reqs {
|
||||
if err := stream.Send(req); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
stream.CloseSend()
|
||||
}()
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_ListCollectionIds() {
|
||||
ctx := context.Background()
|
||||
c, err := firestore.NewClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &firestorepb.ListCollectionIdsRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
it := c.ListCollectionIds(ctx, req)
|
||||
for {
|
||||
resp, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
+1153
File diff suppressed because it is too large
Load Diff
+78
@@ -0,0 +1,78 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// https://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 firestore
|
||||
|
||||
// DatabaseRootPath returns the path for the database root resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s/databases/%s", project, database)
|
||||
// instead.
|
||||
func DatabaseRootPath(project, database string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/databases/" +
|
||||
database +
|
||||
""
|
||||
}
|
||||
|
||||
// DocumentRootPath returns the path for the document root resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s/databases/%s/documents", project, database)
|
||||
// instead.
|
||||
func DocumentRootPath(project, database string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/databases/" +
|
||||
database +
|
||||
"/documents" +
|
||||
""
|
||||
}
|
||||
|
||||
// DocumentPathPath returns the path for the document path resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s/databases/%s/documents/%s", project, database, documentPath)
|
||||
// instead.
|
||||
func DocumentPathPath(project, database, documentPath string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/databases/" +
|
||||
database +
|
||||
"/documents/" +
|
||||
documentPath +
|
||||
""
|
||||
}
|
||||
|
||||
// AnyPathPath returns the path for the any path resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s/databases/%s/documents/%s/%s", project, database, document, anyPath)
|
||||
// instead.
|
||||
func AnyPathPath(project, database, document, anyPath string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/databases/" +
|
||||
database +
|
||||
"/documents/" +
|
||||
document +
|
||||
"/" +
|
||||
anyPath +
|
||||
""
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/api/iterator"
|
||||
|
||||
vkit "cloud.google.com/go/firestore/apiv1beta1"
|
||||
|
||||
"cloud.google.com/go/internal/version"
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
gax "github.com/googleapis/gax-go"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// resourcePrefixHeader is the name of the metadata header used to indicate
|
||||
// the resource being operated on.
|
||||
const resourcePrefixHeader = "google-cloud-resource-prefix"
|
||||
|
||||
// A Client provides access to the Firestore service.
|
||||
type Client struct {
|
||||
c *vkit.Client
|
||||
projectID string
|
||||
databaseID string // A client is tied to a single database.
|
||||
}
|
||||
|
||||
// NewClient creates a new Firestore client that uses the given project.
|
||||
func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error) {
|
||||
vc, err := vkit.NewClient(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vc.SetGoogleClientInfo("gccl", version.Repo)
|
||||
c := &Client{
|
||||
c: vc,
|
||||
projectID: projectID,
|
||||
databaseID: "(default)", // always "(default)", for now
|
||||
}
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
// Close closes any resources held by the client.
|
||||
//
|
||||
// Close need not be called at program exit.
|
||||
func (c *Client) Close() error {
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
func (c *Client) path() string {
|
||||
return fmt.Sprintf("projects/%s/databases/%s", c.projectID, c.databaseID)
|
||||
}
|
||||
|
||||
func withResourceHeader(ctx context.Context, resource string) context.Context {
|
||||
md, _ := metadata.FromOutgoingContext(ctx)
|
||||
md = md.Copy()
|
||||
md[resourcePrefixHeader] = []string{resource}
|
||||
return metadata.NewOutgoingContext(ctx, md)
|
||||
}
|
||||
|
||||
// Collection creates a reference to a collection with the given path.
|
||||
// A path is a sequence of IDs separated by slashes.
|
||||
//
|
||||
// Collection returns nil if path contains an even number of IDs or any ID is empty.
|
||||
func (c *Client) Collection(path string) *CollectionRef {
|
||||
coll, _ := c.idsToRef(strings.Split(path, "/"), c.path())
|
||||
return coll
|
||||
}
|
||||
|
||||
// Doc creates a reference to a document with the given path.
|
||||
// A path is a sequence of IDs separated by slashes.
|
||||
//
|
||||
// Doc returns nil if path contains an odd number of IDs or any ID is empty.
|
||||
func (c *Client) Doc(path string) *DocumentRef {
|
||||
_, doc := c.idsToRef(strings.Split(path, "/"), c.path())
|
||||
return doc
|
||||
}
|
||||
|
||||
func (c *Client) idsToRef(IDs []string, dbPath string) (*CollectionRef, *DocumentRef) {
|
||||
if len(IDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
for _, id := range IDs {
|
||||
if id == "" {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
coll := newTopLevelCollRef(c, dbPath, IDs[0])
|
||||
i := 1
|
||||
for i < len(IDs) {
|
||||
doc := newDocRef(coll, IDs[i])
|
||||
i++
|
||||
if i == len(IDs) {
|
||||
return nil, doc
|
||||
}
|
||||
coll = newCollRefWithParent(c, doc, IDs[i])
|
||||
i++
|
||||
}
|
||||
return coll, nil
|
||||
}
|
||||
|
||||
// GetAll retrieves multiple documents with a single call. The DocumentSnapshots are
|
||||
// returned in the order of the given DocumentRefs.
|
||||
//
|
||||
// If a document is not present, the corresponding DocumentSnapshot's Exists method will return false.
|
||||
func (c *Client) GetAll(ctx context.Context, docRefs []*DocumentRef) ([]*DocumentSnapshot, error) {
|
||||
if err := checkTransaction(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.getAll(ctx, docRefs, nil)
|
||||
}
|
||||
|
||||
func (c *Client) getAll(ctx context.Context, docRefs []*DocumentRef, tid []byte) ([]*DocumentSnapshot, error) {
|
||||
var docNames []string
|
||||
docIndex := map[string]int{} // doc name to position in docRefs
|
||||
for i, dr := range docRefs {
|
||||
if dr == nil {
|
||||
return nil, errNilDocRef
|
||||
}
|
||||
docNames = append(docNames, dr.Path)
|
||||
docIndex[dr.Path] = i
|
||||
}
|
||||
req := &pb.BatchGetDocumentsRequest{
|
||||
Database: c.path(),
|
||||
Documents: docNames,
|
||||
}
|
||||
if tid != nil {
|
||||
req.ConsistencySelector = &pb.BatchGetDocumentsRequest_Transaction{tid}
|
||||
}
|
||||
streamClient, err := c.c.BatchGetDocuments(withResourceHeader(ctx, req.Database), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and remember all results from the stream.
|
||||
var resps []*pb.BatchGetDocumentsResponse
|
||||
for {
|
||||
resp, err := streamClient.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, resp)
|
||||
}
|
||||
|
||||
// Results may arrive out of order. Put each at the right index.
|
||||
docs := make([]*DocumentSnapshot, len(docNames))
|
||||
for _, resp := range resps {
|
||||
var (
|
||||
i int
|
||||
doc *pb.Document
|
||||
err error
|
||||
)
|
||||
switch r := resp.Result.(type) {
|
||||
case *pb.BatchGetDocumentsResponse_Found:
|
||||
i = docIndex[r.Found.Name]
|
||||
doc = r.Found
|
||||
case *pb.BatchGetDocumentsResponse_Missing:
|
||||
i = docIndex[r.Missing]
|
||||
doc = nil
|
||||
default:
|
||||
return nil, errors.New("firestore: unknown BatchGetDocumentsResponse result type")
|
||||
}
|
||||
if docs[i] != nil {
|
||||
return nil, fmt.Errorf("firestore: %q seen twice", docRefs[i].Path)
|
||||
}
|
||||
docs[i], err = newDocumentSnapshot(docRefs[i], doc, c, resp.ReadTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// Collections returns an interator over the top-level collections.
|
||||
func (c *Client) Collections(ctx context.Context) *CollectionIterator {
|
||||
it := &CollectionIterator{
|
||||
err: checkTransaction(ctx),
|
||||
client: c,
|
||||
it: c.c.ListCollectionIds(
|
||||
withResourceHeader(ctx, c.path()),
|
||||
&pb.ListCollectionIdsRequest{Parent: c.path()}),
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(
|
||||
it.fetch,
|
||||
func() int { return len(it.items) },
|
||||
func() interface{} { b := it.items; it.items = nil; return b })
|
||||
return it
|
||||
}
|
||||
|
||||
// Batch returns a WriteBatch.
|
||||
func (c *Client) Batch() *WriteBatch {
|
||||
return &WriteBatch{c: c}
|
||||
}
|
||||
|
||||
// commit calls the Commit RPC outside of a transaction.
|
||||
func (c *Client) commit(ctx context.Context, ws []*pb.Write) ([]*WriteResult, error) {
|
||||
if err := checkTransaction(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &pb.CommitRequest{
|
||||
Database: c.path(),
|
||||
Writes: ws,
|
||||
}
|
||||
res, err := c.c.Commit(withResourceHeader(ctx, req.Database), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(res.WriteResults) == 0 {
|
||||
return nil, errors.New("firestore: missing WriteResult")
|
||||
}
|
||||
var wrs []*WriteResult
|
||||
for _, pwr := range res.WriteResults {
|
||||
wr, err := writeResultFromProto(pwr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrs = append(wrs, wr)
|
||||
}
|
||||
return wrs, nil
|
||||
}
|
||||
|
||||
func (c *Client) commit1(ctx context.Context, ws []*pb.Write) (*WriteResult, error) {
|
||||
wrs, err := c.commit(ctx, ws)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wrs[0], nil
|
||||
}
|
||||
|
||||
// A WriteResult is returned by methods that write documents.
|
||||
type WriteResult struct {
|
||||
// The time at which the document was updated, or created if it did not
|
||||
// previously exist. Writes that do not actually change the document do
|
||||
// not change the update time.
|
||||
UpdateTime time.Time
|
||||
}
|
||||
|
||||
func writeResultFromProto(wr *pb.WriteResult) (*WriteResult, error) {
|
||||
t, err := ptypes.Timestamp(wr.UpdateTime)
|
||||
if err != nil {
|
||||
t = time.Time{}
|
||||
// TODO(jba): Follow up if Delete is supposed to return a nil timestamp.
|
||||
}
|
||||
return &WriteResult{UpdateTime: t}, nil
|
||||
}
|
||||
|
||||
func sleep(ctx context.Context, dur time.Duration) error {
|
||||
switch err := gax.Sleep(ctx, dur); err {
|
||||
case context.Canceled:
|
||||
return status.Error(codes.Canceled, "context canceled")
|
||||
case context.DeadlineExceeded:
|
||||
return status.Error(codes.DeadlineExceeded, "context deadline exceeded")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
tspb "github.com/golang/protobuf/ptypes/timestamp"
|
||||
"golang.org/x/net/context"
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var testClient = &Client{
|
||||
projectID: "projectID",
|
||||
databaseID: "(default)",
|
||||
}
|
||||
|
||||
func TestClientCollectionAndDoc(t *testing.T) {
|
||||
coll1 := testClient.Collection("X")
|
||||
db := "projects/projectID/databases/(default)"
|
||||
wantc1 := &CollectionRef{
|
||||
c: testClient,
|
||||
parentPath: db,
|
||||
Parent: nil,
|
||||
ID: "X",
|
||||
Path: "projects/projectID/databases/(default)/documents/X",
|
||||
Query: Query{c: testClient, collectionID: "X", parentPath: db},
|
||||
}
|
||||
if !testEqual(coll1, wantc1) {
|
||||
t.Fatalf("got\n%+v\nwant\n%+v", coll1, wantc1)
|
||||
}
|
||||
doc1 := testClient.Doc("X/a")
|
||||
wantd1 := &DocumentRef{
|
||||
Parent: coll1,
|
||||
ID: "a",
|
||||
Path: "projects/projectID/databases/(default)/documents/X/a",
|
||||
}
|
||||
|
||||
if !testEqual(doc1, wantd1) {
|
||||
t.Fatalf("got %+v, want %+v", doc1, wantd1)
|
||||
}
|
||||
coll2 := testClient.Collection("X/a/Y")
|
||||
parentPath := "projects/projectID/databases/(default)/documents/X/a"
|
||||
wantc2 := &CollectionRef{
|
||||
c: testClient,
|
||||
parentPath: parentPath,
|
||||
Parent: doc1,
|
||||
ID: "Y",
|
||||
Path: "projects/projectID/databases/(default)/documents/X/a/Y",
|
||||
Query: Query{c: testClient, collectionID: "Y", parentPath: parentPath},
|
||||
}
|
||||
if !testEqual(coll2, wantc2) {
|
||||
t.Fatalf("\ngot %+v\nwant %+v", coll2, wantc2)
|
||||
}
|
||||
doc2 := testClient.Doc("X/a/Y/b")
|
||||
wantd2 := &DocumentRef{
|
||||
Parent: coll2,
|
||||
ID: "b",
|
||||
Path: "projects/projectID/databases/(default)/documents/X/a/Y/b",
|
||||
}
|
||||
if !testEqual(doc2, wantd2) {
|
||||
t.Fatalf("got %+v, want %+v", doc2, wantd2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCollDocErrors(t *testing.T) {
|
||||
for _, badColl := range []string{"", "/", "/a/", "/a/b", "a/b/", "a//b"} {
|
||||
coll := testClient.Collection(badColl)
|
||||
if coll != nil {
|
||||
t.Errorf("coll path %q: got %+v, want nil", badColl, coll)
|
||||
}
|
||||
}
|
||||
for _, badDoc := range []string{"", "a", "/", "/a", "a/", "a/b/c", "a//b/c"} {
|
||||
doc := testClient.Doc(badDoc)
|
||||
if doc != nil {
|
||||
t.Errorf("doc path %q: got %+v, want nil", badDoc, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAll(t *testing.T) {
|
||||
c, srv := newMock(t)
|
||||
defer c.Close()
|
||||
const dbPath = "projects/projectID/databases/(default)"
|
||||
req := &pb.BatchGetDocumentsRequest{
|
||||
Database: dbPath,
|
||||
Documents: []string{
|
||||
dbPath + "/documents/C/a",
|
||||
dbPath + "/documents/C/b",
|
||||
dbPath + "/documents/C/c",
|
||||
},
|
||||
}
|
||||
testGetAll(t, c, srv, dbPath, func(drs []*DocumentRef) ([]*DocumentSnapshot, error) {
|
||||
return c.GetAll(context.Background(), drs)
|
||||
}, req)
|
||||
}
|
||||
|
||||
func testGetAll(t *testing.T, c *Client, srv *mockServer, dbPath string, getAll func([]*DocumentRef) ([]*DocumentSnapshot, error), req *pb.BatchGetDocumentsRequest) {
|
||||
wantPBDocs := []*pb.Document{
|
||||
{
|
||||
Name: dbPath + "/documents/C/a",
|
||||
CreateTime: aTimestamp,
|
||||
UpdateTime: aTimestamp,
|
||||
Fields: map[string]*pb.Value{"f": intval(2)},
|
||||
},
|
||||
nil,
|
||||
{
|
||||
Name: dbPath + "/documents/C/c",
|
||||
CreateTime: aTimestamp,
|
||||
UpdateTime: aTimestamp,
|
||||
Fields: map[string]*pb.Value{"f": intval(1)},
|
||||
},
|
||||
}
|
||||
wantReadTimes := []*tspb.Timestamp{aTimestamp, aTimestamp2, aTimestamp3}
|
||||
srv.addRPC(req,
|
||||
[]interface{}{
|
||||
// deliberately put these out of order
|
||||
&pb.BatchGetDocumentsResponse{
|
||||
Result: &pb.BatchGetDocumentsResponse_Found{wantPBDocs[2]},
|
||||
ReadTime: aTimestamp3,
|
||||
},
|
||||
&pb.BatchGetDocumentsResponse{
|
||||
Result: &pb.BatchGetDocumentsResponse_Found{wantPBDocs[0]},
|
||||
ReadTime: aTimestamp,
|
||||
},
|
||||
&pb.BatchGetDocumentsResponse{
|
||||
Result: &pb.BatchGetDocumentsResponse_Missing{dbPath + "/documents/C/b"},
|
||||
ReadTime: aTimestamp2,
|
||||
},
|
||||
},
|
||||
)
|
||||
coll := c.Collection("C")
|
||||
var docRefs []*DocumentRef
|
||||
for _, name := range []string{"a", "b", "c"} {
|
||||
docRefs = append(docRefs, coll.Doc(name))
|
||||
}
|
||||
docs, err := getAll(docRefs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := len(docs), len(wantPBDocs); got != want {
|
||||
t.Errorf("got %d docs, wanted %d", got, want)
|
||||
}
|
||||
for i, got := range docs {
|
||||
want, err := newDocumentSnapshot(docRefs[i], wantPBDocs[i], c, wantReadTimes[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff := testDiff(got, want); diff != "" {
|
||||
t.Errorf("#%d: got=--, want==++\n%s", i, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const (
|
||||
dbPath = "projects/projectID/databases/(default)"
|
||||
docPath = dbPath + "/documents/C/a"
|
||||
)
|
||||
c, srv := newMock(t)
|
||||
if _, err := c.GetAll(ctx, []*DocumentRef{nil}); err != errNilDocRef {
|
||||
t.Errorf("got %v, want errNilDocRef", err)
|
||||
}
|
||||
|
||||
// Internal server error.
|
||||
srv.addRPC(
|
||||
&pb.BatchGetDocumentsRequest{
|
||||
Database: dbPath,
|
||||
Documents: []string{docPath},
|
||||
},
|
||||
[]interface{}{status.Errorf(codes.Internal, "")},
|
||||
)
|
||||
_, err := c.GetAll(ctx, []*DocumentRef{c.Doc("C/a")})
|
||||
codeEq(t, "GetAll #1", codes.Internal, err)
|
||||
|
||||
// Doc appears as both found and missing (server bug).
|
||||
srv.reset()
|
||||
srv.addRPC(
|
||||
&pb.BatchGetDocumentsRequest{
|
||||
Database: dbPath,
|
||||
Documents: []string{docPath},
|
||||
},
|
||||
[]interface{}{
|
||||
&pb.BatchGetDocumentsResponse{
|
||||
Result: &pb.BatchGetDocumentsResponse_Found{&pb.Document{Name: docPath}},
|
||||
ReadTime: aTimestamp,
|
||||
},
|
||||
&pb.BatchGetDocumentsResponse{
|
||||
Result: &pb.BatchGetDocumentsResponse_Missing{docPath},
|
||||
ReadTime: aTimestamp,
|
||||
},
|
||||
},
|
||||
)
|
||||
if _, err := c.GetAll(ctx, []*DocumentRef{c.Doc("C/a")}); err == nil {
|
||||
t.Error("got nil, want error")
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// A CollectionRef is a reference to Firestore collection.
|
||||
type CollectionRef struct {
|
||||
c *Client
|
||||
|
||||
// Typically Parent.Path, or c.path if Parent is nil.
|
||||
// May be different if this CollectionRef was created from a stored reference
|
||||
// to a different project/DB.
|
||||
parentPath string
|
||||
|
||||
// Parent is the document of which this collection is a part. It is
|
||||
// nil for top-level collections.
|
||||
Parent *DocumentRef
|
||||
|
||||
// The full resource path of the collection: "projects/P/databases/D/documents..."
|
||||
Path string
|
||||
|
||||
// ID is the collection identifier.
|
||||
ID string
|
||||
|
||||
// Use the methods of Query on a CollectionRef to create and run queries.
|
||||
Query
|
||||
}
|
||||
|
||||
func newTopLevelCollRef(c *Client, dbPath, id string) *CollectionRef {
|
||||
return &CollectionRef{
|
||||
c: c,
|
||||
ID: id,
|
||||
parentPath: dbPath,
|
||||
Path: dbPath + "/documents/" + id,
|
||||
Query: Query{c: c, collectionID: id, parentPath: dbPath},
|
||||
}
|
||||
}
|
||||
|
||||
func newCollRefWithParent(c *Client, parent *DocumentRef, id string) *CollectionRef {
|
||||
return &CollectionRef{
|
||||
c: c,
|
||||
Parent: parent,
|
||||
ID: id,
|
||||
parentPath: parent.Path,
|
||||
Path: parent.Path + "/" + id,
|
||||
Query: Query{c: c, collectionID: id, parentPath: parent.Path},
|
||||
}
|
||||
}
|
||||
|
||||
// Doc returns a DocumentRef that refers to the document in the collection with the
|
||||
// given identifier.
|
||||
func (c *CollectionRef) Doc(id string) *DocumentRef {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return newDocRef(c, id)
|
||||
}
|
||||
|
||||
// NewDoc returns a DocumentRef with a uniquely generated ID.
|
||||
func (c *CollectionRef) NewDoc() *DocumentRef {
|
||||
return c.Doc(uniqueID())
|
||||
}
|
||||
|
||||
// Add generates a DocumentRef with a unique ID. It then creates the document
|
||||
// with the given data, which can be a map[string]interface{}, a struct or a
|
||||
// pointer to a struct.
|
||||
//
|
||||
// Add returns an error in the unlikely event that a document with the same ID
|
||||
// already exists.
|
||||
func (c *CollectionRef) Add(ctx context.Context, data interface{}) (*DocumentRef, *WriteResult, error) {
|
||||
d := c.NewDoc()
|
||||
wr, err := d.Create(ctx, data)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return d, wr, nil
|
||||
}
|
||||
|
||||
const alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
var (
|
||||
rngMu sync.Mutex
|
||||
rng = rand.New(rand.NewSource(time.Now().UnixNano() ^ int64(os.Getpid())))
|
||||
)
|
||||
|
||||
func uniqueID() string {
|
||||
var b [20]byte
|
||||
rngMu.Lock()
|
||||
for i := 0; i < len(b); i++ {
|
||||
b[i] = alphanum[rng.Intn(len(alphanum))]
|
||||
}
|
||||
rngMu.Unlock()
|
||||
return string(b[:])
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestDoc(t *testing.T) {
|
||||
coll := testClient.Collection("C")
|
||||
got := coll.Doc("d")
|
||||
want := &DocumentRef{
|
||||
Parent: coll,
|
||||
ID: "d",
|
||||
Path: "projects/projectID/databases/(default)/documents/C/d",
|
||||
}
|
||||
if !testEqual(got, want) {
|
||||
t.Errorf("got %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDoc(t *testing.T) {
|
||||
c := &Client{}
|
||||
coll := c.Collection("C")
|
||||
got := coll.NewDoc()
|
||||
if got.Parent != coll {
|
||||
t.Errorf("got %v, want %v", got.Parent, coll)
|
||||
}
|
||||
if len(got.ID) != 20 {
|
||||
t.Errorf("got %d-char ID, wanted 20", len(got.ID))
|
||||
}
|
||||
|
||||
got2 := coll.NewDoc()
|
||||
if got.ID == got2.ID {
|
||||
t.Error("got same ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, srv := newMock(t)
|
||||
wantReq := commitRequestForSet()
|
||||
w := wantReq.Writes[0]
|
||||
w.CurrentDocument = &pb.Precondition{
|
||||
ConditionType: &pb.Precondition_Exists{false},
|
||||
}
|
||||
srv.addRPCAdjust(wantReq, commitResponseForSet, func(gotReq proto.Message) {
|
||||
// We can't know the doc ID before Add is called, so we take it from
|
||||
// the request.
|
||||
w.Operation.(*pb.Write_Update).Update.Name = gotReq.(*pb.CommitRequest).Writes[0].Operation.(*pb.Write_Update).Update.Name
|
||||
})
|
||||
_, wr, err := c.Collection("C").Add(ctx, testData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testEqual(wr, writeResultForSet) {
|
||||
t.Errorf("got %v, want %v", wr, writeResultForSet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, _ := newMock(t)
|
||||
// Test that a nil CollectionRef results in a nil DocumentRef and errors
|
||||
// where possible.
|
||||
coll := c.Collection("a/b") // nil because "a/b" denotes a doc.
|
||||
if coll != nil {
|
||||
t.Fatal("collection not nil")
|
||||
}
|
||||
if got := coll.Doc("d"); got != nil {
|
||||
t.Fatalf("got %v, want nil", got)
|
||||
}
|
||||
if got := coll.NewDoc(); got != nil {
|
||||
t.Fatalf("got %v, want nil", got)
|
||||
}
|
||||
if _, _, err := coll.Add(ctx, testData); err != errNilDocRef {
|
||||
t.Fatalf("got <%v>, want <%v>", err, errNilDocRef)
|
||||
}
|
||||
}
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// A runner for the cross-language tests.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
pb "cloud.google.com/go/firestore/genproto"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
ts "github.com/golang/protobuf/ptypes/timestamp"
|
||||
"golang.org/x/net/context"
|
||||
fspb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
)
|
||||
|
||||
func TestCrossLanguageTests(t *testing.T) {
|
||||
const dir = "testdata"
|
||||
fis, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := 0
|
||||
for _, fi := range fis {
|
||||
if strings.HasSuffix(fi.Name(), ".textproto") {
|
||||
runTestFromFile(t, filepath.Join(dir, fi.Name()))
|
||||
n++
|
||||
}
|
||||
}
|
||||
t.Logf("ran %d cross-language tests", n)
|
||||
}
|
||||
|
||||
func runTestFromFile(t *testing.T, filename string) {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", filename, err)
|
||||
}
|
||||
var test pb.Test
|
||||
if err := proto.UnmarshalText(string(bytes), &test); err != nil {
|
||||
t.Fatalf("unmarshalling %s: %v", filename, err)
|
||||
}
|
||||
msg := fmt.Sprintf("%s (file %s)", test.Description, filepath.Base(filename))
|
||||
runTest(t, msg, &test)
|
||||
}
|
||||
|
||||
func runTest(t *testing.T, msg string, test *pb.Test) {
|
||||
check := func(gotErr error, wantErr bool) bool {
|
||||
if wantErr && gotErr == nil {
|
||||
t.Errorf("%s: got nil, want error", msg)
|
||||
return false
|
||||
} else if !wantErr && gotErr != nil {
|
||||
t.Errorf("%s: %v", msg, gotErr)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
c, srv := newMock(t)
|
||||
switch tt := test.Test.(type) {
|
||||
case *pb.Test_Get:
|
||||
req := &fspb.BatchGetDocumentsRequest{
|
||||
Database: c.path(),
|
||||
Documents: []string{tt.Get.DocRefPath},
|
||||
}
|
||||
srv.addRPC(req, []interface{}{
|
||||
&fspb.BatchGetDocumentsResponse{
|
||||
Result: &fspb.BatchGetDocumentsResponse_Found{&fspb.Document{
|
||||
Name: tt.Get.DocRefPath,
|
||||
CreateTime: &ts.Timestamp{},
|
||||
UpdateTime: &ts.Timestamp{},
|
||||
}},
|
||||
ReadTime: &ts.Timestamp{},
|
||||
},
|
||||
})
|
||||
ref := docRefFromPath(tt.Get.DocRefPath, c)
|
||||
_, err := ref.Get(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", msg, err)
|
||||
return
|
||||
}
|
||||
// Checking response would just be testing the function converting a Document
|
||||
// proto to a DocumentSnapshot, hence uninteresting.
|
||||
|
||||
case *pb.Test_Create:
|
||||
srv.addRPC(tt.Create.Request, commitResponseForSet)
|
||||
ref := docRefFromPath(tt.Create.DocRefPath, c)
|
||||
data, err := convertData(tt.Create.JsonData)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", msg, err)
|
||||
return
|
||||
}
|
||||
_, err = ref.Create(ctx, data)
|
||||
check(err, tt.Create.IsError)
|
||||
|
||||
case *pb.Test_Set:
|
||||
srv.addRPC(tt.Set.Request, commitResponseForSet)
|
||||
ref := docRefFromPath(tt.Set.DocRefPath, c)
|
||||
data, err := convertData(tt.Set.JsonData)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", msg, err)
|
||||
return
|
||||
}
|
||||
var opts []SetOption
|
||||
if tt.Set.Option != nil {
|
||||
opts = []SetOption{convertSetOption(tt.Set.Option)}
|
||||
}
|
||||
_, err = ref.Set(ctx, data, opts...)
|
||||
check(err, tt.Set.IsError)
|
||||
|
||||
case *pb.Test_Update:
|
||||
// Ignore Update test because we only support UpdatePaths.
|
||||
// Not to worry, every Update test has a corresponding UpdatePaths test.
|
||||
|
||||
case *pb.Test_UpdatePaths:
|
||||
srv.addRPC(tt.UpdatePaths.Request, commitResponseForSet)
|
||||
ref := docRefFromPath(tt.UpdatePaths.DocRefPath, c)
|
||||
preconds := convertPrecondition(t, tt.UpdatePaths.Precondition)
|
||||
paths := convertFieldPaths(tt.UpdatePaths.FieldPaths)
|
||||
var ups []Update
|
||||
for i, path := range paths {
|
||||
val, err := convertJSONValue(tt.UpdatePaths.JsonValues[i])
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", msg, err)
|
||||
}
|
||||
ups = append(ups, Update{
|
||||
FieldPath: path,
|
||||
Value: val,
|
||||
})
|
||||
}
|
||||
_, err := ref.Update(ctx, ups, preconds...)
|
||||
check(err, tt.UpdatePaths.IsError)
|
||||
|
||||
case *pb.Test_Delete:
|
||||
srv.addRPC(tt.Delete.Request, commitResponseForSet)
|
||||
ref := docRefFromPath(tt.Delete.DocRefPath, c)
|
||||
preconds := convertPrecondition(t, tt.Delete.Precondition)
|
||||
_, err := ref.Delete(ctx, preconds...)
|
||||
check(err, tt.Delete.IsError)
|
||||
|
||||
case *pb.Test_Query:
|
||||
q := convertQuery(t, tt.Query)
|
||||
got, err := q.toProto()
|
||||
if check(err, tt.Query.IsError) && err == nil {
|
||||
if want := tt.Query.Query; !proto.Equal(got, want) {
|
||||
t.Errorf("%s\ngot: %s\nwant: %s", msg, proto.MarshalTextString(got), proto.MarshalTextString(want))
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
t.Fatalf("unknown test type %T", tt)
|
||||
}
|
||||
}
|
||||
|
||||
func docRefFromPath(p string, c *Client) *DocumentRef {
|
||||
return &DocumentRef{
|
||||
Path: p,
|
||||
ID: path.Base(p),
|
||||
Parent: &CollectionRef{c: c},
|
||||
}
|
||||
}
|
||||
|
||||
func convertJSONValue(jv string) (interface{}, error) {
|
||||
var val interface{}
|
||||
if err := json.Unmarshal([]byte(jv), &val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertTestValue(val), nil
|
||||
}
|
||||
|
||||
func convertData(jsonData string) (map[string]interface{}, error) {
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertTestMap(m), nil
|
||||
}
|
||||
|
||||
func convertTestMap(m map[string]interface{}) map[string]interface{} {
|
||||
for k, v := range m {
|
||||
m[k] = convertTestValue(v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func convertTestValue(v interface{}) interface{} {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
switch v {
|
||||
case "ServerTimestamp":
|
||||
return ServerTimestamp
|
||||
case "Delete":
|
||||
return Delete
|
||||
case "NaN":
|
||||
return math.NaN()
|
||||
default:
|
||||
return v
|
||||
}
|
||||
case float64:
|
||||
if v == float64(int(v)) {
|
||||
return int(v)
|
||||
}
|
||||
return v
|
||||
case []interface{}:
|
||||
for i, e := range v {
|
||||
v[i] = convertTestValue(e)
|
||||
}
|
||||
return v
|
||||
case map[string]interface{}:
|
||||
return convertTestMap(v)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func convertSetOption(opt *pb.SetOption) SetOption {
|
||||
if opt.All {
|
||||
return MergeAll
|
||||
}
|
||||
return Merge(convertFieldPaths(opt.Fields)...)
|
||||
}
|
||||
|
||||
func convertFieldPaths(fps []*pb.FieldPath) []FieldPath {
|
||||
var res []FieldPath
|
||||
for _, fp := range fps {
|
||||
res = append(res, fp.Field)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func convertPrecondition(t *testing.T, fp *fspb.Precondition) []Precondition {
|
||||
if fp == nil {
|
||||
return nil
|
||||
}
|
||||
var pc Precondition
|
||||
switch fp := fp.ConditionType.(type) {
|
||||
case *fspb.Precondition_Exists:
|
||||
pc = exists(fp.Exists)
|
||||
case *fspb.Precondition_UpdateTime:
|
||||
tm, err := ptypes.Timestamp(fp.UpdateTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pc = LastUpdateTime(tm)
|
||||
default:
|
||||
t.Fatalf("unknown precondition type %T", fp)
|
||||
}
|
||||
return []Precondition{pc}
|
||||
}
|
||||
|
||||
func convertQuery(t *testing.T, qt *pb.QueryTest) Query {
|
||||
parts := strings.Split(qt.CollPath, "/")
|
||||
q := Query{
|
||||
parentPath: strings.Join(parts[:len(parts)-2], "/"),
|
||||
collectionID: parts[len(parts)-1],
|
||||
}
|
||||
for _, c := range qt.Clauses {
|
||||
switch c := c.Clause.(type) {
|
||||
case *pb.Clause_Select:
|
||||
q = q.SelectPaths(convertFieldPaths(c.Select.Fields)...)
|
||||
case *pb.Clause_OrderBy:
|
||||
var dir Direction
|
||||
switch c.OrderBy.Direction {
|
||||
case "asc":
|
||||
dir = Asc
|
||||
case "desc":
|
||||
dir = Desc
|
||||
default:
|
||||
t.Fatalf("bad direction: %q", c.OrderBy.Direction)
|
||||
}
|
||||
q = q.OrderByPath(FieldPath(c.OrderBy.Path.Field), dir)
|
||||
case *pb.Clause_Where:
|
||||
val, err := convertJSONValue(c.Where.JsonValue)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q = q.WherePath(FieldPath(c.Where.Path.Field), c.Where.Op, val)
|
||||
case *pb.Clause_Offset:
|
||||
q = q.Offset(int(c.Offset))
|
||||
case *pb.Clause_Limit:
|
||||
q = q.Limit(int(c.Limit))
|
||||
case *pb.Clause_StartAt:
|
||||
q = q.StartAt(convertCursor(t, c.StartAt)...)
|
||||
case *pb.Clause_StartAfter:
|
||||
q = q.StartAfter(convertCursor(t, c.StartAfter)...)
|
||||
case *pb.Clause_EndAt:
|
||||
q = q.EndAt(convertCursor(t, c.EndAt)...)
|
||||
case *pb.Clause_EndBefore:
|
||||
q = q.EndBefore(convertCursor(t, c.EndBefore)...)
|
||||
default:
|
||||
t.Fatalf("bad clause type %T", c)
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// Returns args to a cursor method (StartAt, etc.).
|
||||
func convertCursor(t *testing.T, c *pb.Cursor) []interface{} {
|
||||
if c.DocSnapshot != nil {
|
||||
ds, err := convertDocSnapshot(c.DocSnapshot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return []interface{}{ds}
|
||||
}
|
||||
var vals []interface{}
|
||||
for _, jv := range c.JsonValues {
|
||||
v, err := convertJSONValue(jv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
vals = append(vals, v)
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
func convertDocSnapshot(ds *pb.DocSnapshot) (*DocumentSnapshot, error) {
|
||||
data, err := convertData(ds.JsonData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc, transformPaths, err := toProtoDocument(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(transformPaths) > 0 {
|
||||
return nil, errors.New("saw transform paths in DocSnapshot")
|
||||
}
|
||||
return &DocumentSnapshot{
|
||||
Ref: &DocumentRef{
|
||||
Path: ds.Path,
|
||||
Parent: &CollectionRef{Path: path.Dir(ds.Path)},
|
||||
},
|
||||
proto: doc,
|
||||
}, nil
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// DO NOT EDIT doc.go. Modify internal/doc.template, then run make -C internal.
|
||||
|
||||
/*
|
||||
Package firestore provides a client for reading and writing to a Cloud Firestore
|
||||
database.
|
||||
|
||||
See https://cloud.google.com/firestore/docs for an introduction
|
||||
to Cloud Firestore and additional help on using the Firestore API.
|
||||
|
||||
Note: you can't use both Cloud Firestore and Cloud Datastore in the same
|
||||
project.
|
||||
|
||||
Creating a Client
|
||||
|
||||
To start working with this package, create a client with a project ID:
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "projectID")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
CollectionRefs and DocumentRefs
|
||||
|
||||
In Firestore, documents are sets of key-value pairs, and collections are groups of
|
||||
documents. A Firestore database consists of a hierarchy of alternating collections
|
||||
and documents, referred to by slash-separated paths like
|
||||
"States/California/Cities/SanFrancisco".
|
||||
|
||||
This client is built around references to collections and documents. CollectionRefs
|
||||
and DocumentRefs are lightweight values that refer to the corresponding database
|
||||
entities. Creating a ref does not involve any network traffic.
|
||||
|
||||
states := client.Collection("States")
|
||||
ny := states.Doc("NewYork")
|
||||
// Or, in a single call:
|
||||
ny = client.Doc("States/NewYork")
|
||||
|
||||
Reading
|
||||
|
||||
Use DocumentRef.Get to read a document. The result is a DocumentSnapshot.
|
||||
Call its Data method to obtain the entire document contents as a map.
|
||||
|
||||
docsnap, err := ny.Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
dataMap := docsnap.Data()
|
||||
fmt.Println(dataMap)
|
||||
|
||||
You can also obtain a single field with DataAt, or extract the data into a struct
|
||||
with DataTo. With the type definition
|
||||
|
||||
type State struct {
|
||||
Capital string `firestore:"capital"`
|
||||
Population float64 `firestore:"pop"` // in millions
|
||||
}
|
||||
|
||||
we can extract the document's data into a value of type State:
|
||||
|
||||
var nyData State
|
||||
if err := docsnap.DataTo(&nyData); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
Note that this client supports struct tags beginning with "firestore:" that work like
|
||||
the tags of the encoding/json package, letting you rename fields, ignore them, or
|
||||
omit their values when empty.
|
||||
|
||||
To retrieve multiple documents from their references in a single call, use
|
||||
Client.GetAll.
|
||||
|
||||
docsnaps, err := client.GetAll(ctx, []*firestore.DocumentRef{
|
||||
states.Doc("Wisconsin"), states.Doc("Ohio"),
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
for _, ds := range docsnaps {
|
||||
_ = ds // TODO: Use ds.
|
||||
}
|
||||
|
||||
|
||||
Writing
|
||||
|
||||
For writing individual documents, use the methods on DocumentReference.
|
||||
Create creates a new document.
|
||||
|
||||
wr, err := ny.Create(ctx, State{
|
||||
Capital: "Albany",
|
||||
Population: 19.8,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(wr)
|
||||
|
||||
The first return value is a WriteResult, which contains the time
|
||||
at which the document was updated.
|
||||
|
||||
Create fails if the document exists. Another method, Set, either replaces an existing
|
||||
document or creates a new one.
|
||||
|
||||
ca := states.Doc("California")
|
||||
_, err = ca.Set(ctx, State{
|
||||
Capital: "Sacramento",
|
||||
Population: 39.14,
|
||||
})
|
||||
|
||||
To update some fields of an existing document, use Update. It takes a list of
|
||||
paths to update and their corresponding values.
|
||||
|
||||
_, err = ca.Update(ctx, []firestore.Update{{Path: "capital", Value: "Sacramento"}})
|
||||
|
||||
Use DocumentRef.Delete to delete a document.
|
||||
|
||||
_, err = ny.Delete(ctx)
|
||||
|
||||
Preconditions
|
||||
|
||||
You can condition Deletes or Updates on when a document was last changed. Specify
|
||||
these preconditions as an option to a Delete or Update method. The check and the
|
||||
write happen atomically with a single RPC.
|
||||
|
||||
docsnap, err = ca.Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_, err = ca.Update(ctx,
|
||||
[]firestore.Update{{Path: "capital", Value: "Sacramento"}},
|
||||
firestore.LastUpdateTime(docsnap.UpdateTime))
|
||||
|
||||
Here we update a doc only if it hasn't changed since we read it.
|
||||
You could also do this with a transaction.
|
||||
|
||||
To perform multiple writes at once, use a WriteBatch. Its methods chain
|
||||
for convenience.
|
||||
|
||||
WriteBatch.Commit sends the collected writes to the server, where they happen
|
||||
atomically.
|
||||
|
||||
writeResults, err := client.Batch().
|
||||
Create(ny, State{Capital: "Albany"}).
|
||||
Update(ca, []firestore.Update{{Path: "capital", Value: "Sacramento"}}).
|
||||
Delete(client.Doc("States/WestDakota")).
|
||||
Commit(ctx)
|
||||
|
||||
Queries
|
||||
|
||||
You can use SQL to select documents from a collection. Begin with the collection, and
|
||||
build up a query using Select, Where and other methods of Query.
|
||||
|
||||
q := states.Where("pop", ">", 10).OrderBy("pop", firestore.Desc)
|
||||
|
||||
Call the Query's Documents method to get an iterator, and use it like
|
||||
the other Google Cloud Client iterators.
|
||||
|
||||
iter := q.Documents(ctx)
|
||||
defer iter.Stop()
|
||||
for {
|
||||
doc, err := iter.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(doc.Data())
|
||||
}
|
||||
|
||||
To get all the documents in a collection, you can use the collection itself
|
||||
as a query.
|
||||
|
||||
iter = client.Collection("States").Documents(ctx)
|
||||
|
||||
Transactions
|
||||
|
||||
Use a transaction to execute reads and writes atomically. All reads must happen
|
||||
before any writes. Transaction creation, commit, rollback and retry are handled for
|
||||
you by the Client.RunTransaction method; just provide a function and use the
|
||||
read and write methods of the Transaction passed to it.
|
||||
|
||||
ny := client.Doc("States/NewYork")
|
||||
err := client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
|
||||
doc, err := tx.Get(ny) // tx.Get, NOT ny.Get!
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pop, err := doc.DataAt("pop")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Update(ny, []firestore.Update{{Path: "pop", Value: pop.(float64) + 0.2}})
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
Authentication
|
||||
|
||||
See examples of authorization and authentication at
|
||||
https://godoc.org/cloud.google.com/go#pkg-examples.
|
||||
*/
|
||||
package firestore
|
||||
+611
@@ -0,0 +1,611 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
vkit "cloud.google.com/go/firestore/apiv1beta1"
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
)
|
||||
|
||||
var errNilDocRef = errors.New("firestore: nil DocumentRef")
|
||||
|
||||
// A DocumentRef is a reference to a Firestore document.
|
||||
type DocumentRef struct {
|
||||
// The CollectionRef that this document is a part of. Never nil.
|
||||
Parent *CollectionRef
|
||||
|
||||
// The full resource path of the document: "projects/P/databases/D/documents..."
|
||||
Path string
|
||||
|
||||
// The ID of the document: the last component of the resource path.
|
||||
ID string
|
||||
}
|
||||
|
||||
func newDocRef(parent *CollectionRef, id string) *DocumentRef {
|
||||
return &DocumentRef{
|
||||
Parent: parent,
|
||||
ID: id,
|
||||
Path: parent.Path + "/" + id,
|
||||
}
|
||||
}
|
||||
|
||||
// Collection returns a reference to sub-collection of this document.
|
||||
func (d *DocumentRef) Collection(id string) *CollectionRef {
|
||||
return newCollRefWithParent(d.Parent.c, d, id)
|
||||
}
|
||||
|
||||
// Get retrieves the document. If the document does not exist, Get return a NotFound error, which
|
||||
// can be checked with
|
||||
// grpc.Code(err) == codes.NotFound
|
||||
// In that case, Get returns a non-nil DocumentSnapshot whose Exists method return false and whose
|
||||
// ReadTime is the time of the failed read operation.
|
||||
func (d *DocumentRef) Get(ctx context.Context) (*DocumentSnapshot, error) {
|
||||
if err := checkTransaction(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d == nil {
|
||||
return nil, errNilDocRef
|
||||
}
|
||||
docsnaps, err := d.Parent.c.getAll(ctx, []*DocumentRef{d}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ds := docsnaps[0]
|
||||
if !ds.Exists() {
|
||||
return ds, status.Errorf(codes.NotFound, "%q not found", d.Path)
|
||||
}
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
// Create creates the document with the given data.
|
||||
// It returns an error if a document with the same ID already exists.
|
||||
//
|
||||
// The data argument can be a map with string keys, a struct, or a pointer to a
|
||||
// struct. The map keys or exported struct fields become the fields of the firestore
|
||||
// document.
|
||||
// The values of data are converted to Firestore values as follows:
|
||||
//
|
||||
// - bool converts to Bool.
|
||||
// - string converts to String.
|
||||
// - int, int8, int16, int32 and int64 convert to Integer.
|
||||
// - uint8, uint16 and uint32 convert to Integer. uint64 is disallowed,
|
||||
// because it can represent values that cannot be represented in an int64, which
|
||||
// is the underlying type of a Integer.
|
||||
// - float32 and float64 convert to Double.
|
||||
// - []byte converts to Bytes.
|
||||
// - time.Time converts to Timestamp.
|
||||
// - latlng.LatLng converts to GeoPoint. latlng is the package
|
||||
// "google.golang.org/genproto/googleapis/type/latlng".
|
||||
// - Slices convert to Array.
|
||||
// - Maps and structs convert to Map.
|
||||
// - nils of any type convert to Null.
|
||||
//
|
||||
// Pointers and interface{} are also permitted, and their elements processed
|
||||
// recursively.
|
||||
//
|
||||
// Struct fields can have tags like those used by the encoding/json package. Tags
|
||||
// begin with "firestore:" and are followed by "-", meaning "ignore this field," or
|
||||
// an alternative name for the field. Following the name, these comma-separated
|
||||
// options may be provided:
|
||||
//
|
||||
// - omitempty: Do not encode this field if it is empty. A value is empty
|
||||
// if it is a zero value, or an array, slice or map of length zero.
|
||||
// - serverTimestamp: The field must be of type time.Time. When writing, if
|
||||
// the field has the zero value, the server will populate the stored document with
|
||||
// the time that the request is processed.
|
||||
func (d *DocumentRef) Create(ctx context.Context, data interface{}) (*WriteResult, error) {
|
||||
ws, err := d.newCreateWrites(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newCreateWrites(data interface{}) ([]*pb.Write, error) {
|
||||
if d == nil {
|
||||
return nil, errNilDocRef
|
||||
}
|
||||
doc, serverTimestampPaths, err := toProtoDocument(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.Name = d.Path
|
||||
pc, err := exists(false).preconditionProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.newUpdateWithTransform(doc, nil, pc, serverTimestampPaths, false), nil
|
||||
}
|
||||
|
||||
// Set creates or overwrites the document with the given data. See DocumentRef.Create
|
||||
// for the acceptable values of data. Without options, Set overwrites the document
|
||||
// completely. Specify one of the Merge options to preserve an existing document's
|
||||
// fields.
|
||||
func (d *DocumentRef) Set(ctx context.Context, data interface{}, opts ...SetOption) (*WriteResult, error) {
|
||||
ws, err := d.newSetWrites(data, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newSetWrites(data interface{}, opts []SetOption) ([]*pb.Write, error) {
|
||||
if d == nil {
|
||||
return nil, errNilDocRef
|
||||
}
|
||||
if data == nil {
|
||||
return nil, errors.New("firestore: nil document contents")
|
||||
}
|
||||
if len(opts) == 0 { // Set without merge
|
||||
doc, serverTimestampPaths, err := toProtoDocument(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc.Name = d.Path
|
||||
return d.newUpdateWithTransform(doc, nil, nil, serverTimestampPaths, true), nil
|
||||
}
|
||||
// Set with merge.
|
||||
// This is just like Update, except for the existence precondition.
|
||||
// So we turn data into a list of (FieldPath, interface{}) pairs (fpv's), as we do
|
||||
// for Update.
|
||||
fieldPaths, allPaths, err := processSetOptions(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fpvs []fpv
|
||||
v := reflect.ValueOf(data)
|
||||
if allPaths {
|
||||
// Set with MergeAll. Collect all the leaves of the map.
|
||||
if v.Kind() != reflect.Map {
|
||||
return nil, errors.New("firestore: MergeAll can only be specified with map data")
|
||||
}
|
||||
fpvsFromData(v, nil, &fpvs)
|
||||
} else {
|
||||
// Set with merge paths. Collect only the values at the given paths.
|
||||
for _, fp := range fieldPaths {
|
||||
val, err := getAtPath(v, fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fpvs = append(fpvs, fpv{fp, val})
|
||||
}
|
||||
}
|
||||
return d.fpvsToWrites(fpvs, nil)
|
||||
}
|
||||
|
||||
// fpvsFromData converts v into a list of (FieldPath, value) pairs.
|
||||
func fpvsFromData(v reflect.Value, prefix FieldPath, fpvs *[]fpv) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
for _, k := range v.MapKeys() {
|
||||
fpvsFromData(v.MapIndex(k), prefix.with(k.String()), fpvs)
|
||||
}
|
||||
case reflect.Interface:
|
||||
fpvsFromData(v.Elem(), prefix, fpvs)
|
||||
|
||||
default:
|
||||
var val interface{}
|
||||
if v.IsValid() {
|
||||
val = v.Interface()
|
||||
}
|
||||
*fpvs = append(*fpvs, fpv{prefix, val})
|
||||
}
|
||||
}
|
||||
|
||||
// removePathsIf creates a new slice of FieldPaths that contains
|
||||
// exactly those elements of fps for which pred returns false.
|
||||
func removePathsIf(fps []FieldPath, pred func(FieldPath) bool) []FieldPath {
|
||||
var result []FieldPath
|
||||
for _, fp := range fps {
|
||||
if !pred(fp) {
|
||||
result = append(result, fp)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Delete deletes the document. If the document doesn't exist, it does nothing
|
||||
// and returns no error.
|
||||
func (d *DocumentRef) Delete(ctx context.Context, preconds ...Precondition) (*WriteResult, error) {
|
||||
ws, err := d.newDeleteWrites(preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newDeleteWrites(preconds []Precondition) ([]*pb.Write, error) {
|
||||
if d == nil {
|
||||
return nil, errNilDocRef
|
||||
}
|
||||
pc, err := processPreconditionsForDelete(preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*pb.Write{{
|
||||
Operation: &pb.Write_Delete{d.Path},
|
||||
CurrentDocument: pc,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newUpdatePathWrites(updates []Update, preconds []Precondition) ([]*pb.Write, error) {
|
||||
if len(updates) == 0 {
|
||||
return nil, errors.New("firestore: no paths to update")
|
||||
}
|
||||
var fpvs []fpv
|
||||
for _, u := range updates {
|
||||
v, err := u.process()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fpvs = append(fpvs, v)
|
||||
}
|
||||
pc, err := processPreconditionsForUpdate(preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.fpvsToWrites(fpvs, pc)
|
||||
}
|
||||
|
||||
func (d *DocumentRef) fpvsToWrites(fpvs []fpv, pc *pb.Precondition) ([]*pb.Write, error) {
|
||||
// Make sure there are no duplications or prefixes among the field paths.
|
||||
var fps []FieldPath
|
||||
for _, fpv := range fpvs {
|
||||
fps = append(fps, fpv.fieldPath)
|
||||
}
|
||||
if err := checkNoDupOrPrefix(fps); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process each fpv.
|
||||
var updatePaths, transformPaths []FieldPath
|
||||
doc := &pb.Document{
|
||||
Name: d.Path,
|
||||
Fields: map[string]*pb.Value{},
|
||||
}
|
||||
for _, fpv := range fpvs {
|
||||
switch fpv.value {
|
||||
case Delete:
|
||||
// Send the field path without a corresponding value.
|
||||
updatePaths = append(updatePaths, fpv.fieldPath)
|
||||
|
||||
case ServerTimestamp:
|
||||
// Use the path in a transform operation.
|
||||
transformPaths = append(transformPaths, fpv.fieldPath)
|
||||
|
||||
default:
|
||||
updatePaths = append(updatePaths, fpv.fieldPath)
|
||||
// Convert the value to a proto and put it into the document.
|
||||
v := reflect.ValueOf(fpv.value)
|
||||
pv, sawServerTimestamp, err := toProtoValue(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setAtPath(doc.Fields, fpv.fieldPath, pv)
|
||||
// Also accumulate any serverTimestamp values within the value.
|
||||
if sawServerTimestamp {
|
||||
stps, err := extractTransformPaths(v, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range stps {
|
||||
transformPaths = append(transformPaths, fpv.fieldPath.concat(p))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return d.newUpdateWithTransform(doc, updatePaths, pc, transformPaths, false), nil
|
||||
}
|
||||
|
||||
var requestTimeTransform = &pb.DocumentTransform_FieldTransform_SetToServerValue{
|
||||
pb.DocumentTransform_FieldTransform_REQUEST_TIME,
|
||||
}
|
||||
|
||||
// newUpdateWithTransform constructs operations for a commit. Most generally, it
|
||||
// returns an update operation followed by a transform.
|
||||
//
|
||||
// If there are no serverTimestampPaths, the transform is omitted.
|
||||
//
|
||||
// If doc.Fields is empty, there are no updatePaths, and there is no precondition,
|
||||
// the update is omitted, unless updateOnEmpty is true.
|
||||
func (d *DocumentRef) newUpdateWithTransform(doc *pb.Document, updatePaths []FieldPath, pc *pb.Precondition, serverTimestampPaths []FieldPath, updateOnEmpty bool) []*pb.Write {
|
||||
// Remove server timestamp fields from updatePaths. Those fields were removed
|
||||
// from the document by toProtoDocument, so they should not be in the update
|
||||
// mask.
|
||||
// Note: this is technically O(n^2), but it is unlikely that there is
|
||||
// more than one server timestamp path.
|
||||
updatePaths = removePathsIf(updatePaths, func(fp FieldPath) bool {
|
||||
return fp.in(serverTimestampPaths)
|
||||
})
|
||||
var ws []*pb.Write
|
||||
if updateOnEmpty || len(doc.Fields) > 0 ||
|
||||
len(updatePaths) > 0 || (pc != nil && len(serverTimestampPaths) == 0) {
|
||||
var mask *pb.DocumentMask
|
||||
if len(updatePaths) > 0 {
|
||||
sfps := toServiceFieldPaths(updatePaths)
|
||||
sort.Strings(sfps) // TODO(jba): make tests pass without this
|
||||
mask = &pb.DocumentMask{FieldPaths: sfps}
|
||||
}
|
||||
w := &pb.Write{
|
||||
Operation: &pb.Write_Update{doc},
|
||||
UpdateMask: mask,
|
||||
CurrentDocument: pc,
|
||||
}
|
||||
ws = append(ws, w)
|
||||
pc = nil // If the precondition is in the write, we don't need it in the transform.
|
||||
}
|
||||
if len(serverTimestampPaths) > 0 || pc != nil {
|
||||
ws = append(ws, d.newTransform(serverTimestampPaths, pc))
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
func (d *DocumentRef) newTransform(serverTimestampFieldPaths []FieldPath, pc *pb.Precondition) *pb.Write {
|
||||
sort.Sort(byPath(serverTimestampFieldPaths)) // TODO(jba): make tests pass without this
|
||||
var fts []*pb.DocumentTransform_FieldTransform
|
||||
for _, p := range serverTimestampFieldPaths {
|
||||
fts = append(fts, &pb.DocumentTransform_FieldTransform{
|
||||
FieldPath: p.toServiceFieldPath(),
|
||||
TransformType: requestTimeTransform,
|
||||
})
|
||||
}
|
||||
return &pb.Write{
|
||||
Operation: &pb.Write_Transform{
|
||||
&pb.DocumentTransform{
|
||||
Document: d.Path,
|
||||
FieldTransforms: fts,
|
||||
// TODO(jba): should the transform have the same preconditions as the write?
|
||||
},
|
||||
},
|
||||
CurrentDocument: pc,
|
||||
}
|
||||
}
|
||||
|
||||
type sentinel int
|
||||
|
||||
const (
|
||||
// Delete is used as a value in a call to Update or Set with merge to indicate
|
||||
// that the corresponding key should be deleted.
|
||||
Delete sentinel = iota
|
||||
|
||||
// ServerTimestamp is used as a value in a call to Update to indicate that the
|
||||
// key's value should be set to the time at which the server processed
|
||||
// the request.
|
||||
ServerTimestamp
|
||||
)
|
||||
|
||||
func (s sentinel) String() string {
|
||||
switch s {
|
||||
case Delete:
|
||||
return "Delete"
|
||||
case ServerTimestamp:
|
||||
return "ServerTimestamp"
|
||||
default:
|
||||
return "<?sentinel?>"
|
||||
}
|
||||
}
|
||||
|
||||
func isStructOrStructPtr(x interface{}) bool {
|
||||
v := reflect.ValueOf(x)
|
||||
if v.Kind() == reflect.Struct {
|
||||
return true
|
||||
}
|
||||
if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// An Update describes an update to a value referred to by a path.
|
||||
// An Update should have either a non-empty Path or a non-empty FieldPath,
|
||||
// but not both.
|
||||
//
|
||||
// See DocumentRef.Create for acceptable values.
|
||||
// To delete a field, specify firestore.Delete as the value.
|
||||
type Update struct {
|
||||
Path string // Will be split on dots, and must not contain any of "˜*/[]".
|
||||
FieldPath FieldPath
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// An fpv is a pair of validated FieldPath and value.
|
||||
type fpv struct {
|
||||
fieldPath FieldPath
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (u *Update) process() (fpv, error) {
|
||||
if (u.Path != "") == (u.FieldPath != nil) {
|
||||
return fpv{}, fmt.Errorf("firestore: update %+v should have exactly one of Path or FieldPath", u)
|
||||
}
|
||||
fp := u.FieldPath
|
||||
var err error
|
||||
if fp == nil {
|
||||
fp, err = parseDotSeparatedString(u.Path)
|
||||
if err != nil {
|
||||
return fpv{}, err
|
||||
}
|
||||
}
|
||||
if err := fp.validate(); err != nil {
|
||||
return fpv{}, err
|
||||
}
|
||||
return fpv{fp, u.Value}, nil
|
||||
}
|
||||
|
||||
// Update updates the document. The values at the given
|
||||
// field paths are replaced, but other fields of the stored document are untouched.
|
||||
func (d *DocumentRef) Update(ctx context.Context, updates []Update, preconds ...Precondition) (*WriteResult, error) {
|
||||
ws, err := d.newUpdatePathWrites(updates, preconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.Parent.c.commit1(ctx, ws)
|
||||
}
|
||||
|
||||
// Collections returns an interator over the immediate sub-collections of the document.
|
||||
func (d *DocumentRef) Collections(ctx context.Context) *CollectionIterator {
|
||||
client := d.Parent.c
|
||||
it := &CollectionIterator{
|
||||
err: checkTransaction(ctx),
|
||||
client: client,
|
||||
parent: d,
|
||||
it: client.c.ListCollectionIds(
|
||||
withResourceHeader(ctx, client.path()),
|
||||
&pb.ListCollectionIdsRequest{Parent: d.Path}),
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(
|
||||
it.fetch,
|
||||
func() int { return len(it.items) },
|
||||
func() interface{} { b := it.items; it.items = nil; return b })
|
||||
return it
|
||||
}
|
||||
|
||||
// CollectionIterator is an iterator over sub-collections of a document.
|
||||
type CollectionIterator struct {
|
||||
client *Client
|
||||
parent *DocumentRef
|
||||
it *vkit.StringIterator
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
items []*CollectionRef
|
||||
err error
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||||
func (it *CollectionIterator) PageInfo() *iterator.PageInfo { return it.pageInfo }
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there
|
||||
// are no more results. Once Next returns Done, all subsequent calls will return
|
||||
// Done.
|
||||
func (it *CollectionIterator) Next() (*CollectionRef, error) {
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *CollectionIterator) fetch(pageSize int, pageToken string) (string, error) {
|
||||
if it.err != nil {
|
||||
return "", it.err
|
||||
}
|
||||
return iterFetch(pageSize, pageToken, it.it.PageInfo(), func() error {
|
||||
id, err := it.it.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var cr *CollectionRef
|
||||
if it.parent == nil {
|
||||
cr = newTopLevelCollRef(it.client, it.client.path(), id)
|
||||
} else {
|
||||
cr = newCollRefWithParent(it.client, it.parent, id)
|
||||
}
|
||||
it.items = append(it.items, cr)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetAll returns all the collections remaining from the iterator.
|
||||
func (it *CollectionIterator) GetAll() ([]*CollectionRef, error) {
|
||||
var crs []*CollectionRef
|
||||
for {
|
||||
cr, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crs = append(crs, cr)
|
||||
}
|
||||
return crs, nil
|
||||
}
|
||||
|
||||
// Common fetch code for iterators that are backed by vkit iterators.
|
||||
// TODO(jba): dedup with same function in logging/logadmin.
|
||||
func iterFetch(pageSize int, pageToken string, pi *iterator.PageInfo, next func() error) (string, error) {
|
||||
pi.MaxSize = pageSize
|
||||
pi.Token = pageToken
|
||||
// Get one item, which will fill the buffer.
|
||||
if err := next(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Collect the rest of the buffer.
|
||||
for pi.Remaining() > 0 {
|
||||
if err := next(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return pi.Token, nil
|
||||
}
|
||||
|
||||
// Snapshots returns an iterator over snapshots of the document. Each time the document
|
||||
// changes or is added or deleted, a new snapshot will be generated.
|
||||
func (d *DocumentRef) Snapshots(ctx context.Context) *DocumentSnapshotIterator {
|
||||
return &DocumentSnapshotIterator{
|
||||
docref: d,
|
||||
ws: newWatchStreamForDocument(ctx, d),
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentSnapshotIterator is an iterator over snapshots of a document.
|
||||
// Call Next on the iterator to get a snapshot of the document each time it changes.
|
||||
// Call Stop on the iterator when done.
|
||||
//
|
||||
// For an example, see DocumentRef.Snapshots.
|
||||
type DocumentSnapshotIterator struct {
|
||||
docref *DocumentRef
|
||||
ws *watchStream
|
||||
}
|
||||
|
||||
// Next blocks until the document changes, then returns the DocumentSnapshot for
|
||||
// the current state of the document. If the document has been deleted, Next
|
||||
// returns a DocumentSnapshot whose Exists method returns false.
|
||||
//
|
||||
// Next never returns iterator.Done unless it is called after Stop.
|
||||
func (it *DocumentSnapshotIterator) Next() (*DocumentSnapshot, error) {
|
||||
btree, _, readTime, err := it.ws.nextSnapshot()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = iterator.Done
|
||||
}
|
||||
// watchStream's error is sticky, so SnapshotIterator does not need to remember it.
|
||||
return nil, err
|
||||
}
|
||||
if btree.Len() == 0 { // document deleted
|
||||
return &DocumentSnapshot{Ref: it.docref, ReadTime: readTime}, nil
|
||||
}
|
||||
snap, _ := btree.At(0)
|
||||
return snap.(*DocumentSnapshot), nil
|
||||
}
|
||||
|
||||
// Stop stops receiving snapshots.
|
||||
// You should always call Stop when you are done with an iterator, to free up resources.
|
||||
// It is not safe to call Stop concurrently with Next.
|
||||
func (it *DocumentSnapshotIterator) Stop() {
|
||||
it.ws.stop()
|
||||
}
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/genproto/googleapis/type/latlng"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var (
|
||||
writeResultForSet = &WriteResult{UpdateTime: aTime}
|
||||
commitResponseForSet = &pb.CommitResponse{
|
||||
WriteResults: []*pb.WriteResult{{UpdateTime: aTimestamp}},
|
||||
}
|
||||
)
|
||||
|
||||
func TestDocGet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, srv := newMock(t)
|
||||
path := "projects/projectID/databases/(default)/documents/C/a"
|
||||
pdoc := &pb.Document{
|
||||
Name: path,
|
||||
CreateTime: aTimestamp,
|
||||
UpdateTime: aTimestamp,
|
||||
Fields: map[string]*pb.Value{"f": intval(1)},
|
||||
}
|
||||
srv.addRPC(&pb.BatchGetDocumentsRequest{
|
||||
Database: c.path(),
|
||||
Documents: []string{path},
|
||||
}, []interface{}{
|
||||
&pb.BatchGetDocumentsResponse{
|
||||
Result: &pb.BatchGetDocumentsResponse_Found{pdoc},
|
||||
ReadTime: aTimestamp2,
|
||||
},
|
||||
})
|
||||
ref := c.Collection("C").Doc("a")
|
||||
gotDoc, err := ref.Get(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantDoc := &DocumentSnapshot{
|
||||
Ref: ref,
|
||||
CreateTime: aTime,
|
||||
UpdateTime: aTime,
|
||||
ReadTime: aTime2,
|
||||
proto: pdoc,
|
||||
c: c,
|
||||
}
|
||||
if !testEqual(gotDoc, wantDoc) {
|
||||
t.Fatalf("\ngot %+v\nwant %+v", gotDoc, wantDoc)
|
||||
}
|
||||
|
||||
path2 := "projects/projectID/databases/(default)/documents/C/b"
|
||||
srv.addRPC(
|
||||
&pb.BatchGetDocumentsRequest{
|
||||
Database: c.path(),
|
||||
Documents: []string{path2},
|
||||
}, []interface{}{
|
||||
&pb.BatchGetDocumentsResponse{
|
||||
Result: &pb.BatchGetDocumentsResponse_Missing{path2},
|
||||
ReadTime: aTimestamp3,
|
||||
},
|
||||
})
|
||||
_, err = c.Collection("C").Doc("b").Get(ctx)
|
||||
if grpc.Code(err) != codes.NotFound {
|
||||
t.Errorf("got %v, want NotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocSet(t *testing.T) {
|
||||
// Most tests for Set are in the cross-language tests.
|
||||
ctx := context.Background()
|
||||
c, srv := newMock(t)
|
||||
|
||||
doc := c.Collection("C").Doc("d")
|
||||
// Merge with a struct and FieldPaths.
|
||||
srv.addRPC(&pb.CommitRequest{
|
||||
Database: "projects/projectID/databases/(default)",
|
||||
Writes: []*pb.Write{
|
||||
{
|
||||
Operation: &pb.Write_Update{
|
||||
Update: &pb.Document{
|
||||
Name: "projects/projectID/databases/(default)/documents/C/d",
|
||||
Fields: map[string]*pb.Value{
|
||||
"*": mapval(map[string]*pb.Value{
|
||||
"~": boolval(true),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
UpdateMask: &pb.DocumentMask{FieldPaths: []string{"`*`.`~`"}},
|
||||
},
|
||||
},
|
||||
}, commitResponseForSet)
|
||||
data := struct {
|
||||
A map[string]bool `firestore:"*"`
|
||||
}{A: map[string]bool{"~": true}}
|
||||
wr, err := doc.Set(ctx, data, Merge([]string{"*", "~"}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testEqual(wr, writeResultForSet) {
|
||||
t.Errorf("got %v, want %v", wr, writeResultForSet)
|
||||
}
|
||||
|
||||
// MergeAll cannot be used with structs.
|
||||
_, err = doc.Set(ctx, data, MergeAll)
|
||||
if err == nil {
|
||||
t.Errorf("got nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocCreate(t *testing.T) {
|
||||
// Verify creation with structs. In particular, make sure zero values
|
||||
// are handled well.
|
||||
// Other tests for Create are handled by the cross-language tests.
|
||||
ctx := context.Background()
|
||||
c, srv := newMock(t)
|
||||
|
||||
type create struct {
|
||||
Time time.Time
|
||||
Bytes []byte
|
||||
Geo *latlng.LatLng
|
||||
}
|
||||
srv.addRPC(
|
||||
&pb.CommitRequest{
|
||||
Database: "projects/projectID/databases/(default)",
|
||||
Writes: []*pb.Write{
|
||||
{
|
||||
Operation: &pb.Write_Update{
|
||||
Update: &pb.Document{
|
||||
Name: "projects/projectID/databases/(default)/documents/C/d",
|
||||
Fields: map[string]*pb.Value{
|
||||
"Time": tsval(time.Time{}),
|
||||
"Bytes": bytesval(nil),
|
||||
"Geo": nullValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
CurrentDocument: &pb.Precondition{
|
||||
ConditionType: &pb.Precondition_Exists{false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
commitResponseForSet,
|
||||
)
|
||||
_, err := c.Collection("C").Doc("d").Create(ctx, &create{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocDelete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, srv := newMock(t)
|
||||
srv.addRPC(
|
||||
&pb.CommitRequest{
|
||||
Database: "projects/projectID/databases/(default)",
|
||||
Writes: []*pb.Write{
|
||||
{Operation: &pb.Write_Delete{"projects/projectID/databases/(default)/documents/C/d"}},
|
||||
},
|
||||
},
|
||||
&pb.CommitResponse{
|
||||
WriteResults: []*pb.WriteResult{{}},
|
||||
})
|
||||
wr, err := c.Collection("C").Doc("d").Delete(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testEqual(wr, &WriteResult{}) {
|
||||
t.Errorf("got %+v, want %+v", wr, writeResultForSet)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
testData = map[string]interface{}{"a": 1}
|
||||
testFields = map[string]*pb.Value{"a": intval(1)}
|
||||
)
|
||||
|
||||
// Update is tested by the cross-language tests.
|
||||
|
||||
func TestFPVsFromData(t *testing.T) {
|
||||
type S struct{ X int }
|
||||
|
||||
for _, test := range []struct {
|
||||
in interface{}
|
||||
want []fpv
|
||||
}{
|
||||
{
|
||||
in: nil,
|
||||
want: []fpv{{nil, nil}},
|
||||
},
|
||||
{
|
||||
in: map[string]interface{}{"a": nil},
|
||||
want: []fpv{{[]string{"a"}, nil}},
|
||||
},
|
||||
{
|
||||
in: map[string]interface{}{"a": 1},
|
||||
want: []fpv{{[]string{"a"}, 1}},
|
||||
},
|
||||
{
|
||||
in: map[string]interface{}{
|
||||
"a": 1,
|
||||
"b": map[string]interface{}{"c": 2},
|
||||
},
|
||||
want: []fpv{{[]string{"a"}, 1}, {[]string{"b", "c"}, 2}},
|
||||
},
|
||||
{
|
||||
in: map[string]interface{}{"s": &S{X: 3}},
|
||||
want: []fpv{{[]string{"s"}, &S{X: 3}}},
|
||||
},
|
||||
} {
|
||||
var got []fpv
|
||||
fpvsFromData(reflect.ValueOf(test.in), nil, &got)
|
||||
sort.Sort(byFieldPath(got))
|
||||
if !testEqual(got, test.want) {
|
||||
t.Errorf("%+v: got %v, want %v", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type byFieldPath []fpv
|
||||
|
||||
func (b byFieldPath) Len() int { return len(b) }
|
||||
func (b byFieldPath) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
func (b byFieldPath) Less(i, j int) bool { return b[i].fieldPath.less(b[j].fieldPath) }
|
||||
|
||||
func commitRequestForSet() *pb.CommitRequest {
|
||||
return &pb.CommitRequest{
|
||||
Database: "projects/projectID/databases/(default)",
|
||||
Writes: []*pb.Write{
|
||||
{
|
||||
Operation: &pb.Write_Update{
|
||||
Update: &pb.Document{
|
||||
Name: "projects/projectID/databases/(default)/documents/C/d",
|
||||
Fields: testFields,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProcess(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in Update
|
||||
want fpv
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
in: Update{Path: "a", Value: 1},
|
||||
want: fpv{fieldPath: []string{"a"}, value: 1},
|
||||
},
|
||||
{
|
||||
in: Update{Path: "c.d", Value: Delete},
|
||||
want: fpv{fieldPath: []string{"c", "d"}, value: Delete},
|
||||
},
|
||||
{
|
||||
in: Update{FieldPath: []string{"*", "~"}, Value: ServerTimestamp},
|
||||
want: fpv{fieldPath: []string{"*", "~"}, value: ServerTimestamp},
|
||||
},
|
||||
{
|
||||
in: Update{Path: "*"},
|
||||
wantErr: true, // bad rune in path
|
||||
},
|
||||
{
|
||||
in: Update{Path: "a", FieldPath: []string{"b"}},
|
||||
wantErr: true, // both Path and FieldPath
|
||||
},
|
||||
{
|
||||
in: Update{Value: 1},
|
||||
wantErr: true, // neither Path nor FieldPath
|
||||
},
|
||||
{
|
||||
in: Update{FieldPath: []string{"", "a"}},
|
||||
wantErr: true, // empty FieldPath component
|
||||
},
|
||||
} {
|
||||
got, err := test.in.process()
|
||||
if test.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("%+v: got nil, want error", test.in)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("%+v: got error %v, want nil", test.in, err)
|
||||
} else if !testEqual(got, test.want) {
|
||||
t.Errorf("%+v: got %+v, want %+v", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
tspb "github.com/golang/protobuf/ptypes/timestamp"
|
||||
)
|
||||
|
||||
// A DocumentSnapshot contains document data and metadata.
|
||||
type DocumentSnapshot struct {
|
||||
// The DocumentRef for this document.
|
||||
Ref *DocumentRef
|
||||
|
||||
// Read-only. The time at which the document was created.
|
||||
// Increases monotonically when a document is deleted then
|
||||
// recreated. It can also be compared to values from other documents and
|
||||
// the read time of a query.
|
||||
CreateTime time.Time
|
||||
|
||||
// Read-only. The time at which the document was last changed. This value
|
||||
// is initally set to CreateTime then increases monotonically with each
|
||||
// change to the document. It can also be compared to values from other
|
||||
// documents and the read time of a query.
|
||||
UpdateTime time.Time
|
||||
|
||||
// Read-only. The time at which the document was read.
|
||||
ReadTime time.Time
|
||||
|
||||
c *Client
|
||||
proto *pb.Document
|
||||
}
|
||||
|
||||
// Exists reports whether the DocumentSnapshot represents an existing document.
|
||||
// Even if Exists returns false, the Ref and ReadTime fields of the DocumentSnapshot
|
||||
// are valid.
|
||||
func (d *DocumentSnapshot) Exists() bool {
|
||||
return d.proto != nil
|
||||
}
|
||||
|
||||
// Data returns the DocumentSnapshot's fields as a map.
|
||||
// It is equivalent to
|
||||
// var m map[string]interface{}
|
||||
// d.DataTo(&m)
|
||||
// except that it returns nil if the document does not exist.
|
||||
func (d *DocumentSnapshot) Data() map[string]interface{} {
|
||||
if !d.Exists() {
|
||||
return nil
|
||||
}
|
||||
m, err := createMapFromValueMap(d.proto.Fields, d.c)
|
||||
// Any error here is a bug in the client.
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("firestore: %v", err))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// DataTo uses the document's fields to populate p, which can be a pointer to a
|
||||
// map[string]interface{} or a pointer to a struct.
|
||||
//
|
||||
// Firestore field values are converted to Go values as follows:
|
||||
// - Null converts to nil.
|
||||
// - Bool converts to bool.
|
||||
// - String converts to string.
|
||||
// - Integer converts int64. When setting a struct field, any signed or unsigned
|
||||
// integer type is permitted except uint64. Overflow is detected and results in
|
||||
// an error.
|
||||
// - Double converts to float64. When setting a struct field, float32 is permitted.
|
||||
// Overflow is detected and results in an error.
|
||||
// - Bytes is converted to []byte.
|
||||
// - Timestamp converts to time.Time.
|
||||
// - GeoPoint converts to latlng.LatLng, where latlng is the package
|
||||
// "google.golang.org/genproto/googleapis/type/latlng".
|
||||
// - Arrays convert to []interface{}. When setting a struct field, the field
|
||||
// may be a slice or array of any type and is populated recursively.
|
||||
// Slices are resized to the incoming value's size, while arrays that are too
|
||||
// long have excess elements filled with zero values. If the array is too short,
|
||||
// excess incoming values will be dropped.
|
||||
// - Maps convert to map[string]interface{}. When setting a struct field,
|
||||
// maps of key type string and any value type are permitted, and are populated
|
||||
// recursively.
|
||||
// - References are converted to DocumentRefs.
|
||||
//
|
||||
// Field names given by struct field tags are observed, as described in
|
||||
// DocumentRef.Create.
|
||||
//
|
||||
// If the document does not exist, DataTo returns a NotFound error.
|
||||
func (d *DocumentSnapshot) DataTo(p interface{}) error {
|
||||
if !d.Exists() {
|
||||
return status.Errorf(codes.NotFound, "document %s does not exist", d.Ref.Path)
|
||||
}
|
||||
return setFromProtoValue(p, &pb.Value{&pb.Value_MapValue{&pb.MapValue{d.proto.Fields}}}, d.c)
|
||||
}
|
||||
|
||||
// DataAt returns the data value denoted by path.
|
||||
//
|
||||
// The path argument can be a single field or a dot-separated sequence of
|
||||
// fields, and must not contain any of the runes "˜*/[]". Use DataAtPath instead for
|
||||
// such a path.
|
||||
//
|
||||
// See DocumentSnapshot.DataTo for how Firestore values are converted to Go values.
|
||||
//
|
||||
// If the document does not exist, DataAt returns a NotFound error.
|
||||
func (d *DocumentSnapshot) DataAt(path string) (interface{}, error) {
|
||||
if !d.Exists() {
|
||||
return nil, status.Errorf(codes.NotFound, "document %s does not exist", d.Ref.Path)
|
||||
}
|
||||
fp, err := parseDotSeparatedString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.DataAtPath(fp)
|
||||
}
|
||||
|
||||
// DataAtPath returns the data value denoted by the FieldPath fp.
|
||||
// If the document does not exist, DataAtPath returns a NotFound error.
|
||||
func (d *DocumentSnapshot) DataAtPath(fp FieldPath) (interface{}, error) {
|
||||
if !d.Exists() {
|
||||
return nil, status.Errorf(codes.NotFound, "document %s does not exist", d.Ref.Path)
|
||||
}
|
||||
v, err := valueAtPath(fp, d.proto.Fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createFromProtoValue(v, d.c)
|
||||
}
|
||||
|
||||
// valueAtPath returns the value of m referred to by fp.
|
||||
func valueAtPath(fp FieldPath, m map[string]*pb.Value) (*pb.Value, error) {
|
||||
for _, k := range fp[:len(fp)-1] {
|
||||
v := m[k]
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("firestore: no field %q", k)
|
||||
}
|
||||
mv := v.GetMapValue()
|
||||
if mv == nil {
|
||||
return nil, fmt.Errorf("firestore: value for field %q is not a map", k)
|
||||
}
|
||||
m = mv.Fields
|
||||
}
|
||||
k := fp[len(fp)-1]
|
||||
v := m[k]
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("firestore: no field %q", k)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// toProtoDocument converts a Go value to a Document proto.
|
||||
// Valid values are: map[string]T, struct, or pointer to a valid value.
|
||||
// It also returns a list of field paths for DocumentTransform (server timestamp).
|
||||
func toProtoDocument(x interface{}) (*pb.Document, []FieldPath, error) {
|
||||
if x == nil {
|
||||
return nil, nil, errors.New("firestore: nil document contents")
|
||||
}
|
||||
v := reflect.ValueOf(x)
|
||||
pv, sawTransform, err := toProtoValue(v)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var fieldPaths []FieldPath
|
||||
if sawTransform {
|
||||
fieldPaths, err = extractTransformPaths(v, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
var fields map[string]*pb.Value
|
||||
if pv != nil {
|
||||
m := pv.GetMapValue()
|
||||
if m == nil {
|
||||
return nil, nil, fmt.Errorf("firestore: cannot covert value of type %T into a map", x)
|
||||
}
|
||||
fields = m.Fields
|
||||
}
|
||||
return &pb.Document{Fields: fields}, fieldPaths, nil
|
||||
}
|
||||
|
||||
func extractTransformPaths(v reflect.Value, prefix FieldPath) ([]FieldPath, error) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
return extractTransformPathsFromMap(v, prefix)
|
||||
case reflect.Struct:
|
||||
return extractTransformPathsFromStruct(v, prefix)
|
||||
case reflect.Ptr:
|
||||
if v.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
return extractTransformPaths(v.Elem(), prefix)
|
||||
case reflect.Interface:
|
||||
if v.NumMethod() == 0 { // empty interface: recurse on its contents
|
||||
return extractTransformPaths(v.Elem(), prefix)
|
||||
}
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func extractTransformPathsFromMap(v reflect.Value, prefix FieldPath) ([]FieldPath, error) {
|
||||
var paths []FieldPath
|
||||
for _, k := range v.MapKeys() {
|
||||
sk := k.Interface().(string) // assume keys are strings; checked in toProtoValue
|
||||
path := prefix.with(sk)
|
||||
mi := v.MapIndex(k)
|
||||
if mi.Interface() == ServerTimestamp {
|
||||
paths = append(paths, path)
|
||||
} else {
|
||||
ps, err := extractTransformPaths(mi, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths = append(paths, ps...)
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func extractTransformPathsFromStruct(v reflect.Value, prefix FieldPath) ([]FieldPath, error) {
|
||||
var paths []FieldPath
|
||||
fields, err := fieldCache.Fields(v.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range fields {
|
||||
fv := v.FieldByIndex(f.Index)
|
||||
path := prefix.with(f.Name)
|
||||
opts := f.ParsedTag.(tagOptions)
|
||||
if opts.serverTimestamp {
|
||||
var isZero bool
|
||||
switch f.Type {
|
||||
case typeOfGoTime:
|
||||
isZero = fv.Interface().(time.Time).IsZero()
|
||||
case reflect.PtrTo(typeOfGoTime):
|
||||
isZero = fv.IsNil() || fv.Elem().Interface().(time.Time).IsZero()
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: field %s of struct %s with serverTimestamp tag must be of type time.Time or *time.Time",
|
||||
f.Name, v.Type())
|
||||
}
|
||||
if isZero {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
} else {
|
||||
ps, err := extractTransformPaths(fv, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths = append(paths, ps...)
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
func newDocumentSnapshot(ref *DocumentRef, proto *pb.Document, c *Client, readTime *tspb.Timestamp) (*DocumentSnapshot, error) {
|
||||
d := &DocumentSnapshot{
|
||||
Ref: ref,
|
||||
c: c,
|
||||
proto: proto,
|
||||
}
|
||||
if proto != nil {
|
||||
ts, err := ptypes.Timestamp(proto.CreateTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.CreateTime = ts
|
||||
ts, err = ptypes.Timestamp(proto.UpdateTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.UpdateTime = ts
|
||||
}
|
||||
if readTime != nil {
|
||||
ts, err := ptypes.Timestamp(readTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.ReadTime = ts
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
|
||||
tspb "github.com/golang/protobuf/ptypes/timestamp"
|
||||
)
|
||||
|
||||
func TestToProtoDocument(t *testing.T) {
|
||||
type s struct{ I int }
|
||||
|
||||
for _, test := range []struct {
|
||||
in interface{}
|
||||
want *pb.Document
|
||||
wantErr bool
|
||||
}{
|
||||
{nil, nil, true},
|
||||
{[]int{1}, nil, true},
|
||||
{map[string]int{"a": 1},
|
||||
&pb.Document{Fields: map[string]*pb.Value{"a": intval(1)}},
|
||||
false},
|
||||
{s{2}, &pb.Document{Fields: map[string]*pb.Value{"I": intval(2)}}, false},
|
||||
{&s{3}, &pb.Document{Fields: map[string]*pb.Value{"I": intval(3)}}, false},
|
||||
} {
|
||||
got, _, gotErr := toProtoDocument(test.in)
|
||||
if (gotErr != nil) != test.wantErr {
|
||||
t.Errorf("%v: got error %v, want %t", test.in, gotErr, test.wantErr)
|
||||
}
|
||||
if gotErr != nil {
|
||||
continue
|
||||
}
|
||||
if !testEqual(got, test.want) {
|
||||
t.Errorf("%v: got %v, want %v", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDocumentSnapshot(t *testing.T) {
|
||||
c := &Client{
|
||||
projectID: "projID",
|
||||
databaseID: "(database)",
|
||||
}
|
||||
docRef := c.Doc("C/a")
|
||||
in := &pb.Document{
|
||||
CreateTime: &tspb.Timestamp{Seconds: 10},
|
||||
UpdateTime: &tspb.Timestamp{Seconds: 20},
|
||||
Fields: map[string]*pb.Value{"a": intval(1)},
|
||||
}
|
||||
want := &DocumentSnapshot{
|
||||
Ref: docRef,
|
||||
CreateTime: time.Unix(10, 0).UTC(),
|
||||
UpdateTime: time.Unix(20, 0).UTC(),
|
||||
ReadTime: aTime,
|
||||
proto: in,
|
||||
c: c,
|
||||
}
|
||||
got, err := newDocumentSnapshot(docRef, in, c, aTimestamp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testEqual(got, want) {
|
||||
t.Errorf("got %+v\nwant %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestData(t *testing.T) {
|
||||
doc := &DocumentSnapshot{
|
||||
proto: &pb.Document{
|
||||
Fields: map[string]*pb.Value{"a": intval(1), "b": strval("x")},
|
||||
},
|
||||
}
|
||||
got := doc.Data()
|
||||
want := map[string]interface{}{"a": int64(1), "b": "x"}
|
||||
if !testEqual(got, want) {
|
||||
t.Errorf("got %#v\nwant %#v", got, want)
|
||||
}
|
||||
var got2 map[string]interface{}
|
||||
if err := doc.DataTo(&got2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testEqual(got2, want) {
|
||||
t.Errorf("got %#v\nwant %#v", got2, want)
|
||||
}
|
||||
|
||||
type s struct {
|
||||
A int
|
||||
B string
|
||||
}
|
||||
var got3 s
|
||||
if err := doc.DataTo(&got3); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want2 := s{A: 1, B: "x"}
|
||||
if !testEqual(got3, want2) {
|
||||
t.Errorf("got %#v\nwant %#v", got3, want2)
|
||||
}
|
||||
}
|
||||
|
||||
var testDoc = &DocumentSnapshot{
|
||||
proto: &pb.Document{
|
||||
Fields: map[string]*pb.Value{
|
||||
"a": intval(1),
|
||||
"b": mapval(map[string]*pb.Value{
|
||||
"`": intval(2),
|
||||
"~": mapval(map[string]*pb.Value{
|
||||
"x": intval(3),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestDataAt(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
fieldPath string
|
||||
want interface{}
|
||||
}{
|
||||
{"a", int64(1)},
|
||||
{"b.`", int64(2)},
|
||||
} {
|
||||
got, err := testDoc.DataAt(test.fieldPath)
|
||||
if err != nil {
|
||||
t.Errorf("%q: %v", test.fieldPath, err)
|
||||
continue
|
||||
}
|
||||
if !testEqual(got, test.want) {
|
||||
t.Errorf("%q: got %v, want %v", test.fieldPath, got, test.want)
|
||||
}
|
||||
}
|
||||
|
||||
for _, bad := range []string{
|
||||
"c.~.x", // bad field path
|
||||
"a.b", // "a" isn't a map
|
||||
"z.b", // bad non-final key
|
||||
"b.z", // bad final key
|
||||
} {
|
||||
_, err := testDoc.DataAt(bad)
|
||||
if err == nil {
|
||||
t.Errorf("%q: got nil, want error", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataAtPath(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
fieldPath FieldPath
|
||||
want interface{}
|
||||
}{
|
||||
{[]string{"a"}, int64(1)},
|
||||
{[]string{"b", "`"}, int64(2)},
|
||||
{[]string{"b", "~"}, map[string]interface{}{"x": int64(3)}},
|
||||
{[]string{"b", "~", "x"}, int64(3)},
|
||||
} {
|
||||
got, err := testDoc.DataAtPath(test.fieldPath)
|
||||
if err != nil {
|
||||
t.Errorf("%v: %v", test.fieldPath, err)
|
||||
continue
|
||||
}
|
||||
if !testEqual(got, test.want) {
|
||||
t.Errorf("%v: got %v, want %v", test.fieldPath, got, test.want)
|
||||
}
|
||||
}
|
||||
|
||||
for _, bad := range []FieldPath{
|
||||
[]string{"c", "", "x"}, // bad field path
|
||||
[]string{"a", "b"}, // "a" isn't a map
|
||||
[]string{"z", "~"}, // bad non-final key
|
||||
[]string{"b", "z"}, // bad final key
|
||||
} {
|
||||
_, err := testDoc.DataAtPath(bad)
|
||||
if err == nil {
|
||||
t.Errorf("%v: got nil, want error", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTransformPaths(t *testing.T) {
|
||||
type S struct {
|
||||
A time.Time `firestore:",serverTimestamp"`
|
||||
B time.Time `firestore:",serverTimestamp"`
|
||||
C *time.Time `firestore:",serverTimestamp"`
|
||||
D *time.Time `firestore:"d.d,serverTimestamp"`
|
||||
E *time.Time `firestore:",serverTimestamp"`
|
||||
F time.Time
|
||||
G int
|
||||
}
|
||||
|
||||
m := map[string]interface{}{
|
||||
"x": 1,
|
||||
"y": &S{
|
||||
// A is a zero time: included
|
||||
B: aTime, // not a zero time: excluded
|
||||
// C is nil: included
|
||||
D: &time.Time{}, // pointer to a zero time: included
|
||||
E: &aTime, // pointer to a non-zero time: excluded
|
||||
// F is a zero time, but does not have the right tag: excluded
|
||||
G: 15, // not a time.Time
|
||||
},
|
||||
"z": map[string]interface{}{"w": ServerTimestamp},
|
||||
}
|
||||
got, err := extractTransformPaths(reflect.ValueOf(m), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sort.Sort(byPath(got))
|
||||
want := []FieldPath{{"y", "A"}, {"y", "C"}, {"y", "d.d"}, {"z", "w"}}
|
||||
if !testEqual(got, want) {
|
||||
t.Errorf("got %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTransformPathsErrors(t *testing.T) {
|
||||
type S struct {
|
||||
A int `firestore:",serverTimestamp"`
|
||||
}
|
||||
_, err := extractTransformPaths(reflect.ValueOf(S{}), nil)
|
||||
if err == nil {
|
||||
t.Error("got nil, want error")
|
||||
}
|
||||
}
|
||||
+528
@@ -0,0 +1,528 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// TODO(jba): add Output comments to examples when feasible.
|
||||
|
||||
package firestore_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"cloud.google.com/go/firestore"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
func ExampleNewClient() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close() // Close client when done.
|
||||
_ = client // TODO: Use client.
|
||||
}
|
||||
|
||||
func ExampleClient_Collection() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
coll1 := client.Collection("States")
|
||||
coll2 := client.Collection("States/NewYork/Cities")
|
||||
fmt.Println(coll1, coll2)
|
||||
}
|
||||
|
||||
func ExampleClient_Doc() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
doc1 := client.Doc("States/NewYork")
|
||||
doc2 := client.Doc("States/NewYork/Cities/Albany")
|
||||
fmt.Println(doc1, doc2)
|
||||
}
|
||||
|
||||
func ExampleClient_GetAll() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
docs, err := client.GetAll(ctx, []*firestore.DocumentRef{
|
||||
client.Doc("States/NorthCarolina"),
|
||||
client.Doc("States/SouthCarolina"),
|
||||
client.Doc("States/WestCarolina"),
|
||||
client.Doc("States/EastCarolina"),
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// docs is a slice with four DocumentSnapshots, but the last two are
|
||||
// nil because there is no West or East Carolina.
|
||||
fmt.Println(docs)
|
||||
}
|
||||
|
||||
func ExampleClient_Batch() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
b := client.Batch()
|
||||
_ = b // TODO: Use batch.
|
||||
}
|
||||
|
||||
func ExampleWriteBatch_Commit() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
type State struct {
|
||||
Capital string `firestore:"capital"`
|
||||
Population float64 `firestore:"pop"` // in millions
|
||||
}
|
||||
|
||||
ny := client.Doc("States/NewYork")
|
||||
ca := client.Doc("States/California")
|
||||
|
||||
writeResults, err := client.Batch().
|
||||
Create(ny, State{Capital: "Albany", Population: 19.8}).
|
||||
Set(ca, State{Capital: "Sacramento", Population: 39.14}).
|
||||
Delete(client.Doc("States/WestDakota")).
|
||||
Commit(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(writeResults)
|
||||
}
|
||||
|
||||
func ExampleCollectionRef_Add() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
doc, wr, err := client.Collection("Users").Add(ctx, map[string]interface{}{
|
||||
"name": "Alice",
|
||||
"email": "aj@example.com",
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(doc, wr)
|
||||
}
|
||||
|
||||
func ExampleCollectionRef_Doc() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
fl := client.Collection("States").Doc("Florida")
|
||||
ta := client.Collection("States").Doc("Florida/Cities/Tampa")
|
||||
|
||||
fmt.Println(fl, ta)
|
||||
}
|
||||
|
||||
func ExampleCollectionRef_NewDoc() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
doc := client.Collection("Users").NewDoc()
|
||||
|
||||
fmt.Println(doc)
|
||||
}
|
||||
|
||||
func ExampleDocumentRef_Collection() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
mi := client.Collection("States").Doc("Michigan")
|
||||
cities := mi.Collection("Cities")
|
||||
|
||||
fmt.Println(cities)
|
||||
}
|
||||
|
||||
func ExampleDocumentRef_Create_map() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
wr, err := client.Doc("States/Colorado").Create(ctx, map[string]interface{}{
|
||||
"capital": "Denver",
|
||||
"pop": 5.5,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(wr.UpdateTime)
|
||||
}
|
||||
|
||||
func ExampleDocumentRef_Create_struct() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
type State struct {
|
||||
Capital string `firestore:"capital"`
|
||||
Population float64 `firestore:"pop"` // in millions
|
||||
}
|
||||
|
||||
wr, err := client.Doc("States/Colorado").Create(ctx, State{
|
||||
Capital: "Denver",
|
||||
Population: 5.5,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(wr.UpdateTime)
|
||||
}
|
||||
|
||||
func ExampleDocumentRef_Set() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Overwrite the document with the given data. Any other fields currently
|
||||
// in the document will be removed.
|
||||
wr, err := client.Doc("States/Alabama").Set(ctx, map[string]interface{}{
|
||||
"capital": "Montgomery",
|
||||
"pop": 4.9,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(wr.UpdateTime)
|
||||
}
|
||||
|
||||
func ExampleDocumentRef_Set_merge() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Overwrite only the fields in the map; preserve all others.
|
||||
_, err = client.Doc("States/Alabama").Set(ctx, map[string]interface{}{
|
||||
"pop": 5.2,
|
||||
}, firestore.MergeAll)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Capital string `firestore:"capital"`
|
||||
Population float64 `firestore:"pop"` // in millions
|
||||
}
|
||||
|
||||
// To do a merging Set with struct data, specify the exact fields to overwrite.
|
||||
// MergeAll is disallowed here, because it would probably be a mistake: the "capital"
|
||||
// field would be overwritten with the empty string.
|
||||
_, err = client.Doc("States/Alabama").Set(ctx, State{Population: 5.2}, firestore.Merge([]string{"pop"}))
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleDocumentRef_Update() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
tenn := client.Doc("States/Tennessee")
|
||||
wr, err := tenn.Update(ctx, []firestore.Update{
|
||||
{Path: "pop", Value: 6.6},
|
||||
{FieldPath: []string{".", "*", "/"}, Value: "odd"},
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(wr.UpdateTime)
|
||||
}
|
||||
|
||||
func ExampleDocumentRef_Delete() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Oops, Ontario is a Canadian province...
|
||||
if _, err = client.Doc("States/Ontario").Delete(ctx); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleDocumentRef_Get() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
docsnap, err := client.Doc("States/Ohio").Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_ = docsnap // TODO: Use DocumentSnapshot.
|
||||
}
|
||||
|
||||
func ExampleDocumentRef_Snapshots() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
iter := client.Doc("States/Idaho").Snapshots(ctx)
|
||||
defer iter.Stop()
|
||||
for {
|
||||
docsnap, err := iter.Next()
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_ = docsnap // TODO: Use DocumentSnapshot.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleDocumentSnapshot_Data() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
docsnap, err := client.Doc("States/Ohio").Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
ohioMap := docsnap.Data()
|
||||
fmt.Println(ohioMap["capital"])
|
||||
}
|
||||
|
||||
func ExampleDocumentSnapshot_DataAt() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
docsnap, err := client.Doc("States/Ohio").Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
cap, err := docsnap.DataAt("capital")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(cap)
|
||||
}
|
||||
|
||||
func ExampleDocumentSnapshot_DataAtPath() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
docsnap, err := client.Doc("States/Ohio").Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
pop, err := docsnap.DataAtPath([]string{"capital", "population"})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(pop)
|
||||
}
|
||||
|
||||
func ExampleDocumentSnapshot_DataTo() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
docsnap, err := client.Doc("States/Ohio").Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Capital string `firestore:"capital"`
|
||||
Population float64 `firestore:"pop"` // in millions
|
||||
}
|
||||
|
||||
var s State
|
||||
if err := docsnap.DataTo(&s); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
func ExampleQuery_Documents() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
q := client.Collection("States").Select("pop").
|
||||
Where("pop", ">", 10).
|
||||
OrderBy("pop", firestore.Desc).
|
||||
Limit(10)
|
||||
iter1 := q.Documents(ctx)
|
||||
_ = iter1 // TODO: Use iter1.
|
||||
|
||||
// You can call Documents directly on a CollectionRef as well.
|
||||
iter2 := client.Collection("States").Documents(ctx)
|
||||
_ = iter2 // TODO: Use iter2.
|
||||
}
|
||||
|
||||
// This example is just like the one above, but illustrates
|
||||
// how to use the XXXPath methods of Query for field paths
|
||||
// that can't be expressed as a dot-separated string.
|
||||
func ExampleQuery_Documents_path_methods() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
q := client.Collection("Unusual").SelectPaths([]string{"*"}, []string{"[~]"}).
|
||||
WherePath([]string{"/"}, ">", 10).
|
||||
OrderByPath([]string{"/"}, firestore.Desc).
|
||||
Limit(10)
|
||||
iter1 := q.Documents(ctx)
|
||||
_ = iter1 // TODO: Use iter1.
|
||||
|
||||
// You can call Documents directly on a CollectionRef as well.
|
||||
iter2 := client.Collection("States").Documents(ctx)
|
||||
_ = iter2 // TODO: Use iter2.
|
||||
}
|
||||
|
||||
func ExampleDocumentIterator_Next() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
q := client.Collection("States").
|
||||
Where("pop", ">", 10).
|
||||
OrderBy("pop", firestore.Desc)
|
||||
iter := q.Documents(ctx)
|
||||
defer iter.Stop()
|
||||
for {
|
||||
doc, err := iter.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(doc.Data())
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleDocumentIterator_GetAll() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
q := client.Collection("States").
|
||||
Where("pop", ">", 10).
|
||||
OrderBy("pop", firestore.Desc).
|
||||
Limit(10) // a good idea with GetAll, to avoid filling memory
|
||||
docs, err := q.Documents(ctx).GetAll()
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
for _, doc := range docs {
|
||||
fmt.Println(doc.Data())
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_RunTransaction() {
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
nm := client.Doc("States/NewMexico")
|
||||
err = client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
|
||||
doc, err := tx.Get(nm) // tx.Get, NOT nm.Get!
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pop, err := doc.DataAt("pop")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Update(nm, []firestore.Update{{Path: "pop", Value: pop.(float64) + 0.2}})
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"cloud.google.com/go/internal/atomiccache"
|
||||
"cloud.google.com/go/internal/fields"
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
)
|
||||
|
||||
// A FieldPath is a non-empty sequence of non-empty fields that reference a value.
|
||||
//
|
||||
// A FieldPath value should only be necessary if one of the field names contains
|
||||
// one of the runes ".˜*/[]". Most methods accept a simpler form of field path
|
||||
// as a string in which the individual fields are separated by dots.
|
||||
// For example,
|
||||
// []string{"a", "b"}
|
||||
// is equivalent to the string form
|
||||
// "a.b"
|
||||
// but
|
||||
// []string{"*"}
|
||||
// has no equivalent string form.
|
||||
type FieldPath []string
|
||||
|
||||
// parseDotSeparatedString constructs a FieldPath from a string that separates
|
||||
// path components with dots. Other than splitting at dots and checking for invalid
|
||||
// characters, it ignores everything else about the string,
|
||||
// including attempts to quote field path compontents. So "a.`b.c`.d" is parsed into
|
||||
// four parts, "a", "`b", "c`" and "d".
|
||||
func parseDotSeparatedString(s string) (FieldPath, error) {
|
||||
const invalidRunes = "~*/[]"
|
||||
if strings.ContainsAny(s, invalidRunes) {
|
||||
return nil, fmt.Errorf("firestore: %q contains an invalid rune (one of %s)", s, invalidRunes)
|
||||
}
|
||||
fp := FieldPath(strings.Split(s, "."))
|
||||
if err := fp.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fp, nil
|
||||
}
|
||||
|
||||
func (fp1 FieldPath) equal(fp2 FieldPath) bool {
|
||||
if len(fp1) != len(fp2) {
|
||||
return false
|
||||
}
|
||||
for i, c1 := range fp1 {
|
||||
if c1 != fp2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (fp1 FieldPath) prefixOf(fp2 FieldPath) bool {
|
||||
return len(fp1) <= len(fp2) && fp1.equal(fp2[:len(fp1)])
|
||||
}
|
||||
|
||||
// Lexicographic ordering.
|
||||
func (fp1 FieldPath) less(fp2 FieldPath) bool {
|
||||
for i := range fp1 {
|
||||
switch {
|
||||
case i >= len(fp2):
|
||||
return false
|
||||
case fp1[i] < fp2[i]:
|
||||
return true
|
||||
case fp1[i] > fp2[i]:
|
||||
return false
|
||||
}
|
||||
}
|
||||
// fp1 and fp2 are equal up to len(fp1).
|
||||
return len(fp1) < len(fp2)
|
||||
}
|
||||
|
||||
// validate checks the validity of fp and returns an error if it is invalid.
|
||||
func (fp FieldPath) validate() error {
|
||||
if len(fp) == 0 {
|
||||
return errors.New("firestore: empty field path")
|
||||
}
|
||||
for _, c := range fp {
|
||||
if len(c) == 0 {
|
||||
return errors.New("firestore: empty component in field path")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// with creates a new FieldPath consisting of fp followed by k.
|
||||
func (fp FieldPath) with(k string) FieldPath {
|
||||
r := make(FieldPath, len(fp), len(fp)+1)
|
||||
copy(r, fp)
|
||||
return append(r, k)
|
||||
}
|
||||
|
||||
// concat creates a new FieldPath consisting of fp1 followed by fp2.
|
||||
func (fp1 FieldPath) concat(fp2 FieldPath) FieldPath {
|
||||
r := make(FieldPath, len(fp1)+len(fp2))
|
||||
copy(r, fp1)
|
||||
copy(r[len(fp1):], fp2)
|
||||
return r
|
||||
}
|
||||
|
||||
// in reports whether fp is equal to one of the fps.
|
||||
func (fp FieldPath) in(fps []FieldPath) bool {
|
||||
for _, e := range fps {
|
||||
if fp.equal(e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checkNoDupOrPrefix checks whether any FieldPath is a prefix of (or equal to)
|
||||
// another.
|
||||
// It modifies the order of FieldPaths in its argument (via sorting).
|
||||
func checkNoDupOrPrefix(fps []FieldPath) error {
|
||||
// Sort fps lexicographically.
|
||||
sort.Sort(byPath(fps))
|
||||
// Check adjacent pairs for prefix.
|
||||
for i := 1; i < len(fps); i++ {
|
||||
if fps[i-1].prefixOf(fps[i]) {
|
||||
return fmt.Errorf("field path %v cannot be used in the same update as %v", fps[i-1], fps[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type byPath []FieldPath
|
||||
|
||||
func (b byPath) Len() int { return len(b) }
|
||||
func (b byPath) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
func (b byPath) Less(i, j int) bool { return b[i].less(b[j]) }
|
||||
|
||||
// setAtPath sets val at the location in m specified by fp, creating sub-maps as
|
||||
// needed. m must not be nil. fp is assumed to be valid.
|
||||
func setAtPath(m map[string]*pb.Value, fp FieldPath, val *pb.Value) {
|
||||
if val == nil {
|
||||
return
|
||||
}
|
||||
if len(fp) == 1 {
|
||||
m[fp[0]] = val
|
||||
} else {
|
||||
v, ok := m[fp[0]]
|
||||
if !ok {
|
||||
v = &pb.Value{&pb.Value_MapValue{&pb.MapValue{map[string]*pb.Value{}}}}
|
||||
m[fp[0]] = v
|
||||
}
|
||||
// The type assertion below cannot fail, because setAtPath is only called
|
||||
// with either an empty map or one filled by setAtPath itself, and the
|
||||
// set of FieldPaths it is called with has been checked to make sure that
|
||||
// no path is the prefix of any other.
|
||||
setAtPath(v.GetMapValue().Fields, fp[1:], val)
|
||||
}
|
||||
}
|
||||
|
||||
// getAtPath gets the value in data referred to by fp. The data argument can
|
||||
// be a map or a struct.
|
||||
// Compare with valueAtPath, which does the same thing for a document.
|
||||
func getAtPath(v reflect.Value, fp FieldPath) (interface{}, error) {
|
||||
var err error
|
||||
for _, k := range fp {
|
||||
v, err = getAtField(v, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return v.Interface(), nil
|
||||
}
|
||||
|
||||
// getAtField returns the equivalent of v[k], if v is a map, or v.k if v is a struct.
|
||||
func getAtField(v reflect.Value, k string) (reflect.Value, error) {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
if r := v.MapIndex(reflect.ValueOf(k)); r.IsValid() {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
fm, err := fieldMap(v.Type())
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
if f, ok := fm[k]; ok {
|
||||
return v.FieldByIndex(f.Index), nil
|
||||
}
|
||||
|
||||
case reflect.Interface:
|
||||
return getAtField(v.Elem(), k)
|
||||
|
||||
case reflect.Ptr:
|
||||
return getAtField(v.Elem(), k)
|
||||
}
|
||||
return reflect.Value{}, fmt.Errorf("firestore: no field %q for value %#v", k, v)
|
||||
}
|
||||
|
||||
// fieldMapCache holds maps from from Firestore field name to struct field,
|
||||
// keyed by struct type.
|
||||
// TODO(jba): replace with sync.Map for Go 1.9.
|
||||
var fieldMapCache atomiccache.Cache
|
||||
|
||||
func fieldMap(t reflect.Type) (map[string]fields.Field, error) {
|
||||
x := fieldMapCache.Get(t, func() interface{} {
|
||||
fieldList, err := fieldCache.Fields(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := map[string]fields.Field{}
|
||||
for _, f := range fieldList {
|
||||
m[f.Name] = f
|
||||
}
|
||||
return m
|
||||
})
|
||||
if err, ok := x.(error); ok {
|
||||
return nil, err
|
||||
}
|
||||
return x.(map[string]fields.Field), nil
|
||||
}
|
||||
|
||||
// toServiceFieldPath converts fp the form required by the Firestore service.
|
||||
// It assumes fp has been validated.
|
||||
func (fp FieldPath) toServiceFieldPath() string {
|
||||
cs := make([]string, len(fp))
|
||||
for i, c := range fp {
|
||||
cs[i] = toServiceFieldPathComponent(c)
|
||||
}
|
||||
return strings.Join(cs, ".")
|
||||
}
|
||||
|
||||
func toServiceFieldPaths(fps []FieldPath) []string {
|
||||
var sfps []string
|
||||
for _, fp := range fps {
|
||||
sfps = append(sfps, fp.toServiceFieldPath())
|
||||
}
|
||||
return sfps
|
||||
}
|
||||
|
||||
// Google SQL syntax for an unquoted field.
|
||||
var unquotedFieldRegexp = regexp.MustCompile("^[A-Za-z_][A-Za-z_0-9]*$")
|
||||
|
||||
// toServiceFieldPathComponent returns a string that represents key and is a valid
|
||||
// field path component.
|
||||
func toServiceFieldPathComponent(key string) string {
|
||||
if unquotedFieldRegexp.MatchString(key) {
|
||||
return key
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
buf.WriteRune('`')
|
||||
for _, r := range key {
|
||||
if r == '`' || r == '\\' {
|
||||
buf.WriteRune('\\')
|
||||
}
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
buf.WriteRune('`')
|
||||
return buf.String()
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
)
|
||||
|
||||
func TestFieldPathValidate(t *testing.T) {
|
||||
for _, in := range [][]string{nil, []string{}, []string{"a", "", "b"}} {
|
||||
if err := FieldPath(in).validate(); err == nil {
|
||||
t.Errorf("%v: want error, got nil", in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldPathLess(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in1, in2 string
|
||||
want bool
|
||||
}{
|
||||
{"a b", "a b", false},
|
||||
{"a", "b", true},
|
||||
{"b", "a", false},
|
||||
{"a", "a b", true},
|
||||
{"a b", "a", false},
|
||||
{"a b c", "a b d", true},
|
||||
{"a b d", "a b c", false},
|
||||
} {
|
||||
fp1 := FieldPath(strings.Fields(test.in1))
|
||||
fp2 := FieldPath(strings.Fields(test.in2))
|
||||
got := fp1.less(fp2)
|
||||
if got != test.want {
|
||||
t.Errorf("%q.less(%q): got %t, want %t", test.in1, test.in2, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForPrefix(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in []string // field paths as space-separated strings
|
||||
wantErr bool
|
||||
}{
|
||||
{in: []string{"a", "b", "c"}, wantErr: false},
|
||||
{in: []string{"a b", "b", "c d"}, wantErr: false},
|
||||
{in: []string{"a b", "a c", "a d"}, wantErr: false},
|
||||
{in: []string{"a b", "b", "b d"}, wantErr: true},
|
||||
{in: []string{"a b", "b", "b d"}, wantErr: true},
|
||||
{in: []string{"b c d", "c d", "b c"}, wantErr: true},
|
||||
} {
|
||||
var fps []FieldPath
|
||||
for _, s := range test.in {
|
||||
fps = append(fps, strings.Fields(s))
|
||||
}
|
||||
err := checkNoDupOrPrefix(fps)
|
||||
if got, want := (err != nil), test.wantErr; got != want {
|
||||
t.Errorf("%#v: got '%v', want %t", test.in, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAtPath(t *testing.T) {
|
||||
type S struct {
|
||||
X int
|
||||
Y int `firestore:"y"`
|
||||
M map[string]interface{}
|
||||
Next *S
|
||||
}
|
||||
|
||||
const fail = "ERROR" // value for expected error
|
||||
|
||||
for _, test := range []struct {
|
||||
val interface{}
|
||||
fp FieldPath
|
||||
want interface{}
|
||||
}{
|
||||
{
|
||||
val: map[string]int(nil),
|
||||
fp: nil,
|
||||
want: map[string]int(nil),
|
||||
},
|
||||
{
|
||||
val: 1,
|
||||
fp: nil,
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
val: 1,
|
||||
fp: []string{"a"},
|
||||
want: fail,
|
||||
},
|
||||
{
|
||||
val: map[string]int{"a": 2},
|
||||
fp: []string{"a"},
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
val: map[string]int{"a": 2},
|
||||
fp: []string{"b"},
|
||||
want: fail,
|
||||
},
|
||||
{
|
||||
val: map[string]interface{}{"a": map[string]int{"b": 3}},
|
||||
fp: []string{"a", "b"},
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
val: map[string]interface{}{"a": map[string]int{"b": 3}},
|
||||
fp: []string{"a", "b", "c"},
|
||||
want: fail,
|
||||
},
|
||||
{
|
||||
val: S{X: 1, Y: 2},
|
||||
fp: nil,
|
||||
want: S{X: 1, Y: 2},
|
||||
},
|
||||
{
|
||||
val: S{X: 1, Y: 2},
|
||||
fp: []string{"X"},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
val: S{X: 1, Y: 2},
|
||||
fp: []string{"Y"},
|
||||
want: fail, // because Y is tagged with name "y"
|
||||
},
|
||||
{
|
||||
val: S{X: 1, Y: 2},
|
||||
fp: []string{"y"},
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
val: &S{X: 1},
|
||||
fp: []string{"X"},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
val: &S{X: 1, Next: nil},
|
||||
fp: []string{"Next"},
|
||||
want: (*S)(nil),
|
||||
},
|
||||
{
|
||||
val: &S{X: 1, Next: nil},
|
||||
fp: []string{"Next", "Next"},
|
||||
want: fail,
|
||||
},
|
||||
{
|
||||
val: map[string]S{"a": S{X: 1, Y: 2}},
|
||||
fp: []string{"a", "y"},
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
val: map[string]S{"a": S{X: 1, Y: 2}},
|
||||
fp: []string{"a", "z"},
|
||||
want: fail,
|
||||
},
|
||||
{
|
||||
val: map[string]*S{
|
||||
"a": &S{
|
||||
M: map[string]interface{}{
|
||||
"b": S{
|
||||
Next: &S{
|
||||
X: 17,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fp: []string{"a", "M", "b", "Next", "X"},
|
||||
want: 17,
|
||||
},
|
||||
} {
|
||||
got, err := getAtPath(reflect.ValueOf(test.val), test.fp)
|
||||
if err != nil && test.want != fail {
|
||||
t.Errorf("%+v: got error <%v>, want nil", test, err)
|
||||
}
|
||||
if err == nil && !testutil.Equal(got, test.want) {
|
||||
t.Errorf("%+v: got %v, want %v, want nil", test, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToServiceFieldPath(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in FieldPath
|
||||
want string
|
||||
}{
|
||||
{[]string{"a"}, "a"},
|
||||
{[]string{"a", "b"}, "a.b"},
|
||||
{[]string{"a.", "[b*", "c2"}, "`a.`.`[b*`.c2"},
|
||||
{[]string{"`a", `b\`}, "`\\`a`.`b\\\\`"},
|
||||
} {
|
||||
got := test.in.toServiceFieldPath()
|
||||
if got != test.want {
|
||||
t.Errorf("%v: got %s, want %s", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToServiceFieldPathComponent(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"", "``"},
|
||||
{"clam_chowder23", "clam_chowder23"},
|
||||
{"23skidoo", "`23skidoo`"},
|
||||
{"bak`tik", "`bak\\`tik`"},
|
||||
{"a\\b", "`a\\\\b`"},
|
||||
{"dots.are.confusing", "`dots.are.confusing`"},
|
||||
} {
|
||||
got := toServiceFieldPathComponent(test.in)
|
||||
if got != test.want {
|
||||
t.Errorf("%q: got %q, want %q", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
)
|
||||
|
||||
func setFromProtoValue(x interface{}, vproto *pb.Value, c *Client) error {
|
||||
v := reflect.ValueOf(x)
|
||||
if v.Kind() != reflect.Ptr || v.IsNil() {
|
||||
return errors.New("firestore: nil or not a pointer")
|
||||
}
|
||||
return setReflectFromProtoValue(v.Elem(), vproto, c)
|
||||
}
|
||||
|
||||
// setReflectFromProtoValue sets v from a Firestore Value.
|
||||
// v must be a settable value.
|
||||
func setReflectFromProtoValue(v reflect.Value, vproto *pb.Value, c *Client) error {
|
||||
typeErr := func() error {
|
||||
return fmt.Errorf("firestore: cannot set type %s to %s", v.Type(), typeString(vproto))
|
||||
}
|
||||
|
||||
val := vproto.ValueType
|
||||
// A Null value sets anything nullable to nil, and has no effect
|
||||
// on anything else.
|
||||
if _, ok := val.(*pb.Value_NullValue); ok {
|
||||
switch v.Kind() {
|
||||
case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
|
||||
v.Set(reflect.Zero(v.Type()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle special types first.
|
||||
switch v.Type() {
|
||||
case typeOfByteSlice:
|
||||
x, ok := val.(*pb.Value_BytesValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
v.SetBytes(x.BytesValue)
|
||||
return nil
|
||||
|
||||
case typeOfGoTime:
|
||||
x, ok := val.(*pb.Value_TimestampValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
t, err := ptypes.Timestamp(x.TimestampValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Set(reflect.ValueOf(t))
|
||||
return nil
|
||||
|
||||
case typeOfProtoTimestamp:
|
||||
x, ok := val.(*pb.Value_TimestampValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
v.Set(reflect.ValueOf(x.TimestampValue))
|
||||
return nil
|
||||
|
||||
case typeOfLatLng:
|
||||
x, ok := val.(*pb.Value_GeoPointValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
v.Set(reflect.ValueOf(x.GeoPointValue))
|
||||
return nil
|
||||
|
||||
case typeOfDocumentRef:
|
||||
x, ok := val.(*pb.Value_ReferenceValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
dr, err := pathToDoc(x.ReferenceValue, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Set(reflect.ValueOf(dr))
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
x, ok := val.(*pb.Value_BooleanValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
v.SetBool(x.BooleanValue)
|
||||
|
||||
case reflect.String:
|
||||
x, ok := val.(*pb.Value_StringValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
v.SetString(x.StringValue)
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
var i int64
|
||||
switch x := val.(type) {
|
||||
case *pb.Value_IntegerValue:
|
||||
i = x.IntegerValue
|
||||
case *pb.Value_DoubleValue:
|
||||
f := x.DoubleValue
|
||||
i = int64(f)
|
||||
if float64(i) != f {
|
||||
return fmt.Errorf("firestore: float %f does not fit into %s", f, v.Type())
|
||||
}
|
||||
default:
|
||||
return typeErr()
|
||||
}
|
||||
if v.OverflowInt(i) {
|
||||
return overflowErr(v, i)
|
||||
}
|
||||
v.SetInt(i)
|
||||
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||
var u uint64
|
||||
switch x := val.(type) {
|
||||
case *pb.Value_IntegerValue:
|
||||
u = uint64(x.IntegerValue)
|
||||
case *pb.Value_DoubleValue:
|
||||
f := x.DoubleValue
|
||||
u = uint64(f)
|
||||
if float64(u) != f {
|
||||
return fmt.Errorf("firestore: float %f does not fit into %s", f, v.Type())
|
||||
}
|
||||
default:
|
||||
return typeErr()
|
||||
}
|
||||
if v.OverflowUint(u) {
|
||||
return overflowErr(v, u)
|
||||
}
|
||||
v.SetUint(u)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
var f float64
|
||||
switch x := val.(type) {
|
||||
case *pb.Value_DoubleValue:
|
||||
f = x.DoubleValue
|
||||
case *pb.Value_IntegerValue:
|
||||
f = float64(x.IntegerValue)
|
||||
if int64(f) != x.IntegerValue {
|
||||
return overflowErr(v, x.IntegerValue)
|
||||
}
|
||||
default:
|
||||
return typeErr()
|
||||
}
|
||||
if v.OverflowFloat(f) {
|
||||
return overflowErr(v, f)
|
||||
}
|
||||
v.SetFloat(f)
|
||||
|
||||
case reflect.Slice:
|
||||
x, ok := val.(*pb.Value_ArrayValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
vals := x.ArrayValue.Values
|
||||
vlen := v.Len()
|
||||
xlen := len(vals)
|
||||
// Make a slice of the right size, avoiding allocation if possible.
|
||||
switch {
|
||||
case vlen < xlen:
|
||||
v.Set(reflect.MakeSlice(v.Type(), xlen, xlen))
|
||||
case vlen > xlen:
|
||||
v.SetLen(xlen)
|
||||
}
|
||||
return populateRepeated(v, vals, xlen, c)
|
||||
|
||||
case reflect.Array:
|
||||
x, ok := val.(*pb.Value_ArrayValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
vals := x.ArrayValue.Values
|
||||
xlen := len(vals)
|
||||
vlen := v.Len()
|
||||
minlen := vlen
|
||||
// Set extra elements to their zero value.
|
||||
if vlen > xlen {
|
||||
z := reflect.Zero(v.Type().Elem())
|
||||
for i := xlen; i < vlen; i++ {
|
||||
v.Index(i).Set(z)
|
||||
}
|
||||
minlen = xlen
|
||||
}
|
||||
return populateRepeated(v, vals, minlen, c)
|
||||
|
||||
case reflect.Map:
|
||||
x, ok := val.(*pb.Value_MapValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
return populateMap(v, x.MapValue.Fields, c)
|
||||
|
||||
case reflect.Ptr:
|
||||
// If the pointer is nil, set it to a zero value.
|
||||
if v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
return setReflectFromProtoValue(v.Elem(), vproto, c)
|
||||
|
||||
case reflect.Struct:
|
||||
x, ok := val.(*pb.Value_MapValue)
|
||||
if !ok {
|
||||
return typeErr()
|
||||
}
|
||||
return populateStruct(v, x.MapValue.Fields, c)
|
||||
|
||||
case reflect.Interface:
|
||||
if v.NumMethod() == 0 { // empty interface
|
||||
// If v holds a pointer, set the pointer.
|
||||
if !v.IsNil() && v.Elem().Kind() == reflect.Ptr {
|
||||
return setReflectFromProtoValue(v.Elem(), vproto, c)
|
||||
}
|
||||
// Otherwise, create a fresh value.
|
||||
x, err := createFromProtoValue(vproto, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Set(reflect.ValueOf(x))
|
||||
return nil
|
||||
}
|
||||
// Any other kind of interface is an error.
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
return fmt.Errorf("firestore: cannot set type %s", v.Type())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// populateRepeated sets the first n elements of vr, which must be a slice or
|
||||
// array, to the corresponding elements of vals.
|
||||
func populateRepeated(vr reflect.Value, vals []*pb.Value, n int, c *Client) error {
|
||||
for i := 0; i < n; i++ {
|
||||
if err := setReflectFromProtoValue(vr.Index(i), vals[i], c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// populateMap sets the elements of vm, which must be a map, from the
|
||||
// corresponding elements of pm.
|
||||
//
|
||||
// Since a map value is not settable, this function always creates a new
|
||||
// element for each corresponding map key. Existing values of vm are
|
||||
// overwritten. This happens even if the map value is something like a pointer
|
||||
// to a struct, where we could in theory populate the existing struct value
|
||||
// instead of discarding it. This behavior matches encoding/json.
|
||||
func populateMap(vm reflect.Value, pm map[string]*pb.Value, c *Client) error {
|
||||
t := vm.Type()
|
||||
if t.Key().Kind() != reflect.String {
|
||||
return errors.New("firestore: map key type is not string")
|
||||
}
|
||||
if vm.IsNil() {
|
||||
vm.Set(reflect.MakeMap(t))
|
||||
}
|
||||
et := t.Elem()
|
||||
for k, vproto := range pm {
|
||||
el := reflect.New(et).Elem()
|
||||
if err := setReflectFromProtoValue(el, vproto, c); err != nil {
|
||||
return err
|
||||
}
|
||||
vm.SetMapIndex(reflect.ValueOf(k), el)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createMapFromValueMap creates a fresh map and populates it with pm.
|
||||
func createMapFromValueMap(pm map[string]*pb.Value, c *Client) (map[string]interface{}, error) {
|
||||
m := map[string]interface{}{}
|
||||
for k, pv := range pm {
|
||||
v, err := createFromProtoValue(pv, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m[k] = v
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// populateStruct sets the fields of vs, which must be a struct, from
|
||||
// the matching elements of pm.
|
||||
func populateStruct(vs reflect.Value, pm map[string]*pb.Value, c *Client) error {
|
||||
fields, err := fieldCache.Fields(vs.Type())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, vproto := range pm {
|
||||
f := fields.Match(k)
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
if err := setReflectFromProtoValue(vs.FieldByIndex(f.Index), vproto, c); err != nil {
|
||||
return fmt.Errorf("%s.%s: %v", vs.Type(), f.Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFromProtoValue(vproto *pb.Value, c *Client) (interface{}, error) {
|
||||
switch v := vproto.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 ptypes.Timestamp(v.TimestampValue)
|
||||
case *pb.Value_StringValue:
|
||||
return v.StringValue, nil
|
||||
case *pb.Value_BytesValue:
|
||||
return v.BytesValue, nil
|
||||
case *pb.Value_ReferenceValue:
|
||||
return pathToDoc(v.ReferenceValue, c)
|
||||
case *pb.Value_GeoPointValue:
|
||||
return v.GeoPointValue, nil
|
||||
|
||||
case *pb.Value_ArrayValue:
|
||||
vals := v.ArrayValue.Values
|
||||
ret := make([]interface{}, len(vals))
|
||||
for i, v := range vals {
|
||||
r, err := createFromProtoValue(v, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[i] = r
|
||||
}
|
||||
return ret, nil
|
||||
|
||||
case *pb.Value_MapValue:
|
||||
fields := v.MapValue.Fields
|
||||
ret := make(map[string]interface{}, len(fields))
|
||||
for k, v := range fields {
|
||||
r, err := createFromProtoValue(v, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[k] = r
|
||||
}
|
||||
return ret, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: unknown value type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert a document path to a DocumentRef.
|
||||
func pathToDoc(docPath string, c *Client) (*DocumentRef, error) {
|
||||
projID, dbID, docIDs, err := parseDocumentPath(docPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentResourceName := fmt.Sprintf("projects/%s/databases/%s", projID, dbID)
|
||||
_, doc := c.idsToRef(docIDs, parentResourceName)
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// A document path should be of the form "projects/P/databases/D/documents/coll1/doc1/coll2/doc2/...".
|
||||
func parseDocumentPath(path string) (projectID, databaseID string, docPath []string, err error) {
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 6 || parts[0] != "projects" || parts[2] != "databases" || parts[4] != "documents" {
|
||||
return "", "", nil, fmt.Errorf("firestore: malformed document path %q", path)
|
||||
}
|
||||
docp := parts[5:]
|
||||
if len(docp)%2 != 0 {
|
||||
return "", "", nil, fmt.Errorf("firestore: path %q refers to collection, not document", path)
|
||||
}
|
||||
return parts[1], parts[3], docp, nil
|
||||
}
|
||||
|
||||
func typeString(vproto *pb.Value) string {
|
||||
switch vproto.ValueType.(type) {
|
||||
case *pb.Value_NullValue:
|
||||
return "null"
|
||||
case *pb.Value_BooleanValue:
|
||||
return "bool"
|
||||
case *pb.Value_IntegerValue:
|
||||
return "int"
|
||||
case *pb.Value_DoubleValue:
|
||||
return "float"
|
||||
case *pb.Value_TimestampValue:
|
||||
return "timestamp"
|
||||
case *pb.Value_StringValue:
|
||||
return "string"
|
||||
case *pb.Value_BytesValue:
|
||||
return "bytes"
|
||||
case *pb.Value_ReferenceValue:
|
||||
return "reference"
|
||||
case *pb.Value_GeoPointValue:
|
||||
return "GeoPoint"
|
||||
case *pb.Value_MapValue:
|
||||
return "map"
|
||||
case *pb.Value_ArrayValue:
|
||||
return "array"
|
||||
default:
|
||||
return "<unknown Value type>"
|
||||
}
|
||||
}
|
||||
|
||||
func overflowErr(v reflect.Value, x interface{}) error {
|
||||
return fmt.Errorf("firestore: value %v overflows type %s", x, v.Type())
|
||||
}
|
||||
+550
@@ -0,0 +1,550 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ts "github.com/golang/protobuf/ptypes/timestamp"
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
|
||||
"google.golang.org/genproto/googleapis/type/latlng"
|
||||
)
|
||||
|
||||
var (
|
||||
tm = time.Date(2016, 12, 25, 0, 0, 0, 123456789, time.UTC)
|
||||
ll = &latlng.LatLng{Latitude: 20, Longitude: 30}
|
||||
ptm = &ts.Timestamp{12345, 67890}
|
||||
)
|
||||
|
||||
func TestCreateFromProtoValue(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in *pb.Value
|
||||
want interface{}
|
||||
}{
|
||||
{in: nullValue, want: nil},
|
||||
{in: boolval(true), want: true},
|
||||
{in: intval(3), want: int64(3)},
|
||||
{in: floatval(1.5), want: 1.5},
|
||||
{in: strval("str"), want: "str"},
|
||||
{in: tsval(tm), want: tm},
|
||||
{
|
||||
in: bytesval([]byte{1, 2}),
|
||||
want: []byte{1, 2},
|
||||
},
|
||||
{
|
||||
in: &pb.Value{&pb.Value_GeoPointValue{ll}},
|
||||
want: ll,
|
||||
},
|
||||
{
|
||||
in: arrayval(intval(1), intval(2)),
|
||||
want: []interface{}{int64(1), int64(2)},
|
||||
},
|
||||
{
|
||||
in: arrayval(),
|
||||
want: []interface{}{},
|
||||
},
|
||||
{
|
||||
in: mapval(map[string]*pb.Value{"a": intval(1), "b": intval(2)}),
|
||||
want: map[string]interface{}{"a": int64(1), "b": int64(2)},
|
||||
},
|
||||
{
|
||||
in: mapval(map[string]*pb.Value{}),
|
||||
want: map[string]interface{}{},
|
||||
},
|
||||
{
|
||||
in: refval("projects/P/databases/D/documents/c/d"),
|
||||
want: &DocumentRef{
|
||||
ID: "d",
|
||||
Parent: &CollectionRef{
|
||||
ID: "c",
|
||||
parentPath: "projects/P/databases/D",
|
||||
Path: "projects/P/databases/D/documents/c",
|
||||
Query: Query{collectionID: "c", parentPath: "projects/P/databases/D"},
|
||||
},
|
||||
Path: "projects/P/databases/D/documents/c/d",
|
||||
},
|
||||
},
|
||||
} {
|
||||
got, err := createFromProtoValue(test.in, nil)
|
||||
if err != nil {
|
||||
t.Errorf("%v: %v", test.in, err)
|
||||
continue
|
||||
}
|
||||
if !testEqual(got, test.want) {
|
||||
t.Errorf("%+v:\ngot\n%#v\nwant\n%#v", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFromProtoValue(t *testing.T) {
|
||||
testSetFromProtoValue(t, "json", jsonTester{})
|
||||
testSetFromProtoValue(t, "firestore", protoTester{})
|
||||
}
|
||||
|
||||
func testSetFromProtoValue(t *testing.T, prefix string, r tester) {
|
||||
pi := newfloat(7)
|
||||
s := []float64{7, 8}
|
||||
ar1 := [1]float64{7}
|
||||
ar2 := [2]float64{7, 8}
|
||||
ar3 := [3]float64{7, 8, 9}
|
||||
mf := map[string]float64{"a": 7}
|
||||
|
||||
type T struct {
|
||||
I **float64
|
||||
J float64
|
||||
}
|
||||
|
||||
one := newfloat(1)
|
||||
six := newfloat(6)
|
||||
st := []*T{&T{I: &six}, nil, &T{I: &six, J: 7}}
|
||||
vs := interface{}(T{J: 1})
|
||||
vm := interface{}(map[string]float64{"i": 1})
|
||||
var (
|
||||
i int
|
||||
i8 int8
|
||||
i16 int16
|
||||
i32 int32
|
||||
i64 int64
|
||||
u8 uint8
|
||||
u16 uint16
|
||||
u32 uint32
|
||||
b bool
|
||||
ll *latlng.LatLng
|
||||
mi map[string]interface{}
|
||||
ms map[string]T
|
||||
)
|
||||
|
||||
for i, test := range []struct {
|
||||
in interface{}
|
||||
val interface{}
|
||||
want interface{}
|
||||
}{
|
||||
{&pi, r.Null(), (*float64)(nil)},
|
||||
{pi, r.Float(1), 1.0},
|
||||
{&s, r.Null(), ([]float64)(nil)},
|
||||
{&s, r.Array(r.Float(1), r.Float(2)), []float64{1, 2}},
|
||||
{&ar1, r.Array(r.Float(1), r.Float(2)), [1]float64{1}},
|
||||
{&ar2, r.Array(r.Float(1), r.Float(2)), [2]float64{1, 2}},
|
||||
{&ar3, r.Array(r.Float(1), r.Float(2)), [3]float64{1, 2, 0}},
|
||||
{&mf, r.Null(), (map[string]float64)(nil)},
|
||||
{&mf, r.Map("a", r.Float(1), "b", r.Float(2)), map[string]float64{"a": 1, "b": 2}},
|
||||
{&st, r.Array(
|
||||
r.Null(), // overwrites st[0] with nil
|
||||
r.Map("i", r.Float(1)), // sets st[1] to a new struct
|
||||
r.Map("i", r.Float(2)), // modifies st[2]
|
||||
),
|
||||
[]*T{nil, &T{I: &one}, &T{I: &six, J: 7}}},
|
||||
{&mi, r.Map("a", r.Float(1), "b", r.Float(2)), map[string]interface{}{"a": 1.0, "b": 2.0}},
|
||||
{&ms, r.Map("a", r.Map("j", r.Float(1))), map[string]T{"a": T{J: 1}}},
|
||||
{&vs, r.Map("i", r.Float(2)), map[string]interface{}{"i": 2.0}},
|
||||
{&vm, r.Map("i", r.Float(2)), map[string]interface{}{"i": 2.0}},
|
||||
{&ll, r.Null(), (*latlng.LatLng)(nil)},
|
||||
{&i, r.Int(1), int(1)},
|
||||
{&i8, r.Int(1), int8(1)},
|
||||
{&i16, r.Int(1), int16(1)},
|
||||
{&i32, r.Int(1), int32(1)},
|
||||
{&i64, r.Int(1), int64(1)},
|
||||
{&u8, r.Int(1), uint8(1)},
|
||||
{&u16, r.Int(1), uint16(1)},
|
||||
{&u32, r.Int(1), uint32(1)},
|
||||
{&b, r.Bool(true), true},
|
||||
{&i, r.Float(1), int(1)}, // can put a float with no fractional part into an int
|
||||
{pi, r.Int(1), float64(1)}, // can put an int into a float
|
||||
} {
|
||||
if err := r.Set(test.in, test.val); err != nil {
|
||||
t.Errorf("%s: #%d: got error %v", prefix, i, err)
|
||||
continue
|
||||
}
|
||||
got := reflect.ValueOf(test.in).Elem().Interface()
|
||||
if !testEqual(got, test.want) {
|
||||
t.Errorf("%s: #%d, %v:\ngot\n%+v (%T)\nwant\n%+v (%T)",
|
||||
prefix, i, test.val, got, got, test.want, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFromProtoValueNoJSON(t *testing.T) {
|
||||
// Test code paths that we cannot compare to JSON.
|
||||
var (
|
||||
bs []byte
|
||||
tmi time.Time
|
||||
lli *latlng.LatLng
|
||||
tmp *ts.Timestamp
|
||||
)
|
||||
bytes := []byte{1, 2, 3}
|
||||
|
||||
for i, test := range []struct {
|
||||
in interface{}
|
||||
val *pb.Value
|
||||
want interface{}
|
||||
}{
|
||||
{&bs, bytesval(bytes), bytes},
|
||||
{&tmi, tsval(tm), tm},
|
||||
{&tmp, &pb.Value{&pb.Value_TimestampValue{ptm}}, ptm},
|
||||
{&lli, geoval(ll), ll},
|
||||
} {
|
||||
if err := setFromProtoValue(test.in, test.val, &Client{}); err != nil {
|
||||
t.Errorf("#%d: got error %v", i, err)
|
||||
continue
|
||||
}
|
||||
got := reflect.ValueOf(test.in).Elem().Interface()
|
||||
if !testEqual(got, test.want) {
|
||||
t.Errorf("#%d, %v:\ngot\n%+v (%T)\nwant\n%+v (%T)",
|
||||
i, test.val, got, got, test.want, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFromProtoValueErrors(t *testing.T) {
|
||||
c := &Client{}
|
||||
ival := intval(3)
|
||||
for i, test := range []struct {
|
||||
in interface{}
|
||||
val *pb.Value
|
||||
}{
|
||||
{3, ival}, // not a pointer
|
||||
{new(int8), intval(128)}, // int overflow
|
||||
{new(uint8), intval(256)}, // uint overflow
|
||||
{new(float32), floatval(2 * math.MaxFloat32)}, // float overflow
|
||||
{new(uint), ival}, // cannot set type
|
||||
{new(uint64), ival}, // cannot set type
|
||||
{new(io.Reader), ival}, // cannot set type
|
||||
{new(map[int]int),
|
||||
mapval(map[string]*pb.Value{"x": ival})}, // map key type is not string
|
||||
// the rest are all type mismatches
|
||||
{new(bool), ival},
|
||||
{new(*latlng.LatLng), ival},
|
||||
{new(time.Time), ival},
|
||||
{new(string), ival},
|
||||
{new([]byte), ival},
|
||||
{new([]int), ival},
|
||||
{new([1]int), ival},
|
||||
{new(map[string]int), ival},
|
||||
{new(*bool), ival},
|
||||
{new(struct{}), ival},
|
||||
{new(int), floatval(2.5)}, // float must be integral
|
||||
{new(uint16), intval(-1)}, // uint cannot be negative
|
||||
{new(int16), floatval(math.MaxFloat32)}, // doesn't fit
|
||||
{new(uint16), floatval(math.MaxFloat32)}, // doesn't fit
|
||||
{new(float32),
|
||||
&pb.Value{&pb.Value_IntegerValue{math.MaxInt64}}}, // overflow
|
||||
} {
|
||||
err := setFromProtoValue(test.in, test.val, c)
|
||||
if err == nil {
|
||||
t.Errorf("#%d: %v, %v: got nil, want error", i, test.in, test.val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFromProtoValuePointers(t *testing.T) {
|
||||
// Verify that pointers are set, instead of being replaced.
|
||||
// Confirm that the behavior matches encoding/json.
|
||||
testSetPointer(t, "json", jsonTester{})
|
||||
testSetPointer(t, "firestore", protoTester{&Client{}})
|
||||
}
|
||||
|
||||
func testSetPointer(t *testing.T, prefix string, r tester) {
|
||||
// If an interface{} holds a pointer, the pointer is set.
|
||||
|
||||
set := func(x, val interface{}) {
|
||||
if err := r.Set(x, val); err != nil {
|
||||
t.Fatalf("%s: set(%v, %v): %v", prefix, x, val, err)
|
||||
}
|
||||
}
|
||||
p := new(float64)
|
||||
var st struct {
|
||||
I interface{}
|
||||
}
|
||||
|
||||
// A pointer in a slice of interface{} is set.
|
||||
s := []interface{}{p}
|
||||
set(&s, r.Array(r.Float(1)))
|
||||
if s[0] != p {
|
||||
t.Errorf("%s: pointers not identical", prefix)
|
||||
}
|
||||
if *p != 1 {
|
||||
t.Errorf("%s: got %f, want 1", prefix, *p)
|
||||
}
|
||||
// Setting a null will set the pointer to nil.
|
||||
set(&s, r.Array(r.Null()))
|
||||
if got := s[0]; got != nil {
|
||||
t.Errorf("%s: got %v, want null", prefix, got)
|
||||
}
|
||||
|
||||
// It doesn't matter how deep the pointers nest.
|
||||
p = new(float64)
|
||||
p2 := &p
|
||||
p3 := &p2
|
||||
s = []interface{}{p3}
|
||||
set(&s, r.Array(r.Float(1)))
|
||||
if s[0] != p3 {
|
||||
t.Errorf("%s: pointers not identical", prefix)
|
||||
}
|
||||
if *p != 1 {
|
||||
t.Errorf("%s: got %f, want 1", prefix, *p)
|
||||
}
|
||||
|
||||
// A pointer in an interface{} field is set.
|
||||
p = new(float64)
|
||||
st.I = p
|
||||
set(&st, r.Map("i", r.Float(1)))
|
||||
if st.I != p {
|
||||
t.Errorf("%s: pointers not identical", prefix)
|
||||
}
|
||||
if *p != 1 {
|
||||
t.Errorf("%s: got %f, want 1", prefix, *p)
|
||||
}
|
||||
// Setting a null will set the pointer to nil.
|
||||
set(&st, r.Map("i", r.Null()))
|
||||
if got := st.I; got != nil {
|
||||
t.Errorf("%s: got %v, want null", prefix, got)
|
||||
}
|
||||
|
||||
// A pointer to a slice (instead of to float64) is set.
|
||||
psi := &[]float64{7, 8, 9}
|
||||
st.I = psi
|
||||
set(&st, r.Map("i", r.Array(r.Float(1))))
|
||||
if st.I != psi {
|
||||
t.Errorf("%s: pointers not identical", prefix)
|
||||
}
|
||||
// The slice itself should be truncated and filled, not replaced.
|
||||
if got, want := cap(*psi), 3; got != want {
|
||||
t.Errorf("cap: got %d, want %d", got, want)
|
||||
}
|
||||
if want := &[]float64{1}; !testEqual(st.I, want) {
|
||||
t.Errorf("got %+v, want %+v", st.I, want)
|
||||
}
|
||||
|
||||
// A pointer to a map is set.
|
||||
pmf := &map[string]float64{"a": 7, "b": 8}
|
||||
st.I = pmf
|
||||
set(&st, r.Map("i", r.Map("a", r.Float(1))))
|
||||
if st.I != pmf {
|
||||
t.Errorf("%s: pointers not identical", prefix)
|
||||
}
|
||||
if want := map[string]float64{"a": 1, "b": 8}; !testEqual(*pmf, want) {
|
||||
t.Errorf("%s: got %+v, want %+v", prefix, *pmf, want)
|
||||
}
|
||||
|
||||
// Maps are different: since the map values aren't addressable, they
|
||||
// are always discarded, even if the map element type is not interface{}.
|
||||
|
||||
// A map's values are discarded if the value type is a pointer type.
|
||||
p = new(float64)
|
||||
m := map[string]*float64{"i": p}
|
||||
set(&m, r.Map("i", r.Float(1)))
|
||||
if m["i"] == p {
|
||||
t.Errorf("%s: pointers are identical", prefix)
|
||||
}
|
||||
if got, want := *m["i"], 1.0; got != want {
|
||||
t.Errorf("%s: got %v, want %v", prefix, got, want)
|
||||
}
|
||||
// A map's values are discarded if the value type is interface{}.
|
||||
p = new(float64)
|
||||
m2 := map[string]interface{}{"i": p}
|
||||
set(&m2, r.Map("i", r.Float(1)))
|
||||
if m2["i"] == p {
|
||||
t.Errorf("%s: pointers are identical", prefix)
|
||||
}
|
||||
if got, want := m2["i"].(float64), 1.0; got != want {
|
||||
t.Errorf("%s: got %f, want %f", prefix, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// An interface for setting and building values, to facilitate comparing firestore deserialization
|
||||
// with encoding/json.
|
||||
type tester interface {
|
||||
Set(x, val interface{}) error
|
||||
Null() interface{}
|
||||
Int(int) interface{}
|
||||
Float(float64) interface{}
|
||||
Bool(bool) interface{}
|
||||
Array(...interface{}) interface{}
|
||||
Map(keysvals ...interface{}) interface{}
|
||||
}
|
||||
|
||||
type protoTester struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
func (p protoTester) Set(x, val interface{}) error { return setFromProtoValue(x, val.(*pb.Value), p.c) }
|
||||
func (protoTester) Null() interface{} { return nullValue }
|
||||
func (protoTester) Int(i int) interface{} { return intval(i) }
|
||||
func (protoTester) Float(f float64) interface{} { return floatval(f) }
|
||||
func (protoTester) Bool(b bool) interface{} { return boolval(b) }
|
||||
|
||||
func (protoTester) Array(els ...interface{}) interface{} {
|
||||
var s []*pb.Value
|
||||
for _, el := range els {
|
||||
s = append(s, el.(*pb.Value))
|
||||
}
|
||||
return arrayval(s...)
|
||||
}
|
||||
|
||||
func (protoTester) Map(keysvals ...interface{}) interface{} {
|
||||
m := map[string]*pb.Value{}
|
||||
for i := 0; i < len(keysvals); i += 2 {
|
||||
m[keysvals[i].(string)] = keysvals[i+1].(*pb.Value)
|
||||
}
|
||||
return mapval(m)
|
||||
}
|
||||
|
||||
type jsonTester struct{}
|
||||
|
||||
func (jsonTester) Set(x, val interface{}) error { return json.Unmarshal([]byte(val.(string)), x) }
|
||||
func (jsonTester) Null() interface{} { return "null" }
|
||||
func (jsonTester) Int(i int) interface{} { return fmt.Sprint(i) }
|
||||
func (jsonTester) Float(f float64) interface{} { return fmt.Sprint(f) }
|
||||
|
||||
func (jsonTester) Bool(b bool) interface{} {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (jsonTester) Array(els ...interface{}) interface{} {
|
||||
var s []string
|
||||
for _, el := range els {
|
||||
s = append(s, el.(string))
|
||||
}
|
||||
return "[" + strings.Join(s, ", ") + "]"
|
||||
}
|
||||
|
||||
func (jsonTester) Map(keysvals ...interface{}) interface{} {
|
||||
var s []string
|
||||
for i := 0; i < len(keysvals); i += 2 {
|
||||
s = append(s, fmt.Sprintf("%q: %v", keysvals[i], keysvals[i+1]))
|
||||
}
|
||||
return "{" + strings.Join(s, ", ") + "}"
|
||||
}
|
||||
|
||||
func newfloat(f float64) *float64 {
|
||||
p := new(float64)
|
||||
*p = f
|
||||
return p
|
||||
}
|
||||
|
||||
func TestParseDocumentPath(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in string
|
||||
pid, dbid string
|
||||
dpath []string
|
||||
}{
|
||||
{"projects/foo-bar/databases/db2/documents/c1/d1",
|
||||
"foo-bar", "db2", []string{"c1", "d1"}},
|
||||
{"projects/P/databases/D/documents/c1/d1/c2/d2",
|
||||
"P", "D", []string{"c1", "d1", "c2", "d2"}},
|
||||
} {
|
||||
gotPid, gotDbid, gotDpath, err := parseDocumentPath(test.in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := gotPid, test.pid; got != want {
|
||||
t.Errorf("project ID: got %q, want %q", got, want)
|
||||
}
|
||||
if got, want := gotDbid, test.dbid; got != want {
|
||||
t.Errorf("db ID: got %q, want %q", got, want)
|
||||
}
|
||||
if got, want := gotDpath, test.dpath; !testEqual(got, want) {
|
||||
t.Errorf("doc path: got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDocumentPathErrors(t *testing.T) {
|
||||
for _, badPath := range []string{
|
||||
"projects/P/databases/D/documents/c", // collection path
|
||||
"/projects/P/databases/D/documents/c/d", // initial slash
|
||||
"projects/P/databases/D/c/d", // missing "documents"
|
||||
"project/P/database/D/document/c/d",
|
||||
} {
|
||||
// Every prefix of a bad path is also bad.
|
||||
for i := 0; i <= len(badPath); i++ {
|
||||
in := badPath[:i]
|
||||
_, _, _, err := parseDocumentPath(in)
|
||||
if err == nil {
|
||||
t.Errorf("%q: got nil, want error", in)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathToDoc(t *testing.T) {
|
||||
c := &Client{}
|
||||
path := "projects/P/databases/D/documents/c1/d1/c2/d2"
|
||||
got, err := pathToDoc(path, c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := &DocumentRef{
|
||||
ID: "d2",
|
||||
Path: "projects/P/databases/D/documents/c1/d1/c2/d2",
|
||||
Parent: &CollectionRef{
|
||||
ID: "c2",
|
||||
parentPath: "projects/P/databases/D/documents/c1/d1",
|
||||
Path: "projects/P/databases/D/documents/c1/d1/c2",
|
||||
c: c,
|
||||
Query: Query{c: c, collectionID: "c2", parentPath: "projects/P/databases/D/documents/c1/d1"},
|
||||
Parent: &DocumentRef{
|
||||
ID: "d1",
|
||||
Path: "projects/P/databases/D/documents/c1/d1",
|
||||
Parent: &CollectionRef{
|
||||
ID: "c1",
|
||||
c: c,
|
||||
parentPath: "projects/P/databases/D",
|
||||
Path: "projects/P/databases/D/documents/c1",
|
||||
Parent: nil,
|
||||
Query: Query{c: c, collectionID: "c1", parentPath: "projects/P/databases/D"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if !testEqual(got, want) {
|
||||
t.Errorf("\ngot %+v\nwant %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeString(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in *pb.Value
|
||||
want string
|
||||
}{
|
||||
{nullValue, "null"},
|
||||
{intval(1), "int"},
|
||||
{floatval(1), "float"},
|
||||
{boolval(true), "bool"},
|
||||
{strval(""), "string"},
|
||||
{tsval(tm), "timestamp"},
|
||||
{geoval(ll), "GeoPoint"},
|
||||
{bytesval(nil), "bytes"},
|
||||
{refval(""), "reference"},
|
||||
{arrayval(nil), "array"},
|
||||
{mapval(nil), "map"},
|
||||
} {
|
||||
got := typeString(test.in)
|
||||
if got != test.want {
|
||||
t.Errorf("%+v: got %q, want %q", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1243
File diff suppressed because it is too large
Load Diff
+1133
File diff suppressed because it is too large
Load Diff
+16
@@ -0,0 +1,16 @@
|
||||
# Build doc.go from template and snippets.
|
||||
|
||||
SHELL=/bin/bash
|
||||
|
||||
../doc.go: build doc-snippets.go doc.template snipdoc.awk
|
||||
@tmp=$$(mktemp) && \
|
||||
awk -f snipdoc.awk doc-snippets.go doc.template > $$tmp && \
|
||||
chmod +w $@ && \
|
||||
mv $$tmp $@ && \
|
||||
chmod -w $@
|
||||
@echo "wrote $@"
|
||||
|
||||
.PHONY: build
|
||||
|
||||
build:
|
||||
go build doc-snippets.go
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
firestore "cloud.google.com/go/firestore"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
const ELLIPSIS = 0
|
||||
|
||||
//[ structDef
|
||||
type State struct {
|
||||
Capital string `firestore:"capital"`
|
||||
Population float64 `firestore:"pop"` // in millions
|
||||
}
|
||||
|
||||
//]
|
||||
|
||||
func f1() {
|
||||
//[ NewClient
|
||||
ctx := context.Background()
|
||||
client, err := firestore.NewClient(ctx, "projectID")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
//]
|
||||
//[ refs
|
||||
states := client.Collection("States")
|
||||
ny := states.Doc("NewYork")
|
||||
// Or, in a single call:
|
||||
ny = client.Doc("States/NewYork")
|
||||
//]
|
||||
//[ docref.Get
|
||||
docsnap, err := ny.Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
dataMap := docsnap.Data()
|
||||
fmt.Println(dataMap)
|
||||
//]
|
||||
//[ DataTo
|
||||
var nyData State
|
||||
if err := docsnap.DataTo(&nyData); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
//]
|
||||
//[ GetAll
|
||||
docsnaps, err := client.GetAll(ctx, []*firestore.DocumentRef{
|
||||
states.Doc("Wisconsin"), states.Doc("Ohio"),
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
for _, ds := range docsnaps {
|
||||
_ = ds // TODO: Use ds.
|
||||
}
|
||||
//[ docref.Create
|
||||
wr, err := ny.Create(ctx, State{
|
||||
Capital: "Albany",
|
||||
Population: 19.8,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(wr)
|
||||
//]
|
||||
//[ docref.Set
|
||||
ca := states.Doc("California")
|
||||
_, err = ca.Set(ctx, State{
|
||||
Capital: "Sacramento",
|
||||
Population: 39.14,
|
||||
})
|
||||
//]
|
||||
|
||||
//[ docref.Update
|
||||
_, err = ca.Update(ctx, []firestore.Update{{Path: "capital", Value: "Sacramento"}})
|
||||
//]
|
||||
|
||||
//[ docref.Delete
|
||||
_, err = ny.Delete(ctx)
|
||||
//]
|
||||
|
||||
//[ LUT-precond
|
||||
docsnap, err = ca.Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_, err = ca.Update(ctx,
|
||||
[]firestore.Update{{Path: "capital", Value: "Sacramento"}},
|
||||
firestore.LastUpdateTime(docsnap.UpdateTime))
|
||||
//]
|
||||
|
||||
//[ WriteBatch
|
||||
writeResults, err := client.Batch().
|
||||
Create(ny, State{Capital: "Albany"}).
|
||||
Update(ca, []firestore.Update{{Path: "capital", Value: "Sacramento"}}).
|
||||
Delete(client.Doc("States/WestDakota")).
|
||||
Commit(ctx)
|
||||
//]
|
||||
_ = writeResults
|
||||
|
||||
//[ Query
|
||||
q := states.Where("pop", ">", 10).OrderBy("pop", firestore.Desc)
|
||||
//]
|
||||
//[ Documents
|
||||
iter := q.Documents(ctx)
|
||||
for {
|
||||
doc, err := iter.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(doc.Data())
|
||||
}
|
||||
//]
|
||||
|
||||
//[ CollQuery
|
||||
iter = client.Collection("States").Documents(ctx)
|
||||
//]
|
||||
}
|
||||
|
||||
func txn() {
|
||||
var ctx context.Context
|
||||
var client *firestore.Client
|
||||
//[ Transaction
|
||||
ny := client.Doc("States/NewYork")
|
||||
err := client.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error {
|
||||
doc, err := tx.Get(ny) // tx.Get, NOT ny.Get!
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pop, err := doc.DataAt("pop")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Update(ny, []firestore.Update{{Path: "pop", Value: pop.(float64) + 0.2}})
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
//]
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// DO NOT EDIT doc.go. Modify internal/doc.template, then run make -C internal.
|
||||
|
||||
/*
|
||||
Package firestore provides a client for reading and writing to a Cloud Firestore
|
||||
database.
|
||||
|
||||
See https://cloud.google.com/firestore/docs for an introduction
|
||||
to Cloud Firestore and additional help on using the Firestore API.
|
||||
|
||||
Note: you can't use both Cloud Firestore and Cloud Datastore in the same
|
||||
project.
|
||||
|
||||
Creating a Client
|
||||
|
||||
To start working with this package, create a client with a project ID:
|
||||
|
||||
[NewClient]
|
||||
|
||||
CollectionRefs and DocumentRefs
|
||||
|
||||
In Firestore, documents are sets of key-value pairs, and collections are groups of
|
||||
documents. A Firestore database consists of a hierarchy of alternating collections
|
||||
and documents, referred to by slash-separated paths like
|
||||
"States/California/Cities/SanFrancisco".
|
||||
|
||||
This client is built around references to collections and documents. CollectionRefs
|
||||
and DocumentRefs are lightweight values that refer to the corresponding database
|
||||
entities. Creating a ref does not involve any network traffic.
|
||||
|
||||
[refs]
|
||||
|
||||
Reading
|
||||
|
||||
Use DocumentRef.Get to read a document. The result is a DocumentSnapshot.
|
||||
Call its Data method to obtain the entire document contents as a map.
|
||||
|
||||
[docref.Get]
|
||||
|
||||
You can also obtain a single field with DataAt, or extract the data into a struct
|
||||
with DataTo. With the type definition
|
||||
|
||||
[structDef]
|
||||
|
||||
we can extract the document's data into a value of type State:
|
||||
|
||||
[DataTo]
|
||||
|
||||
Note that this client supports struct tags beginning with "firestore:" that work like
|
||||
the tags of the encoding/json package, letting you rename fields, ignore them, or
|
||||
omit their values when empty.
|
||||
|
||||
To retrieve multiple documents from their references in a single call, use
|
||||
Client.GetAll.
|
||||
|
||||
[GetAll]
|
||||
|
||||
Writing
|
||||
|
||||
For writing individual documents, use the methods on DocumentReference.
|
||||
Create creates a new document.
|
||||
|
||||
[docref.Create]
|
||||
|
||||
The first return value is a WriteResult, which contains the time
|
||||
at which the document was updated.
|
||||
|
||||
Create fails if the document exists. Another method, Set, either replaces an existing
|
||||
document or creates a new one.
|
||||
|
||||
[docref.Set]
|
||||
|
||||
To update some fields of an existing document, use Update. It takes a list of
|
||||
paths to update and their corresponding values.
|
||||
|
||||
[docref.Update]
|
||||
|
||||
Use DocumentRef.Delete to delete a document.
|
||||
|
||||
[docref.Delete]
|
||||
|
||||
Preconditions
|
||||
|
||||
You can condition Deletes or Updates on when a document was last changed. Specify
|
||||
these preconditions as an option to a Delete or Update method. The check and the
|
||||
write happen atomically with a single RPC.
|
||||
|
||||
[LUT-precond]
|
||||
|
||||
Here we update a doc only if it hasn't changed since we read it.
|
||||
You could also do this with a transaction.
|
||||
|
||||
To perform multiple writes at once, use a WriteBatch. Its methods chain
|
||||
for convenience.
|
||||
|
||||
WriteBatch.Commit sends the collected writes to the server, where they happen
|
||||
atomically.
|
||||
|
||||
[WriteBatch]
|
||||
|
||||
Queries
|
||||
|
||||
You can use SQL to select documents from a collection. Begin with the collection, and
|
||||
build up a query using Select, Where and other methods of Query.
|
||||
|
||||
[Query]
|
||||
|
||||
Call the Query's Documents method to get an iterator, and use it like
|
||||
the other Google Cloud Client iterators.
|
||||
|
||||
[Documents]
|
||||
|
||||
To get all the documents in a collection, you can use the collection itself
|
||||
as a query.
|
||||
|
||||
[CollQuery]
|
||||
|
||||
Transactions
|
||||
|
||||
Use a transaction to execute reads and writes atomically. All reads must happen
|
||||
before any writes. Transaction creation, commit, rollback and retry are handled for
|
||||
you by the Client.RunTransaction method; just provide a function and use the
|
||||
read and write methods of the Transaction passed to it.
|
||||
|
||||
[Transaction]
|
||||
|
||||
Authentication
|
||||
|
||||
See examples of authorization and authentication at
|
||||
https://godoc.org/cloud.google.com/go#pkg-examples.
|
||||
*/
|
||||
package firestore
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
# Copyright 2017 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# snipdoc merges code snippets from Go source files into a template to
|
||||
# produce another go file (typically doc.go).
|
||||
#
|
||||
# Call with one or more .go files and a template file.
|
||||
#
|
||||
# awk -f snipmd.awk foo.go bar.go doc.template
|
||||
#
|
||||
# In the Go files, start a snippet with
|
||||
# //[ NAME
|
||||
# and end it with
|
||||
# //]
|
||||
#
|
||||
# In the template, write
|
||||
# [NAME]
|
||||
# on a line by itself to insert the snippet NAME on that line.
|
||||
#
|
||||
# The following transformations are made to the Go code:
|
||||
# - Trailing blank lines are removed.
|
||||
# - `ELLIPSIS` and `_ = ELLIPSIS` are replaced by `...`
|
||||
|
||||
|
||||
/^[ \t]*\/\/\[/ { # start snippet in Go file
|
||||
if (inGo()) {
|
||||
if ($2 == "") {
|
||||
die("missing snippet name")
|
||||
}
|
||||
curSnip = $2
|
||||
next
|
||||
}
|
||||
}
|
||||
|
||||
/^[ \t]*\/\/]/ { # end snippet in Go file
|
||||
if (inGo()) {
|
||||
if (curSnip != "") {
|
||||
# Remove all trailing newlines.
|
||||
gsub(/[\t\n]+$/, "", snips[curSnip])
|
||||
curSnip = ""
|
||||
next
|
||||
} else {
|
||||
die("//] without corresponding //[")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ENDFILE {
|
||||
if (curSnip != "") {
|
||||
die("unclosed snippet: " curSnip)
|
||||
}
|
||||
}
|
||||
|
||||
/^\[.*\]$/ { # Snippet marker in template file.
|
||||
if (inTemplate()) {
|
||||
name = substr($1, 2, length($1)-2)
|
||||
if (snips[name] == "") {
|
||||
die("no snippet named " name)
|
||||
}
|
||||
printf("%s\n", snips[name])
|
||||
afterSnip = 1
|
||||
next
|
||||
}
|
||||
}
|
||||
|
||||
# Matches every line.
|
||||
{
|
||||
if (curSnip != "") {
|
||||
# If the first line in the snip has no indent, add the indent.
|
||||
if (snips[curSnip] == "") {
|
||||
if (index($0, "\t") == 1) {
|
||||
extraIndent = ""
|
||||
} else {
|
||||
extraIndent = "\t"
|
||||
}
|
||||
}
|
||||
|
||||
line = $0
|
||||
# Replace ELLIPSIS.
|
||||
gsub(/_ = ELLIPSIS/, "...", line)
|
||||
gsub(/ELLIPSIS/, "...", line)
|
||||
|
||||
snips[curSnip] = snips[curSnip] extraIndent line "\n"
|
||||
} else if (inTemplate()) {
|
||||
afterSnip = 0
|
||||
# Copy to output.
|
||||
print
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function inTemplate() {
|
||||
return match(FILENAME, /\.template$/)
|
||||
}
|
||||
|
||||
function inGo() {
|
||||
return match(FILENAME, /\.go$/)
|
||||
}
|
||||
|
||||
|
||||
function die(msg) {
|
||||
printf("%s:%d: %s\n", FILENAME, FNR, msg) > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
// A simple mock server.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type mockServer struct {
|
||||
pb.FirestoreServer
|
||||
|
||||
Addr string
|
||||
|
||||
reqItems []reqItem
|
||||
resps []interface{}
|
||||
}
|
||||
|
||||
type reqItem struct {
|
||||
wantReq proto.Message
|
||||
adjust func(gotReq proto.Message)
|
||||
}
|
||||
|
||||
func newMockServer() (*mockServer, error) {
|
||||
srv, err := testutil.NewServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mock := &mockServer{Addr: srv.Addr}
|
||||
pb.RegisterFirestoreServer(srv.Gsrv, mock)
|
||||
srv.Start()
|
||||
return mock, nil
|
||||
}
|
||||
|
||||
// addRPC adds a (request, response) pair to the server's list of expected
|
||||
// interactions. The server will compare the incoming request with wantReq
|
||||
// using proto.Equal. The response can be a message or an error.
|
||||
//
|
||||
// For the Listen RPC, resp should be a []interface{}, where each element
|
||||
// is either ListenResponse or an error.
|
||||
//
|
||||
// Passing nil for wantReq disables the request check.
|
||||
func (s *mockServer) addRPC(wantReq proto.Message, resp interface{}) {
|
||||
s.addRPCAdjust(wantReq, resp, nil)
|
||||
}
|
||||
|
||||
// addRPCAdjust is like addRPC, but accepts a function that can be used
|
||||
// to tweak the requests before comparison, for example to adjust for
|
||||
// randomness.
|
||||
func (s *mockServer) addRPCAdjust(wantReq proto.Message, resp interface{}, adjust func(proto.Message)) {
|
||||
s.reqItems = append(s.reqItems, reqItem{wantReq, adjust})
|
||||
s.resps = append(s.resps, resp)
|
||||
}
|
||||
|
||||
// popRPC compares the request with the next expected (request, response) pair.
|
||||
// It returns the response, or an error if the request doesn't match what
|
||||
// was expected or there are no expected rpcs.
|
||||
func (s *mockServer) popRPC(gotReq proto.Message) (interface{}, error) {
|
||||
if len(s.reqItems) == 0 {
|
||||
panic("out of RPCs")
|
||||
}
|
||||
ri := s.reqItems[0]
|
||||
s.reqItems = s.reqItems[1:]
|
||||
if ri.wantReq != nil {
|
||||
if ri.adjust != nil {
|
||||
ri.adjust(gotReq)
|
||||
}
|
||||
if !proto.Equal(gotReq, ri.wantReq) {
|
||||
return nil, fmt.Errorf("mockServer: bad request\ngot: %T\n%s\nwant: %T\n%s",
|
||||
gotReq, proto.MarshalTextString(gotReq),
|
||||
ri.wantReq, proto.MarshalTextString(ri.wantReq))
|
||||
}
|
||||
}
|
||||
resp := s.resps[0]
|
||||
s.resps = s.resps[1:]
|
||||
if err, ok := resp.(error); ok {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *mockServer) reset() {
|
||||
s.reqItems = nil
|
||||
s.resps = nil
|
||||
}
|
||||
|
||||
func (s *mockServer) GetDocument(_ context.Context, req *pb.GetDocumentRequest) (*pb.Document, error) {
|
||||
res, err := s.popRPC(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.(*pb.Document), nil
|
||||
}
|
||||
|
||||
func (s *mockServer) Commit(_ context.Context, req *pb.CommitRequest) (*pb.CommitResponse, error) {
|
||||
res, err := s.popRPC(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.(*pb.CommitResponse), nil
|
||||
}
|
||||
|
||||
func (s *mockServer) BatchGetDocuments(req *pb.BatchGetDocumentsRequest, bs pb.Firestore_BatchGetDocumentsServer) error {
|
||||
res, err := s.popRPC(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
responses := res.([]interface{})
|
||||
for _, res := range responses {
|
||||
switch res := res.(type) {
|
||||
case *pb.BatchGetDocumentsResponse:
|
||||
if err := bs.Send(res); err != nil {
|
||||
return err
|
||||
}
|
||||
case error:
|
||||
return res
|
||||
default:
|
||||
panic(fmt.Sprintf("bad response type in BatchGetDocuments: %+v", res))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockServer) RunQuery(req *pb.RunQueryRequest, qs pb.Firestore_RunQueryServer) error {
|
||||
res, err := s.popRPC(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
responses := res.([]interface{})
|
||||
for _, res := range responses {
|
||||
switch res := res.(type) {
|
||||
case *pb.RunQueryResponse:
|
||||
if err := qs.Send(res); err != nil {
|
||||
return err
|
||||
}
|
||||
case error:
|
||||
return res
|
||||
default:
|
||||
panic(fmt.Sprintf("bad response type in RunQuery: %+v", res))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockServer) BeginTransaction(_ context.Context, req *pb.BeginTransactionRequest) (*pb.BeginTransactionResponse, error) {
|
||||
res, err := s.popRPC(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.(*pb.BeginTransactionResponse), nil
|
||||
}
|
||||
|
||||
func (s *mockServer) Rollback(_ context.Context, req *pb.RollbackRequest) (*empty.Empty, error) {
|
||||
res, err := s.popRPC(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.(*empty.Empty), nil
|
||||
}
|
||||
|
||||
func (s *mockServer) Listen(stream pb.Firestore_ListenServer) error {
|
||||
req, err := stream.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
responses, err := s.popRPC(req)
|
||||
if err != nil {
|
||||
if status.Code(err) == codes.Unknown && strings.Contains(err.Error(), "mockServer") {
|
||||
// The stream will retry on Unknown, but we don't want that to happen if
|
||||
// the error comes from us.
|
||||
panic(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, res := range responses.([]interface{}) {
|
||||
if err, ok := res.(error); ok {
|
||||
return err
|
||||
}
|
||||
if err := stream.Send(res.(*pb.ListenResponse)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
)
|
||||
|
||||
// A Precondition modifies a Firestore update or delete operation.
|
||||
type Precondition interface {
|
||||
// Returns the corresponding Precondition proto.
|
||||
preconditionProto() (*pb.Precondition, error)
|
||||
}
|
||||
|
||||
// Exists is a Precondition that checks for the existence of a resource before
|
||||
// writing to it. If the check fails, the write does not occur.
|
||||
var Exists Precondition
|
||||
|
||||
func init() {
|
||||
// Initialize here so godoc doesn't show the internal value.
|
||||
Exists = exists(true)
|
||||
}
|
||||
|
||||
type exists bool
|
||||
|
||||
func (e exists) preconditionProto() (*pb.Precondition, error) {
|
||||
return &pb.Precondition{
|
||||
ConditionType: &pb.Precondition_Exists{bool(e)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e exists) String() string {
|
||||
if e {
|
||||
return "Exists"
|
||||
} else {
|
||||
return "DoesNotExist"
|
||||
}
|
||||
}
|
||||
|
||||
// LastUpdateTime returns a Precondition that checks that a resource must exist and
|
||||
// must have last been updated at the given time. If the check fails, the write
|
||||
// does not occur.
|
||||
func LastUpdateTime(t time.Time) Precondition { return lastUpdateTime(t) }
|
||||
|
||||
type lastUpdateTime time.Time
|
||||
|
||||
func (u lastUpdateTime) preconditionProto() (*pb.Precondition, error) {
|
||||
ts, err := ptypes.TimestampProto(time.Time(u))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.Precondition{
|
||||
ConditionType: &pb.Precondition_UpdateTime{ts},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u lastUpdateTime) String() string { return fmt.Sprintf("LastUpdateTime(%s)", time.Time(u)) }
|
||||
|
||||
func processPreconditionsForDelete(preconds []Precondition) (*pb.Precondition, error) {
|
||||
// At most one option permitted.
|
||||
switch len(preconds) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
return preconds[0].preconditionProto()
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: conflicting preconditions: %+v", preconds)
|
||||
}
|
||||
}
|
||||
|
||||
func processPreconditionsForUpdate(preconds []Precondition) (*pb.Precondition, error) {
|
||||
// At most one option permitted, and it cannot be Exists.
|
||||
switch len(preconds) {
|
||||
case 0:
|
||||
// If the user doesn't provide any options, default to Exists(true).
|
||||
return exists(true).preconditionProto()
|
||||
case 1:
|
||||
if _, ok := preconds[0].(exists); ok {
|
||||
return nil, errors.New("Cannot use Exists with Update")
|
||||
}
|
||||
return preconds[0].preconditionProto()
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: conflicting preconditions: %+v", preconds)
|
||||
}
|
||||
}
|
||||
|
||||
func processPreconditionsForVerify(preconds []Precondition) (*pb.Precondition, error) {
|
||||
// At most one option permitted.
|
||||
switch len(preconds) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
return preconds[0].preconditionProto()
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: conflicting preconditions: %+v", preconds)
|
||||
}
|
||||
}
|
||||
|
||||
// A SetOption modifies a Firestore set operation.
|
||||
type SetOption interface {
|
||||
fieldPaths() (fps []FieldPath, all bool, err error)
|
||||
}
|
||||
|
||||
// MergeAll is a SetOption that causes all the field paths given in the data argument
|
||||
// to Set to be overwritten. It is not supported for struct data.
|
||||
var MergeAll SetOption = merge{all: true}
|
||||
|
||||
// Merge returns a SetOption that causes only the given field paths to be
|
||||
// overwritten. Other fields on the existing document will be untouched. It is an
|
||||
// error if a provided field path does not refer to a value in the data passed to
|
||||
// Set.
|
||||
func Merge(fps ...FieldPath) SetOption {
|
||||
for _, fp := range fps {
|
||||
if err := fp.validate(); err != nil {
|
||||
return merge{err: err}
|
||||
}
|
||||
}
|
||||
return merge{paths: fps}
|
||||
}
|
||||
|
||||
type merge struct {
|
||||
all bool
|
||||
paths []FieldPath
|
||||
err error
|
||||
}
|
||||
|
||||
func (m merge) String() string {
|
||||
if m.err != nil {
|
||||
return fmt.Sprintf("<Merge error: %v>", m.err)
|
||||
}
|
||||
if m.all {
|
||||
return "MergeAll"
|
||||
}
|
||||
return fmt.Sprintf("Merge(%+v)", m.paths)
|
||||
}
|
||||
|
||||
func (m merge) fieldPaths() (fps []FieldPath, all bool, err error) {
|
||||
if m.err != nil {
|
||||
return nil, false, m.err
|
||||
}
|
||||
if err := checkNoDupOrPrefix(m.paths); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if m.all {
|
||||
return nil, true, nil
|
||||
}
|
||||
return m.paths, false, nil
|
||||
}
|
||||
|
||||
func processSetOptions(opts []SetOption) (fps []FieldPath, all bool, err error) {
|
||||
switch len(opts) {
|
||||
case 0:
|
||||
return nil, false, nil
|
||||
case 1:
|
||||
return opts[0].fieldPaths()
|
||||
default:
|
||||
return nil, false, fmt.Errorf("conflicting options: %+v", opts)
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
)
|
||||
|
||||
func TestProcessPreconditionsForVerify(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in []Precondition
|
||||
want *pb.Precondition
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
in: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
in: []Precondition{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
in: []Precondition{Exists},
|
||||
want: &pb.Precondition{&pb.Precondition_Exists{true}},
|
||||
},
|
||||
{
|
||||
in: []Precondition{LastUpdateTime(aTime)},
|
||||
want: &pb.Precondition{&pb.Precondition_UpdateTime{aTimestamp}},
|
||||
},
|
||||
{
|
||||
in: []Precondition{Exists, LastUpdateTime(aTime)},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
in: []Precondition{Exists, Exists},
|
||||
wantErr: true,
|
||||
},
|
||||
} {
|
||||
got, err := processPreconditionsForVerify(test.in)
|
||||
switch {
|
||||
case test.wantErr && err == nil:
|
||||
t.Errorf("%v: got nil, want error", test.in)
|
||||
case !test.wantErr && err != nil:
|
||||
t.Errorf("%v: got <%v>, want no error", test.in, err)
|
||||
case !test.wantErr && err == nil && !testEqual(got, test.want):
|
||||
t.Errorf("%v: got %+v, want %v", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPreconditionsForDelete(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in []Precondition
|
||||
want *pb.Precondition
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
in: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
in: []Precondition{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
in: []Precondition{Exists},
|
||||
want: &pb.Precondition{&pb.Precondition_Exists{true}},
|
||||
},
|
||||
{
|
||||
in: []Precondition{LastUpdateTime(aTime)},
|
||||
want: &pb.Precondition{&pb.Precondition_UpdateTime{aTimestamp}},
|
||||
},
|
||||
{
|
||||
in: []Precondition{Exists, LastUpdateTime(aTime)},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
in: []Precondition{Exists, Exists},
|
||||
wantErr: true,
|
||||
},
|
||||
} {
|
||||
got, err := processPreconditionsForDelete(test.in)
|
||||
switch {
|
||||
case test.wantErr && err == nil:
|
||||
t.Errorf("%v: got nil, want error", test.in)
|
||||
case !test.wantErr && err != nil:
|
||||
t.Errorf("%v: got <%v>, want no error", test.in, err)
|
||||
case !test.wantErr && err == nil && !testEqual(got, test.want):
|
||||
t.Errorf("%v: got %+v, want %v", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPreconditionsForUpdate(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in []Precondition
|
||||
want *pb.Precondition
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
in: nil,
|
||||
want: &pb.Precondition{&pb.Precondition_Exists{true}},
|
||||
},
|
||||
{
|
||||
in: []Precondition{},
|
||||
want: &pb.Precondition{&pb.Precondition_Exists{true}},
|
||||
},
|
||||
|
||||
{
|
||||
in: []Precondition{Exists},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
in: []Precondition{LastUpdateTime(aTime)},
|
||||
want: &pb.Precondition{&pb.Precondition_UpdateTime{aTimestamp}},
|
||||
},
|
||||
{
|
||||
in: []Precondition{Exists, LastUpdateTime(aTime)},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
in: []Precondition{Exists, Exists},
|
||||
wantErr: true,
|
||||
},
|
||||
} {
|
||||
got, err := processPreconditionsForUpdate(test.in)
|
||||
switch {
|
||||
case test.wantErr && err == nil:
|
||||
t.Errorf("%v: got nil, want error", test.in)
|
||||
case !test.wantErr && err != nil:
|
||||
t.Errorf("%v: got <%v>, want no error", test.in, err)
|
||||
case !test.wantErr && err == nil && !testEqual(got, test.want):
|
||||
t.Errorf("%v: got %+v, want %v", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
tspb "github.com/golang/protobuf/ptypes/timestamp"
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
)
|
||||
|
||||
// Returns a negative number, zero, or a positive number depending on whether a is
|
||||
// less than, equal to, or greater than b according to Firestore's ordering of
|
||||
// values.
|
||||
func compareValues(a, b *pb.Value) int {
|
||||
ta := typeOrder(a)
|
||||
tb := typeOrder(b)
|
||||
if ta != tb {
|
||||
return compareInt64s(int64(ta), int64(tb))
|
||||
}
|
||||
switch a := a.ValueType.(type) {
|
||||
case *pb.Value_NullValue:
|
||||
return 0 // nulls are equal
|
||||
|
||||
case *pb.Value_BooleanValue:
|
||||
av := a.BooleanValue
|
||||
bv := b.GetBooleanValue()
|
||||
switch {
|
||||
case av && !bv:
|
||||
return 1
|
||||
case bv && !av:
|
||||
return -1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
|
||||
case *pb.Value_IntegerValue:
|
||||
return compareNumbers(float64(a.IntegerValue), toFloat(b))
|
||||
|
||||
case *pb.Value_DoubleValue:
|
||||
return compareNumbers(a.DoubleValue, toFloat(b))
|
||||
|
||||
case *pb.Value_TimestampValue:
|
||||
return compareTimestamps(a.TimestampValue, b.GetTimestampValue())
|
||||
|
||||
case *pb.Value_StringValue:
|
||||
return strings.Compare(a.StringValue, b.GetStringValue())
|
||||
|
||||
case *pb.Value_BytesValue:
|
||||
return bytes.Compare(a.BytesValue, b.GetBytesValue())
|
||||
|
||||
case *pb.Value_ReferenceValue:
|
||||
return compareReferences(a.ReferenceValue, b.GetReferenceValue())
|
||||
|
||||
case *pb.Value_GeoPointValue:
|
||||
ag := a.GeoPointValue
|
||||
bg := b.GetGeoPointValue()
|
||||
if ag.Latitude != bg.Latitude {
|
||||
return compareFloat64s(ag.Latitude, bg.Latitude)
|
||||
}
|
||||
return compareFloat64s(ag.Longitude, bg.Longitude)
|
||||
|
||||
case *pb.Value_ArrayValue:
|
||||
return compareArrays(a.ArrayValue.Values, b.GetArrayValue().Values)
|
||||
|
||||
case *pb.Value_MapValue:
|
||||
return compareMaps(a.MapValue.Fields, b.GetMapValue().Fields)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("bad value type: %v", a))
|
||||
}
|
||||
}
|
||||
|
||||
// Treats NaN as less than any non-NaN.
|
||||
func compareNumbers(a, b float64) int {
|
||||
switch {
|
||||
case math.IsNaN(a):
|
||||
if math.IsNaN(b) {
|
||||
return 0
|
||||
}
|
||||
return -1
|
||||
case math.IsNaN(b):
|
||||
return 1
|
||||
default:
|
||||
return compareFloat64s(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Return v as a float64, assuming it's an Integer or Double.
|
||||
func toFloat(v *pb.Value) float64 {
|
||||
if x, ok := v.ValueType.(*pb.Value_IntegerValue); ok {
|
||||
return float64(x.IntegerValue)
|
||||
}
|
||||
return v.GetDoubleValue()
|
||||
}
|
||||
|
||||
func compareTimestamps(a, b *tspb.Timestamp) int {
|
||||
if c := compareInt64s(a.Seconds, b.Seconds); c != 0 {
|
||||
return c
|
||||
}
|
||||
return compareInt64s(int64(a.Nanos), int64(b.Nanos))
|
||||
}
|
||||
|
||||
func compareReferences(a, b string) int {
|
||||
// Compare path components lexicographically.
|
||||
pa := strings.Split(a, "/")
|
||||
pb := strings.Split(b, "/")
|
||||
return compareSequences(len(pa), len(pb), func(i int) int {
|
||||
return strings.Compare(pa[i], pb[i])
|
||||
})
|
||||
}
|
||||
|
||||
func compareArrays(a, b []*pb.Value) int {
|
||||
return compareSequences(len(a), len(b), func(i int) int {
|
||||
return compareValues(a[i], b[i])
|
||||
})
|
||||
}
|
||||
|
||||
func compareMaps(a, b map[string]*pb.Value) int {
|
||||
sortedKeys := func(m map[string]*pb.Value) []string {
|
||||
var ks []string
|
||||
for k := range m {
|
||||
ks = append(ks, k)
|
||||
}
|
||||
sort.Strings(ks)
|
||||
return ks
|
||||
}
|
||||
|
||||
aks := sortedKeys(a)
|
||||
bks := sortedKeys(b)
|
||||
return compareSequences(len(aks), len(bks), func(i int) int {
|
||||
if c := strings.Compare(aks[i], bks[i]); c != 0 {
|
||||
return c
|
||||
}
|
||||
k := aks[i]
|
||||
return compareValues(a[k], b[k])
|
||||
})
|
||||
}
|
||||
|
||||
func compareSequences(len1, len2 int, compare func(int) int) int {
|
||||
for i := 0; i < len1 && i < len2; i++ {
|
||||
if c := compare(i); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return compareInt64s(int64(len1), int64(len2))
|
||||
}
|
||||
|
||||
func compareFloat64s(a, b float64) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func compareInt64s(a, b int64) int {
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Return an integer corresponding to the type of value stored in v, such that
|
||||
// comparing the resulting integers gives the Firestore ordering for types.
|
||||
func typeOrder(v *pb.Value) int {
|
||||
switch v.ValueType.(type) {
|
||||
case *pb.Value_NullValue:
|
||||
return 0
|
||||
case *pb.Value_BooleanValue:
|
||||
return 1
|
||||
case *pb.Value_IntegerValue:
|
||||
return 2
|
||||
case *pb.Value_DoubleValue:
|
||||
return 2
|
||||
case *pb.Value_TimestampValue:
|
||||
return 3
|
||||
case *pb.Value_StringValue:
|
||||
return 4
|
||||
case *pb.Value_BytesValue:
|
||||
return 5
|
||||
case *pb.Value_ReferenceValue:
|
||||
return 6
|
||||
case *pb.Value_GeoPointValue:
|
||||
return 7
|
||||
case *pb.Value_ArrayValue:
|
||||
return 8
|
||||
case *pb.Value_MapValue:
|
||||
return 9
|
||||
default:
|
||||
panic(fmt.Sprintf("bad value type: %v", v))
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
"google.golang.org/genproto/googleapis/type/latlng"
|
||||
)
|
||||
|
||||
func TestCompareValues(t *testing.T) {
|
||||
// Ordered list of values.
|
||||
vals := []*pb.Value{
|
||||
nullValue,
|
||||
boolval(false),
|
||||
boolval(true),
|
||||
floatval(math.NaN()),
|
||||
floatval(math.Inf(-1)),
|
||||
floatval(-math.MaxFloat64),
|
||||
int64val(math.MinInt64),
|
||||
floatval(-1.1),
|
||||
intval(-1),
|
||||
intval(0),
|
||||
floatval(math.SmallestNonzeroFloat64),
|
||||
intval(1),
|
||||
floatval(1.1),
|
||||
intval(2),
|
||||
int64val(math.MaxInt64),
|
||||
floatval(math.MaxFloat64),
|
||||
floatval(math.Inf(1)),
|
||||
tsval(time.Date(2016, 5, 20, 10, 20, 0, 0, time.UTC)),
|
||||
tsval(time.Date(2016, 10, 21, 15, 32, 0, 0, time.UTC)),
|
||||
strval(""),
|
||||
strval("\u0000\ud7ff\ue000\uffff"),
|
||||
strval("(╯°□°)╯︵ ┻━┻"),
|
||||
strval("a"),
|
||||
strval("abc def"),
|
||||
strval("e\u0301b"),
|
||||
strval("æ"),
|
||||
strval("\u00e9a"),
|
||||
bytesval([]byte{}),
|
||||
bytesval([]byte{0}),
|
||||
bytesval([]byte{0, 1, 2, 3, 4}),
|
||||
bytesval([]byte{0, 1, 2, 4, 3}),
|
||||
bytesval([]byte{255}),
|
||||
refval("projects/p1/databases/d1/documents/c1/doc1"),
|
||||
refval("projects/p1/databases/d1/documents/c1/doc2"),
|
||||
refval("projects/p1/databases/d1/documents/c1/doc2/c2/doc1"),
|
||||
refval("projects/p1/databases/d1/documents/c1/doc2/c2/doc2"),
|
||||
refval("projects/p1/databases/d1/documents/c10/doc1"),
|
||||
refval("projects/p1/databases/dkkkkklkjnjkkk1/documents/c2/doc1"),
|
||||
refval("projects/p2/databases/d2/documents/c1/doc1"),
|
||||
refval("projects/p2/databases/d2/documents/c1-/doc1"),
|
||||
geopoint(-90, -180),
|
||||
geopoint(-90, 0),
|
||||
geopoint(-90, 180),
|
||||
geopoint(0, -180),
|
||||
geopoint(0, 0),
|
||||
geopoint(0, 180),
|
||||
geopoint(1, -180),
|
||||
geopoint(1, 0),
|
||||
geopoint(1, 180),
|
||||
geopoint(90, -180),
|
||||
geopoint(90, 0),
|
||||
geopoint(90, 180),
|
||||
arrayval(),
|
||||
arrayval(strval("bar")),
|
||||
arrayval(strval("foo")),
|
||||
arrayval(strval("foo"), intval(1)),
|
||||
arrayval(strval("foo"), intval(2)),
|
||||
arrayval(strval("foo"), strval("0")),
|
||||
mapval(map[string]*pb.Value{"bar": intval(0)}),
|
||||
mapval(map[string]*pb.Value{"bar": intval(0), "foo": intval(1)}),
|
||||
mapval(map[string]*pb.Value{"foo": intval(1)}),
|
||||
mapval(map[string]*pb.Value{"foo": intval(2)}),
|
||||
mapval(map[string]*pb.Value{"foo": strval("0")}),
|
||||
}
|
||||
|
||||
for i, v1 := range vals {
|
||||
if got := compareValues(v1, v1); got != 0 {
|
||||
t.Errorf("compare(%v, %v) == %d, want 0", v1, v1, got)
|
||||
}
|
||||
for _, v2 := range vals[i+1:] {
|
||||
if got := compareValues(v1, v2); got != -1 {
|
||||
t.Errorf("compare(%v, %v) == %d, want -1", v1, v2, got)
|
||||
}
|
||||
if got := compareValues(v2, v1); got != 1 {
|
||||
t.Errorf("compare(%v, %v) == %d, want 1", v1, v2, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Integers and Doubles order the same.
|
||||
n1 := intval(17)
|
||||
n2 := floatval(17)
|
||||
if got := compareValues(n1, n2); got != 0 {
|
||||
t.Errorf("compare(%v, %v) == %d, want 0", n1, n2, got)
|
||||
}
|
||||
}
|
||||
|
||||
func geopoint(lat, lng float64) *pb.Value {
|
||||
return geoval(&latlng.LatLng{Latitude: lat, Longitude: lng})
|
||||
}
|
||||
+757
@@ -0,0 +1,757 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
|
||||
"cloud.google.com/go/internal/btree"
|
||||
"github.com/golang/protobuf/ptypes/wrappers"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
// Query represents a Firestore query.
|
||||
//
|
||||
// Query values are immutable. Each Query method creates
|
||||
// a new Query; it does not modify the old.
|
||||
type Query struct {
|
||||
c *Client
|
||||
parentPath string // path of the collection's parent
|
||||
collectionID string
|
||||
selection []FieldPath
|
||||
filters []filter
|
||||
orders []order
|
||||
offset int32
|
||||
limit *wrappers.Int32Value
|
||||
startVals, endVals []interface{}
|
||||
startDoc, endDoc *DocumentSnapshot
|
||||
startBefore, endBefore bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (q *Query) collectionPath() string {
|
||||
return q.parentPath + "/documents/" + q.collectionID
|
||||
}
|
||||
|
||||
// DocumentID is the special field name representing the ID of a document
|
||||
// in queries.
|
||||
const DocumentID = "__name__"
|
||||
|
||||
// Select returns a new Query that specifies the paths
|
||||
// to return from the result documents.
|
||||
// Each path argument can be a single field or a dot-separated sequence of
|
||||
// fields, and must not contain any of the runes "˜*/[]".
|
||||
func (q Query) Select(paths ...string) Query {
|
||||
var fps []FieldPath
|
||||
for _, s := range paths {
|
||||
fp, err := parseDotSeparatedString(s)
|
||||
if err != nil {
|
||||
q.err = err
|
||||
return q
|
||||
}
|
||||
fps = append(fps, fp)
|
||||
}
|
||||
return q.SelectPaths(fps...)
|
||||
}
|
||||
|
||||
// SelectPaths returns a new Query that specifies the field paths
|
||||
// to return from the result documents.
|
||||
func (q Query) SelectPaths(fieldPaths ...FieldPath) Query {
|
||||
if len(fieldPaths) == 0 {
|
||||
q.selection = []FieldPath{{DocumentID}}
|
||||
} else {
|
||||
q.selection = fieldPaths
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
// Where returns a new Query that filters the set of results.
|
||||
// A Query can have multiple filters.
|
||||
// The path argument can be a single field or a dot-separated sequence of
|
||||
// fields, and must not contain any of the runes "˜*/[]".
|
||||
// The op argument must be one of "==", "<", "<=", ">" or ">=".
|
||||
func (q Query) Where(path, op string, value interface{}) Query {
|
||||
fp, err := parseDotSeparatedString(path)
|
||||
if err != nil {
|
||||
q.err = err
|
||||
return q
|
||||
}
|
||||
q.filters = append(append([]filter(nil), q.filters...), filter{fp, op, value})
|
||||
return q
|
||||
}
|
||||
|
||||
// WherePath returns a new Query that filters the set of results.
|
||||
// A Query can have multiple filters.
|
||||
// The op argument must be one of "==", "<", "<=", ">" or ">=".
|
||||
func (q Query) WherePath(fp FieldPath, op string, value interface{}) Query {
|
||||
q.filters = append(append([]filter(nil), q.filters...), filter{fp, op, value})
|
||||
return q
|
||||
}
|
||||
|
||||
// Direction is the sort direction for result ordering.
|
||||
type Direction int32
|
||||
|
||||
const (
|
||||
// Asc sorts results from smallest to largest.
|
||||
Asc Direction = Direction(pb.StructuredQuery_ASCENDING)
|
||||
|
||||
// Desc sorts results from largest to smallest.
|
||||
Desc Direction = Direction(pb.StructuredQuery_DESCENDING)
|
||||
)
|
||||
|
||||
// OrderBy returns a new Query that specifies the order in which results are
|
||||
// returned. A Query can have multiple OrderBy/OrderByPath specifications. OrderBy
|
||||
// appends the specification to the list of existing ones.
|
||||
//
|
||||
// The path argument can be a single field or a dot-separated sequence of
|
||||
// fields, and must not contain any of the runes "˜*/[]".
|
||||
//
|
||||
// To order by document name, use the special field path DocumentID.
|
||||
func (q Query) OrderBy(path string, dir Direction) Query {
|
||||
fp, err := parseDotSeparatedString(path)
|
||||
if err != nil {
|
||||
q.err = err
|
||||
return q
|
||||
}
|
||||
q.orders = append(q.copyOrders(), order{fp, dir})
|
||||
return q
|
||||
}
|
||||
|
||||
// OrderByPath returns a new Query that specifies the order in which results are
|
||||
// returned. A Query can have multiple OrderBy/OrderByPath specifications.
|
||||
// OrderByPath appends the specification to the list of existing ones.
|
||||
func (q Query) OrderByPath(fp FieldPath, dir Direction) Query {
|
||||
q.orders = append(q.copyOrders(), order{fp, dir})
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *Query) copyOrders() []order {
|
||||
return append([]order(nil), q.orders...)
|
||||
}
|
||||
|
||||
// Offset returns a new Query that specifies the number of initial results to skip.
|
||||
// It must not be negative.
|
||||
func (q Query) Offset(n int) Query {
|
||||
q.offset = trunc32(n)
|
||||
return q
|
||||
}
|
||||
|
||||
// Limit returns a new Query that specifies the maximum number of results to return.
|
||||
// It must not be negative.
|
||||
func (q Query) Limit(n int) Query {
|
||||
q.limit = &wrappers.Int32Value{trunc32(n)}
|
||||
return q
|
||||
}
|
||||
|
||||
// StartAt returns a new Query that specifies that results should start at
|
||||
// the document with the given field values.
|
||||
//
|
||||
// If StartAt is called with a single DocumentSnapshot, its field values are used.
|
||||
// The DocumentSnapshot must have all the fields mentioned in the OrderBy clauses.
|
||||
//
|
||||
// Otherwise, StartAt should be called with one field value for each OrderBy clause,
|
||||
// in the order that they appear. For example, in
|
||||
// q.OrderBy("X", Asc).OrderBy("Y", Desc).StartAt(1, 2)
|
||||
// results will begin at the first document where X = 1 and Y = 2.
|
||||
//
|
||||
// If an OrderBy call uses the special DocumentID field path, the corresponding value
|
||||
// should be the document ID relative to the query's collection. For example, to
|
||||
// start at the document "NewYork" in the "States" collection, write
|
||||
//
|
||||
// client.Collection("States").OrderBy(DocumentID, firestore.Asc).StartAt("NewYork")
|
||||
//
|
||||
// Calling StartAt overrides a previous call to StartAt or StartAfter.
|
||||
func (q Query) StartAt(docSnapshotOrFieldValues ...interface{}) Query {
|
||||
q.startBefore = true
|
||||
q.startVals, q.startDoc, q.err = q.processCursorArg("StartAt", docSnapshotOrFieldValues)
|
||||
return q
|
||||
}
|
||||
|
||||
// StartAfter returns a new Query that specifies that results should start just after
|
||||
// the document with the given field values. See Query.StartAt for more information.
|
||||
//
|
||||
// Calling StartAfter overrides a previous call to StartAt or StartAfter.
|
||||
func (q Query) StartAfter(docSnapshotOrFieldValues ...interface{}) Query {
|
||||
q.startBefore = false
|
||||
q.startVals, q.startDoc, q.err = q.processCursorArg("StartAfter", docSnapshotOrFieldValues)
|
||||
return q
|
||||
}
|
||||
|
||||
// EndAt returns a new Query that specifies that results should end at the
|
||||
// document with the given field values. See Query.StartAt for more information.
|
||||
//
|
||||
// Calling EndAt overrides a previous call to EndAt or EndBefore.
|
||||
func (q Query) EndAt(docSnapshotOrFieldValues ...interface{}) Query {
|
||||
q.endBefore = false
|
||||
q.endVals, q.endDoc, q.err = q.processCursorArg("EndAt", docSnapshotOrFieldValues)
|
||||
return q
|
||||
}
|
||||
|
||||
// EndBefore returns a new Query that specifies that results should end just before
|
||||
// the document with the given field values. See Query.StartAt for more information.
|
||||
//
|
||||
// Calling EndBefore overrides a previous call to EndAt or EndBefore.
|
||||
func (q Query) EndBefore(docSnapshotOrFieldValues ...interface{}) Query {
|
||||
q.endBefore = true
|
||||
q.endVals, q.endDoc, q.err = q.processCursorArg("EndBefore", docSnapshotOrFieldValues)
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *Query) processCursorArg(name string, docSnapshotOrFieldValues []interface{}) ([]interface{}, *DocumentSnapshot, error) {
|
||||
for _, e := range docSnapshotOrFieldValues {
|
||||
if ds, ok := e.(*DocumentSnapshot); ok {
|
||||
if len(docSnapshotOrFieldValues) == 1 {
|
||||
return nil, ds, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("firestore: a document snapshot must be the only argument to %s", name)
|
||||
}
|
||||
}
|
||||
return docSnapshotOrFieldValues, nil, nil
|
||||
}
|
||||
|
||||
func (q Query) query() *Query { return &q }
|
||||
|
||||
func (q Query) toProto() (*pb.StructuredQuery, error) {
|
||||
if q.err != nil {
|
||||
return nil, q.err
|
||||
}
|
||||
if q.collectionID == "" {
|
||||
return nil, errors.New("firestore: query created without CollectionRef")
|
||||
}
|
||||
p := &pb.StructuredQuery{
|
||||
From: []*pb.StructuredQuery_CollectionSelector{{CollectionId: q.collectionID}},
|
||||
Offset: q.offset,
|
||||
Limit: q.limit,
|
||||
}
|
||||
if len(q.selection) > 0 {
|
||||
p.Select = &pb.StructuredQuery_Projection{}
|
||||
for _, fp := range q.selection {
|
||||
if err := fp.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Select.Fields = append(p.Select.Fields, fref(fp))
|
||||
}
|
||||
}
|
||||
// If there is only filter, use it directly. Otherwise, construct
|
||||
// a CompositeFilter.
|
||||
if len(q.filters) == 1 {
|
||||
pf, err := q.filters[0].toProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Where = pf
|
||||
} else if len(q.filters) > 1 {
|
||||
cf := &pb.StructuredQuery_CompositeFilter{
|
||||
Op: pb.StructuredQuery_CompositeFilter_AND,
|
||||
}
|
||||
p.Where = &pb.StructuredQuery_Filter{
|
||||
FilterType: &pb.StructuredQuery_Filter_CompositeFilter{cf},
|
||||
}
|
||||
for _, f := range q.filters {
|
||||
pf, err := f.toProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cf.Filters = append(cf.Filters, pf)
|
||||
}
|
||||
}
|
||||
orders := q.orders
|
||||
if q.startDoc != nil || q.endDoc != nil {
|
||||
orders = q.adjustOrders()
|
||||
}
|
||||
for _, ord := range orders {
|
||||
po, err := ord.toProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.OrderBy = append(p.OrderBy, po)
|
||||
}
|
||||
|
||||
cursor, err := q.toCursor(q.startVals, q.startDoc, q.startBefore, orders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.StartAt = cursor
|
||||
cursor, err = q.toCursor(q.endVals, q.endDoc, q.endBefore, orders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.EndAt = cursor
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// If there is a start/end that uses a Document Snapshot, we may need to adjust the OrderBy
|
||||
// clauses that the user provided: we add OrderBy(__name__) if it isn't already present, and
|
||||
// we make sure we don't invalidate the original query by adding an OrderBy for inequality filters.
|
||||
func (q *Query) adjustOrders() []order {
|
||||
// If the user is already ordering by document ID, don't change anything.
|
||||
for _, ord := range q.orders {
|
||||
if ord.isDocumentID() {
|
||||
return q.orders
|
||||
}
|
||||
}
|
||||
// If there are OrderBy clauses, append an OrderBy(DocumentID), using the direction of the last OrderBy clause.
|
||||
if len(q.orders) > 0 {
|
||||
return append(q.copyOrders(), order{
|
||||
fieldPath: FieldPath{DocumentID},
|
||||
dir: q.orders[len(q.orders)-1].dir,
|
||||
})
|
||||
}
|
||||
// If there are no OrderBy clauses but there is an inequality, add an OrderBy clause
|
||||
// for the field of the first inequality.
|
||||
var orders []order
|
||||
for _, f := range q.filters {
|
||||
if f.op != "==" {
|
||||
orders = []order{{fieldPath: f.fieldPath, dir: Asc}}
|
||||
break
|
||||
}
|
||||
}
|
||||
// Add an ascending OrderBy(DocumentID).
|
||||
return append(orders, order{fieldPath: FieldPath{DocumentID}, dir: Asc})
|
||||
}
|
||||
|
||||
func (q *Query) toCursor(fieldValues []interface{}, ds *DocumentSnapshot, before bool, orders []order) (*pb.Cursor, error) {
|
||||
var vals []*pb.Value
|
||||
var err error
|
||||
if ds != nil {
|
||||
vals, err = q.docSnapshotToCursorValues(ds, orders)
|
||||
} else if len(fieldValues) != 0 {
|
||||
vals, err = q.fieldValuesToCursorValues(fieldValues)
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.Cursor{Values: vals, Before: before}, nil
|
||||
}
|
||||
|
||||
// toPositionValues converts the field values to protos.
|
||||
func (q *Query) fieldValuesToCursorValues(fieldValues []interface{}) ([]*pb.Value, error) {
|
||||
if len(fieldValues) != len(q.orders) {
|
||||
return nil, errors.New("firestore: number of field values in StartAt/StartAfter/EndAt/EndBefore does not match number of OrderBy fields")
|
||||
}
|
||||
vals := make([]*pb.Value, len(fieldValues))
|
||||
var err error
|
||||
for i, ord := range q.orders {
|
||||
fval := fieldValues[i]
|
||||
if ord.isDocumentID() {
|
||||
// TODO(jba): support DocumentRefs as well as strings.
|
||||
// TODO(jba): error if document ref does not belong to the right collection.
|
||||
docID, ok := fval.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("firestore: expected doc ID for DocumentID field, got %T", fval)
|
||||
}
|
||||
vals[i] = &pb.Value{&pb.Value_ReferenceValue{q.collectionPath() + "/" + docID}}
|
||||
} else {
|
||||
var sawTransform bool
|
||||
vals[i], sawTransform, err = toProtoValue(reflect.ValueOf(fval))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sawTransform {
|
||||
return nil, errors.New("firestore: ServerTimestamp disallowed in query value")
|
||||
}
|
||||
}
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func (q *Query) docSnapshotToCursorValues(ds *DocumentSnapshot, orders []order) ([]*pb.Value, error) {
|
||||
// TODO(jba): error if doc snap does not belong to the right collection.
|
||||
vals := make([]*pb.Value, len(orders))
|
||||
for i, ord := range orders {
|
||||
if ord.isDocumentID() {
|
||||
dp, qp := ds.Ref.Parent.Path, q.collectionPath()
|
||||
if dp != qp {
|
||||
return nil, fmt.Errorf("firestore: document snapshot for %s passed to query on %s", dp, qp)
|
||||
}
|
||||
vals[i] = &pb.Value{&pb.Value_ReferenceValue{ds.Ref.Path}}
|
||||
} else {
|
||||
val, err := valueAtPath(ord.fieldPath, ds.proto.Fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vals[i] = val
|
||||
}
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
// Returns a function that compares DocumentSnapshots according to q's ordering.
|
||||
func (q Query) compareFunc() func(d1, d2 *DocumentSnapshot) (int, error) {
|
||||
// Add implicit sorting by name, using the last specified direction.
|
||||
lastDir := Asc
|
||||
if len(q.orders) > 0 {
|
||||
lastDir = q.orders[len(q.orders)-1].dir
|
||||
}
|
||||
orders := append(q.copyOrders(), order{[]string{DocumentID}, lastDir})
|
||||
return func(d1, d2 *DocumentSnapshot) (int, error) {
|
||||
for _, ord := range orders {
|
||||
var cmp int
|
||||
if len(ord.fieldPath) == 1 && ord.fieldPath[0] == DocumentID {
|
||||
cmp = compareReferences(d1.Ref.Path, d2.Ref.Path)
|
||||
} else {
|
||||
v1, err := valueAtPath(ord.fieldPath, d1.proto.Fields)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v2, err := valueAtPath(ord.fieldPath, d2.proto.Fields)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
cmp = compareValues(v1, v2)
|
||||
}
|
||||
if cmp != 0 {
|
||||
if ord.dir == Desc {
|
||||
cmp = -cmp
|
||||
}
|
||||
return cmp, nil
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
type filter struct {
|
||||
fieldPath FieldPath
|
||||
op string
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (f filter) toProto() (*pb.StructuredQuery_Filter, error) {
|
||||
if err := f.fieldPath.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uop, ok := unaryOpFor(f.value); ok {
|
||||
if f.op != "==" {
|
||||
return nil, fmt.Errorf("firestore: must use '==' when comparing %v", f.value)
|
||||
}
|
||||
return &pb.StructuredQuery_Filter{
|
||||
FilterType: &pb.StructuredQuery_Filter_UnaryFilter{
|
||||
UnaryFilter: &pb.StructuredQuery_UnaryFilter{
|
||||
OperandType: &pb.StructuredQuery_UnaryFilter_Field{
|
||||
Field: fref(f.fieldPath),
|
||||
},
|
||||
Op: uop,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
var op pb.StructuredQuery_FieldFilter_Operator
|
||||
switch f.op {
|
||||
case "<":
|
||||
op = pb.StructuredQuery_FieldFilter_LESS_THAN
|
||||
case "<=":
|
||||
op = pb.StructuredQuery_FieldFilter_LESS_THAN_OR_EQUAL
|
||||
case ">":
|
||||
op = pb.StructuredQuery_FieldFilter_GREATER_THAN
|
||||
case ">=":
|
||||
op = pb.StructuredQuery_FieldFilter_GREATER_THAN_OR_EQUAL
|
||||
case "==":
|
||||
op = pb.StructuredQuery_FieldFilter_EQUAL
|
||||
default:
|
||||
return nil, fmt.Errorf("firestore: invalid operator %q", f.op)
|
||||
}
|
||||
val, sawTransform, err := toProtoValue(reflect.ValueOf(f.value))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sawTransform {
|
||||
return nil, errors.New("firestore: ServerTimestamp disallowed in query value")
|
||||
}
|
||||
return &pb.StructuredQuery_Filter{
|
||||
FilterType: &pb.StructuredQuery_Filter_FieldFilter{
|
||||
FieldFilter: &pb.StructuredQuery_FieldFilter{
|
||||
Field: fref(f.fieldPath),
|
||||
Op: op,
|
||||
Value: val,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func unaryOpFor(value interface{}) (pb.StructuredQuery_UnaryFilter_Operator, bool) {
|
||||
switch {
|
||||
case value == nil:
|
||||
return pb.StructuredQuery_UnaryFilter_IS_NULL, true
|
||||
case isNaN(value):
|
||||
return pb.StructuredQuery_UnaryFilter_IS_NAN, true
|
||||
default:
|
||||
return pb.StructuredQuery_UnaryFilter_OPERATOR_UNSPECIFIED, false
|
||||
}
|
||||
}
|
||||
|
||||
func isNaN(x interface{}) bool {
|
||||
switch x := x.(type) {
|
||||
case float32:
|
||||
return math.IsNaN(float64(x))
|
||||
case float64:
|
||||
return math.IsNaN(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type order struct {
|
||||
fieldPath FieldPath
|
||||
dir Direction
|
||||
}
|
||||
|
||||
func (r order) isDocumentID() bool {
|
||||
return len(r.fieldPath) == 1 && r.fieldPath[0] == DocumentID
|
||||
}
|
||||
|
||||
func (r order) toProto() (*pb.StructuredQuery_Order, error) {
|
||||
if err := r.fieldPath.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.StructuredQuery_Order{
|
||||
Field: fref(r.fieldPath),
|
||||
Direction: pb.StructuredQuery_Direction(r.dir),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fref(fp FieldPath) *pb.StructuredQuery_FieldReference {
|
||||
return &pb.StructuredQuery_FieldReference{fp.toServiceFieldPath()}
|
||||
}
|
||||
|
||||
func trunc32(i int) int32 {
|
||||
if i > math.MaxInt32 {
|
||||
i = math.MaxInt32
|
||||
}
|
||||
return int32(i)
|
||||
}
|
||||
|
||||
// Documents returns an iterator over the query's resulting documents.
|
||||
func (q Query) Documents(ctx context.Context) *DocumentIterator {
|
||||
return &DocumentIterator{
|
||||
iter: newQueryDocumentIterator(withResourceHeader(ctx, q.c.path()), &q, nil),
|
||||
err: checkTransaction(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentIterator is an iterator over documents returned by a query.
|
||||
type DocumentIterator struct {
|
||||
iter docIterator
|
||||
err error
|
||||
}
|
||||
|
||||
// Unexported interface so we can have two different kinds of DocumentIterator: one
|
||||
// for straight queries, and one for query snapshots. We do it this way instead of
|
||||
// making DocumentIterator an interface because in the client libraries, iterators are
|
||||
// always concrete types, and the fact that this one has two different implementations
|
||||
// is an internal detail.
|
||||
type docIterator interface {
|
||||
next() (*DocumentSnapshot, error)
|
||||
stop()
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there
|
||||
// are no more results. Once Next returns Done, all subsequent calls will return
|
||||
// Done.
|
||||
func (it *DocumentIterator) Next() (*DocumentSnapshot, error) {
|
||||
if it.err != nil {
|
||||
return nil, it.err
|
||||
}
|
||||
ds, err := it.iter.next()
|
||||
if err != nil {
|
||||
it.err = err
|
||||
}
|
||||
return ds, err
|
||||
}
|
||||
|
||||
// Stop stops the iterator, freeing its resources.
|
||||
// Always call Stop when you are done with an iterator.
|
||||
// It is not safe to call Stop concurrently with Next.
|
||||
func (it *DocumentIterator) Stop() {
|
||||
if it.iter != nil { // possible in error cases
|
||||
it.iter.stop()
|
||||
}
|
||||
if it.err == nil {
|
||||
it.err = iterator.Done
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll returns all the documents remaining from the iterator.
|
||||
// It is not necessary to call Stop on the iterator after calling GetAll.
|
||||
func (it *DocumentIterator) GetAll() ([]*DocumentSnapshot, error) {
|
||||
defer it.Stop()
|
||||
var docs []*DocumentSnapshot
|
||||
for {
|
||||
doc, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
type queryDocumentIterator struct {
|
||||
ctx context.Context
|
||||
cancel func()
|
||||
q *Query
|
||||
tid []byte // transaction ID, if any
|
||||
streamClient pb.Firestore_RunQueryClient
|
||||
}
|
||||
|
||||
func newQueryDocumentIterator(ctx context.Context, q *Query, tid []byte) *queryDocumentIterator {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &queryDocumentIterator{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
q: q,
|
||||
tid: tid,
|
||||
}
|
||||
}
|
||||
|
||||
func (it *queryDocumentIterator) next() (*DocumentSnapshot, error) {
|
||||
client := it.q.c
|
||||
if it.streamClient == nil {
|
||||
sq, err := it.q.toProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &pb.RunQueryRequest{
|
||||
Parent: it.q.parentPath,
|
||||
QueryType: &pb.RunQueryRequest_StructuredQuery{sq},
|
||||
}
|
||||
if it.tid != nil {
|
||||
req.ConsistencySelector = &pb.RunQueryRequest_Transaction{it.tid}
|
||||
}
|
||||
it.streamClient, err = client.c.RunQuery(it.ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var res *pb.RunQueryResponse
|
||||
var err error
|
||||
for {
|
||||
res, err = it.streamClient.Recv()
|
||||
if err == io.EOF {
|
||||
return nil, iterator.Done
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.Document != nil {
|
||||
break
|
||||
}
|
||||
// No document => partial progress; keep receiving.
|
||||
}
|
||||
docRef, err := pathToDoc(res.Document.Name, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc, err := newDocumentSnapshot(docRef, res.Document, client, res.ReadTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func (it *queryDocumentIterator) stop() {
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
// Snapshots returns an iterator over snapshots of the query. Each time the query
|
||||
// results change, a new snapshot will be generated.
|
||||
func (q Query) Snapshots(ctx context.Context) *QuerySnapshotIterator {
|
||||
ws, err := newWatchStreamForQuery(ctx, q)
|
||||
if err != nil {
|
||||
return &QuerySnapshotIterator{err: err}
|
||||
}
|
||||
return &QuerySnapshotIterator{
|
||||
Query: q,
|
||||
ws: ws,
|
||||
}
|
||||
}
|
||||
|
||||
// QuerySnapshotIterator is an iterator over snapshots of a query.
|
||||
// Call Next on the iterator to get a snapshot of the query's results each time they change.
|
||||
// Call Stop on the iterator when done.
|
||||
//
|
||||
// For an example, see Query.Snapshots.
|
||||
type QuerySnapshotIterator struct {
|
||||
// The Query used to construct this iterator.
|
||||
Query Query
|
||||
|
||||
// The time at which the most recent snapshot was obtained from Firestore.
|
||||
ReadTime time.Time
|
||||
|
||||
// The number of results in the most recent snapshot.
|
||||
Size int
|
||||
|
||||
// The changes since the previous snapshot.
|
||||
Changes []DocumentChange
|
||||
|
||||
ws *watchStream
|
||||
err error
|
||||
}
|
||||
|
||||
// Next blocks until the query's results change, then returns a DocumentIterator for
|
||||
// the current results.
|
||||
//
|
||||
// Next never returns iterator.Done unless it is called after Stop.
|
||||
func (it *QuerySnapshotIterator) Next() (*DocumentIterator, error) {
|
||||
if it.err != nil {
|
||||
return nil, it.err
|
||||
}
|
||||
btree, changes, readTime, err := it.ws.nextSnapshot()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = iterator.Done
|
||||
}
|
||||
it.err = err
|
||||
return nil, it.err
|
||||
}
|
||||
it.Changes = changes
|
||||
it.ReadTime = readTime
|
||||
it.Size = btree.Len()
|
||||
return &DocumentIterator{
|
||||
iter: (*btreeDocumentIterator)(btree.BeforeIndex(0)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Stop stops receiving snapshots.
|
||||
// You should always call Stop when you are done with an iterator, to free up resources.
|
||||
// It is not safe to call Stop concurrently with Next.
|
||||
func (it *QuerySnapshotIterator) Stop() {
|
||||
it.ws.stop()
|
||||
}
|
||||
|
||||
type btreeDocumentIterator btree.Iterator
|
||||
|
||||
func (it *btreeDocumentIterator) next() (*DocumentSnapshot, error) {
|
||||
if !(*btree.Iterator)(it).Next() {
|
||||
return nil, iterator.Done
|
||||
}
|
||||
return it.Key.(*DocumentSnapshot), nil
|
||||
}
|
||||
|
||||
func (*btreeDocumentIterator) stop() {}
|
||||
+717
@@ -0,0 +1,717 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package firestore
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"cloud.google.com/go/internal/pretty"
|
||||
pb "google.golang.org/genproto/googleapis/firestore/v1beta1"
|
||||
|
||||
tspb "github.com/golang/protobuf/ptypes/timestamp"
|
||||
"github.com/golang/protobuf/ptypes/wrappers"
|
||||
)
|
||||
|
||||
func TestFilterToProto(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
in filter
|
||||
want *pb.StructuredQuery_Filter
|
||||
}{
|
||||
{
|
||||
filter{[]string{"a"}, ">", 1},
|
||||
&pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_FieldFilter{
|
||||
FieldFilter: &pb.StructuredQuery_FieldFilter{
|
||||
Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"},
|
||||
Op: pb.StructuredQuery_FieldFilter_GREATER_THAN,
|
||||
Value: intval(1),
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
filter{[]string{"a"}, "==", nil},
|
||||
&pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_UnaryFilter{
|
||||
UnaryFilter: &pb.StructuredQuery_UnaryFilter{
|
||||
OperandType: &pb.StructuredQuery_UnaryFilter_Field{
|
||||
Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"},
|
||||
},
|
||||
Op: pb.StructuredQuery_UnaryFilter_IS_NULL,
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
filter{[]string{"a"}, "==", math.NaN()},
|
||||
&pb.StructuredQuery_Filter{FilterType: &pb.StructuredQuery_Filter_UnaryFilter{
|
||||
UnaryFilter: &pb.StructuredQuery_UnaryFilter{
|
||||
OperandType: &pb.StructuredQuery_UnaryFilter_Field{
|
||||
Field: &pb.StructuredQuery_FieldReference{FieldPath: "a"},
|
||||
},
|
||||
Op: pb.StructuredQuery_UnaryFilter_IS_NAN,
|
||||
},
|
||||
}},
|
||||
},
|
||||
} {
|
||||
got, err := test.in.toProto()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testEqual(got, test.want) {
|
||||
t.Errorf("%+v:\ngot\n%v\nwant\n%v", test.in, pretty.Value(got), pretty.Value(test.want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryToProto(t *testing.T) {
|
||||
filtr := func(path []string, op string, val interface{}) *pb.StructuredQuery_Filter {
|
||||
f, err := filter{path, op, val}.toProto()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
c := &Client{projectID: "P", databaseID: "DB"}
|
||||
coll := c.Collection("C")
|
||||
q := coll.Query
|
||||
type S struct {
|
||||
A int `firestore:"a"`
|
||||
}
|
||||
docsnap := &DocumentSnapshot{
|
||||
Ref: coll.Doc("D"),
|
||||
proto: &pb.Document{
|
||||
Fields: map[string]*pb.Value{"a": intval(7), "b": intval(8)},
|
||||
},
|
||||
}
|
||||
for _, test := range []struct {
|
||||
desc string
|
||||
in Query
|
||||
want *pb.StructuredQuery
|
||||
}{
|
||||
{
|
||||
desc: "q.Select()",
|
||||
in: q.Select(),
|
||||
want: &pb.StructuredQuery{
|
||||
Select: &pb.StructuredQuery_Projection{
|
||||
Fields: []*pb.StructuredQuery_FieldReference{fref1("__name__")},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.Select("a", "b")`,
|
||||
in: q.Select("a", "b"),
|
||||
want: &pb.StructuredQuery{
|
||||
Select: &pb.StructuredQuery_Projection{
|
||||
Fields: []*pb.StructuredQuery_FieldReference{fref1("a"), fref1("b")},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.Select("a", "b").Select("c")`,
|
||||
in: q.Select("a", "b").Select("c"), // last wins
|
||||
want: &pb.StructuredQuery{
|
||||
Select: &pb.StructuredQuery_Projection{
|
||||
Fields: []*pb.StructuredQuery_FieldReference{fref1("c")},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.SelectPaths([]string{"*"}, []string{"/"})`,
|
||||
in: q.SelectPaths([]string{"*"}, []string{"/"}),
|
||||
want: &pb.StructuredQuery{
|
||||
Select: &pb.StructuredQuery_Projection{
|
||||
Fields: []*pb.StructuredQuery_FieldReference{fref1("*"), fref1("/")},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.Where("a", ">", 5)`,
|
||||
in: q.Where("a", ">", 5),
|
||||
want: &pb.StructuredQuery{Where: filtr([]string{"a"}, ">", 5)},
|
||||
},
|
||||
{
|
||||
desc: `q.Where("a", "==", NaN)`,
|
||||
in: q.Where("a", "==", float32(math.NaN())),
|
||||
want: &pb.StructuredQuery{Where: filtr([]string{"a"}, "==", math.NaN())},
|
||||
},
|
||||
{
|
||||
desc: `q.Where("a", ">", 5).Where("b", "<", "foo")`,
|
||||
in: q.Where("a", ">", 5).Where("b", "<", "foo"),
|
||||
want: &pb.StructuredQuery{
|
||||
Where: &pb.StructuredQuery_Filter{
|
||||
&pb.StructuredQuery_Filter_CompositeFilter{
|
||||
&pb.StructuredQuery_CompositeFilter{
|
||||
Op: pb.StructuredQuery_CompositeFilter_AND,
|
||||
Filters: []*pb.StructuredQuery_Filter{
|
||||
filtr([]string{"a"}, ">", 5), filtr([]string{"b"}, "<", "foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: ` q.WherePath([]string{"/", "*"}, ">", 5)`,
|
||||
in: q.WherePath([]string{"/", "*"}, ">", 5),
|
||||
want: &pb.StructuredQuery{Where: filtr([]string{"/", "*"}, ">", 5)},
|
||||
},
|
||||
{
|
||||
desc: `q.OrderBy("b", Asc).OrderBy("a", Desc).OrderByPath([]string{"~"}, Asc)`,
|
||||
in: q.OrderBy("b", Asc).OrderBy("a", Desc).OrderByPath([]string{"~"}, Asc),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("b"), pb.StructuredQuery_ASCENDING},
|
||||
{fref1("a"), pb.StructuredQuery_DESCENDING},
|
||||
{fref1("~"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.Offset(2).Limit(3)`,
|
||||
in: q.Offset(2).Limit(3),
|
||||
want: &pb.StructuredQuery{
|
||||
Offset: 2,
|
||||
Limit: &wrappers.Int32Value{3},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.Offset(2).Limit(3).Limit(4).Offset(5)`,
|
||||
in: q.Offset(2).Limit(3).Limit(4).Offset(5), // last wins
|
||||
want: &pb.StructuredQuery{
|
||||
Offset: 5,
|
||||
Limit: &wrappers.Int32Value{4},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.OrderBy("a", Asc).StartAt(7).EndBefore(9)`,
|
||||
in: q.OrderBy("a", Asc).StartAt(7).EndBefore(9),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(7)},
|
||||
Before: true,
|
||||
},
|
||||
EndAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(9)},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.OrderBy("a", Asc).StartAt(7).EndAt(9)`,
|
||||
in: q.OrderBy("a", Asc).StartAt(7).EndAt(9),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(7)},
|
||||
Before: true,
|
||||
},
|
||||
EndAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(9)},
|
||||
Before: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.OrderBy("a", Asc).StartAfter(7).EndAt(9)`,
|
||||
in: q.OrderBy("a", Asc).StartAfter(7).EndAt(9),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(7)},
|
||||
Before: false,
|
||||
},
|
||||
EndAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(9)},
|
||||
Before: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.OrderBy(DocumentID, Asc).StartAfter("foo").EndBefore("bar")`,
|
||||
in: q.OrderBy(DocumentID, Asc).StartAfter("foo").EndBefore("bar"),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("__name__"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{refval(coll.parentPath + "/documents/C/foo")},
|
||||
Before: false,
|
||||
},
|
||||
EndAt: &pb.Cursor{
|
||||
Values: []*pb.Value{refval(coll.parentPath + "/documents/C/bar")},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.OrderBy("a", Asc).OrderBy("b", Desc).StartAfter(7, 8).EndAt(9, 10)`,
|
||||
in: q.OrderBy("a", Asc).OrderBy("b", Desc).StartAfter(7, 8).EndAt(9, 10),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_ASCENDING},
|
||||
{fref1("b"), pb.StructuredQuery_DESCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(7), intval(8)},
|
||||
Before: false,
|
||||
},
|
||||
EndAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(9), intval(10)},
|
||||
Before: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// last of StartAt/After wins, same for End
|
||||
desc: `q.OrderBy("a", Asc).StartAfter(1).StartAt(2).EndAt(3).EndBefore(4)`,
|
||||
in: q.OrderBy("a", Asc).
|
||||
StartAfter(1).StartAt(2).
|
||||
EndAt(3).EndBefore(4),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(2)},
|
||||
Before: true,
|
||||
},
|
||||
EndAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(4)},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Start/End with DocumentSnapshot
|
||||
// These tests are from the "Document Snapshot Cursors" doc.
|
||||
{
|
||||
desc: `q.StartAt(docsnap)`,
|
||||
in: q.StartAt(docsnap),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("__name__"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{refval(coll.parentPath + "/documents/C/D")},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.OrderBy("a", Asc).StartAt(docsnap)`,
|
||||
in: q.OrderBy("a", Asc).StartAt(docsnap),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_ASCENDING},
|
||||
{fref1("__name__"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(7), refval(coll.parentPath + "/documents/C/D")},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
desc: `q.OrderBy("a", Desc).StartAt(docsnap)`,
|
||||
in: q.OrderBy("a", Desc).StartAt(docsnap),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_DESCENDING},
|
||||
{fref1("__name__"), pb.StructuredQuery_DESCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(7), refval(coll.parentPath + "/documents/C/D")},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.OrderBy("a", Desc).OrderBy("b", Asc).StartAt(docsnap)`,
|
||||
in: q.OrderBy("a", Desc).OrderBy("b", Asc).StartAt(docsnap),
|
||||
want: &pb.StructuredQuery{
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_DESCENDING},
|
||||
{fref1("b"), pb.StructuredQuery_ASCENDING},
|
||||
{fref1("__name__"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(7), intval(8), refval(coll.parentPath + "/documents/C/D")},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.Where("a", "==", 3).StartAt(docsnap)`,
|
||||
in: q.Where("a", "==", 3).StartAt(docsnap),
|
||||
want: &pb.StructuredQuery{
|
||||
Where: filtr([]string{"a"}, "==", 3),
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("__name__"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{refval(coll.parentPath + "/documents/C/D")},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.Where("a", "<", 3).StartAt(docsnap)`,
|
||||
in: q.Where("a", "<", 3).StartAt(docsnap),
|
||||
want: &pb.StructuredQuery{
|
||||
Where: filtr([]string{"a"}, "<", 3),
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_ASCENDING},
|
||||
{fref1("__name__"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(7), refval(coll.parentPath + "/documents/C/D")},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `q.Where("b", "==", 1).Where("a", "<", 3).StartAt(docsnap)`,
|
||||
in: q.Where("b", "==", 1).Where("a", "<", 3).StartAt(docsnap),
|
||||
want: &pb.StructuredQuery{
|
||||
Where: &pb.StructuredQuery_Filter{
|
||||
&pb.StructuredQuery_Filter_CompositeFilter{
|
||||
&pb.StructuredQuery_CompositeFilter{
|
||||
Op: pb.StructuredQuery_CompositeFilter_AND,
|
||||
Filters: []*pb.StructuredQuery_Filter{
|
||||
filtr([]string{"b"}, "==", 1),
|
||||
filtr([]string{"a"}, "<", 3),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
OrderBy: []*pb.StructuredQuery_Order{
|
||||
{fref1("a"), pb.StructuredQuery_ASCENDING},
|
||||
{fref1("__name__"), pb.StructuredQuery_ASCENDING},
|
||||
},
|
||||
StartAt: &pb.Cursor{
|
||||
Values: []*pb.Value{intval(7), refval(coll.parentPath + "/documents/C/D")},
|
||||
Before: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
got, err := test.in.toProto()
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", test.desc, err)
|
||||
continue
|
||||
}
|
||||
test.want.From = []*pb.StructuredQuery_CollectionSelector{{CollectionId: "C"}}
|
||||
if !testEqual(got, test.want) {
|
||||
t.Errorf("%s:\ngot\n%v\nwant\n%v", test.desc, pretty.Value(got), pretty.Value(test.want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fref1(s string) *pb.StructuredQuery_FieldReference {
|
||||
return fref([]string{s})
|
||||
}
|
||||
|
||||
func TestQueryToProtoErrors(t *testing.T) {
|
||||
st := map[string]interface{}{"a": ServerTimestamp}
|
||||
del := map[string]interface{}{"a": Delete}
|
||||
c := &Client{projectID: "P", databaseID: "DB"}
|
||||
coll := c.Collection("C")
|
||||
docsnap := &DocumentSnapshot{
|
||||
Ref: coll.Doc("D"),
|
||||
proto: &pb.Document{
|
||||
Fields: map[string]*pb.Value{"a": intval(7)},
|
||||
},
|
||||
}
|
||||
q := coll.Query
|
||||
for _, query := range []Query{
|
||||
Query{}, // no collection ID
|
||||
q.Where("x", "!=", 1), // invalid operator
|
||||
q.Where("~", ">", 1), // invalid path
|
||||
q.WherePath([]string{"*", ""}, ">", 1), // invalid path
|
||||
q.StartAt(1), // no OrderBy
|
||||
q.StartAt(2).OrderBy("x", Asc).OrderBy("y", Desc), // wrong # OrderBy
|
||||
q.Select("*"), // invalid path
|
||||
q.SelectPaths([]string{"/", "", "~"}), // invalid path
|
||||
q.OrderBy("[", Asc), // invalid path
|
||||
q.OrderByPath([]string{""}, Desc), // invalid path
|
||||
q.Where("x", "==", st), // ServerTimestamp in filter
|
||||
q.OrderBy("a", Asc).StartAt(st), // ServerTimestamp in Start
|
||||
q.OrderBy("a", Asc).EndAt(st), // ServerTimestamp in End
|
||||
q.Where("x", "==", del), // Delete in filter
|
||||
q.OrderBy("a", Asc).StartAt(del), // Delete in Start
|
||||
q.OrderBy("a", Asc).EndAt(del), // Delete in End
|
||||
q.OrderBy(DocumentID, Asc).StartAt(7), // wrong type for __name__
|
||||
q.OrderBy(DocumentID, Asc).EndAt(7), // wrong type for __name__
|
||||
q.OrderBy("b", Asc).StartAt(docsnap), // doc snapshot does not have order-by field
|
||||
q.StartAt(docsnap).EndAt("x"), // mixed doc snapshot and fields
|
||||
q.StartAfter("x").EndBefore(docsnap), // mixed doc snapshot and fields
|
||||
} {
|
||||
_, err := query.toProto()
|
||||
if err == nil {
|
||||
t.Errorf("%+v: got nil, want error", query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryMethodsDoNotModifyReceiver(t *testing.T) {
|
||||
var empty Query
|
||||
|
||||
q := Query{}
|
||||
_ = q.Select("a", "b")
|
||||
if !testEqual(q, empty) {
|
||||
t.Errorf("got %+v, want empty", q)
|
||||
}
|
||||
|
||||
q = Query{}
|
||||
q1 := q.Where("a", ">", 3)
|
||||
if !testEqual(q, empty) {
|
||||
t.Errorf("got %+v, want empty", q)
|
||||
}
|
||||
// Extra check because Where appends to a slice.
|
||||
q1before := q.Where("a", ">", 3) // same as q1
|
||||
_ = q1.Where("b", "<", "foo")
|
||||
if !testEqual(q1, q1before) {
|
||||
t.Errorf("got %+v, want %+v", q1, q1before)
|
||||
}
|
||||
|
||||
q = Query{}
|
||||
q1 = q.OrderBy("a", Asc)
|
||||
if !testEqual(q, empty) {
|
||||
t.Errorf("got %+v, want empty", q)
|
||||
}
|
||||
// Extra check because Where appends to a slice.
|
||||
q1before = q.OrderBy("a", Asc) // same as q1
|
||||
_ = q1.OrderBy("b", Desc)
|
||||
if !testEqual(q1, q1before) {
|
||||
t.Errorf("got %+v, want %+v", q1, q1before)
|
||||
}
|
||||
|
||||
q = Query{}
|
||||
_ = q.Offset(5)
|
||||
if !testEqual(q, empty) {
|
||||
t.Errorf("got %+v, want empty", q)
|
||||
}
|
||||
|
||||
q = Query{}
|
||||
_ = q.Limit(5)
|
||||
if !testEqual(q, empty) {
|
||||
t.Errorf("got %+v, want empty", q)
|
||||
}
|
||||
|
||||
q = Query{}
|
||||
_ = q.StartAt(5)
|
||||
if !testEqual(q, empty) {
|
||||
t.Errorf("got %+v, want empty", q)
|
||||
}
|
||||
|
||||
q = Query{}
|
||||
_ = q.StartAfter(5)
|
||||
if !testEqual(q, empty) {
|
||||
t.Errorf("got %+v, want empty", q)
|
||||
}
|
||||
|
||||
q = Query{}
|
||||
_ = q.EndAt(5)
|
||||
if !testEqual(q, empty) {
|
||||
t.Errorf("got %+v, want empty", q)
|
||||
}
|
||||
|
||||
q = Query{}
|
||||
_ = q.EndBefore(5)
|
||||
if !testEqual(q, empty) {
|
||||
t.Errorf("got %+v, want empty", q)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFromCollectionRef(t *testing.T) {
|
||||
c := &Client{}
|
||||
coll := c.Collection("C")
|
||||
got := coll.Select("x").Offset(8)
|
||||
want := Query{
|
||||
c: c,
|
||||
parentPath: c.path(),
|
||||
collectionID: "C",
|
||||
selection: []FieldPath{{"x"}},
|
||||
offset: 8,
|
||||
}
|
||||
if !testEqual(got, want) {
|
||||
t.Fatalf("got %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryGetAll(t *testing.T) {
|
||||
// This implicitly tests DocumentIterator as well.
|
||||
const dbPath = "projects/projectID/databases/(default)"
|
||||
ctx := context.Background()
|
||||
c, srv := newMock(t)
|
||||
docNames := []string{"C/a", "C/b"}
|
||||
wantPBDocs := []*pb.Document{
|
||||
{
|
||||
Name: dbPath + "/documents/" + docNames[0],
|
||||
CreateTime: aTimestamp,
|
||||
UpdateTime: aTimestamp,
|
||||
Fields: map[string]*pb.Value{"f": intval(2)},
|
||||
},
|
||||
{
|
||||
Name: dbPath + "/documents/" + docNames[1],
|
||||
CreateTime: aTimestamp2,
|
||||
UpdateTime: aTimestamp3,
|
||||
Fields: map[string]*pb.Value{"f": intval(1)},
|
||||
},
|
||||
}
|
||||
wantReadTimes := []*tspb.Timestamp{aTimestamp, aTimestamp2}
|
||||
srv.addRPC(nil, []interface{}{
|
||||
&pb.RunQueryResponse{Document: wantPBDocs[0], ReadTime: aTimestamp},
|
||||
&pb.RunQueryResponse{Document: wantPBDocs[1], ReadTime: aTimestamp2},
|
||||
})
|
||||
gotDocs, err := c.Collection("C").Documents(ctx).GetAll()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := len(gotDocs), len(wantPBDocs); got != want {
|
||||
t.Errorf("got %d docs, wanted %d", got, want)
|
||||
}
|
||||
for i, got := range gotDocs {
|
||||
want, err := newDocumentSnapshot(c.Doc(docNames[i]), wantPBDocs[i], c, wantReadTimes[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testEqual(got, want) {
|
||||
// avoid writing a cycle
|
||||
got.c = nil
|
||||
want.c = nil
|
||||
t.Errorf("#%d: got %+v, want %+v", i, pretty.Value(got), pretty.Value(want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryCompareFunc(t *testing.T) {
|
||||
mv := func(fields ...interface{}) map[string]*pb.Value {
|
||||
m := map[string]*pb.Value{}
|
||||
for i := 0; i < len(fields); i += 2 {
|
||||
m[fields[i].(string)] = fields[i+1].(*pb.Value)
|
||||
}
|
||||
return m
|
||||
}
|
||||
snap := func(ref *DocumentRef, fields map[string]*pb.Value) *DocumentSnapshot {
|
||||
return &DocumentSnapshot{Ref: ref, proto: &pb.Document{Fields: fields}}
|
||||
}
|
||||
|
||||
c := &Client{}
|
||||
coll := c.Collection("C")
|
||||
doc1 := coll.Doc("doc1")
|
||||
doc2 := coll.Doc("doc2")
|
||||
doc3 := coll.Doc("doc3")
|
||||
doc4 := coll.Doc("doc4")
|
||||
for _, test := range []struct {
|
||||
q Query
|
||||
in []*DocumentSnapshot
|
||||
want []*DocumentSnapshot
|
||||
}{
|
||||
{
|
||||
q: coll.OrderBy("foo", Asc),
|
||||
in: []*DocumentSnapshot{
|
||||
snap(doc3, mv("foo", intval(2))),
|
||||
snap(doc4, mv("foo", intval(1))),
|
||||
snap(doc2, mv("foo", intval(2))),
|
||||
},
|
||||
want: []*DocumentSnapshot{
|
||||
snap(doc4, mv("foo", intval(1))),
|
||||
snap(doc2, mv("foo", intval(2))),
|
||||
snap(doc3, mv("foo", intval(2))),
|
||||
},
|
||||
},
|
||||
{
|
||||
q: coll.OrderBy("foo", Desc),
|
||||
in: []*DocumentSnapshot{
|
||||
snap(doc3, mv("foo", intval(2))),
|
||||
snap(doc4, mv("foo", intval(1))),
|
||||
snap(doc2, mv("foo", intval(2))),
|
||||
},
|
||||
want: []*DocumentSnapshot{
|
||||
snap(doc3, mv("foo", intval(2))),
|
||||
snap(doc2, mv("foo", intval(2))),
|
||||
snap(doc4, mv("foo", intval(1))),
|
||||
},
|
||||
},
|
||||
{
|
||||
q: coll.OrderBy("foo.bar", Asc),
|
||||
in: []*DocumentSnapshot{
|
||||
snap(doc1, mv("foo", mapval(mv("bar", intval(1))))),
|
||||
snap(doc2, mv("foo", mapval(mv("bar", intval(2))))),
|
||||
snap(doc3, mv("foo", mapval(mv("bar", intval(2))))),
|
||||
},
|
||||
want: []*DocumentSnapshot{
|
||||
snap(doc1, mv("foo", mapval(mv("bar", intval(1))))),
|
||||
snap(doc2, mv("foo", mapval(mv("bar", intval(2))))),
|
||||
snap(doc3, mv("foo", mapval(mv("bar", intval(2))))),
|
||||
},
|
||||
},
|
||||
{
|
||||
q: coll.OrderBy("foo.bar", Desc),
|
||||
in: []*DocumentSnapshot{
|
||||
snap(doc1, mv("foo", mapval(mv("bar", intval(1))))),
|
||||
snap(doc2, mv("foo", mapval(mv("bar", intval(2))))),
|
||||
snap(doc3, mv("foo", mapval(mv("bar", intval(2))))),
|
||||
},
|
||||
want: []*DocumentSnapshot{
|
||||
snap(doc3, mv("foo", mapval(mv("bar", intval(2))))),
|
||||
snap(doc2, mv("foo", mapval(mv("bar", intval(2))))),
|
||||
snap(doc1, mv("foo", mapval(mv("bar", intval(1))))),
|
||||
},
|
||||
},
|
||||
} {
|
||||
got := append([]*DocumentSnapshot(nil), test.in...)
|
||||
sort.Sort(byQuery{test.q.compareFunc(), got})
|
||||
if diff := testDiff(got, test.want); diff != "" {
|
||||
t.Errorf("%+v: %s", test.q, diff)
|
||||
}
|
||||
}
|
||||
|
||||
// Want error on missing field.
|
||||
q := coll.OrderBy("bar", Asc)
|
||||
if q.err != nil {
|
||||
t.Fatalf("bad query: %v", q.err)
|
||||
}
|
||||
cf := q.compareFunc()
|
||||
s := snap(doc1, mv("foo", intval(1)))
|
||||
if _, err := cf(s, s); err == nil {
|
||||
t.Error("got nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
type byQuery struct {
|
||||
compare func(d1, d2 *DocumentSnapshot) (int, error)
|
||||
docs []*DocumentSnapshot
|
||||
}
|
||||
|
||||
func (b byQuery) Len() int { return len(b.docs) }
|
||||
func (b byQuery) Swap(i, j int) { b.docs[i], b.docs[j] = b.docs[j], b.docs[i] }
|
||||
func (b byQuery) Less(i, j int) bool {
|
||||
c, err := b.compare(b.docs[i], b.docs[j])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c < 0
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
SHA1(/usr/local/google/home/jba/go/src/github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/testdata/test-suite.binproto)= 1502b0250a2ecd854b80509e3e456e46ade89ea7
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A simple call, resulting in a single update operation.
|
||||
|
||||
description: "create: basic"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": 1}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
integer_value: 1
|
||||
>
|
||||
>
|
||||
>
|
||||
current_document: <
|
||||
exists: false
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A call to a write method with complicated input data.
|
||||
|
||||
description: "create: complex"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": [1, 2.5], \"b\": {\"c\": [\"three\", {\"d\": true}]}}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
array_value: <
|
||||
values: <
|
||||
integer_value: 1
|
||||
>
|
||||
values: <
|
||||
double_value: 2.5
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "b"
|
||||
value: <
|
||||
map_value: <
|
||||
fields: <
|
||||
key: "c"
|
||||
value: <
|
||||
array_value: <
|
||||
values: <
|
||||
string_value: "three"
|
||||
>
|
||||
values: <
|
||||
map_value: <
|
||||
fields: <
|
||||
key: "d"
|
||||
value: <
|
||||
boolean_value: true
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
current_document: <
|
||||
exists: false
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The Delete sentinel must be the value of a field. Deletes are implemented by
|
||||
# turning the path to the Delete sentinel into a FieldPath, and FieldPaths do not
|
||||
# support array indexing.
|
||||
|
||||
description: "create: Delete cannot be anywhere inside an array value"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": [1, {\"b\": \"Delete\"}]}"
|
||||
is_error: true
|
||||
>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The Delete sentinel must be the value of a field. Deletes are implemented by
|
||||
# turning the path to the Delete sentinel into a FieldPath, and FieldPaths do not
|
||||
# support array indexing.
|
||||
|
||||
description: "create: Delete cannot be in an array value"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": [1, 2, \"Delete\"]}"
|
||||
is_error: true
|
||||
>
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
|
||||
description: "create: creating or setting an empty map"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
>
|
||||
current_document: <
|
||||
exists: false
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The Delete sentinel cannot be used in Create, or in Set without a Merge option.
|
||||
|
||||
description: "create: Delete cannot appear in data"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": 1, \"b\": \"Delete\"}"
|
||||
is_error: true
|
||||
>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Create and Set treat their map keys literally. They do not split on dots.
|
||||
|
||||
description: "create: don\342\200\231t split on dots"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{ \"a.b\": { \"c.d\": 1 }, \"e\": 2 }"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a.b"
|
||||
value: <
|
||||
map_value: <
|
||||
fields: <
|
||||
key: "c.d"
|
||||
value: <
|
||||
integer_value: 1
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "e"
|
||||
value: <
|
||||
integer_value: 2
|
||||
>
|
||||
>
|
||||
>
|
||||
current_document: <
|
||||
exists: false
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Create and Set treat their map keys literally. They do not escape special
|
||||
# characters.
|
||||
|
||||
description: "create: non-alpha characters in map keys"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{ \"*\": { \".\": 1 }, \"~\": 2 }"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "*"
|
||||
value: <
|
||||
map_value: <
|
||||
fields: <
|
||||
key: "."
|
||||
value: <
|
||||
integer_value: 1
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "~"
|
||||
value: <
|
||||
integer_value: 2
|
||||
>
|
||||
>
|
||||
>
|
||||
current_document: <
|
||||
exists: false
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# If the only values in the input are ServerTimestamps, then no update operation
|
||||
# should be produced.
|
||||
|
||||
description: "create: ServerTimestamp alone"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": \"ServerTimestamp\"}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
transform: <
|
||||
document: "projects/projectID/databases/(default)/documents/C/d"
|
||||
field_transforms: <
|
||||
field_path: "a"
|
||||
set_to_server_value: REQUEST_TIME
|
||||
>
|
||||
>
|
||||
current_document: <
|
||||
exists: false
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A document can have more than one ServerTimestamp field. Since all the
|
||||
# ServerTimestamp fields are removed, the only field in the update is "a".
|
||||
|
||||
description: "create: multiple ServerTimestamp fields"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": 1, \"b\": \"ServerTimestamp\", \"c\": {\"d\": \"ServerTimestamp\"}}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
integer_value: 1
|
||||
>
|
||||
>
|
||||
>
|
||||
current_document: <
|
||||
exists: false
|
||||
>
|
||||
>
|
||||
writes: <
|
||||
transform: <
|
||||
document: "projects/projectID/databases/(default)/documents/C/d"
|
||||
field_transforms: <
|
||||
field_path: "b"
|
||||
set_to_server_value: REQUEST_TIME
|
||||
>
|
||||
field_transforms: <
|
||||
field_path: "c.d"
|
||||
set_to_server_value: REQUEST_TIME
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A ServerTimestamp value can occur at any depth. In this case, the transform
|
||||
# applies to the field path "b.c". Since "c" is removed from the update, "b"
|
||||
# becomes empty, so it is also removed from the update.
|
||||
|
||||
description: "create: nested ServerTimestamp field"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": 1, \"b\": {\"c\": \"ServerTimestamp\"}}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
integer_value: 1
|
||||
>
|
||||
>
|
||||
>
|
||||
current_document: <
|
||||
exists: false
|
||||
>
|
||||
>
|
||||
writes: <
|
||||
transform: <
|
||||
document: "projects/projectID/databases/(default)/documents/C/d"
|
||||
field_transforms: <
|
||||
field_path: "b.c"
|
||||
set_to_server_value: REQUEST_TIME
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# There cannot be an array value anywhere on the path from the document root to
|
||||
# the ServerTimestamp sentinel. Firestore transforms don't support array indexing.
|
||||
|
||||
description: "create: ServerTimestamp cannot be anywhere inside an array value"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": [1, {\"b\": \"ServerTimestamp\"}]}"
|
||||
is_error: true
|
||||
>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The ServerTimestamp sentinel must be the value of a field. Firestore transforms
|
||||
# don't support array indexing.
|
||||
|
||||
description: "create: ServerTimestamp cannot be in an array value"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": [1, 2, \"ServerTimestamp\"]}"
|
||||
is_error: true
|
||||
>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A key with the special ServerTimestamp sentinel is removed from the data in the
|
||||
# update operation. Instead it appears in a separate Transform operation. Note
|
||||
# that in these tests, the string "ServerTimestamp" should be replaced with the
|
||||
# special ServerTimestamp value.
|
||||
|
||||
description: "create: ServerTimestamp with data"
|
||||
create: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": 1, \"b\": \"ServerTimestamp\"}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
integer_value: 1
|
||||
>
|
||||
>
|
||||
>
|
||||
current_document: <
|
||||
exists: false
|
||||
>
|
||||
>
|
||||
writes: <
|
||||
transform: <
|
||||
document: "projects/projectID/databases/(default)/documents/C/d"
|
||||
field_transforms: <
|
||||
field_path: "b"
|
||||
set_to_server_value: REQUEST_TIME
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Delete supports an exists precondition.
|
||||
|
||||
description: "delete: delete with exists precondition"
|
||||
delete: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
precondition: <
|
||||
exists: true
|
||||
>
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
delete: "projects/projectID/databases/(default)/documents/C/d"
|
||||
current_document: <
|
||||
exists: true
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# An ordinary Delete call.
|
||||
|
||||
description: "delete: delete without precondition"
|
||||
delete: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
delete: "projects/projectID/databases/(default)/documents/C/d"
|
||||
>
|
||||
>
|
||||
>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Delete supports a last-update-time precondition.
|
||||
|
||||
description: "delete: delete with last-update-time precondition"
|
||||
delete: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
precondition: <
|
||||
update_time: <
|
||||
seconds: 42
|
||||
>
|
||||
>
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
delete: "projects/projectID/databases/(default)/documents/C/d"
|
||||
current_document: <
|
||||
update_time: <
|
||||
seconds: 42
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A call to DocumentRef.Get.
|
||||
|
||||
description: "get: get a document"
|
||||
get: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
request: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
>
|
||||
>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# You can only compare NaN for equality.
|
||||
|
||||
description: "query: where clause with non-== comparison with NaN"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: "<"
|
||||
json_value: "\"NaN\""
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# You can only compare Null for equality.
|
||||
|
||||
description: "query: where clause with non-== comparison with Null"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: ">"
|
||||
json_value: "null"
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
Generated
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# When a document snapshot is used, the client appends a __name__ order-by clause
|
||||
# with the direction of the last order-by clause.
|
||||
|
||||
description: "query: cursor methods with a document snapshot, existing orderBy"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "b"
|
||||
>
|
||||
direction: "desc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
start_after: <
|
||||
doc_snapshot: <
|
||||
path: "projects/projectID/databases/(default)/documents/C/D"
|
||||
json_data: "{\"a\": 7, \"b\": 8}"
|
||||
>
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "b"
|
||||
>
|
||||
direction: DESCENDING
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "__name__"
|
||||
>
|
||||
direction: DESCENDING
|
||||
>
|
||||
start_at: <
|
||||
values: <
|
||||
integer_value: 7
|
||||
>
|
||||
values: <
|
||||
integer_value: 8
|
||||
>
|
||||
values: <
|
||||
reference_value: "projects/projectID/databases/(default)/documents/C/D"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
Generated
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# If there is an existing orderBy clause on __name__, no changes are made to the
|
||||
# list of orderBy clauses.
|
||||
|
||||
description: "query: cursor method, doc snapshot, existing orderBy __name__"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "desc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "__name__"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
start_at: <
|
||||
doc_snapshot: <
|
||||
path: "projects/projectID/databases/(default)/documents/C/D"
|
||||
json_data: "{\"a\": 7, \"b\": 8}"
|
||||
>
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_at: <
|
||||
doc_snapshot: <
|
||||
path: "projects/projectID/databases/(default)/documents/C/D"
|
||||
json_data: "{\"a\": 7, \"b\": 8}"
|
||||
>
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
direction: DESCENDING
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "__name__"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
start_at: <
|
||||
values: <
|
||||
integer_value: 7
|
||||
>
|
||||
values: <
|
||||
reference_value: "projects/projectID/databases/(default)/documents/C/D"
|
||||
>
|
||||
before: true
|
||||
>
|
||||
end_at: <
|
||||
values: <
|
||||
integer_value: 7
|
||||
>
|
||||
values: <
|
||||
reference_value: "projects/projectID/databases/(default)/documents/C/D"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
Generated
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A Where clause using equality doesn't change the implicit orderBy clauses.
|
||||
|
||||
description: "query: cursor methods with a document snapshot and an equality where clause"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: "=="
|
||||
json_value: "3"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_at: <
|
||||
doc_snapshot: <
|
||||
path: "projects/projectID/databases/(default)/documents/C/D"
|
||||
json_data: "{\"a\": 7, \"b\": 8}"
|
||||
>
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
where: <
|
||||
field_filter: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
op: EQUAL
|
||||
value: <
|
||||
integer_value: 3
|
||||
>
|
||||
>
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "__name__"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
end_at: <
|
||||
values: <
|
||||
reference_value: "projects/projectID/databases/(default)/documents/C/D"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
Generated
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# If there is an OrderBy clause, the inequality Where clause does not result in a
|
||||
# new OrderBy clause. We still add a __name__ OrderBy clause
|
||||
|
||||
description: "query: cursor method, doc snapshot, inequality where clause, and existing orderBy clause"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "desc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: "<"
|
||||
json_value: "4"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
start_at: <
|
||||
doc_snapshot: <
|
||||
path: "projects/projectID/databases/(default)/documents/C/D"
|
||||
json_data: "{\"a\": 7, \"b\": 8}"
|
||||
>
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
where: <
|
||||
field_filter: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
op: LESS_THAN
|
||||
value: <
|
||||
integer_value: 4
|
||||
>
|
||||
>
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
direction: DESCENDING
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "__name__"
|
||||
>
|
||||
direction: DESCENDING
|
||||
>
|
||||
start_at: <
|
||||
values: <
|
||||
integer_value: 7
|
||||
>
|
||||
values: <
|
||||
reference_value: "projects/projectID/databases/(default)/documents/C/D"
|
||||
>
|
||||
before: true
|
||||
>
|
||||
>
|
||||
>
|
||||
Generated
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A Where clause with an inequality results in an OrderBy clause on that clause's
|
||||
# path, if there are no other OrderBy clauses.
|
||||
|
||||
description: "query: cursor method with a document snapshot and an inequality where clause"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: "<="
|
||||
json_value: "3"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_before: <
|
||||
doc_snapshot: <
|
||||
path: "projects/projectID/databases/(default)/documents/C/D"
|
||||
json_data: "{\"a\": 7, \"b\": 8}"
|
||||
>
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
where: <
|
||||
field_filter: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
op: LESS_THAN_OR_EQUAL
|
||||
value: <
|
||||
integer_value: 3
|
||||
>
|
||||
>
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "__name__"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
end_at: <
|
||||
values: <
|
||||
integer_value: 7
|
||||
>
|
||||
values: <
|
||||
reference_value: "projects/projectID/databases/(default)/documents/C/D"
|
||||
>
|
||||
before: true
|
||||
>
|
||||
>
|
||||
>
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# When a document snapshot is used, the client appends a __name__ order-by clause.
|
||||
|
||||
description: "query: cursor methods with a document snapshot"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
start_at: <
|
||||
doc_snapshot: <
|
||||
path: "projects/projectID/databases/(default)/documents/C/D"
|
||||
json_data: "{\"a\": 7, \"b\": 8}"
|
||||
>
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "__name__"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
start_at: <
|
||||
values: <
|
||||
reference_value: "projects/projectID/databases/(default)/documents/C/D"
|
||||
>
|
||||
before: true
|
||||
>
|
||||
>
|
||||
>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# If a cursor method with a list of values is provided, there must be at least as
|
||||
# many explicit orderBy clauses as values.
|
||||
|
||||
description: "query: cursor method without orderBy"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
start_at: <
|
||||
json_values: "2"
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Cursor methods take the same number of values as there are OrderBy clauses.
|
||||
|
||||
description: "query: StartAt/EndBefore with values"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
start_at: <
|
||||
json_values: "7"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_before: <
|
||||
json_values: "9"
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
start_at: <
|
||||
values: <
|
||||
integer_value: 7
|
||||
>
|
||||
before: true
|
||||
>
|
||||
end_at: <
|
||||
values: <
|
||||
integer_value: 9
|
||||
>
|
||||
before: true
|
||||
>
|
||||
>
|
||||
>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Cursor methods take the same number of values as there are OrderBy clauses.
|
||||
|
||||
description: "query: StartAfter/EndAt with values"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
start_after: <
|
||||
json_values: "7"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_at: <
|
||||
json_values: "9"
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
start_at: <
|
||||
values: <
|
||||
integer_value: 7
|
||||
>
|
||||
>
|
||||
end_at: <
|
||||
values: <
|
||||
integer_value: 9
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Cursor methods take the same number of values as there are OrderBy clauses.
|
||||
|
||||
description: "query: Start/End with two values"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "b"
|
||||
>
|
||||
direction: "desc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
start_at: <
|
||||
json_values: "7"
|
||||
json_values: "8"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_at: <
|
||||
json_values: "9"
|
||||
json_values: "10"
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "b"
|
||||
>
|
||||
direction: DESCENDING
|
||||
>
|
||||
start_at: <
|
||||
values: <
|
||||
integer_value: 7
|
||||
>
|
||||
values: <
|
||||
integer_value: 8
|
||||
>
|
||||
before: true
|
||||
>
|
||||
end_at: <
|
||||
values: <
|
||||
integer_value: 9
|
||||
>
|
||||
values: <
|
||||
integer_value: 10
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Cursor values corresponding to a __name__ field take the document path relative
|
||||
# to the query's collection.
|
||||
|
||||
description: "query: cursor methods with __name__"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "__name__"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
start_after: <
|
||||
json_values: "\"D1\""
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_before: <
|
||||
json_values: "\"D2\""
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "__name__"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
start_at: <
|
||||
values: <
|
||||
reference_value: "projects/projectID/databases/(default)/documents/C/D1"
|
||||
>
|
||||
>
|
||||
end_at: <
|
||||
values: <
|
||||
reference_value: "projects/projectID/databases/(default)/documents/C/D2"
|
||||
>
|
||||
before: true
|
||||
>
|
||||
>
|
||||
>
|
||||
Generated
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# When multiple Start* or End* calls occur, the values of the last one are used.
|
||||
|
||||
description: "query: cursor methods, last one wins"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
start_after: <
|
||||
json_values: "1"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
start_at: <
|
||||
json_values: "2"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_at: <
|
||||
json_values: "3"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_before: <
|
||||
json_values: "4"
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
start_at: <
|
||||
values: <
|
||||
integer_value: 2
|
||||
>
|
||||
before: true
|
||||
>
|
||||
end_at: <
|
||||
values: <
|
||||
integer_value: 4
|
||||
>
|
||||
before: true
|
||||
>
|
||||
>
|
||||
>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Sentinel values are not permitted in queries.
|
||||
|
||||
description: "query: Delete in cursor method"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_before: <
|
||||
json_values: "\"Delete\""
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Sentinel values are not permitted in queries.
|
||||
|
||||
description: "query: Delete in Where"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: "=="
|
||||
json_value: "\"Delete\""
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The != operator is not supported.
|
||||
|
||||
description: "query: invalid operator in Where clause"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: "!="
|
||||
json_value: "4"
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The path has an empty component.
|
||||
|
||||
description: "query: invalid path in OrderBy clause"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "*"
|
||||
field: ""
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The path has an empty component.
|
||||
|
||||
description: "query: invalid path in Where clause"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
select: <
|
||||
fields: <
|
||||
field: "*"
|
||||
field: ""
|
||||
>
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The path has an empty component.
|
||||
|
||||
description: "query: invalid path in Where clause"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "*"
|
||||
field: ""
|
||||
>
|
||||
op: "=="
|
||||
json_value: "4"
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# With multiple Offset or Limit clauses, the last one wins.
|
||||
|
||||
description: "query: multiple Offset and Limit clauses"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
offset: 2
|
||||
>
|
||||
clauses: <
|
||||
limit: 3
|
||||
>
|
||||
clauses: <
|
||||
limit: 4
|
||||
>
|
||||
clauses: <
|
||||
offset: 5
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
offset: 5
|
||||
limit: <
|
||||
value: 4
|
||||
>
|
||||
>
|
||||
>
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Offset and Limit clauses.
|
||||
|
||||
description: "query: Offset and Limit clauses"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
offset: 2
|
||||
>
|
||||
clauses: <
|
||||
limit: 3
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
offset: 2
|
||||
limit: <
|
||||
value: 3
|
||||
>
|
||||
>
|
||||
>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Multiple OrderBy clauses combine.
|
||||
|
||||
description: "query: basic OrderBy clauses"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "b"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "desc"
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "b"
|
||||
>
|
||||
direction: ASCENDING
|
||||
>
|
||||
order_by: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
direction: DESCENDING
|
||||
>
|
||||
>
|
||||
>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# An empty Select clause selects just the document ID.
|
||||
|
||||
description: "query: empty Select clause"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
select: <
|
||||
>
|
||||
>
|
||||
query: <
|
||||
select: <
|
||||
fields: <
|
||||
field_path: "__name__"
|
||||
>
|
||||
>
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
>
|
||||
>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The last Select clause is the only one used.
|
||||
|
||||
description: "query: two Select clauses"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
select: <
|
||||
fields: <
|
||||
field: "a"
|
||||
>
|
||||
fields: <
|
||||
field: "b"
|
||||
>
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
select: <
|
||||
fields: <
|
||||
field: "c"
|
||||
>
|
||||
>
|
||||
>
|
||||
query: <
|
||||
select: <
|
||||
fields: <
|
||||
field_path: "c"
|
||||
>
|
||||
>
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
>
|
||||
>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# An ordinary Select clause.
|
||||
|
||||
description: "query: Select clause with some fields"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
select: <
|
||||
fields: <
|
||||
field: "a"
|
||||
>
|
||||
fields: <
|
||||
field: "b"
|
||||
>
|
||||
>
|
||||
>
|
||||
query: <
|
||||
select: <
|
||||
fields: <
|
||||
field_path: "a"
|
||||
>
|
||||
fields: <
|
||||
field_path: "b"
|
||||
>
|
||||
>
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
>
|
||||
>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Sentinel values are not permitted in queries.
|
||||
|
||||
description: "query: ServerTimestamp in cursor method"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
order_by: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
direction: "asc"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
end_before: <
|
||||
json_values: "\"ServerTimestamp\""
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Sentinel values are not permitted in queries.
|
||||
|
||||
description: "query: ServerTimestamp in Where"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: "=="
|
||||
json_value: "\"ServerTimestamp\""
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Multiple Where clauses are combined into a composite filter.
|
||||
|
||||
description: "query: two Where clauses"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: ">="
|
||||
json_value: "5"
|
||||
>
|
||||
>
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "b"
|
||||
>
|
||||
op: "<"
|
||||
json_value: "\"foo\""
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
where: <
|
||||
composite_filter: <
|
||||
op: AND
|
||||
filters: <
|
||||
field_filter: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
op: GREATER_THAN_OR_EQUAL
|
||||
value: <
|
||||
integer_value: 5
|
||||
>
|
||||
>
|
||||
>
|
||||
filters: <
|
||||
field_filter: <
|
||||
field: <
|
||||
field_path: "b"
|
||||
>
|
||||
op: LESS_THAN
|
||||
value: <
|
||||
string_value: "foo"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A Where clause that tests for equality with NaN results in a unary filter.
|
||||
|
||||
description: "query: a Where clause comparing to NaN"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: "=="
|
||||
json_value: "\"NaN\""
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
where: <
|
||||
unary_filter: <
|
||||
op: IS_NAN
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A Where clause that tests for equality with null results in a unary filter.
|
||||
|
||||
description: "query: a Where clause comparing to null"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: "=="
|
||||
json_value: "null"
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
where: <
|
||||
unary_filter: <
|
||||
op: IS_NULL
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A simple Where clause.
|
||||
|
||||
description: "query: Where clause"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
where: <
|
||||
path: <
|
||||
field: "a"
|
||||
>
|
||||
op: ">"
|
||||
json_value: "5"
|
||||
>
|
||||
>
|
||||
query: <
|
||||
from: <
|
||||
collection_id: "C"
|
||||
>
|
||||
where: <
|
||||
field_filter: <
|
||||
field: <
|
||||
field_path: "a"
|
||||
>
|
||||
op: GREATER_THAN
|
||||
value: <
|
||||
integer_value: 5
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# If a document snapshot is passed to a Start*/End* method, it must be in the same
|
||||
# collection as the query.
|
||||
|
||||
description: "query: doc snapshot with wrong collection in cursor method"
|
||||
query: <
|
||||
coll_path: "projects/projectID/databases/(default)/documents/C"
|
||||
clauses: <
|
||||
end_before: <
|
||||
doc_snapshot: <
|
||||
path: "projects/projectID/databases/(default)/documents/C2/D"
|
||||
json_data: "{\"a\": 7, \"b\": 8}"
|
||||
>
|
||||
>
|
||||
>
|
||||
is_error: true
|
||||
>
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A simple call, resulting in a single update operation.
|
||||
|
||||
description: "set: basic"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": 1}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
integer_value: 1
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A call to a write method with complicated input data.
|
||||
|
||||
description: "set: complex"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": [1, 2.5], \"b\": {\"c\": [\"three\", {\"d\": true}]}}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
array_value: <
|
||||
values: <
|
||||
integer_value: 1
|
||||
>
|
||||
values: <
|
||||
double_value: 2.5
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "b"
|
||||
value: <
|
||||
map_value: <
|
||||
fields: <
|
||||
key: "c"
|
||||
value: <
|
||||
array_value: <
|
||||
values: <
|
||||
string_value: "three"
|
||||
>
|
||||
values: <
|
||||
map_value: <
|
||||
fields: <
|
||||
key: "d"
|
||||
value: <
|
||||
boolean_value: true
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A Delete sentinel can appear with a merge option. If the delete paths are the
|
||||
# only ones to be merged, then no document is sent, just an update mask.
|
||||
|
||||
description: "set-merge: Delete with merge"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
option: <
|
||||
fields: <
|
||||
field: "b"
|
||||
field: "c"
|
||||
>
|
||||
>
|
||||
json_data: "{\"a\": 1, \"b\": {\"c\": \"Delete\"}}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
>
|
||||
update_mask: <
|
||||
field_paths: "b.c"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A Delete sentinel can appear with a merge option.
|
||||
|
||||
description: "set-merge: Delete with merge"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
option: <
|
||||
fields: <
|
||||
field: "a"
|
||||
>
|
||||
fields: <
|
||||
field: "b"
|
||||
field: "c"
|
||||
>
|
||||
>
|
||||
json_data: "{\"a\": 1, \"b\": {\"c\": \"Delete\"}}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
integer_value: 1
|
||||
>
|
||||
>
|
||||
>
|
||||
update_mask: <
|
||||
field_paths: "a"
|
||||
field_paths: "b.c"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A Delete sentinel can appear with a mergeAll option.
|
||||
|
||||
description: "set: Delete with MergeAll"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
option: <
|
||||
all: true
|
||||
>
|
||||
json_data: "{\"a\": 1, \"b\": {\"c\": \"Delete\"}}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
integer_value: 1
|
||||
>
|
||||
>
|
||||
>
|
||||
update_mask: <
|
||||
field_paths: "a"
|
||||
field_paths: "b.c"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The Delete sentinel must be the value of a field. Deletes are implemented by
|
||||
# turning the path to the Delete sentinel into a FieldPath, and FieldPaths do not
|
||||
# support array indexing.
|
||||
|
||||
description: "set: Delete cannot be anywhere inside an array value"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": [1, {\"b\": \"Delete\"}]}"
|
||||
is_error: true
|
||||
>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The Delete sentinel must be the value of a field. Deletes are implemented by
|
||||
# turning the path to the Delete sentinel into a FieldPath, and FieldPaths do not
|
||||
# support array indexing.
|
||||
|
||||
description: "set: Delete cannot be in an array value"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": [1, 2, \"Delete\"]}"
|
||||
is_error: true
|
||||
>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# The client signals an error if the Delete sentinel is in the input data, but not
|
||||
# selected by a merge option, because this is most likely a programming bug.
|
||||
|
||||
description: "set-merge: Delete cannot appear in an unmerged field"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
option: <
|
||||
fields: <
|
||||
field: "a"
|
||||
>
|
||||
>
|
||||
json_data: "{\"a\": 1, \"b\": \"Delete\"}"
|
||||
is_error: true
|
||||
>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# If a Delete is part of the value at a merge path, then the user is confused:
|
||||
# their merge path says "replace this entire value" but their Delete says "delete
|
||||
# this part of the value". This should be an error, just as if they specified
|
||||
# Delete in a Set with no merge.
|
||||
|
||||
description: "set-merge: Delete cannot appear as part of a merge path"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
option: <
|
||||
fields: <
|
||||
field: "h"
|
||||
>
|
||||
>
|
||||
json_data: "{\"h\": {\"g\": \"Delete\"}}"
|
||||
is_error: true
|
||||
>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# Without a merge option, Set replaces the document with the input data. A Delete
|
||||
# sentinel in the data makes no sense in this case.
|
||||
|
||||
description: "set: Delete cannot appear unless a merge option is specified"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{\"a\": 1, \"b\": \"Delete\"}"
|
||||
is_error: true
|
||||
>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
|
||||
description: "set: creating or setting an empty map"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
json_data: "{}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A merge with fields that use special characters.
|
||||
|
||||
description: "set-merge: Merge with FieldPaths"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
option: <
|
||||
fields: <
|
||||
field: "*"
|
||||
field: "~"
|
||||
>
|
||||
>
|
||||
json_data: "{\"*\": {\"~\": true}}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "*"
|
||||
value: <
|
||||
map_value: <
|
||||
fields: <
|
||||
key: "~"
|
||||
value: <
|
||||
boolean_value: true
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
update_mask: <
|
||||
field_paths: "`*`.`~`"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# A merge option where the field is not at top level. Only fields mentioned in the
|
||||
# option are present in the update operation.
|
||||
|
||||
description: "set-merge: Merge with a nested field"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
option: <
|
||||
fields: <
|
||||
field: "h"
|
||||
field: "g"
|
||||
>
|
||||
>
|
||||
json_data: "{\"h\": {\"g\": 4, \"f\": 5}}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "h"
|
||||
value: <
|
||||
map_value: <
|
||||
fields: <
|
||||
key: "g"
|
||||
value: <
|
||||
integer_value: 4
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
update_mask: <
|
||||
field_paths: "h.g"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# DO NOT MODIFY. This file was generated by
|
||||
# github.com/GoogleCloudPlatform/google-cloud-common/testing/firestore/cmd/generate-firestore-tests/generate-firestore-tests.go.
|
||||
|
||||
# If a field path is in a merge option, the value at that path replaces the stored
|
||||
# value. That is true even if the value is complex.
|
||||
|
||||
description: "set-merge: Merge field is not a leaf"
|
||||
set: <
|
||||
doc_ref_path: "projects/projectID/databases/(default)/documents/C/d"
|
||||
option: <
|
||||
fields: <
|
||||
field: "h"
|
||||
>
|
||||
>
|
||||
json_data: "{\"h\": {\"f\": 5, \"g\": 6}, \"e\": 7}"
|
||||
request: <
|
||||
database: "projects/projectID/databases/(default)"
|
||||
writes: <
|
||||
update: <
|
||||
name: "projects/projectID/databases/(default)/documents/C/d"
|
||||
fields: <
|
||||
key: "h"
|
||||
value: <
|
||||
map_value: <
|
||||
fields: <
|
||||
key: "f"
|
||||
value: <
|
||||
integer_value: 5
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "g"
|
||||
value: <
|
||||
integer_value: 6
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
update_mask: <
|
||||
field_paths: "h"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user