Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(142)

Unified Diff: service/datastore/properties.go

Issue 2342063003: Differentiate between single- and multi- props. (Closed)
Patch Set: Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: service/datastore/properties.go
diff --git a/service/datastore/properties.go b/service/datastore/properties.go
index 2808c3804702d400198792664ac025441fe2a3da..f00eb51a8459580c8f426c4300c0bf5cfb24cfaf 100644
--- a/service/datastore/properties.go
+++ b/service/datastore/properties.go
@@ -336,6 +336,13 @@ func UpconvertUnderlyingType(o interface{}) interface{} {
return o
}
+func (Property) isAPropertyData() {}
+
+func (p Property) estimateSize() int64 { return p.EstimateSize() }
+
+// Clone implements the PropertyData interface.
+func (p Property) Clone() PropertyData { return p }
+
func (p Property) String() string {
switch p.propType {
case PTString, PTBlobKey:
@@ -628,6 +635,33 @@ func (p *Property) Compare(other *Property) int {
}
}
+// EstimateSize estimates the amount of space that this Property would consume
+// if it were committed as part of an entity in the real production datastore.
+//
+// It uses https://cloud.google.com/appengine/articles/storage_breakdown?csw=1
+// as a guide for these values.
+func (p *Property) EstimateSize() int64 {
+ switch p.Type() {
+ case PTNull:
+ return 1
+ case PTBool:
+ return 1 + 4
+ case PTInt, PTTime, PTFloat:
+ return 1 + 8
+ case PTGeoPoint:
+ return 1 + (8 * 2)
+ case PTString:
+ return 1 + int64(len(p.Value().(string)))
+ case PTBlobKey:
+ return 1 + int64(len(p.Value().(blobstore.Key)))
+ case PTBytes:
+ return 1 + int64(len(p.Value().([]byte)))
+ case PTKey:
+ return 1 + p.Value().(*Key).EstimateSize()
+ }
+ panic(fmt.Errorf("Unknown property type: %s", p.Type().String()))
+}
+
// GQL returns a correctly formatted Cloud Datastore GQL literal which
// is valid for a comparison value in the `WHERE` clause.
//
@@ -672,39 +706,36 @@ func (p *Property) GQL() string {
}
// PropertySlice is a slice of Properties. It implements sort.Interface.
+//
+// PropertySlice holds multiple Properties. Writing a PropertySlice to datastore
+// implicitly marks the property as "multiple", even if it only has one element.
type PropertySlice []Property
-func (s PropertySlice) Len() int { return len(s) }
-func (s PropertySlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
-func (s PropertySlice) Less(i, j int) bool { return s[i].Less(&s[j]) }
+func (PropertySlice) isAPropertyData() {}
-// EstimateSize estimates the amount of space that this Property would consume
-// if it were committed as part of an entity in the real production datastore.
-//
-// It uses https://cloud.google.com/appengine/articles/storage_breakdown?csw=1
-// as a guide for these values.
-func (p *Property) EstimateSize() int64 {
- switch p.Type() {
- case PTNull:
- return 1
- case PTBool:
- return 1 + 4
- case PTInt, PTTime, PTFloat:
- return 1 + 8
- case PTGeoPoint:
- return 1 + (8 * 2)
- case PTString:
- return 1 + int64(len(p.Value().(string)))
- case PTBlobKey:
- return 1 + int64(len(p.Value().(blobstore.Key)))
- case PTBytes:
- return 1 + int64(len(p.Value().([]byte)))
- case PTKey:
- return 1 + p.Value().(*Key).EstimateSize()
+// Clone implements the PropertyData interface.
+func (s PropertySlice) Clone() PropertyData {
+ if len(s) == 0 {
+ return PropertySlice(nil)
}
- panic(fmt.Errorf("Unknown property type: %s", p.Type().String()))
+ return append(make(PropertySlice, 0, len(s)), s...)
}
+// EstimateSize estimates the amount of space that this PropertySlice would
+// consume if it were committed as part of an entity in the real production
+// datastore.
+func (s PropertySlice) estimateSize() (v int64) {
+ for _, prop := range s {
+ // Use the public one so we don't have to copy.
+ v += prop.estimateSize()
+ }
+ return
+}
+
+func (s PropertySlice) Len() int { return len(s) }
+func (s PropertySlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s PropertySlice) Less(i, j int) bool { return s[i].Less(&s[j]) }
+
// MetaGetter is a subinterface of PropertyLoadSaver, but is also used to
// abstract the meta argument for RawInterface.GetMulti.
type MetaGetter interface {
@@ -785,6 +816,20 @@ type MetaGetterSetter interface {
SetMeta(key string, val interface{}) bool
}
+// PropertyData is an interface implemented by Property and PropertySlice to
+// identify themselves as valid PropertyMap values.
+type PropertyData interface {
+ // isAPropertyData is a tag that forces PropertyData implementations to only
+ // be supplied by this package.
+ isAPropertyData()
+
+ // estimateSize estimates the aggregate size of the property data.
+ estimateSize() int64
+
+ // Clone creates a duplicate copy of this PropertyData.
+ Clone() PropertyData
+}
+
// PropertyMap represents the contents of a datastore entity in a generic way.
// It maps from property name to a list of property values which correspond to
// that property name. It is the spiritual successor to PropertyList from the
@@ -808,14 +853,14 @@ type MetaGetterSetter interface {
//
// Additionally, Save returns a copy of the map with the meta keys omitted (e.g.
// these keys are not going to be serialized to the datastore).
-type PropertyMap map[string][]Property
+type PropertyMap map[string]PropertyData
var _ PropertyLoadSaver = PropertyMap(nil)
// Load implements PropertyLoadSaver.Load
func (pm PropertyMap) Load(props PropertyMap) error {
for k, v := range props {
- pm[k] = append(pm[k], v...)
+ pm[k] = v.Clone()
dnj 2016/09/16 19:07:22 I'm going to write some code to clear out "pm" her
dnj 2016/09/16 19:21:33 Actually nm on this.
}
return nil
}
@@ -829,7 +874,9 @@ func (pm PropertyMap) Save(withMeta bool) (PropertyMap, error) {
ret := make(PropertyMap, len(pm))
for k, v := range pm {
if withMeta || !isMetaKey(k) {
- ret[k] = append(ret[k], v...)
+ if err := ret.appendPropertyData(k, v); err != nil {
iannucci 2016/09/16 07:11:03 I think this just needs to be ret[k] = v.Clone(
dnj 2016/09/16 19:07:22 Oh good catch. Yes, correct.
+ return nil, fmt.Errorf("gae: failed to save property %q: %v", k, err)
+ }
}
}
return ret, nil
@@ -838,11 +885,11 @@ func (pm PropertyMap) Save(withMeta bool) (PropertyMap, error) {
// GetMeta implements PropertyLoadSaver.GetMeta, and returns the current value
// associated with the metadata key.
func (pm PropertyMap) GetMeta(key string) (interface{}, bool) {
- v, ok := pm["$"+key]
- if !ok || len(v) == 0 {
- return nil, false
+ pslice := pm.Slice("$" + key)
+ if len(pslice) > 0 {
+ return pslice[0].Value(), true
}
- return v[0].Value(), true
+ return nil, false
}
// GetAllMeta implements PropertyLoadSaver.GetAllMeta.
@@ -850,9 +897,7 @@ func (pm PropertyMap) GetAllMeta() PropertyMap {
ret := make(PropertyMap, 8)
for k, v := range pm {
if isMetaKey(k) {
- newV := make([]Property, len(v))
- copy(newV, v)
- ret[k] = newV
+ ret[k] = v.Clone()
}
}
return ret
@@ -865,7 +910,7 @@ func (pm PropertyMap) SetMeta(key string, val interface{}) bool {
if err := prop.SetValue(val, NoIndex); err != nil {
return false
}
- pm["$"+key] = []Property{prop}
+ pm["$"+key] = prop
return true
}
@@ -874,6 +919,26 @@ func (pm PropertyMap) Problem() error {
return nil
}
+// Slice returns a PropertySlice for the given key
+//
+// If the value associated with that key is nil, an empty slice will be
+// returned. If the value is single Property, a slice of size 1 with that
+// Property in it will be returned.
+func (pm PropertyMap) Slice(key string) PropertySlice {
iannucci 2016/09/16 07:11:03 should we also have a `First(key string)`? Or Sing
dnj 2016/09/16 19:07:22 I thought about this. TBH I am of the opinion that
+ pdata := pm[key]
+ if pdata == nil {
+ return nil
+ }
+ switch t := pdata.(type) {
+ case Property:
+ return PropertySlice{t}
+ case PropertySlice:
+ return t
+ default:
+ panic(fmt.Errorf("unknown PropertyData type %T", t))
+ }
+}
+
// EstimateSize estimates the size that it would take to encode this PropertyMap
// in the production Appengine datastore. The calculation excludes metadata
// fields in the map.
@@ -885,14 +950,40 @@ func (pm PropertyMap) EstimateSize() int64 {
for k, vals := range pm {
if !isMetaKey(k) {
ret += int64(len(k))
- for i := range vals {
- ret += vals[i].EstimateSize()
- }
+ ret += vals.estimateSize()
}
}
return ret
}
+func (pm PropertyMap) appendPropertyData(key string, pdata PropertyData) error {
+ cur := pm[key]
+ if cur == nil {
+ pm[key] = pdata.Clone()
+ return nil
+ }
+
+ // If there's already a value here, we can only append a new value if the
+ // current data is a slice.
+ pslice, ok := cur.(PropertySlice)
+ if !ok {
+ return errors.New("cannot add additional value to single-value property")
+ }
+
+ switch add := pdata.(type) {
+ case Property:
+ // Don't need to clone, since Property is by-value.
+ pm[key] = append(pslice, add)
+
+ case PropertySlice:
+ // Don't need to clone, since we're appending pdata, not retaining it.
+ if len(add) > 0 {
+ pm[key] = append(pslice, add...)
+ }
+ }
+ return nil
+}
+
func isMetaKey(k string) bool {
// empty counts as a metakey since it's not a valid data key, but it's
// not really a valid metakey either.

Powered by Google App Engine
This is Rietveld 408576698