go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/impl/cloud/datastore.go (about)

     1  // Copyright 2016 The LUCI 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  //      http://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 cloud
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"cloud.google.com/go/datastore"
    26  	"google.golang.org/api/iterator"
    27  	pb "google.golang.org/genproto/googleapis/datastore/v1"
    28  
    29  	"go.chromium.org/luci/common/errors"
    30  
    31  	"go.chromium.org/luci/gae/impl/prod/constraints"
    32  	ds "go.chromium.org/luci/gae/service/datastore"
    33  )
    34  
    35  type cloudDatastore struct {
    36  	client *datastore.Client
    37  }
    38  
    39  func (cds *cloudDatastore) use(c context.Context) context.Context {
    40  	return ds.SetRawFactory(c, func(ic context.Context) ds.RawInterface {
    41  		return &boundDatastore{
    42  			Context:        ic,
    43  			cloudDatastore: cds,
    44  			transaction:    datastoreTransaction(ic),
    45  			kc:             ds.GetKeyContext(ic),
    46  		}
    47  	})
    48  }
    49  
    50  // boundDatastore is a bound instance of the cloudDatastore installed in the
    51  // Context.
    52  type boundDatastore struct {
    53  	context.Context
    54  
    55  	// Context is the bound user Context. It includes the datastore namespace, if
    56  	// one is set.
    57  	*cloudDatastore
    58  
    59  	transaction *transactionWrapper
    60  	kc          ds.KeyContext
    61  }
    62  
    63  func (bds *boundDatastore) AllocateIDs(keys []*ds.Key, cb ds.NewKeyCB) error {
    64  	nativeKeys, err := bds.client.AllocateIDs(bds, gaeKeysToNative(keys))
    65  	if err != nil {
    66  		return normalizeError(err)
    67  	}
    68  	for i, key := range nativeKeys {
    69  		cb(i, nativeKeyToGAE(bds.kc, key), nil)
    70  	}
    71  	return nil
    72  }
    73  
    74  func (bds *boundDatastore) RunInTransaction(fn func(context.Context) error, opts *ds.TransactionOptions) error {
    75  	if bds.transaction != nil {
    76  		return errors.New("nested transactions are not supported")
    77  	}
    78  
    79  	var txOpts []datastore.TransactionOption
    80  	if opts != nil {
    81  		if opts.ReadOnly {
    82  			txOpts = append(txOpts, datastore.ReadOnly)
    83  		}
    84  		if opts.Attempts > 0 {
    85  			txOpts = append(txOpts, datastore.MaxAttempts(opts.Attempts))
    86  		}
    87  	}
    88  
    89  	_, err := bds.client.RunInTransaction(bds, func(tx *datastore.Transaction) error {
    90  		return fn(withDatastoreTransaction(bds, tx))
    91  	}, txOpts...)
    92  	return normalizeError(err)
    93  }
    94  
    95  func (bds *boundDatastore) DecodeCursor(s string) (ds.Cursor, error) {
    96  	cursor, err := datastore.DecodeCursor(s)
    97  	return cursor, normalizeError(err)
    98  }
    99  
   100  func (bds *boundDatastore) Run(q *ds.FinalizedQuery, cb ds.RawRunCB) error {
   101  	it := bds.client.Run(bds, bds.prepareNativeQuery(q))
   102  	cursorFn := func() (ds.Cursor, error) {
   103  		return it.Cursor()
   104  	}
   105  
   106  	for {
   107  		var npl *nativePropertyLoader
   108  		if !q.KeysOnly() {
   109  			npl = &nativePropertyLoader{kc: bds.kc}
   110  		}
   111  		nativeKey, err := it.Next(npl)
   112  		if err != nil {
   113  			if err == iterator.Done {
   114  				return nil
   115  			}
   116  			return normalizeError(err)
   117  		}
   118  
   119  		var pmap ds.PropertyMap
   120  		if npl != nil {
   121  			pmap = npl.pmap
   122  		}
   123  		if err := cb(nativeKeyToGAE(bds.kc, nativeKey), pmap, cursorFn); err != nil {
   124  			if err == ds.Stop {
   125  				return nil
   126  			}
   127  			return normalizeError(err)
   128  		}
   129  	}
   130  }
   131  
   132  func (bds *boundDatastore) Count(q *ds.FinalizedQuery) (int64, error) {
   133  	// If the query is eventually consistent, use faster server-side aggregation.
   134  	// For strongly-consistent queries we'll have to do local counting.
   135  	if q.EventuallyConsistent() {
   136  		res, err := bds.client.RunAggregationQuery(bds,
   137  			bds.prepareNativeQuery(q).
   138  				NewAggregationQuery().
   139  				WithCount("total"),
   140  		)
   141  		if err != nil {
   142  			return -1, normalizeError(err)
   143  		}
   144  		total, _ := res["total"].(*pb.Value)
   145  		if total == nil {
   146  			return -1, fmt.Errorf("aggregation result is unexpectedly missing")
   147  		}
   148  		return int64(total.GetIntegerValue()), nil
   149  	}
   150  	// Local counting. It is the only strongly-consistent method.
   151  	v, err := bds.client.Count(bds, bds.prepareNativeQuery(q))
   152  	if err != nil {
   153  		return -1, normalizeError(err)
   154  	}
   155  	return int64(v), nil
   156  }
   157  
   158  func fixMultiError(err error) error {
   159  	if err == nil {
   160  		return nil
   161  	}
   162  	if baseME, ok := err.(datastore.MultiError); ok {
   163  		return errors.NewMultiError(baseME...)
   164  	}
   165  	return err
   166  }
   167  
   168  func idxCallbacker(err error, amt int, cb func(idx int, err error)) error {
   169  	if err == nil {
   170  		for i := 0; i < amt; i++ {
   171  			cb(i, nil)
   172  		}
   173  		return nil
   174  	}
   175  
   176  	err = fixMultiError(err)
   177  	if me, ok := err.(errors.MultiError); ok {
   178  		for i, err := range me {
   179  			cb(i, normalizeError(err))
   180  		}
   181  		return nil
   182  	}
   183  	return normalizeError(err)
   184  }
   185  
   186  func (bds *boundDatastore) GetMulti(keys []*ds.Key, _meta ds.MultiMetaGetter, cb ds.GetMultiCB) error {
   187  	nativeKeys := gaeKeysToNative(keys)
   188  	nativePLS := make([]*nativePropertyLoader, len(nativeKeys))
   189  	for i := range nativePLS {
   190  		nativePLS[i] = &nativePropertyLoader{kc: bds.kc}
   191  	}
   192  
   193  	var err error
   194  	if bds.transaction != nil {
   195  		// Transactional GetMulti.
   196  		err = bds.transaction.GetMulti(nativeKeys, nativePLS)
   197  	} else {
   198  		// Non-transactional GetMulti.
   199  		err = bds.client.GetMulti(bds, nativeKeys, nativePLS)
   200  	}
   201  
   202  	return idxCallbacker(err, len(nativePLS), func(idx int, err error) {
   203  		cb(idx, nativePLS[idx].pmap, err)
   204  	})
   205  }
   206  
   207  func (bds *boundDatastore) PutMulti(keys []*ds.Key, vals []ds.PropertyMap, cb ds.NewKeyCB) error {
   208  	nativeKeys := gaeKeysToNative(keys)
   209  	nativePLS := make([]*nativePropertySaver, len(vals))
   210  	for i := range nativePLS {
   211  		nativePLS[i] = &nativePropertySaver{kc: bds.kc, pmap: vals[i]}
   212  	}
   213  
   214  	var err error
   215  	if bds.transaction != nil {
   216  		// Transactional PutMulti.
   217  		//
   218  		// In order to simulate the presence of mid-transaction key allocation, we
   219  		// will identify any incomplete keys and allocate IDs for them. This is
   220  		// potentially wasteful in the event of failed or retried transactions, but
   221  		// it is required to maintain API compatibility with the datastore
   222  		// interface.
   223  		var incompleteKeys []*datastore.Key
   224  		var incompleteKeyMap map[int]int
   225  		for i, k := range nativeKeys {
   226  			if k.Incomplete() {
   227  				if incompleteKeyMap == nil {
   228  					// Optimization: if there are any incomplete keys, allocate room for
   229  					// the full range.
   230  					incompleteKeyMap = make(map[int]int, len(nativeKeys)-i)
   231  					incompleteKeys = make([]*datastore.Key, 0, len(nativeKeys)-i)
   232  				}
   233  				incompleteKeyMap[len(incompleteKeys)] = i
   234  				incompleteKeys = append(incompleteKeys, k)
   235  			}
   236  		}
   237  		if len(incompleteKeys) > 0 {
   238  			idKeys, err := bds.client.AllocateIDs(bds, incompleteKeys)
   239  			if err != nil {
   240  				return err
   241  			}
   242  			for i, idKey := range idKeys {
   243  				nativeKeys[incompleteKeyMap[i]] = idKey
   244  			}
   245  		}
   246  
   247  		_, err = bds.transaction.PutMulti(nativeKeys, nativePLS)
   248  	} else {
   249  		// Non-transactional PutMulti.
   250  		nativeKeys, err = bds.client.PutMulti(bds, nativeKeys, nativePLS)
   251  	}
   252  
   253  	return idxCallbacker(err, len(nativeKeys), func(idx int, err error) {
   254  		if err == nil {
   255  			cb(idx, nativeKeyToGAE(bds.kc, nativeKeys[idx]), nil)
   256  			return
   257  		}
   258  		cb(idx, nil, err)
   259  	})
   260  }
   261  
   262  func (bds *boundDatastore) DeleteMulti(keys []*ds.Key, cb ds.DeleteMultiCB) error {
   263  	nativeKeys := gaeKeysToNative(keys)
   264  
   265  	var err error
   266  	if bds.transaction != nil {
   267  		// Transactional DeleteMulti.
   268  		err = bds.transaction.DeleteMulti(nativeKeys)
   269  	} else {
   270  		// Non-transactional DeleteMulti.
   271  		err = bds.client.DeleteMulti(bds, nativeKeys)
   272  	}
   273  
   274  	return idxCallbacker(err, len(nativeKeys), cb)
   275  }
   276  
   277  func (bds *boundDatastore) WithoutTransaction() context.Context {
   278  	return withoutDatastoreTransaction(bds)
   279  }
   280  
   281  func (bds *boundDatastore) CurrentTransaction() ds.Transaction {
   282  	if bds.transaction == nil {
   283  		return nil
   284  	}
   285  	return bds.transaction
   286  }
   287  
   288  func (bds *boundDatastore) Constraints() ds.Constraints { return constraints.DS() }
   289  
   290  func (bds *boundDatastore) GetTestable() ds.Testable { return nil }
   291  
   292  func (bds *boundDatastore) prepareNativeQuery(fq *ds.FinalizedQuery) *datastore.Query {
   293  	nq := datastore.NewQuery(fq.Kind())
   294  	if bds.transaction != nil {
   295  		// NOTE: As of 2021 Q1 this is safe because it's documented that:
   296  		//
   297  		//   "Queries are re-usable and it is safe to call Query.Run from concurrent
   298  		//   goroutines"
   299  		//
   300  		// Inspecting the datastore client code reveals that it only uses the `id`
   301  		// field of the *Transaction object, not any of the state within the
   302  		// *Transaction object which needs protection via the *transactionWrapper.
   303  		nq = nq.Transaction(bds.transaction.tx)
   304  	}
   305  	if ns := bds.kc.Namespace; ns != "" {
   306  		nq = nq.Namespace(ns)
   307  	}
   308  
   309  	// nativeFilter translates a filter field. If the translation fails, we'll
   310  	// pass the result through to the underlying datastore and allow it to
   311  	// reject it.
   312  	nativeFilter := func(prop ds.Property) any {
   313  		if np, err := gaePropertyToNative(bds.kc, "", prop); err == nil {
   314  			return np.Value
   315  		}
   316  		return prop.Value()
   317  	}
   318  
   319  	// Equality filters.
   320  	for field, props := range fq.EqFilters() {
   321  		if field != "__ancestor__" {
   322  			for _, prop := range props {
   323  				nq = nq.FilterField(field, "=", nativeFilter(prop))
   324  			}
   325  		}
   326  	}
   327  	for field, slices := range fq.InFilters() {
   328  		for _, slice := range slices {
   329  			native := make([]any, len(slice))
   330  			for idx, prop := range slice {
   331  				native[idx] = nativeFilter(prop)
   332  			}
   333  			nq = nq.FilterField(field, "in", native)
   334  		}
   335  	}
   336  
   337  	// Inequality filters.
   338  	if ineq := fq.IneqFilterProp(); ineq != "" {
   339  		if field, op, prop := fq.IneqFilterLow(); field != "" {
   340  			nq = nq.FilterField(field, op, nativeFilter(prop))
   341  		}
   342  		if field, op, prop := fq.IneqFilterHigh(); field != "" {
   343  			nq = nq.FilterField(field, op, nativeFilter(prop))
   344  		}
   345  	}
   346  
   347  	start, end := fq.Bounds()
   348  	if start != nil {
   349  		nq = nq.Start(start.(datastore.Cursor))
   350  	}
   351  	if end != nil {
   352  		nq = nq.End(end.(datastore.Cursor))
   353  	}
   354  
   355  	if fq.Distinct() {
   356  		nq = nq.Distinct()
   357  	}
   358  	if fq.KeysOnly() {
   359  		nq = nq.KeysOnly()
   360  	}
   361  	if limit, ok := fq.Limit(); ok {
   362  		nq = nq.Limit(int(limit))
   363  	}
   364  	if offset, ok := fq.Offset(); ok {
   365  		nq = nq.Offset(int(offset))
   366  	}
   367  	if proj := fq.Project(); proj != nil {
   368  		nq = nq.Project(proj...)
   369  	}
   370  	if ancestor := fq.Ancestor(); ancestor != nil {
   371  		nq = nq.Ancestor(gaeKeyToNative(ancestor))
   372  	}
   373  	if fq.EventuallyConsistent() {
   374  		nq = nq.EventualConsistency()
   375  	}
   376  
   377  	for _, ic := range fq.Orders() {
   378  		prop := ic.Property
   379  		if ic.Descending {
   380  			prop = "-" + prop
   381  		}
   382  		nq = nq.Order(prop)
   383  	}
   384  
   385  	return nq
   386  }
   387  
   388  func gaePropertyToNative(kc ds.KeyContext, name string, pdata ds.PropertyData) (nativeProp datastore.Property, err error) {
   389  	nativeProp.Name = name
   390  
   391  	convert := func(prop *ds.Property) (any, error) {
   392  		switch pt := prop.Type(); pt {
   393  		case ds.PTNull, ds.PTInt, ds.PTTime, ds.PTBool, ds.PTBytes, ds.PTString, ds.PTFloat:
   394  			return prop.Value(), nil
   395  
   396  		case ds.PTGeoPoint:
   397  			gp := prop.Value().(ds.GeoPoint)
   398  			return datastore.GeoPoint{Lat: gp.Lat, Lng: gp.Lng}, nil
   399  
   400  		case ds.PTKey:
   401  			return gaeKeyToNative(prop.Value().(*ds.Key)), nil
   402  
   403  		case ds.PTPropertyMap:
   404  			return gaeEntityToNative(kc, prop.Value().(ds.PropertyMap)), nil
   405  
   406  		default:
   407  			return nil, fmt.Errorf("unsupported property type: %v", pt)
   408  		}
   409  	}
   410  
   411  	switch t := pdata.(type) {
   412  	case ds.Property:
   413  		if nativeProp.Value, err = convert(&t); err != nil {
   414  			return
   415  		}
   416  		nativeProp.NoIndex = (t.IndexSetting() != ds.ShouldIndex)
   417  
   418  	case ds.PropertySlice:
   419  		// Don't index by default. If *any* sub-property requests being indexed,
   420  		// then we will index.
   421  		nativeProp.NoIndex = true
   422  
   423  		// Pack this into an any so it is marked as a multi-value.
   424  		multiProp := make([]any, len(t))
   425  		for i := range t {
   426  			prop := &t[i]
   427  			if multiProp[i], err = convert(prop); err != nil {
   428  				return
   429  			}
   430  
   431  			if prop.IndexSetting() == ds.ShouldIndex {
   432  				nativeProp.NoIndex = false
   433  			}
   434  		}
   435  		nativeProp.Value = multiProp
   436  
   437  	default:
   438  		err = fmt.Errorf("unsupported PropertyData type for %q: %T", name, pdata)
   439  	}
   440  
   441  	return
   442  }
   443  
   444  func nativePropertyToGAE(kc ds.KeyContext, nativeProp datastore.Property) (name string, pdata ds.PropertyData, err error) {
   445  	name = nativeProp.Name
   446  
   447  	convert := func(nv any, prop *ds.Property) error {
   448  		switch nvt := nv.(type) {
   449  		case nil:
   450  			nv = nil
   451  
   452  		case int64, bool, string, float64:
   453  			break
   454  
   455  		case []byte:
   456  			if len(nvt) == 0 {
   457  				// Cloud datastore library returns []byte{} if it is empty.
   458  				// Make it nil as more convenient to deal with in tests.
   459  				nv = []byte(nil)
   460  			}
   461  
   462  		case time.Time:
   463  			// Cloud datastore library returns local time.
   464  			nv = nvt.UTC()
   465  
   466  		case datastore.GeoPoint:
   467  			nv = ds.GeoPoint{Lat: nvt.Lat, Lng: nvt.Lng}
   468  
   469  		case *datastore.Key:
   470  			nv = nativeKeyToGAE(kc, nvt)
   471  
   472  		case *datastore.Entity:
   473  			nv = nativeEntityToGAE(kc, nvt)
   474  
   475  		default:
   476  			return fmt.Errorf("unsupported datastore.Value type for %q: %T", name, nvt)
   477  		}
   478  
   479  		indexSetting := ds.ShouldIndex
   480  		if nativeProp.NoIndex {
   481  			indexSetting = ds.NoIndex
   482  		}
   483  		prop.SetValue(nv, indexSetting)
   484  		return nil
   485  	}
   486  
   487  	// Slice of supported native type. Convert this into PropertySlice.
   488  	//
   489  	// It must be an []any.
   490  	if nativeValues, ok := nativeProp.Value.([]any); ok {
   491  		pslice := make(ds.PropertySlice, len(nativeValues))
   492  		for i, nv := range nativeValues {
   493  			if err = convert(nv, &pslice[i]); err != nil {
   494  				return
   495  			}
   496  		}
   497  		pdata = pslice
   498  		return
   499  	}
   500  
   501  	var prop ds.Property
   502  	if err = convert(nativeProp.Value, &prop); err != nil {
   503  		return
   504  	}
   505  	pdata = prop
   506  	return
   507  }
   508  
   509  func gaeKeyToNative(key *ds.Key) *datastore.Key {
   510  	var nativeKey *datastore.Key
   511  
   512  	_, _, toks := key.Split()
   513  	for _, tok := range toks {
   514  		nativeKey = &datastore.Key{
   515  			Kind:      tok.Kind,
   516  			ID:        tok.IntID,
   517  			Name:      tok.StringID,
   518  			Parent:    nativeKey,
   519  			Namespace: key.Namespace(),
   520  		}
   521  	}
   522  
   523  	return nativeKey
   524  }
   525  
   526  func gaeKeysToNative(keys []*ds.Key) []*datastore.Key {
   527  	nativeKeys := make([]*datastore.Key, len(keys))
   528  	for i, key := range keys {
   529  		nativeKeys[i] = gaeKeyToNative(key)
   530  	}
   531  	return nativeKeys
   532  }
   533  
   534  func nativeKeyToGAE(kc ds.KeyContext, nativeKey *datastore.Key) *ds.Key {
   535  	toks := make([]ds.KeyTok, 0, 2)
   536  
   537  	cur := nativeKey
   538  	for {
   539  		toks = append(toks, ds.KeyTok{Kind: cur.Kind, IntID: cur.ID, StringID: cur.Name})
   540  		cur = cur.Parent
   541  		if cur == nil {
   542  			break
   543  		}
   544  	}
   545  
   546  	// Reverse "toks" so we have ancestor-to-child lineage.
   547  	for i := 0; i < len(toks)/2; i++ {
   548  		ri := len(toks) - i - 1
   549  		toks[i], toks[ri] = toks[ri], toks[i]
   550  	}
   551  
   552  	kc.Namespace = nativeKey.Namespace
   553  	return kc.NewKeyToks(toks)
   554  }
   555  
   556  // nativeEntityToGAE returns a ds.PropertyMap representation of the given
   557  // *datastore.Entity. Since properties can themselves be *datastore.Entities,
   558  // the caller is responsible for ensuring there are no reference cycles.
   559  func nativeEntityToGAE(kc ds.KeyContext, ent *datastore.Entity) ds.PropertyMap {
   560  	if ent == nil {
   561  		return nil
   562  	}
   563  	pm := make(ds.PropertyMap, len(ent.Properties)+4)
   564  	if ent.Key != nil {
   565  		// Populate all potentially supported meta properties. Whatever consumes
   566  		// the property map (usually the default struct PLS) will choose properties
   567  		// it cares about and ignore the rest.
   568  		ds.PopulateKey(pm, nativeKeyToGAE(kc, ent.Key))
   569  	}
   570  	// Property ordering is lost since it's encoded to a map, but *datastore.Entity is
   571  	// sourced from https://godoc.org/google.golang.org/genproto/googleapis/datastore/v1#Entity
   572  	// which originally held properties in a map to begin with, meaning order is irrelevant.
   573  	for _, p := range ent.Properties {
   574  		_, prop, err := nativePropertyToGAE(kc, p)
   575  		if err != nil {
   576  			// Shouldn't happen. It means the *datastore.Entity contained an unsupported type.
   577  			panic(err)
   578  		}
   579  		pm[p.Name] = prop
   580  	}
   581  	return pm
   582  }
   583  
   584  // gaeEntityToNative returns a *datastore.Entity representation of the given
   585  // PropertyMap (assumed to have been produced by nativeEntityToGAE).
   586  func gaeEntityToNative(kc ds.KeyContext, pm ds.PropertyMap) *datastore.Entity {
   587  	// Ensure stable order. Skip meta fields, they'll be used in NewKeyFromMeta.
   588  	keys := make([]string, 0, len(pm))
   589  	for name := range pm {
   590  		if !strings.HasPrefix(name, "$") {
   591  			keys = append(keys, name)
   592  		}
   593  	}
   594  	sort.Strings(keys)
   595  
   596  	ent := &datastore.Entity{
   597  		Properties: make([]datastore.Property, 0, len(keys)),
   598  	}
   599  
   600  	// Try to extract the entity key from available meta fields. Ignore incomplete
   601  	// keys. This actually happens for structs that don't have any explicitly
   602  	// defined meta properties (because `$kind` is implicitly defined, so they end
   603  	// up with an incomplete key, since they have no `$id`).
   604  	if key, _ := kc.NewKeyFromMeta(pm); key != nil && !key.IsIncomplete() {
   605  		ent.Key = gaeKeyToNative(key)
   606  	}
   607  
   608  	// Convert non-meta fields.
   609  	for _, name := range keys {
   610  		p, err := gaePropertyToNative(kc, name, pm[name])
   611  		if err != nil {
   612  			// Shouldn't happen. It means nativeEntityToGAE encoded an unsupported type.
   613  			panic(err)
   614  		}
   615  		ent.Properties = append(ent.Properties, p)
   616  	}
   617  	return ent
   618  }
   619  
   620  // nativePropertyLoader is a datastore.PropertyLoadSaver that implement Load
   621  // by writing properties into a ds.PropertyMap.
   622  type nativePropertyLoader struct {
   623  	kc   ds.KeyContext
   624  	pmap ds.PropertyMap // starts as nil, gets created and populated in Load
   625  }
   626  
   627  var _ datastore.PropertyLoadSaver = (*nativePropertyLoader)(nil)
   628  
   629  func (npl *nativePropertyLoader) Load(props []datastore.Property) error {
   630  	if npl.pmap == nil {
   631  		npl.pmap = make(ds.PropertyMap, len(props))
   632  	}
   633  
   634  	for _, nativeProp := range props {
   635  		name, pdata, err := nativePropertyToGAE(npl.kc, nativeProp)
   636  		if err != nil {
   637  			return err
   638  		}
   639  		if _, ok := npl.pmap[name]; ok {
   640  			return fmt.Errorf("duplicate properties for %q", name)
   641  		}
   642  		npl.pmap[name] = pdata
   643  	}
   644  	return nil
   645  }
   646  
   647  func (npl *nativePropertyLoader) Save() ([]datastore.Property, error) {
   648  	panic("must not be called")
   649  }
   650  
   651  // nativePropertySaver is a datastore.PropertyLoadSaver that implement Save
   652  // by reading properties from a ds.PropertyMap.
   653  type nativePropertySaver struct {
   654  	kc   ds.KeyContext
   655  	pmap ds.PropertyMap // must be set by the caller
   656  }
   657  
   658  var _ datastore.PropertyLoadSaver = (*nativePropertySaver)(nil)
   659  
   660  func (nps *nativePropertySaver) Load(props []datastore.Property) error {
   661  	panic("must not be called")
   662  }
   663  
   664  func (nps *nativePropertySaver) Save() ([]datastore.Property, error) {
   665  	if len(nps.pmap) == 0 {
   666  		return nil, nil
   667  	}
   668  
   669  	props := make([]datastore.Property, 0, len(nps.pmap))
   670  	for name, pdata := range nps.pmap {
   671  		// Strip meta.
   672  		if strings.HasPrefix(name, "$") {
   673  			continue
   674  		}
   675  
   676  		nativeProp, err := gaePropertyToNative(nps.kc, name, pdata)
   677  		if err != nil {
   678  			return nil, err
   679  		}
   680  		props = append(props, nativeProp)
   681  	}
   682  	return props, nil
   683  }
   684  
   685  // transactionWrapper provides a Mutex around mutation calls on the Transaction.
   686  //
   687  // This is required until https://github.com/googleapis/google-cloud-go/issues/3750
   688  // is fixed.
   689  type transactionWrapper struct {
   690  	mu sync.Mutex
   691  	tx *datastore.Transaction
   692  }
   693  
   694  func (tw *transactionWrapper) GetMulti(keys []*datastore.Key, dst any) (err error) {
   695  	// We don't acquire a lock here because as of 2021 Q1 Transaction.GetMulti
   696  	// only reads the Transaction.id field, and doesn't make any mutations to the
   697  	// *Transaction state at all.
   698  	return tw.tx.GetMulti(keys, dst)
   699  }
   700  
   701  func (tw *transactionWrapper) PutMulti(keys []*datastore.Key, src any) (ret []*datastore.PendingKey, err error) {
   702  	tw.mu.Lock()
   703  	defer tw.mu.Unlock()
   704  	return tw.tx.PutMulti(keys, src)
   705  }
   706  
   707  func (tw *transactionWrapper) DeleteMulti(keys []*datastore.Key) (err error) {
   708  	tw.mu.Lock()
   709  	defer tw.mu.Unlock()
   710  	return tw.tx.DeleteMulti(keys)
   711  }
   712  
   713  var datastoreTransactionKey = "*transactionWrapper"
   714  
   715  func withDatastoreTransaction(c context.Context, tx *datastore.Transaction) context.Context {
   716  	return context.WithValue(c, &datastoreTransactionKey, &transactionWrapper{tx: tx})
   717  }
   718  
   719  func withoutDatastoreTransaction(c context.Context) context.Context {
   720  	return context.WithValue(c, &datastoreTransactionKey, nil)
   721  }
   722  
   723  func datastoreTransaction(c context.Context) *transactionWrapper {
   724  	if tw, ok := c.Value(&datastoreTransactionKey).(*transactionWrapper); ok {
   725  		return tw
   726  	}
   727  	return nil
   728  }
   729  
   730  func normalizeError(err error) error {
   731  	switch err {
   732  	case datastore.ErrNoSuchEntity:
   733  		return ds.ErrNoSuchEntity
   734  	case datastore.ErrConcurrentTransaction:
   735  		return ds.ErrConcurrentTransaction
   736  	case datastore.ErrInvalidKey:
   737  		return ds.MakeErrInvalidKey("").Err()
   738  	default:
   739  		return err
   740  	}
   741  }