initial commit

Signed-off-by: Derek McQuay <derekmcquay@gmail.com>
This commit is contained in:
dm
2018-04-10 19:17:26 -07:00
commit 59af8fc84f
2025 changed files with 456739 additions and 0 deletions
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
}
}
File diff suppressed because it is too large Load Diff
+78
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
>
>
>
>
@@ -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
>
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
>
>
>
>
@@ -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
>
>
>
>
@@ -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
>
>
>
>
@@ -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
>
>
>
>
>
@@ -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
>
>
>
>
>
@@ -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
>
@@ -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
View File
@@ -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
>
>
>
>
>
@@ -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
>
>
>
>
@@ -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"
>
>
>
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
>
@@ -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"
>
>
>
>
@@ -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"
>
>
>
>
@@ -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"
>
>
>
>
@@ -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
>
>
>
@@ -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
>
>
>
@@ -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
>
>
>
@@ -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
>
@@ -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
>
>
>
@@ -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
>
>
>
>
@@ -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
>
>
>
>
@@ -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
>
>
>
@@ -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
>
>
>
@@ -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
>
@@ -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
>
@@ -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
>
@@ -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
>
@@ -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
>
@@ -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
>
@@ -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
>
>
>
@@ -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
View File
@@ -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
>
>
>
@@ -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"
>
>
>
@@ -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
View File
@@ -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"
>
>
>
@@ -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
View File
@@ -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
View File
@@ -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"
>
>
>
>
>
>
>
@@ -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"
>
>
>
>
>
@@ -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
View File
@@ -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
>
>
>
>
>
@@ -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
View File
@@ -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
View File
@@ -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
>
>
>
>
>
>
>
>
>
>
>
>
>
>
@@ -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
View File
@@ -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"
>
>
>
>
@@ -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"
>
>
>
>
@@ -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
>
@@ -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
>
@@ -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
>
@@ -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
>
@@ -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
View File
@@ -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
View File
@@ -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: "`*`.`~`"
>
>
>
>
@@ -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"
>
>
>
>
@@ -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