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  }