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

     1  // Copyright 2015 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 datastore
    16  
    17  import (
    18  	"fmt"
    19  	"reflect"
    20  )
    21  
    22  // GetPLS resolves obj into default struct PropertyLoadSaver and
    23  // MetaGetterSetter implementation.
    24  //
    25  // obj must be a non-nil pointer to a struct of some sort.
    26  //
    27  // By default, exported fields will be serialized to/from the datastore. If the
    28  // field is not exported, it will be skipped by the serialization routines.
    29  //
    30  // If a field is of a non-supported type (see Property for the list of supported
    31  // property types), this function will panic. Other problems include duplicate
    32  // field names (due to tagging), recursively defined structs, nested structures
    33  // with multiple slices (e.g.  slices of slices, either directly `[][]type` or
    34  // indirectly `[]Embedded` where Embedded contains a slice.)
    35  //
    36  // The following field types are supported:
    37  //   - int64, int32, int16, int8, int
    38  //   - uint32, uint16, uint8, byte
    39  //   - float64, float32
    40  //   - string
    41  //   - []byte
    42  //   - bool
    43  //   - time.Time
    44  //   - GeoPoint
    45  //   - *Key
    46  //   - any Type whose underlying type is one of the above types
    47  //   - Types which implement PropertyConverter on (*Type)
    48  //   - Types which implement proto.Message
    49  //   - A struct composed of the above types (except for nested slices)
    50  //   - A slice of any of the above types
    51  //
    52  // GetPLS supports the following struct tag syntax:
    53  //
    54  //	`gae:"[fieldName][,noindex]"` -- `fieldName`, if supplied, is an alternate
    55  //	   datastore property name for an exportable field. By default this library
    56  //	   uses the Go field name as the datastore property name, but sometimes
    57  //	   this is undesirable (e.g. for datastore compatibility with another,
    58  //	   likely python, application which named the field with a lowercase
    59  //	   first letter).
    60  //
    61  //	   A fieldName of "-" means that gae will ignore the field for all
    62  //	   serialization/deserialization.
    63  //
    64  //	   if noindex is specified, then this field will not be indexed in the
    65  //	   datastore, even if it was an otherwise indexable type. If fieldName is
    66  //	   blank, and noindex is specifed, then fieldName will default to the
    67  //	   field's actual name. Note that by default, all fields (with indexable
    68  //	   types) are indexed.
    69  //
    70  //	`gae:"[fieldName][,nocompress|zstd|legacy]"` -- for fields of type
    71  //	   `proto.Message`. Protobuf fields are _never_ indexed, but are stored
    72  //	   as encoded blobs.
    73  //
    74  //	   Like for other fields, `fieldName` is optional, and defaults to the Go
    75  //	   struct field name if omitted.
    76  //
    77  //	   By default (with no options), protos are stored with binary encoding
    78  //	   without compression. This is the same as "nocompress".
    79  //
    80  //	   You may optionally use "zstd" compression by specifying this option.
    81  //
    82  //	   It is valid to switch between "nocompress" and "zstd"; the library
    83  //	   knows how to decode and encode both, even when the in-datastore format
    84  //	   doesn't match the tag.
    85  //
    86  //	   The "legacy" option will store the protobuf without compression, BUT this
    87  //	   encoding doesn't have a "mode" bit. This is purely for compatibility with
    88  //	   the deprecated `proto-gae` generator, and is not recommended. The format
    89  //	   is a `[]byte` containing the binary serialization of the proto with no
    90  //	   other metadata.
    91  //
    92  //	`gae:"[fieldName],lsp[,noindex]` -- for nested struct-valued fields (structs
    93  //	   specifically, not pointers to structs). "lsp" stands for "local
    94  //	   structured property", since this feature is primarily used for
    95  //	   compatibility with Python's ndb.LocalStructuredProperty. Fields that use
    96  //	   this option are stored as nested entities inside the larger outer entity.
    97  //
    98  //	   By default fields of nested entities are indexed. E.g. if an entity
    99  //	   property `nested` contains a nested entity with a property `prop`,
   100  //	   there's a datastore index on a field called `nested.prop` that can be
   101  //	   used to e.g. query for all entities that have a nested entity with `prop`
   102  //	   property set to some value.
   103  //
   104  //	   Use "noindex" option to suppress indexing of *all* fields of the nested
   105  //	   entity (recursively). Without "noindex" the indexing decision is done
   106  //	   based on options set on the inner fields.
   107  //
   108  //	   NOTE: Python's ndb.LocalStructuredProperty doesn't support indexes, so
   109  //	   any nested entities written from Python will be unindexed.
   110  //
   111  //	   Finally, nested entities can have keys (defined as usual via meta
   112  //	   fields, see below). Semantically they are just indexed key-valued
   113  //	   properties internally named `__key__`. In particular to query based on
   114  //	   a nested property key use e.g. `nested.__key__` field name. "noindex"
   115  //	   option on the "lsp" field will turn of indexing of the nested key.
   116  //	   There's no way to index some inner property, and *not* index the inner
   117  //	   key at the same time. Only complete keys are stored, e.g. if an inner
   118  //	   entity has `$id` meta-field, but its value is 0 (indicating an incomplete
   119  //	   key), this key won't be stored at all. This can have observable
   120  //	   consequences when using `$id` and `$parent` meta fields together or just
   121  //	   using partially populated key in `$key` meta field.
   122  //
   123  //	   Keys are round-tripped correctly when using Cloud Datastore APIs, but
   124  //	   Python's ndb.LocalStructuredProperty would drop them, so better not to
   125  //	   depend on them when doing interop with Python.
   126  //
   127  //	`gae:"$metaKey[,<value>]` -- indicates a field is metadata. Metadata
   128  //	   can be used to control filter behavior, or to store key data when using
   129  //	   the Interface.KeyForObj* methods. The supported field types are:
   130  //	     - *Key
   131  //	     - int64, int32, int16, int8, uint32, uint16, uint8, byte
   132  //	     - string
   133  //	     - Toggle (GetMeta and SetMeta treat the field as if it were bool)
   134  //	     - Any type which implements PropertyConverter
   135  //	     - Any type which implements proto.Message
   136  //	   Additionally, numeric, string and Toggle types allow setting a default
   137  //	   value in the struct field tag (the "<value>" portion).
   138  //
   139  //	   Only exported fields allow SetMeta, but all fields of appropriate type
   140  //	   allow tagged defaults for use with GetMeta. See Examples.
   141  //
   142  //	`gae:"[-],extra"` -- for fields of type PropertyMap. Indicates that any
   143  //	   extra, unrecognized or mismatched property types (type in datastore
   144  //	   doesn't match your struct's field type) should be loaded into and
   145  //	   saved from this field. This form allows you to control the behavior
   146  //	   of reads and writes when your schema changes, or to implement something
   147  //	   like ndb.Expando with a mix of structured and unstructured fields.
   148  //
   149  //	   If the `-` is present, then datastore write operations will not put
   150  //	   elements of this map into the datastore.
   151  //
   152  //	   If the field is non-exported, then read operations from the datastore
   153  //	   will not populate the members of this map, but extra fields or
   154  //	   structural differences encountered when reading into this struct will be
   155  //	   silently ignored. This is useful if you want to just ignore old fields.
   156  //
   157  //	   If there is a conflict between a field in the struct and a same-named
   158  //	   Property in the extra field, the field in the struct takes precedence.
   159  //
   160  //	   Recursive structs are supported, but all extra properties go to the
   161  //	   topmost structure's Extra field. This is a bit non-intuitive, but the
   162  //	   implementation complexity was deemed not worth it, since that sort of
   163  //	   thing is generally only useful on schema changes, which should be
   164  //	   transient.
   165  //
   166  //	   Examples:
   167  //	     // "black hole": ignore mismatches, ignore on write
   168  //	     _ PropertyMap `gae:"-,extra"
   169  //
   170  //	     // "expando": full content is read/written
   171  //	     Expando PropertyMap `gae:",extra"
   172  //
   173  //	     // "convert": content is read from datastore, but lost on writes. This
   174  //	     // is useful for doing conversions from an old schema to a new one,
   175  //	     // since you can retrieve the old data and populate it into new fields,
   176  //	     // for example. Probably should be used in conjunction with an
   177  //	     // implementation of the PropertyLoadSaver interface so that you can
   178  //	     // transparently upconvert to the new schema on load.
   179  //	     Convert PropertyMap `gae:"-,extra"
   180  //
   181  // Example "special" structure. This is supposed to be some sort of datastore
   182  // singleton object.
   183  //
   184  //	struct secretFoo {
   185  //	  // _id and _kind are not exported, so setting their values will not be
   186  //	  // reflected by GetMeta.
   187  //	  _id   int64  `gae:"$id,1"`
   188  //	  _kind string `gae:"$kind,InternalFooSingleton"`
   189  //
   190  //	  // Value is exported, so can be read and written by the PropertyLoadSaver,
   191  //	  // but secretFoo is shared with a python appengine module which has
   192  //	  // stored this field as 'value' instead of 'Value'.
   193  //	  Value int64  `gae:"value"`
   194  //	}
   195  //
   196  // Example "normal" structure that you might use in a go-only appengine app.
   197  //
   198  //	struct User {
   199  //	  ID string `gae:"$id"`
   200  //	  // "kind" is automatically implied by the struct name: "User"
   201  //	  // "parent" is nil... Users are root entities
   202  //
   203  //	  // 'Name' will be serialized to the datastore in the field 'Name'
   204  //	  Name string
   205  //	}
   206  //
   207  //	struct Comment {
   208  //	  ID int64 `gae:"$id"`
   209  //	  // "kind" is automatically implied by the struct name: "Comment"
   210  //
   211  //	  // Parent will be enforced by the application to be a User key.
   212  //	  Parent *Key `gae:"$parent"`
   213  //
   214  //	  // 'Lines' will serialized to the datastore in the field 'Lines'
   215  //	  Lines []string
   216  //	}
   217  //
   218  // A pointer-to-struct may also implement MetaGetterSetter to provide more
   219  // sophisticated metadata values. Explicitly defined fields (as shown above)
   220  // always take precedence over fields manipulated by the MetaGetterSetter
   221  // methods. So if your GetMeta handles "kind", but you explicitly have a
   222  // $kind field, the $kind field will take precedence and your GetMeta
   223  // implementation will not be called for "kind".
   224  //
   225  // A struct overloading any of the PropertyLoadSaver or MetaGetterSetter
   226  // interfaces may evoke the default struct behavior by using GetPLS on itself.
   227  // For example:
   228  //
   229  //	struct Special {
   230  //	  Name string
   231  //
   232  //	  foo string
   233  //	}
   234  //
   235  //	func (s *Special) Load(props PropertyMap) error {
   236  //	  if foo, ok := props["foo"]; ok && len(foo) == 1 {
   237  //	    s.foo = foo
   238  //	    delete(props, "foo")
   239  //	  }
   240  //	  return GetPLS(s).Load(props)
   241  //	}
   242  //
   243  //	func (s *Special) Save(withMeta bool) (PropertyMap, error) {
   244  //	  props, err := GetPLS(s).Save(withMeta)
   245  //	  if err != nil {
   246  //	    return nil, err
   247  //	  }
   248  //	  props["foo"] = []Property{MkProperty(s.foo)}
   249  //	  return props, nil
   250  //	}
   251  //
   252  //	func (s *Special) Problem() error {
   253  //	  return GetPLS(s).Problem()
   254  //	}
   255  //
   256  // Additionally, any field ptr-to-type may implement the PropertyConverter
   257  // interface to allow a single field to, for example, implement some alternate
   258  // encoding (json, gzip), or even just serialize to/from a simple string field.
   259  // This applies to normal fields, as well as metadata fields. It can be useful
   260  // for storing struct '$id's which have multi-field meanings. For example, the
   261  // Person struct below could be initialized in go as `&Person{Name{"Jane",
   262  // "Doe"}}`, retaining Jane's name as manipulable Go fields. However, in the
   263  // datastore, it would have a key of `/Person,"Jane|Doe"`, and loading the
   264  // struct from the datastore as part of a Query, for example, would correctly
   265  // populate Person.Name.First and Person.Name.Last.
   266  //
   267  //	type Name struct {
   268  //	  First string
   269  //	  Last string
   270  //	}
   271  //
   272  //	func (n *Name) ToProperty() (Property, error) {
   273  //	  return MkProperty(fmt.Sprintf("%s|%s", n.First, n.Last)), nil
   274  //	}
   275  //
   276  //	func (n *Name) FromProperty(p Property) error {
   277  //	  // check p to be a PTString
   278  //	  // split on "|"
   279  //	  // assign to n.First, n.Last
   280  //	}
   281  //
   282  //	type Person struct {
   283  //	  ID Name `gae:"$id"`
   284  //	}
   285  func GetPLS(obj any) interface {
   286  	PropertyLoadSaver
   287  	MetaGetterSetter
   288  } {
   289  	v := reflect.ValueOf(obj)
   290  	if !v.IsValid() {
   291  		panic(fmt.Errorf("cannot GetPLS(%T): failed to reflect", obj))
   292  	}
   293  	if v.Kind() == reflect.Ptr {
   294  		if v.IsNil() {
   295  			panic(fmt.Errorf("cannot GetPLS(%T): pointer is nil", obj))
   296  		}
   297  		v = v.Elem()
   298  		if v.Kind() == reflect.Struct {
   299  			s := structPLS{
   300  				c: getCodec(v.Type()),
   301  				o: v,
   302  			}
   303  
   304  			// If our object implements MetaGetterSetter, use this instead of the built-in
   305  			// PLS MetaGetterSetter.
   306  			if mgs, ok := obj.(MetaGetterSetter); ok {
   307  				s.mgs = mgs
   308  			}
   309  			return &s
   310  		}
   311  	}
   312  	panic(fmt.Errorf("cannot GetPLS(%T): not a pointer-to-struct", obj))
   313  }
   314  
   315  func getMGS(obj any) MetaGetterSetter {
   316  	if mgs, ok := obj.(MetaGetterSetter); ok {
   317  		return mgs
   318  	}
   319  	return GetPLS(obj)
   320  }
   321  
   322  func getCodec(structType reflect.Type) *structCodec {
   323  	structCodecsMutex.RLock()
   324  	c, ok := structCodecs[structType]
   325  	structCodecsMutex.RUnlock()
   326  	if !ok {
   327  		structCodecsMutex.Lock()
   328  		defer structCodecsMutex.Unlock()
   329  		c = getStructCodecLocked(structType)
   330  	}
   331  	if c.problem != nil {
   332  		panic(c.problem)
   333  	}
   334  	return c
   335  }