firestore: better treatment of ServerTimestamp and Delete
- More checking for incorrect use of these sentinels.
- A small optimization: a map with only ServerTimestamps as values
turns into nil, instead of an empty map.
Change-Id: Ia79348d6637eac845e77acbb4a622539a9147f44
Reviewed-on: https://code-review.googlesource.com/19050
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Michael Darakananda <pongad@google.com>
diff --git a/firestore/docref.go b/firestore/docref.go
index ca88136..abbdda8 100644
--- a/firestore/docref.go
+++ b/firestore/docref.go
@@ -148,10 +148,10 @@
return nil, errNilDocRef
}
origFieldPaths, allPaths, err := processSetOptions(opts)
- isMerge := len(origFieldPaths) > 0 || allPaths // was some Merge option specified?
if err != nil {
return nil, err
}
+ isMerge := len(origFieldPaths) > 0 || allPaths // was some Merge option specified?
doc, serverTimestampPaths, err := toProtoDocument(data)
if err != nil {
return nil, err
@@ -214,7 +214,7 @@
// There were field paths, but they all got removed.
// The write does nothing but enforce the precondition.
w = &pb.Write{CurrentDocument: pc}
- case !isMerge:
+ case !isMerge && (pc != nil || doc.Fields != nil):
// Set without merge, so no update mask.
w = &pb.Write{
Operation: &pb.Write_Update{doc},
diff --git a/firestore/docref_test.go b/firestore/docref_test.go
index f77106a..4a8961d 100644
--- a/firestore/docref_test.go
+++ b/firestore/docref_test.go
@@ -163,13 +163,25 @@
transform: []string{"b"},
},
{
+ desc: "a ServerTimestamp alone",
+ data: map[string]interface{}{"b": ServerTimestamp},
+ write: nil,
+ transform: []string{"b"},
+ },
+ {
+ desc: "a ServerTimestamp alone with a path",
+ data: map[string]interface{}{"b": ServerTimestamp},
+ opt: MergePaths([]string{"b"}),
+ write: nil,
+ transform: []string{"b"},
+ },
+ {
desc: "nested ServerTimestamp field",
data: map[string]interface{}{
"a": 1,
"b": map[string]interface{}{"c": ServerTimestamp},
},
- // TODO(jba): make this be map[string]*pb.Value{"a": intval(1)},
- write: map[string]*pb.Value{"a": intval(1), "b": mapval(map[string]*pb.Value{})},
+ write: map[string]*pb.Value{"a": intval(1)},
transform: []string{"b.c"},
},
{
@@ -179,9 +191,7 @@
"b": ServerTimestamp,
"c": map[string]interface{}{"d": ServerTimestamp},
},
- // TODO(jba): make this be map[string]*pb.Value{"a": intval(1)},
- write: map[string]*pb.Value{"a": intval(1),
- "c": mapval(map[string]*pb.Value{})},
+ write: map[string]*pb.Value{"a": intval(1)},
transform: []string{"b", "c.d"},
},
{
diff --git a/firestore/document.go b/firestore/document.go
index 538ef56..7efc14b 100644
--- a/firestore/document.go
+++ b/firestore/document.go
@@ -156,19 +156,26 @@
return nil, nil, errors.New("firestore: nil document contents")
}
v := reflect.ValueOf(x)
- pv, err := toProtoValue(v)
+ pv, sawTransform, err := toProtoValue(v)
if err != nil {
return nil, nil, err
}
- fieldPaths, err := extractTransformPaths(v, nil)
- if err != nil {
- return nil, nil, err
+ var fieldPaths []FieldPath
+ if sawTransform {
+ fieldPaths, err = extractTransformPaths(v, nil)
+ if err != nil {
+ return nil, nil, err
+ }
}
- m := pv.GetMapValue()
- if m == nil {
- return nil, nil, fmt.Errorf("firestore: cannot covert value of type %T into a map", x)
+ var fields map[string]*pb.Value
+ if pv != nil {
+ m := pv.GetMapValue()
+ if m == nil {
+ return nil, nil, fmt.Errorf("firestore: cannot covert value of type %T into a map", x)
+ }
+ fields = m.Fields
}
- return &pb.Document{Fields: m.Fields}, fieldPaths, nil
+ return &pb.Document{Fields: fields}, fieldPaths, nil
}
func extractTransformPaths(v reflect.Value, prefix FieldPath) ([]FieldPath, error) {
diff --git a/firestore/integration_test.go b/firestore/integration_test.go
index e282a2f..54b6e20 100644
--- a/firestore/integration_test.go
+++ b/firestore/integration_test.go
@@ -217,6 +217,9 @@
checkTimeBetween(t, wr.UpdateTime, start, end)
_, err := doc.Create(ctx, integrationTestMap)
codeEq(t, "Create on a present doc", codes.AlreadyExists, err)
+ // OK to create an empty document.
+ _, err = integrationColl(t).NewDoc().Create(ctx, map[string]interface{}{})
+ codeEq(t, "Create empty doc", codes.OK, err)
}
func TestIntegration_Get(t *testing.T) {
@@ -234,8 +237,6 @@
if want := wantIntegrationTestMap; !testEqual(got, want) {
t.Errorf("got\n%v\nwant\n%v", pretty.Value(got), pretty.Value(want))
}
-
- //
_, err = integrationColl(t).NewDoc().Get(ctx)
codeEq(t, "Get on a missing doc", codes.NotFound, err)
}
@@ -425,6 +426,8 @@
er(doc.UpdateMap(ctx, um, LastUpdateTime(wr.UpdateTime.Add(-time.Millisecond)))))
codeEq(t, "UpdateMap with right LastUpdateTime", codes.OK,
er(doc.UpdateMap(ctx, um, LastUpdateTime(wr.UpdateTime))))
+ codeEq(t, "just server transform", codes.OK,
+ er(doc.UpdateMap(ctx, map[string]interface{}{"a": ServerTimestamp})))
}
func TestIntegration_UpdateStruct(t *testing.T) {
diff --git a/firestore/mock_test.go b/firestore/mock_test.go
index 047d345..d0cdba9 100644
--- a/firestore/mock_test.go
+++ b/firestore/mock_test.go
@@ -83,8 +83,9 @@
ri.adjust(gotReq)
}
if !proto.Equal(gotReq, ri.wantReq) {
- return nil, fmt.Errorf("mockServer: bad request\ngot: %T\n%+v\nwant: %T\n%+v",
- gotReq, gotReq, ri.wantReq, ri.wantReq)
+ return nil, fmt.Errorf("mockServer: bad request\ngot: %T\n%s\nwant: %T\n%s",
+ gotReq, proto.MarshalTextString(gotReq),
+ ri.wantReq, proto.MarshalTextString(ri.wantReq))
}
}
resp := s.resps[0]
diff --git a/firestore/query.go b/firestore/query.go
index 13b5cfb..0574504 100644
--- a/firestore/query.go
+++ b/firestore/query.go
@@ -277,10 +277,14 @@
}
vals[i] = &pb.Value{&pb.Value_ReferenceValue{q.parentPath + "/documents/" + q.collectionID + "/" + docID}}
} else {
- vals[i], err = toProtoValue(reflect.ValueOf(fval))
+ var sawTransform bool
+ vals[i], sawTransform, err = toProtoValue(reflect.ValueOf(fval))
if err != nil {
return nil, err
}
+ if sawTransform {
+ return nil, errors.New("firestore: ServerTimestamp disallowed in query value")
+ }
}
}
return vals, nil
@@ -311,10 +315,13 @@
default:
return nil, fmt.Errorf("firestore: invalid operator %q", f.op)
}
- val, err := toProtoValue(reflect.ValueOf(f.value))
+ val, sawTransform, err := toProtoValue(reflect.ValueOf(f.value))
if err != nil {
return nil, err
}
+ if sawTransform {
+ return nil, errors.New("firestore: ServerTimestamp disallowed in query value")
+ }
return &pb.StructuredQuery_Filter{
FilterType: &pb.StructuredQuery_Filter_FieldFilter{
&pb.StructuredQuery_FieldFilter{
diff --git a/firestore/query_test.go b/firestore/query_test.go
index 20f901f..0fea114 100644
--- a/firestore/query_test.go
+++ b/firestore/query_test.go
@@ -228,6 +228,8 @@
}
func TestQueryToProtoErrors(t *testing.T) {
+ st := map[string]interface{}{"a": ServerTimestamp}
+ del := map[string]interface{}{"a": Delete}
q := (&Client{}).Collection("C").Query
for _, query := range []Query{
Query{}, // no collection ID
@@ -240,6 +242,12 @@
q.SelectPaths([]string{"/", "", "~"}), // invalid path
q.OrderBy("[", Asc), // invalid path
q.OrderByPath([]string{""}, Desc), // invalid path
+ q.Where("x", "==", st), // ServerTimestamp in filter
+ q.OrderBy("a", Asc).StartAt(st), // ServerTimestamp in Start
+ q.OrderBy("a", Asc).EndAt(st), // ServerTimestamp in End
+ q.Where("x", "==", del), // Delete in filter
+ q.OrderBy("a", Asc).StartAt(del), // Delete in Start
+ q.OrderBy("a", Asc).EndAt(del), // Delete in End
} {
_, err := query.toProto()
if err == nil {
diff --git a/firestore/to_value.go b/firestore/to_value.go
index ea106fc..c4a6820 100644
--- a/firestore/to_value.go
+++ b/firestore/to_value.go
@@ -41,40 +41,44 @@
// toProtoValue converts a Go value to a Firestore Value protobuf.
// Some corner cases:
// - All nils (nil interface, nil slice, nil map, nil pointer) are converted to
-// a NullValue (not a nil *pb.Value). toProtoValue never returns (nil, nil).
+// a NullValue (not a nil *pb.Value). toProtoValue never returns (nil, false, nil).
+// It returns (nil, true, nil) if everything in the value is ServerTimestamp.
// - An error is returned for uintptr, uint and uint64, because Firestore uses
// an int64 to represent integral values, and those types can't be properly
// represented in an int64.
// - An error is returned for the special Delete value.
-func toProtoValue(v reflect.Value) (*pb.Value, error) {
+func toProtoValue(v reflect.Value) (pbv *pb.Value, sawServerTimestamp bool, err error) {
if !v.IsValid() {
- return nullValue, nil
+ return nullValue, false, nil
}
vi := v.Interface()
if vi == Delete {
- return nil, errors.New("firestore: cannot use Delete in value")
+ return nil, false, errors.New("firestore: cannot use Delete in value")
+ }
+ if vi == ServerTimestamp {
+ return nil, false, errors.New("firestore: must use ServerTimestamp as a map value")
}
switch x := vi.(type) {
case []byte:
- return &pb.Value{&pb.Value_BytesValue{x}}, nil
+ return &pb.Value{&pb.Value_BytesValue{x}}, false, nil
case time.Time:
ts, err := ptypes.TimestampProto(x)
if err != nil {
- return nil, err
+ return nil, false, err
}
- return &pb.Value{&pb.Value_TimestampValue{ts}}, nil
+ return &pb.Value{&pb.Value_TimestampValue{ts}}, false, nil
case *latlng.LatLng:
if x == nil {
// gRPC doesn't like nil oneofs. Use NullValue.
- return nullValue, nil
+ return nullValue, false, nil
}
- return &pb.Value{&pb.Value_GeoPointValue{x}}, nil
+ return &pb.Value{&pb.Value_GeoPointValue{x}}, false, nil
case *DocumentRef:
if x == nil {
// gRPC doesn't like nil oneofs. Use NullValue.
- return nullValue, nil
+ return nullValue, false, nil
}
- return &pb.Value{&pb.Value_ReferenceValue{x.Path}}, nil
+ return &pb.Value{&pb.Value_ReferenceValue{x.Path}}, false, nil
// Do not add bool, string, int, etc. to this switch; leave them in the
// reflect-based switch below. Moving them here would drop support for
// types whose underlying types are those primitives.
@@ -83,15 +87,15 @@
}
switch v.Kind() {
case reflect.Bool:
- return &pb.Value{&pb.Value_BooleanValue{v.Bool()}}, nil
+ return &pb.Value{&pb.Value_BooleanValue{v.Bool()}}, false, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- return &pb.Value{&pb.Value_IntegerValue{v.Int()}}, nil
+ return &pb.Value{&pb.Value_IntegerValue{v.Int()}}, false, nil
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
- return &pb.Value{&pb.Value_IntegerValue{int64(v.Uint())}}, nil
+ return &pb.Value{&pb.Value_IntegerValue{int64(v.Uint())}}, false, nil
case reflect.Float32, reflect.Float64:
- return &pb.Value{&pb.Value_DoubleValue{v.Float()}}, nil
+ return &pb.Value{&pb.Value_DoubleValue{v.Float()}}, false, nil
case reflect.String:
- return &pb.Value{&pb.Value_StringValue{v.String()}}, nil
+ return &pb.Value{&pb.Value_StringValue{v.String()}}, false, nil
case reflect.Slice:
return sliceToProtoValue(v)
case reflect.Map:
@@ -100,7 +104,7 @@
return structToProtoValue(v)
case reflect.Ptr:
if v.IsNil() {
- return nullValue, nil
+ return nullValue, false, nil
}
return toProtoValue(v.Elem())
case reflect.Interface:
@@ -110,71 +114,105 @@
fallthrough // any other interface value is an error
default:
- return nil, fmt.Errorf("firestore: cannot convert type %s to value", v.Type())
+ return nil, false, fmt.Errorf("firestore: cannot convert type %s to value", v.Type())
}
}
-func sliceToProtoValue(v reflect.Value) (*pb.Value, error) {
+func sliceToProtoValue(v reflect.Value) (*pb.Value, bool, error) {
// A nil slice is converted to a null value.
if v.IsNil() {
- return nullValue, nil
+ return nullValue, false, nil
}
vals := make([]*pb.Value, v.Len())
for i := 0; i < v.Len(); i++ {
- val, err := toProtoValue(v.Index(i))
+ val, sawServerTimestamp, err := toProtoValue(v.Index(i))
if err != nil {
- return nil, err
+ return nil, false, err
+ }
+ if sawServerTimestamp {
+ return nil, false, errors.New("firestore: ServerTimestamp cannot occur in an array")
}
vals[i] = val
}
- return &pb.Value{&pb.Value_ArrayValue{&pb.ArrayValue{vals}}}, nil
+ return &pb.Value{&pb.Value_ArrayValue{&pb.ArrayValue{vals}}}, false, nil
}
-func mapToProtoValue(v reflect.Value) (*pb.Value, error) {
+func mapToProtoValue(v reflect.Value) (*pb.Value, bool, error) {
if v.Type().Key().Kind() != reflect.String {
- return nil, errors.New("firestore: map key type must be string")
+ return nil, false, errors.New("firestore: map key type must be string")
}
// A nil map is converted to a null value.
if v.IsNil() {
- return nullValue, nil
+ return nullValue, false, nil
}
m := map[string]*pb.Value{}
+ sawServerTimestamp := false
for _, k := range v.MapKeys() {
mi := v.MapIndex(k)
if mi.Interface() == ServerTimestamp {
+ sawServerTimestamp = true
continue
}
- val, err := toProtoValue(mi)
+ val, sst, err := toProtoValue(mi)
if err != nil {
- return nil, err
+ return nil, false, err
+ }
+ if sst {
+ sawServerTimestamp = true
+ }
+ if val == nil { // value was a map with all ServerTimestamp values
+ continue
}
m[k.String()] = val
}
- return &pb.Value{&pb.Value_MapValue{&pb.MapValue{m}}}, nil
+ var pv *pb.Value
+ if len(m) == 0 && sawServerTimestamp {
+ // The entire map consisted of ServerTimestamp values.
+ pv = nil
+ } else {
+ pv = &pb.Value{&pb.Value_MapValue{&pb.MapValue{m}}}
+ }
+ return pv, sawServerTimestamp, nil
}
-func structToProtoValue(v reflect.Value) (*pb.Value, error) {
+func structToProtoValue(v reflect.Value) (*pb.Value, bool, error) {
m := map[string]*pb.Value{}
fields, err := fieldCache.Fields(v.Type())
if err != nil {
- return nil, err
+ return nil, false, err
}
+ sawServerTimestamp := false
for _, f := range fields {
fv := v.FieldByIndex(f.Index)
opts := f.ParsedTag.(tagOptions)
if opts.serverTimestamp {
+ // TODO(jba): should we return a non-zero time?
+ sawServerTimestamp = true
continue
}
if opts.omitEmpty && isEmptyValue(fv) {
continue
}
- val, err := toProtoValue(fv)
+ val, sst, err := toProtoValue(fv)
if err != nil {
- return nil, err
+ return nil, false, err
+ }
+ if sst {
+ sawServerTimestamp = true
+ }
+ if val == nil { // value was a map with all ServerTimestamp values
+ continue
}
m[f.Name] = val
}
- return &pb.Value{&pb.Value_MapValue{&pb.MapValue{m}}}, nil
+ var pv *pb.Value
+ if len(m) == 0 && sawServerTimestamp {
+ // The entire struct consisted of ServerTimestamp or omitempty values.
+ pv = nil
+ } else {
+ pv = &pb.Value{&pb.Value_MapValue{&pb.MapValue{m}}}
+ }
+ return pv, sawServerTimestamp, nil
}
type tagOptions struct {
diff --git a/firestore/to_value_test.go b/firestore/to_value_test.go
index 13ddd2c..ae58a13 100644
--- a/firestore/to_value_test.go
+++ b/firestore/to_value_test.go
@@ -117,8 +117,47 @@
},
refval("projects/P/databases/D/documents/c/d"),
},
+ // ServerTimestamps are removed, possibly leaving nil.
+ {map[string]interface{}{"a": ServerTimestamp}, nil},
+ {
+ map[string]interface{}{
+ "a": map[string]interface{}{
+ "b": map[string]interface{}{
+ "c": ServerTimestamp,
+ },
+ },
+ },
+ nil,
+ },
+ {
+ map[string]interface{}{
+ "a": map[string]interface{}{
+ "b": map[string]interface{}{
+ "c": ServerTimestamp,
+ "d": ServerTimestamp,
+ },
+ },
+ },
+ nil,
+ },
+ {
+ map[string]interface{}{
+ "a": map[string]interface{}{
+ "b": map[string]interface{}{
+ "c": ServerTimestamp,
+ "d": ServerTimestamp,
+ "e": 1,
+ },
+ },
+ },
+ mapval(map[string]*pb.Value{
+ "a": mapval(map[string]*pb.Value{
+ "b": mapval(map[string]*pb.Value{"e": intval(1)}),
+ }),
+ }),
+ },
} {
- got, err := toProtoValue(reflect.ValueOf(test.in))
+ got, _, err := toProtoValue(reflect.ValueOf(test.in))
if err != nil {
t.Errorf("%v (%T): %v", test.in, test.in, err)
continue
@@ -139,8 +178,18 @@
map[int]bool{}, // map key type is not string
make(chan int), // can't handle type
map[string]fmt.Stringer{"a": stringy{}}, // only empty interfaces
+ ServerTimestamp, // ServerTimestamp can only be a field value
+ []interface{}{ServerTimestamp},
+ map[string]interface{}{"a": []interface{}{ServerTimestamp}},
+ map[string]interface{}{"a": []interface{}{
+ map[string]interface{}{"b": ServerTimestamp},
+ }},
+ Delete, // Delete should never appear
+ []interface{}{Delete},
+ map[string]interface{}{"a": Delete},
+ map[string]interface{}{"a": []interface{}{Delete}},
} {
- _, err := toProtoValue(reflect.ValueOf(in))
+ _, _, err := toProtoValue(reflect.ValueOf(in))
if err == nil {
t.Errorf("%v: got nil, want error", in)
}
@@ -161,7 +210,7 @@
OmitEmpty: 3,
OmitEmptyTime: aTime,
}
- got, err := toProtoValue(reflect.ValueOf(in))
+ got, _, err := toProtoValue(reflect.ValueOf(in))
if err != nil {
t.Fatal(err)
}
@@ -174,7 +223,7 @@
t.Errorf("got %+v, want %+v", got, want)
}
- got, err = toProtoValue(reflect.ValueOf(testStruct2{}))
+ got, _, err = toProtoValue(reflect.ValueOf(testStruct2{}))
if err != nil {
t.Fatal(err)
}
@@ -191,7 +240,7 @@
*latlng.LatLng
}
- got, err := toProtoValue(reflect.ValueOf(embed{tm, ll}))
+ got, _, err := toProtoValue(reflect.ValueOf(embed{tm, ll}))
if err != nil {
t.Fatal(err)
}