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 }