go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/service/datastore/optional.go (about) 1 // Copyright 2023 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 "time" 21 22 "golang.org/x/exp/constraints" 23 ) 24 25 // Indexed indicates to Optional or Nullable to produce indexed properties. 26 type Indexed struct{} 27 28 // Unindexed indicates to Optional or Nullable to produce unindexed properties. 29 type Unindexed struct{} 30 31 // Indexing is implemented by Indexed and Unindexed. 32 type Indexing interface{ shouldIndex() IndexSetting } 33 34 func (Indexed) shouldIndex() IndexSetting { return ShouldIndex } 35 func (Unindexed) shouldIndex() IndexSetting { return NoIndex } 36 37 // Elementary is a type set with all "elementary" datastore types. 38 type Elementary interface { 39 constraints.Integer | constraints.Float | ~bool | ~string | ~[]byte | time.Time | GeoPoint | *Key 40 } 41 42 // Optional wraps an elementary property type, adding "is set" flag to it. 43 // 44 // A pointer to Optional[T, I] implements PropertyConverter, allowing values of 45 // Optional[T, I] to appear as fields in structs representing entities. 46 // 47 // This is useful for rare cases when it is necessary to distinguish a zero 48 // value of T from an absent value. For example, a zero integer property ends up 49 // in indices, but an absent property doesn't. 50 // 51 // A zero value of Optional[T, I] represents an unset property. Setting a value 52 // via Set(...) (even if this is a zero value of T) marks the property as set. 53 // 54 // Unset properties are not stored into the datastore at all and they are 55 // totally invisible to all queries. Conversely, when an entity is being loaded, 56 // its Optional[T, I] fields that don't match any loaded properties will remain 57 // in unset state. Additionally, PTNull properties are treated as unset as well. 58 // 59 // To store unset properties as PTNull, use Nullable[T, I] instead. The primary 60 // benefit is the ability to filter queries by null, i.e. absence of a property. 61 // 62 // Type parameter I controls if the stored properties should be indexed or 63 // not. It should either be Indexed or Unindexed. 64 type Optional[T Elementary, I Indexing] struct { 65 isSet bool 66 val T 67 } 68 69 var _ PropertyConverter = &Optional[bool, Indexed]{} 70 71 // NewIndexedOptional creates a new, already set, indexed optional. 72 // 73 // To get an unset optional, just use the zero of Optional[T, I]. 74 func NewIndexedOptional[T Elementary](val T) Optional[T, Indexed] { 75 return Optional[T, Indexed]{isSet: true, val: val} 76 } 77 78 // NewUnindexedOptional creates a new, already set, unindexed optional. 79 // 80 // To get an unset optional, just use the zero of Optional[T, I]. 81 func NewUnindexedOptional[T Elementary](val T) Optional[T, Unindexed] { 82 return Optional[T, Unindexed]{isSet: true, val: val} 83 } 84 85 // IsSet returns true if the value is set. 86 func (o Optional[T, I]) IsSet() bool { 87 return o.isSet 88 } 89 90 // Get returns the value stored inside or a zero T if the value is unset. 91 func (o Optional[T, I]) Get() T { 92 return o.val 93 } 94 95 // Set stores the value and marks the optional as set. 96 func (o *Optional[T, I]) Set(val T) { 97 o.isSet = true 98 o.val = val 99 } 100 101 // Unset flips the optional into the unset state and clears the value to zero. 102 func (o *Optional[T, I]) Unset() { 103 var zero T 104 o.isSet = false 105 o.val = zero 106 } 107 108 // FromProperty implements PropertyConverter. 109 func (o *Optional[T, I]) FromProperty(prop Property) error { 110 if prop.Type() == PTNull { 111 o.Unset() 112 return nil 113 } 114 var val T 115 if err := loadElementary(&val, prop); err != nil { 116 return err 117 } 118 o.isSet = true 119 o.val = val 120 return nil 121 } 122 123 // ToProperty implements PropertyConverter. 124 func (o *Optional[T, I]) ToProperty() (prop Property, err error) { 125 if !o.isSet { 126 return Property{}, ErrSkipProperty 127 } 128 var idx I 129 err = prop.SetValue(o.val, idx.shouldIndex()) 130 return 131 } 132 133 // Nullable is almost the same as Optional, except absent properties are stored 134 // as PTNull (instead of being skipped), which means absence of a property can 135 // be filtered on in queries. 136 // 137 // Note that unindexed nullables are represented by unindexed PTNull in the 138 // datastore. APIs that work on a PropertyMap level can distinguish such 139 // properties from unindexed optionals (they will see the key in the property 140 // map). But when using the default PropertyLoadSaver, unindexed nullables and 141 // unindexed optionals are indistinguishable. 142 type Nullable[T Elementary, I Indexing] struct { 143 isSet bool 144 val T 145 } 146 147 var _ PropertyConverter = &Nullable[bool, Indexed]{} 148 149 // NewIndexedNullable creates a new, already set, indexed nullable. 150 // 151 // To get an unset nullable, just use the zero of Nullable[T, I]. 152 func NewIndexedNullable[T Elementary](val T) Nullable[T, Indexed] { 153 return Nullable[T, Indexed]{isSet: true, val: val} 154 } 155 156 // NewUnindexedNullable creates a new, already set, unindexed nullable. 157 // 158 // To get an unset nullable, just use the zero of Nullable[T, I]. 159 func NewUnindexedNullable[T Elementary](val T) Nullable[T, Unindexed] { 160 return Nullable[T, Unindexed]{isSet: true, val: val} 161 } 162 163 // IsSet returns true if the value is set. 164 func (o Nullable[T, I]) IsSet() bool { 165 return o.isSet 166 } 167 168 // Get returns the value stored inside or a zero T if the value is unset. 169 func (o Nullable[T, I]) Get() T { 170 return o.val 171 } 172 173 // Set stores the value and marks the nullable as set. 174 func (o *Nullable[T, I]) Set(val T) { 175 o.isSet = true 176 o.val = val 177 } 178 179 // Unset flips the nullable into the unset state and clears the value to zero. 180 func (o *Nullable[T, I]) Unset() { 181 var zero T 182 o.isSet = false 183 o.val = zero 184 } 185 186 // FromProperty implements PropertyConverter. 187 func (o *Nullable[T, I]) FromProperty(prop Property) error { 188 if prop.Type() == PTNull { 189 o.Unset() 190 return nil 191 } 192 var val T 193 if err := loadElementary(&val, prop); err != nil { 194 return err 195 } 196 o.isSet = true 197 o.val = val 198 return nil 199 } 200 201 // ToProperty implements PropertyConverter. 202 func (o *Nullable[T, I]) ToProperty() (prop Property, err error) { 203 var idx I 204 if o.isSet { 205 err = prop.SetValue(o.val, idx.shouldIndex()) 206 } else { 207 err = prop.SetValue(nil, idx.shouldIndex()) 208 } 209 return 210 } 211 212 // loadElementary is common implementation of FromProperty for optionals and 213 // nullables. 214 func loadElementary[T Elementary](val *T, prop Property) error { 215 loader := elementaryLoader(reflect.ValueOf(val).Elem()) 216 if loader.project == PTNull { 217 panic("impossible per the generic constraints") 218 } 219 pVal, err := prop.Project(loader.project) 220 if err != nil { 221 return err 222 } 223 if loader.overflow != nil && loader.overflow(pVal) { 224 return fmt.Errorf("value %v overflows type %T", pVal, val) 225 } 226 loader.set(pVal) 227 return nil 228 }