github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/docstore/gcpfirestore/fs.go (about)

     1  // Copyright 2019 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package gcpfirestore provides a docstore implementation backed by Google Cloud
    16  // Firestore.
    17  // Use OpenCollection to construct a *docstore.Collection.
    18  //
    19  // Docstore types not supported by the Go firestore client, cloud.google.com/go/firestore:
    20  //   - unsigned integers: encoded is int64s
    21  //   - arrays: encoded as Firestore array values
    22  //
    23  // Firestore types not supported by Docstore:
    24  //   - Document reference (a pointer to another Firestore document)
    25  //
    26  // # URLs
    27  //
    28  // For docstore.OpenCollection, gcpfirestore registers for the scheme
    29  // "firestore".
    30  // The default URL opener will create a connection using default credentials
    31  // from the environment, as described in
    32  // https://cloud.google.com/docs/authentication/production.
    33  // To customize the URL opener, or for more details on the URL format,
    34  // see URLOpener.
    35  // See https://gocloud.dev/concepts/urls/ for background information.
    36  //
    37  // # As
    38  //
    39  // gcpfirestore exposes the following types for as functions.
    40  // The pb package is google.golang.org/genproto/googleapis/firestore/v1.
    41  // The firestore  package is cloud.google.com/go/firestore/apiv1.
    42  //   - Collection.As: *firestore.Client
    43  //   - ActionList.BeforeDo: *pb.BatchGetDocumentsRequest or *pb.CommitRequest.
    44  //   - Query.BeforeQuery: *pb.RunQueryRequest
    45  //   - DocumentIterator: firestore.Firestore_RunQueryClient
    46  //   - Error: *google.golang.org/grpc/status.Status
    47  //
    48  // # Queries
    49  //
    50  // Firestore allows only one field in a query to be compared with an inequality
    51  // operator (one other than "="). This driver selects the first field in a Where
    52  // clause with an inequality to pass to Firestore and handles the rest locally. For
    53  // example, if the query specifies the three clauses A > 1, B > 2 and A < 3, then
    54  // A > 1 and A < 3 will be sent to Firestore, and the results will be filtered by
    55  // B > 2 in this driver.
    56  //
    57  // Firestore requires a composite index if a query contains both an equality and an
    58  // inequality comparison. This driver returns an error if the necessary index does
    59  // not exist. You must create the index manually. See
    60  // https://cloud.google.com/firestore/docs/query-data/indexing for details.
    61  //
    62  // See https://cloud.google.com/firestore/docs/query-data/queries for more information on Firestore queries.
    63  package gcpfirestore // import "gocloud.dev/docstore/gcpfirestore"
    64  
    65  import (
    66  	"bytes"
    67  	"context"
    68  	"fmt"
    69  	"io"
    70  	"os"
    71  	"reflect"
    72  	"regexp"
    73  	"strings"
    74  
    75  	vkit "cloud.google.com/go/firestore/apiv1"
    76  	"github.com/google/wire"
    77  	"gocloud.dev/docstore"
    78  	"gocloud.dev/docstore/driver"
    79  	"gocloud.dev/gcerrors"
    80  	"gocloud.dev/gcp"
    81  	"gocloud.dev/internal/gcerr"
    82  	"gocloud.dev/internal/useragent"
    83  	"google.golang.org/api/option"
    84  	pb "google.golang.org/genproto/googleapis/firestore/v1"
    85  	"google.golang.org/grpc"
    86  	"google.golang.org/grpc/metadata"
    87  	"google.golang.org/grpc/status"
    88  	"google.golang.org/protobuf/proto"
    89  	tspb "google.golang.org/protobuf/types/known/timestamppb"
    90  )
    91  
    92  // Dial returns a client to use with Firestore and a clean-up function to close
    93  // the client after used.
    94  // If the 'FIRESTORE_EMULATOR_HOST' environment variable is set the client connects
    95  // to the GCP firestore emulator by overriding the default endpoint.
    96  func Dial(ctx context.Context, ts gcp.TokenSource) (*vkit.Client, func(), error) {
    97  	opts := []option.ClientOption{
    98  		useragent.ClientOption("docstore"),
    99  	}
   100  	if host := os.Getenv("FIRESTORE_EMULATOR_HOST"); host != "" {
   101  		conn, err := grpc.DialContext(ctx, host, grpc.WithInsecure())
   102  		if err != nil {
   103  			return nil, nil, err
   104  		}
   105  		opts = append(opts,
   106  			option.WithEndpoint(host),
   107  			option.WithGRPCConn(conn),
   108  		)
   109  	} else {
   110  		opts = append(opts, option.WithTokenSource(ts))
   111  	}
   112  	c, err := vkit.NewClient(ctx, opts...)
   113  	return c, func() { c.Close() }, err
   114  }
   115  
   116  // Set holds Wire providers for this package.
   117  var Set = wire.NewSet(
   118  	Dial,
   119  	wire.Struct(new(URLOpener), "Client"),
   120  )
   121  
   122  type collection struct {
   123  	nameField string
   124  	nameFunc  func(docstore.Document) string
   125  	client    *vkit.Client
   126  	dbPath    string // e.g. "projects/P/databases/(default)"
   127  	collPath  string // e.g. "projects/P/databases/(default)/documents/States/Wisconsin/cities"
   128  	opts      *Options
   129  }
   130  
   131  // Options contains optional arguments to the OpenCollection functions.
   132  type Options struct {
   133  	// If true, allow queries that require client-side evaluation of filters (Where clauses)
   134  	// to run.
   135  	AllowLocalFilters bool
   136  	// The name of the field holding the document revision.
   137  	// Defaults to docstore.DefaultRevisionField.
   138  	RevisionField string
   139  
   140  	// The maximum number of RPCs that can be in progress for a single call to
   141  	// ActionList.Do.
   142  	// If less than 1, there is no limit.
   143  	MaxOutstandingActionRPCs int
   144  }
   145  
   146  // CollectionResourceID constructs a resource ID for a collection from the project ID and the collection path.
   147  // See the OpenCollection example for use.
   148  func CollectionResourceID(projectID, collPath string) string {
   149  	return fmt.Sprintf("projects/%s/databases/(default)/documents/%s", projectID, collPath)
   150  }
   151  
   152  // OpenCollection creates a *docstore.Collection representing a Firestore collection.
   153  //
   154  // collResourceID must be of the form "project/<projectID>/databases/(default)/documents/<collPath>".
   155  // <collPath> may be a top-level collection, like "States", or it may be a path to a nested
   156  // collection, like "States/Wisconsin/Cities".
   157  // See https://cloud.google.com/firestore/docs/reference/rest/ for more detail.
   158  //
   159  // gcpfirestore requires that a single field, nameField, be designated the primary
   160  // key. Its values must be strings, and must be unique over all documents in the
   161  // collection. The primary key must be provided to retrieve a document.
   162  func OpenCollection(client *vkit.Client, collResourceID, nameField string, opts *Options) (*docstore.Collection, error) {
   163  	c, err := newCollection(client, collResourceID, nameField, nil, opts)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	return docstore.NewCollection(c), nil
   168  }
   169  
   170  // OpenCollectionWithNameFunc creates a *docstore.Collection representing a Firestore collection.
   171  //
   172  // collResourceID must be of the form "project/<projectID>/databases/(default)/documents/<collPath>".
   173  // <collPath> may be a top-level collection, like "States", or it may be a path to a nested
   174  // collection, like "States/Wisconsin/Cities".
   175  //
   176  // The nameFunc argument is function that accepts a document and returns the value to
   177  // be used for the document's primary key. It should return the empty string if the
   178  // document is missing the information to construct a name. This will cause all
   179  // actions, even Create, to fail.
   180  //
   181  // Providing a function to construct the primary key is useful in two situations: if
   182  // your desired primary key field is not a string, or if there is more than one field
   183  // you want to use as a primary key.
   184  //
   185  // For the collection to be usable with Query.Delete and Query.Update, nameFunc
   186  // must work with both map and struct types representing the same underlying
   187  // data structure. See gocloud.dev/docstore/drivertest.HighScoreKey for an example.
   188  func OpenCollectionWithNameFunc(client *vkit.Client, collResourceID string, nameFunc func(docstore.Document) string, opts *Options) (*docstore.Collection, error) {
   189  	c, err := newCollection(client, collResourceID, "", nameFunc, opts)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	return docstore.NewCollection(c), nil
   194  }
   195  
   196  var resourceIDRE = regexp.MustCompile(`^(projects/[^/]+/databases/\(default\))/documents/.+`)
   197  
   198  func newCollection(client *vkit.Client, collResourceID, nameField string, nameFunc func(docstore.Document) string, opts *Options) (*collection, error) {
   199  	if nameField == "" && nameFunc == nil {
   200  		return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "one of nameField or nameFunc must be provided")
   201  	}
   202  	matches := resourceIDRE.FindStringSubmatch(collResourceID)
   203  	if matches == nil {
   204  		return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "bad collection resource ID %q; must match %v",
   205  			collResourceID, resourceIDRE)
   206  	}
   207  	dbPath := matches[1]
   208  	if opts == nil {
   209  		opts = &Options{}
   210  	}
   211  	if opts.RevisionField == "" {
   212  		opts.RevisionField = docstore.DefaultRevisionField
   213  	}
   214  	return &collection{
   215  		client:    client,
   216  		nameField: nameField,
   217  		nameFunc:  nameFunc,
   218  		dbPath:    dbPath,
   219  		collPath:  collResourceID,
   220  		opts:      opts,
   221  	}, nil
   222  }
   223  
   224  // Key returns the document key, if present. This is either the value of the field
   225  // called c.nameField, or the result of calling c.nameFunc.
   226  func (c *collection) Key(doc driver.Document) (interface{}, error) {
   227  	if c.nameField != "" {
   228  		name, err := doc.GetField(c.nameField)
   229  		vn := reflect.ValueOf(name)
   230  		if err != nil || name == nil || driver.IsEmptyValue(vn) {
   231  			// missing field is not an error
   232  			return nil, nil
   233  		}
   234  		// Check that the reflect kind is String so we can support any type whose underlying type
   235  		// is string. E.g. "type DocName string".
   236  		if vn.Kind() != reflect.String {
   237  			return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "key field %q with value %v is not a string",
   238  				c.nameField, name)
   239  		}
   240  		sname := vn.String()
   241  		if sname == "" { // empty string is the same as missing
   242  			return nil, nil
   243  		}
   244  		return sname, nil
   245  	}
   246  	sname := c.nameFunc(doc.Origin)
   247  	if sname == "" {
   248  		return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "missing document key")
   249  	}
   250  	return sname, nil
   251  }
   252  
   253  func (c *collection) RevisionField() string {
   254  	return c.opts.RevisionField
   255  }
   256  
   257  // RunActions implements driver.RunActions.
   258  func (c *collection) RunActions(ctx context.Context, actions []*driver.Action, opts *driver.RunActionsOptions) driver.ActionListError {
   259  	errs := make([]error, len(actions))
   260  	beforeGets, gets, writes, afterGets := driver.GroupActions(actions)
   261  	calls := c.buildCommitCalls(writes, errs)
   262  	// runGets does not issue concurrent RPCs, so it doesn't need a throttle.
   263  	c.runGets(ctx, beforeGets, errs, opts)
   264  	t := driver.NewThrottle(c.opts.MaxOutstandingActionRPCs)
   265  	for _, call := range calls {
   266  		call := call
   267  		t.Acquire()
   268  		go func() {
   269  			defer t.Release()
   270  			c.doCommitCall(ctx, call, errs, opts)
   271  		}()
   272  	}
   273  	t.Acquire()
   274  	c.runGets(ctx, gets, errs, opts)
   275  	t.Release()
   276  	t.Wait()
   277  	c.runGets(ctx, afterGets, errs, opts)
   278  	return driver.NewActionListError(errs)
   279  }
   280  
   281  // runGets executes a group of Get actions by calling the BatchGetDocuments RPC.
   282  // It may make several calls, because all gets in a single RPC must have the same set of field paths.
   283  func (c *collection) runGets(ctx context.Context, actions []*driver.Action, errs []error, opts *driver.RunActionsOptions) {
   284  	for _, group := range driver.GroupByFieldPath(actions) {
   285  		c.batchGet(ctx, group, errs, opts)
   286  	}
   287  }
   288  
   289  // Run a single BatchGet RPC with the given Get actions, all of which have the same set of field paths.
   290  // Populate errs, a slice of per-action errors indexed by the original action list position.
   291  func (c *collection) batchGet(ctx context.Context, gets []*driver.Action, errs []error, opts *driver.RunActionsOptions) {
   292  	setErr := func(err error) {
   293  		for _, g := range gets {
   294  			errs[g.Index] = err
   295  		}
   296  	}
   297  
   298  	req, err := c.newGetRequest(gets)
   299  	if err != nil {
   300  		setErr(err)
   301  		return
   302  	}
   303  	indexByPath := map[string]int{} // from document path to index in gets
   304  	for i, path := range req.Documents {
   305  		indexByPath[path] = i
   306  	}
   307  	if opts.BeforeDo != nil {
   308  		if err := opts.BeforeDo(driver.AsFunc(req)); err != nil {
   309  			setErr(err)
   310  			return
   311  		}
   312  	}
   313  	streamClient, err := c.client.BatchGetDocuments(withResourceHeader(ctx, req.Database), req)
   314  	if err != nil {
   315  		setErr(err)
   316  		return
   317  	}
   318  	for {
   319  		resp, err := streamClient.Recv()
   320  		if err == io.EOF {
   321  			break
   322  		}
   323  		if err != nil {
   324  			setErr(err)
   325  			return
   326  		}
   327  		switch r := resp.Result.(type) {
   328  		case *pb.BatchGetDocumentsResponse_Found:
   329  			pdoc := r.Found
   330  			i, ok := indexByPath[pdoc.Name]
   331  			if !ok {
   332  				setErr(gcerr.Newf(gcerr.Internal, nil, "no index for path %s", pdoc.Name))
   333  			} else {
   334  				errs[gets[i].Index] = decodeDoc(pdoc, gets[i].Doc, c.nameField, c.opts.RevisionField)
   335  			}
   336  		case *pb.BatchGetDocumentsResponse_Missing:
   337  			i := indexByPath[r.Missing]
   338  			errs[gets[i].Index] = gcerr.Newf(gcerr.NotFound, nil, "document at path %q is missing", r.Missing)
   339  		default:
   340  			setErr(gcerr.Newf(gcerr.Internal, nil, "unknown BatchGetDocumentsResponse result type"))
   341  			return
   342  		}
   343  	}
   344  }
   345  
   346  func (c *collection) newGetRequest(gets []*driver.Action) (*pb.BatchGetDocumentsRequest, error) {
   347  	req := &pb.BatchGetDocumentsRequest{Database: c.dbPath}
   348  	for _, a := range gets {
   349  		req.Documents = append(req.Documents, c.collPath+"/"+a.Key.(string))
   350  	}
   351  	// groupActions has already made sure that all the actions have the same field paths,
   352  	// so just use the first one.
   353  	var fps []string // field paths that will go in the mask
   354  	for _, fp := range gets[0].FieldPaths {
   355  		fps = append(fps, toServiceFieldPath(fp))
   356  	}
   357  	if fps != nil {
   358  		req.Mask = &pb.DocumentMask{FieldPaths: fps}
   359  	}
   360  	return req, nil
   361  }
   362  
   363  // commitCall holds information needed to make a Commit RPC and to follow up after it is done.
   364  type commitCall struct {
   365  	writes   []*pb.Write      // writes to commit
   366  	actions  []*driver.Action // actions corresponding to those writes
   367  	newNames []string         // new names for Create; parallel to actions
   368  }
   369  
   370  // Construct a set of concurrently runnable calls to Commit.
   371  func (c *collection) buildCommitCalls(actions []*driver.Action, errs []error) []*commitCall {
   372  	// Convert each action to one or more writes, collecting names for newly created
   373  	// documents along the way. Divide writes into those with preconditions and those without.
   374  	// Writes without preconditions can't fail, so we can execute them all in one Commit RPC.
   375  	// All other writes must be run as separate Commits.
   376  	var (
   377  		nCall  = &commitCall{} // for writes without preconditions
   378  		pCalls []*commitCall   // for writes with preconditions
   379  	)
   380  	for _, a := range actions {
   381  		ws, nn, err := c.actionToWrites(a)
   382  		if err != nil {
   383  			errs[a.Index] = err
   384  		} else if ws[0].CurrentDocument == nil { // no precondition
   385  			nCall.writes = append(nCall.writes, ws...)
   386  			nCall.actions = append(nCall.actions, a)
   387  			nCall.newNames = append(nCall.newNames, nn)
   388  		} else { // writes have a precondition
   389  			pCalls = append(pCalls, &commitCall{
   390  				writes:   ws,
   391  				actions:  []*driver.Action{a},
   392  				newNames: []string{nn},
   393  			})
   394  		}
   395  	}
   396  	if len(nCall.writes) == 0 {
   397  		return pCalls
   398  	}
   399  	return append(pCalls, nCall)
   400  }
   401  
   402  // Convert an action to one or more Firestore Write protos.
   403  func (c *collection) actionToWrites(a *driver.Action) ([]*pb.Write, string, error) {
   404  	var (
   405  		w       *pb.Write
   406  		ws      []*pb.Write
   407  		err     error
   408  		docName string
   409  		newName string // for Create with no name
   410  	)
   411  	if a.Key != nil {
   412  		docName = a.Key.(string)
   413  	}
   414  	switch a.Kind {
   415  	case driver.Create:
   416  		// Make a name for this document if it doesn't have one.
   417  		if a.Key == nil {
   418  			docName = driver.UniqueString()
   419  			newName = docName
   420  		}
   421  		w, err = c.putWrite(a.Doc, docName, &pb.Precondition{ConditionType: &pb.Precondition_Exists{Exists: false}})
   422  
   423  	case driver.Replace:
   424  		// If the given document has a revision, use it as the precondition (it implies existence).
   425  		pc, perr := c.revisionPrecondition(a.Doc)
   426  		if perr != nil {
   427  			return nil, "", perr
   428  		}
   429  		// Otherwise, just require that the document exists.
   430  		if pc == nil {
   431  			pc = &pb.Precondition{ConditionType: &pb.Precondition_Exists{Exists: true}}
   432  		}
   433  		w, err = c.putWrite(a.Doc, docName, pc)
   434  
   435  	case driver.Put:
   436  		pc, perr := c.revisionPrecondition(a.Doc)
   437  		if perr != nil {
   438  			return nil, "", perr
   439  		}
   440  		w, err = c.putWrite(a.Doc, docName, pc)
   441  
   442  	case driver.Update:
   443  		ws, err = c.updateWrites(a.Doc, docName, a.Mods)
   444  
   445  	case driver.Delete:
   446  		w, err = c.deleteWrite(a.Doc, docName)
   447  
   448  	default:
   449  		err = gcerr.Newf(gcerr.Internal, nil, "bad action %+v", a)
   450  	}
   451  	if err != nil {
   452  		return nil, "", err
   453  	}
   454  	if ws == nil {
   455  		ws = []*pb.Write{w}
   456  	}
   457  	return ws, newName, nil
   458  }
   459  
   460  func (c *collection) putWrite(doc driver.Document, docName string, pc *pb.Precondition) (*pb.Write, error) {
   461  	pdoc, err := encodeDoc(doc, c.nameField)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  	pdoc.Name = c.collPath + "/" + docName
   466  	return &pb.Write{
   467  		Operation:       &pb.Write_Update{Update: pdoc},
   468  		CurrentDocument: pc,
   469  	}, nil
   470  }
   471  
   472  func (c *collection) deleteWrite(doc driver.Document, docName string) (*pb.Write, error) {
   473  	pc, err := c.revisionPrecondition(doc)
   474  	if err != nil {
   475  		return nil, err
   476  	}
   477  	return &pb.Write{
   478  		Operation:       &pb.Write_Delete{Delete: c.collPath + "/" + docName},
   479  		CurrentDocument: pc,
   480  	}, nil
   481  }
   482  
   483  // updateWrites returns a slice of writes because we may need two: one for setting
   484  // and deleting values, the other for transforms.
   485  func (c *collection) updateWrites(doc driver.Document, docName string, mods []driver.Mod) ([]*pb.Write, error) {
   486  	ts, err := c.revisionTimestamp(doc)
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  	fields, paths, transforms, err := processMods(mods)
   491  	if err != nil {
   492  		return nil, err
   493  	}
   494  	return newUpdateWrites(c.collPath+"/"+docName, ts, fields, paths, transforms)
   495  }
   496  
   497  func newUpdateWrites(docPath string, ts *tspb.Timestamp, fields map[string]*pb.Value, paths []string, transforms []*pb.DocumentTransform_FieldTransform) ([]*pb.Write, error) {
   498  	pc := preconditionFromTimestamp(ts)
   499  	// If there is no revision in the document, add a precondition that the document exists.
   500  	if pc == nil {
   501  		pc = &pb.Precondition{ConditionType: &pb.Precondition_Exists{Exists: true}}
   502  	}
   503  	var ws []*pb.Write
   504  	if len(fields) > 0 || len(paths) > 0 {
   505  		ws = []*pb.Write{{
   506  			Operation: &pb.Write_Update{Update: &pb.Document{
   507  				Name:   docPath,
   508  				Fields: fields,
   509  			}},
   510  			UpdateMask:      &pb.DocumentMask{FieldPaths: paths},
   511  			CurrentDocument: pc,
   512  		}}
   513  		pc = nil // If the precondition is in the write, we don't need it in the transform.
   514  	}
   515  	if len(transforms) > 0 {
   516  		ws = append(ws, &pb.Write{
   517  			Operation: &pb.Write_Transform{
   518  				Transform: &pb.DocumentTransform{
   519  					Document:        docPath,
   520  					FieldTransforms: transforms,
   521  				},
   522  			},
   523  			CurrentDocument: pc,
   524  		})
   525  	}
   526  	return ws, nil
   527  }
   528  
   529  // To update a document, we need to send:
   530  // - A document with all the fields we want to add or change.
   531  // - A mask with the field paths of all the fields we want to add, change or delete.
   532  // processMods converts the mods into the fields for the document, and a list of
   533  // valid Firestore field paths for the mask.
   534  func processMods(mods []driver.Mod) (fields map[string]*pb.Value, maskPaths []string, transforms []*pb.DocumentTransform_FieldTransform, err error) {
   535  	fields = map[string]*pb.Value{}
   536  	for _, m := range mods {
   537  		sfp := toServiceFieldPath(m.FieldPath)
   538  		// If m.Value is nil, we want to delete it. In that case, we put the field in
   539  		// the mask but not in the doc.
   540  		if inc, ok := m.Value.(driver.IncOp); ok {
   541  			pv, err := encodeValue(inc.Amount)
   542  			if err != nil {
   543  				return nil, nil, nil, err
   544  			}
   545  			transforms = append(transforms, &pb.DocumentTransform_FieldTransform{
   546  				FieldPath: sfp,
   547  				TransformType: &pb.DocumentTransform_FieldTransform_Increment{
   548  					Increment: pv,
   549  				},
   550  			})
   551  		} else {
   552  			// The field path of every other mod belongs in the mask.
   553  			maskPaths = append(maskPaths, sfp)
   554  			if m.Value != nil {
   555  				pv, err := encodeValue(m.Value)
   556  				if err != nil {
   557  					return nil, nil, nil, err
   558  				}
   559  				if err := setAtFieldPath(fields, m.FieldPath, pv); err != nil {
   560  					return nil, nil, nil, err
   561  				}
   562  			}
   563  		}
   564  	}
   565  	return fields, maskPaths, transforms, nil
   566  }
   567  
   568  // doCommitCall Calls the Commit RPC with a list of writes, and handles the results.
   569  func (c *collection) doCommitCall(ctx context.Context, call *commitCall, errs []error, opts *driver.RunActionsOptions) {
   570  	wrs, err := c.commit(ctx, call.writes, opts)
   571  	if err != nil {
   572  		for _, a := range call.actions {
   573  			errs[a.Index] = err
   574  		}
   575  		return
   576  	}
   577  	// Set the revision fields of the documents.
   578  	// The actions and writes may not correspond, because Update actions may require
   579  	// two writes. We can tell which writes correspond to actions by the type of write.
   580  	j := 0
   581  	for i, a := range call.actions {
   582  		wr := wrs[j]
   583  		if a.Doc.HasField(c.opts.RevisionField) {
   584  			if err := a.Doc.SetField(c.opts.RevisionField, wr.UpdateTime); err != nil {
   585  				errs[a.Index] = err
   586  			}
   587  		}
   588  		if call.newNames[i] != "" {
   589  			// c.nameField should not be empty since we only create new names when there
   590  			// is a nameField.
   591  			_ = a.Doc.SetField(c.nameField, call.newNames[i])
   592  		}
   593  		if hasFollowingTransform(call.writes, j) {
   594  			j = j + 2
   595  		} else {
   596  			j++
   597  		}
   598  	}
   599  	return
   600  }
   601  
   602  func hasFollowingTransform(writes []*pb.Write, i int) bool {
   603  	if i >= len(writes)-1 {
   604  		return false
   605  	}
   606  	curr, ok := writes[i].Operation.(*pb.Write_Update)
   607  	if !ok {
   608  		return false
   609  	}
   610  	next, ok := writes[i+1].Operation.(*pb.Write_Transform)
   611  	if !ok {
   612  		return false
   613  	}
   614  	return curr.Update.Name == next.Transform.Document
   615  }
   616  
   617  func (c *collection) commit(ctx context.Context, ws []*pb.Write, opts *driver.RunActionsOptions) ([]*pb.WriteResult, error) {
   618  	req := &pb.CommitRequest{
   619  		Database: c.dbPath,
   620  		Writes:   ws,
   621  	}
   622  	if opts.BeforeDo != nil {
   623  		if err := opts.BeforeDo(driver.AsFunc(req)); err != nil {
   624  			return nil, err
   625  		}
   626  	}
   627  	res, err := c.client.Commit(withResourceHeader(ctx, req.Database), req)
   628  	if err != nil {
   629  		return nil, err
   630  	}
   631  	if len(res.WriteResults) != len(ws) {
   632  		return nil, gcerr.Newf(gcerr.Internal, nil, "wrong number of WriteResults from firestore commit")
   633  	}
   634  	return res.WriteResults, nil
   635  }
   636  
   637  ///////////////
   638  // From memdocstore/mem.go.
   639  
   640  // setAtFieldPath sets m's value at fp to val. It creates intermediate maps as
   641  // needed. It returns an error if a non-final component of fp does not denote a map.
   642  func setAtFieldPath(m map[string]*pb.Value, fp []string, val *pb.Value) error {
   643  	m2, err := getParentMap(m, fp, true)
   644  	if err != nil {
   645  		return err
   646  	}
   647  	m2[fp[len(fp)-1]] = val
   648  	return nil
   649  }
   650  
   651  // getParentMap returns the map that directly contains the given field path;
   652  // that is, the value of m at the field path that excludes the last component
   653  // of fp. If a non-map is encountered along the way, an InvalidArgument error is
   654  // returned. If nil is encountered, nil is returned unless create is true, in
   655  // which case a map is added at that point.
   656  func getParentMap(m map[string]*pb.Value, fp []string, create bool) (map[string]*pb.Value, error) {
   657  	for _, k := range fp[:len(fp)-1] {
   658  		if m[k] == nil {
   659  			if !create {
   660  				return nil, nil
   661  			}
   662  			m[k] = &pb.Value{ValueType: &pb.Value_MapValue{&pb.MapValue{Fields: map[string]*pb.Value{}}}}
   663  		}
   664  		mv := m[k].GetMapValue()
   665  		if mv == nil {
   666  			return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "invalid field path %q at %q", strings.Join(fp, "."), k)
   667  		}
   668  		m = mv.Fields
   669  	}
   670  	return m, nil
   671  }
   672  
   673  ////////////////
   674  // From fieldpath.go in cloud.google.com/go/firestore.
   675  
   676  // Convert a docstore field path, which is a []string, into the kind of field path
   677  // that the Firestore service expects: a string of dot-separated components, some of
   678  // which may be quoted.
   679  func toServiceFieldPath(fp []string) string {
   680  	cs := make([]string, len(fp))
   681  	for i, c := range fp {
   682  		cs[i] = toServiceFieldPathComponent(c)
   683  	}
   684  	return strings.Join(cs, ".")
   685  }
   686  
   687  // Google SQL syntax for an unquoted field.
   688  var unquotedFieldRE = regexp.MustCompile("^[A-Za-z_][A-Za-z_0-9]*$")
   689  
   690  // toServiceFieldPathComponent returns a string that represents key and is a valid
   691  // Firestore field path component. Components must be quoted with backticks if
   692  // they don't match the above regexp.
   693  func toServiceFieldPathComponent(key string) string {
   694  	if unquotedFieldRE.MatchString(key) {
   695  		return key
   696  	}
   697  	var buf bytes.Buffer
   698  	buf.WriteRune('`')
   699  	for _, r := range key {
   700  		if r == '`' || r == '\\' {
   701  			buf.WriteRune('\\')
   702  		}
   703  		buf.WriteRune(r)
   704  	}
   705  	buf.WriteRune('`')
   706  	return buf.String()
   707  }
   708  
   709  // revisionPrecondition returns a Firestore precondition that asserts that the stored document's
   710  // revision matches the revision of doc.
   711  func (c *collection) revisionPrecondition(doc driver.Document) (*pb.Precondition, error) {
   712  	rev, err := c.revisionTimestamp(doc)
   713  	if err != nil {
   714  		return nil, err
   715  	}
   716  	return preconditionFromTimestamp(rev), nil
   717  }
   718  
   719  // revisionTimestamp extracts the timestamp from the revision field of doc, if there is one.
   720  // It only returns an error if the revision field is present and does not contain the right type.
   721  func (c *collection) revisionTimestamp(doc driver.Document) (*tspb.Timestamp, error) {
   722  	v, err := doc.GetField(c.opts.RevisionField)
   723  	if err != nil { // revision field not present
   724  		return nil, nil
   725  	}
   726  	if v == nil { // revision field is present, but nil
   727  		return nil, nil
   728  	}
   729  	rev, ok := v.(*tspb.Timestamp)
   730  	if !ok {
   731  		return nil, gcerr.Newf(gcerr.InvalidArgument, nil,
   732  			"%s field contains wrong type: got %T, want proto Timestamp",
   733  			c.opts.RevisionField, v)
   734  	}
   735  	return rev, nil
   736  }
   737  
   738  func preconditionFromTimestamp(ts *tspb.Timestamp) *pb.Precondition {
   739  	if ts == nil || (ts.Seconds == 0 && ts.Nanos == 0) { // ignore a missing or zero revision
   740  		return nil
   741  	}
   742  	return &pb.Precondition{ConditionType: &pb.Precondition_UpdateTime{ts}}
   743  }
   744  
   745  func (c *collection) ErrorCode(err error) gcerrors.ErrorCode {
   746  	return gcerr.GRPCCode(err)
   747  }
   748  
   749  // resourcePrefixHeader is the name of the metadata header used to indicate
   750  // the resource being operated on.
   751  const resourcePrefixHeader = "google-cloud-resource-prefix"
   752  
   753  // withResourceHeader returns a new context that includes resource in a special header.
   754  // Firestore uses the resource header for routing.
   755  func withResourceHeader(ctx context.Context, resource string) context.Context {
   756  	md, _ := metadata.FromOutgoingContext(ctx)
   757  	md = md.Copy()
   758  	md[resourcePrefixHeader] = []string{resource}
   759  	return metadata.NewOutgoingContext(ctx, md)
   760  }
   761  
   762  // RevisionToBytes implements driver.RevisionToBytes.
   763  func (c *collection) RevisionToBytes(rev interface{}) ([]byte, error) {
   764  	r, ok := rev.(*tspb.Timestamp)
   765  	if !ok {
   766  		return nil, gcerr.Newf(gcerr.InvalidArgument, nil, "revision %v of type %[1]T is not a proto Timestamp", rev)
   767  	}
   768  	return proto.Marshal(r)
   769  }
   770  
   771  // BytesToRevision implements driver.BytesToRevision.
   772  func (c *collection) BytesToRevision(b []byte) (interface{}, error) {
   773  	var ts tspb.Timestamp
   774  	if err := proto.Unmarshal(b, &ts); err != nil {
   775  		return nil, err
   776  	}
   777  	return &ts, nil
   778  }
   779  
   780  func (c *collection) As(i interface{}) bool {
   781  	p, ok := i.(**vkit.Client)
   782  	if !ok {
   783  		return false
   784  	}
   785  	*p = c.client
   786  	return true
   787  }
   788  
   789  // ErrorAs implements driver.Collection.ErrorAs.
   790  func (c *collection) ErrorAs(err error, i interface{}) bool {
   791  	s, ok := status.FromError(err)
   792  	if !ok {
   793  		return false
   794  	}
   795  	p, ok := i.(**status.Status)
   796  	if !ok {
   797  		return false
   798  	}
   799  	*p = s
   800  	return true
   801  }
   802  
   803  // Close implements driver.Collection.Close.
   804  func (c *collection) Close() error { return nil }