Skip to content

Commit 6db6cc7

Browse files
Store the coerced base value in the BaseMutation (#3292)
1 parent 6910d82 commit 6db6cc7

File tree

18 files changed

+329
-132
lines changed

18 files changed

+329
-132
lines changed

Firestore/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Unreleased
2+
- [fixed] Fixed an internal assertion that was triggered when an update
3+
with a `FieldValue.serverTimestamp()` and an update with a
4+
`FieldValue.increment()` were pending for the same document.
25

36
# 1.4.1
47
- [fixed] Fixed certificate loading for non-CocoaPods builds that may not

Firestore/Example/Firestore.xcodeproj/project.pbxproj

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
12158DFCEE09D24B7988A340 /* maybe_document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7E20B89AAC00B5BCE7 /* maybe_document.pb.cc */; };
6363
1235769322B7E99F007DDFA9 /* EncodableFieldValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1235769122B7E915007DDFA9 /* EncodableFieldValueTests.swift */; };
6464
1235769522B86E65007DDFA9 /* FirestoreEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1235769422B86E65007DDFA9 /* FirestoreEncoderTests.swift */; };
65-
124C933122C16ACB00CA8C2D /* CodableIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 124C932B22C1642C00CA8C2D /* CodableIntegrationTests.swift */; };
6665
127CC0D222B3ADDC00A3E42A /* CodableTimestampTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B65C996438B84DBC7616640 /* CodableTimestampTests.swift */; };
6766
1291D9F5300AFACD1FBD262D /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; };
6867
12BB9ED1CA98AA52B92F497B /* log_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54C2294E1FECABAE007D065B /* log_test.cc */; };
@@ -1412,9 +1411,9 @@
14121411
54C9EDF22040E16300A969CD /* SwiftTests */ = {
14131412
isa = PBXGroup;
14141413
children = (
1415-
124C932A22C1635300CA8C2D /* Integration */,
14161414
544A20ED20F6C046004E52CD /* API */,
14171415
5495EB012040E90200EBA509 /* Codable */,
1416+
124C932A22C1635300CA8C2D /* Integration */,
14181417
620C1427763BA5D3CCFB5A1F /* BridgingHeader.h */,
14191418
54C9EDF52040E16300A969CD /* Info.plist */,
14201419
);
@@ -3652,7 +3651,6 @@
36523651
D5B252EE3F4037405DB1ECE3 /* FIRNumericTransformTests.mm in Sources */,
36533652
5492E072202154D600B64F25 /* FIRQueryTests.mm in Sources */,
36543653
5492E077202154D600B64F25 /* FIRServerTimestampTests.mm in Sources */,
3655-
124C933122C16ACB00CA8C2D /* CodableIntegrationTests.swift in Sources */,
36563654
5492E07A202154D600B64F25 /* FIRTypeTests.mm in Sources */,
36573655
5492E076202154D600B64F25 /* FIRValidationTests.mm in Sources */,
36583656
5492E078202154D600B64F25 /* FIRWriteBatchTests.mm in Sources */,

Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests_macOS.xcscheme

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
<BuildAction
66
parallelizeBuildables = "YES"
77
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForRunning = "YES">
11+
<BuildableReference
12+
BuildableIdentifier = "primary"
13+
BlueprintIdentifier = "DAFF0CF421E64AC30062958F"
14+
BuildableName = "Firestore_Example_macOS.app"
15+
BlueprintName = "Firestore_Example_macOS"
16+
ReferencedContainer = "container:Firestore.xcodeproj">
17+
</BuildableReference>
18+
</BuildActionEntry>
19+
</BuildActionEntries>
820
</BuildAction>
921
<TestAction
1022
buildConfiguration = "Debug"

Firestore/Example/Tests/Integration/API/FIRNumericTransformTests.mm

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,63 @@ - (void)testDoubleIncrementWithExistingString {
136136
[self expectApproximateLocalAndRemoteValue:13.37];
137137
}
138138

139+
- (void)testIncrementTwiceInABatch {
140+
[self writeInitialData:@{@"sum" : @"overwrite"}];
141+
142+
FIRWriteBatch *batch = _docRef.firestore.batch;
143+
[batch updateData:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}
144+
forDocument:_docRef];
145+
[batch updateData:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}
146+
forDocument:_docRef];
147+
[batch
148+
commitWithCompletion:[self completionForExpectationWithName:@"testIncrementTwiceInABatch"]];
149+
[self awaitExpectations];
150+
151+
[self expectApproximateLocalAndRemoteValue:2];
152+
}
153+
154+
- (void)testIncrementDeleteIncrementInABatch {
155+
[self writeInitialData:@{@"sum" : @"overwrite"}];
156+
157+
FIRWriteBatch *batch = _docRef.firestore.batch;
158+
[batch updateData:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}
159+
forDocument:_docRef];
160+
[batch updateData:@{@"sum" : [FIRFieldValue fieldValueForDelete]} forDocument:_docRef];
161+
[batch updateData:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:3]}
162+
forDocument:_docRef];
163+
[batch commitWithCompletion:
164+
[self completionForExpectationWithName:@"testIncrementDeleteIncrementInABatch"]];
165+
[self awaitExpectations];
166+
167+
[self expectApproximateLocalAndRemoteValue:3];
168+
}
169+
170+
- (void)testServerTimestampAndIncrement {
171+
// This test stacks two pending transforms (a ServerTimestamp and an Increment transform)
172+
// and reproduces the setup that was reported in
173+
// https://github.com/firebase/firebase-android-sdk/issues/491
174+
// In our original code, a NumericIncrementTransform could cause us to decode the
175+
// ServerTimestamp as part of a FSTPatchMutation, which triggered an assertion failure.
176+
[self writeInitialData:@{@"val" : @"overwrite"}];
177+
178+
[self disableNetwork];
179+
180+
[_docRef updateData:@{@"val" : [FIRFieldValue fieldValueForServerTimestamp]}];
181+
[_docRef updateData:@{@"val" : [FIRFieldValue fieldValueForIntegerIncrement:1]}];
182+
183+
FIRDocumentSnapshot *snap = [_accumulator awaitLocalEvent];
184+
XCTAssertNotNil([snap valueForField:@"val"
185+
serverTimestampBehavior:FIRServerTimestampBehaviorEstimate]);
186+
187+
snap = [_accumulator awaitLocalEvent];
188+
XCTAssertEqualObjects(@1, snap[@"val"]);
189+
190+
[self enableNetwork];
191+
192+
snap = [_accumulator awaitRemoteEvent];
193+
XCTAssertEqualObjects(@1, snap[@"val"]);
194+
}
195+
139196
- (void)testMultipleDoubleIncrements {
140197
[self writeInitialData:@{@"sum" : @"0.0"}];
141198

Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ - (void)testServerTimestampsWithEstimatedValue {
171171
}
172172

173173
- (void)testServerTimestampsWithPreviousValue {
174+
// The following test includes an update of the nested map "deep", which updates it to contain
175+
// a single ServerTimestamp. This update is split into two mutations: One that sets "deep" to
176+
// an empty map and overwrites the previous ServerTimestamp value and a second transform that
177+
// writes the new ServerTimestamp. This step in the test verifies that we can still access the
178+
// old ServerTimestamp value (from `previousSnapshot`) even though it was removed in an
179+
// intermediate step.
174180
[self writeDocumentRef:_docRef data:_setData];
175181
[self verifyTimestampsInSnapshot:[_accumulator awaitLocalEvent] fromPreviousSnapshot:nil];
176182
FIRDocumentSnapshot *remoteSnapshot = [_accumulator awaitRemoteEvent];

Firestore/Example/Tests/Model/FSTMutationTests.mm

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,73 @@ - (void)testPatchWithMutationResult {
529529
DocumentState::kCommittedMutations));
530530
}
531531

532+
- (void)testNonTransformMutationBaseValue {
533+
NSDictionary *docData = @{@"foo" : @"foo"};
534+
FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, DocumentState::kSynced);
535+
536+
FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"bar"});
537+
XCTAssertFalse([set extractBaseValue:baseDoc]);
538+
539+
FSTMutation *patch = FSTTestPatchMutation("collection/key", @{@"foo" : @"bar"}, {});
540+
XCTAssertFalse([patch extractBaseValue:baseDoc]);
541+
542+
FSTMutation *deleter = FSTTestDeleteMutation(@"collection/key");
543+
XCTAssertFalse([deleter extractBaseValue:baseDoc]);
544+
}
545+
546+
- (void)testServerTimestampBaseValue {
547+
NSDictionary *docData = @{@"time" : @"foo", @"nested" : @{@"time" : @"foo"}};
548+
FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, DocumentState::kSynced);
549+
550+
FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @{
551+
@"time" : [FIRFieldValue fieldValueForServerTimestamp],
552+
@"nested.time" : [FIRFieldValue fieldValueForServerTimestamp]
553+
});
554+
555+
// Server timestamps are idempotent and don't require base values.
556+
XCTAssertFalse([transform extractBaseValue:baseDoc]);
557+
}
558+
559+
- (void)testNumericIncrementBaseValue {
560+
NSDictionary *docData = @{
561+
@"ignore" : @"foo",
562+
@"double" : @42.0,
563+
@"long" : @42,
564+
@"string" : @"foo",
565+
@"map" : @{},
566+
@"nested" :
567+
@{@"ignore" : @"foo", @"double" : @42.0, @"long" : @42, @"string" : @"foo", @"map" : @{}}
568+
};
569+
FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, DocumentState::kSynced);
570+
571+
FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @{
572+
@"double" : [FIRFieldValue fieldValueForIntegerIncrement:1],
573+
@"long" : [FIRFieldValue fieldValueForIntegerIncrement:1],
574+
@"string" : [FIRFieldValue fieldValueForIntegerIncrement:1],
575+
@"map" : [FIRFieldValue fieldValueForIntegerIncrement:1],
576+
@"missing" : [FIRFieldValue fieldValueForIntegerIncrement:1],
577+
@"nested.double" : [FIRFieldValue fieldValueForIntegerIncrement:1],
578+
@"nested.long" : [FIRFieldValue fieldValueForIntegerIncrement:1],
579+
@"nested.string" : [FIRFieldValue fieldValueForIntegerIncrement:1],
580+
@"nested.map" : [FIRFieldValue fieldValueForIntegerIncrement:1],
581+
@"nested.missing" : [FIRFieldValue fieldValueForIntegerIncrement:1]
582+
});
583+
584+
ObjectValue expectedBaseValue = FSTTestObjectValue(@{
585+
@"double" : @42.0,
586+
@"long" : @42,
587+
@"string" : @0,
588+
@"map" : @0,
589+
@"missing" : @0,
590+
@"nested" : @{@"double" : @42.0, @"long" : @42, @"string" : @0, @"map" : @0, @"missing" : @0}
591+
});
592+
593+
// Server timestamps are idempotent and don't require base values.
594+
absl::optional<ObjectValue> actualBaseValue = [transform extractBaseValue:baseDoc];
595+
XCTAssertTrue([transform extractBaseValue:baseDoc]);
596+
XCTAssertEqual(expectedBaseValue, *actualBaseValue);
597+
}
598+
532599
#define ASSERT_VERSION_TRANSITION(mutation, base, result, expected) \
533600
do { \
534601
FSTMaybeDocument *actual = [mutation applyToRemoteDocument:base mutationResult:result]; \

Firestore/Example/Tests/Model/transform_operations_test.mm

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ FieldValue ApplyToRemoteDocument(const absl::optional<model::FieldValue>& /* pre
4242
return FieldValue::Null();
4343
}
4444

45-
bool idempotent() const override {
46-
return true;
45+
absl::optional<model::FieldValue> ComputeBaseValue(
46+
const absl::optional<model::FieldValue>& previous_value) const override {
47+
return absl::nullopt;
4748
}
4849

4950
bool operator==(const TransformOperation& other) const override {

Firestore/Source/Local/FSTLocalStore.mm

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
#include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
5050
#include "Firestore/core/src/firebase/firestore/util/log.h"
5151
#include "absl/memory/memory.h"
52+
#include "absl/types/optional.h"
5253

5354
using firebase::Timestamp;
5455
using firebase::firestore::auth::User;
@@ -192,29 +193,17 @@ - (FSTLocalWriteResult *)locallyWriteMutations:(std::vector<FSTMutation *> &&)mu
192193
// transform.
193194
std::vector<FSTMutation *> baseMutations;
194195
for (FSTMutation *mutation : mutations) {
195-
if (mutation.idempotent) {
196-
continue;
197-
}
196+
auto base_document_it = existingDocuments.find(mutation.key);
197+
FSTMaybeDocument *base_document =
198+
base_document_it != existingDocuments.end() ? base_document_it->second : nil;
198199

199-
// Theoretically, we should only include non-idempotent fields in this field mask as this mask
200-
// is used to prevent flicker for non-idempotent transforms by providing consistent base
201-
// values. By including the fields for all DocumentTransforms, we incorrectly prevent rebasing
202-
// of idempotent transforms (such as `arrayUnion()`) when any non-idempotent transforms are
203-
// present.
204-
// TODO(mrschmidt): Expose a method that only returns the a field mask for non-idempotent
205-
// transforms
206-
const FieldMask *fieldMask = [mutation fieldMask];
207-
if (fieldMask) {
208-
// `documentsForKeys` is guaranteed to return a (nullable) entry for every document key.
209-
FSTMaybeDocument *maybeDocument = existingDocuments.find(mutation.key)->second;
210-
ObjectValue baseValues = [maybeDocument isKindOfClass:[FSTDocument class]]
211-
? fieldMask->ApplyTo(((FSTDocument *)maybeDocument).data)
212-
: ObjectValue::Empty();
200+
absl::optional<ObjectValue> base_value = [mutation extractBaseValue:base_document];
201+
if (base_value) {
213202
// NOTE: The base state should only be applied if there's some existing document to
214203
// override, so use a Precondition of exists=true
215204
baseMutations.push_back([[FSTPatchMutation alloc] initWithKey:mutation.key
216-
fieldMask:*fieldMask
217-
value:std::move(baseValues)
205+
fieldMask:base_value->ToFieldMask()
206+
value:*base_value
218207
precondition:Precondition::Exists(true)]);
219208
}
220209
}

Firestore/Source/Model/FSTMutation.h

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,25 @@ NS_ASSUME_NONNULL_BEGIN
154154
baseDocument:(nullable FSTMaybeDocument *)baseDoc
155155
localWriteTime:(const firebase::Timestamp &)localWriteTime;
156156

157-
- (const model::DocumentKey &)key;
158-
159-
- (const model::Precondition &)precondition;
160-
161157
/**
162-
* If applicable, returns the field mask for this mutation. Fields that are not included in this
163-
* field mask are not modified when this mutation is applied. Mutations that replace all document
164-
* values return 'nullptr'.
158+
* If this mutation is not idempotent, returns the base value to persist with this mutation.
159+
* If a base value is returned, the mutation is always applied to this base value, even if
160+
* document has already been updated.
161+
*
162+
* The base value is a sparse object that consists of only the document fields for which this
163+
* mutation contains a non-idempotent transformation (e.g. a numeric increment). The provided
164+
* value guarantees consistent behavior for non-idempotent transforms and allow us to return the
165+
* same latency-compensated value even if the backend has already applied the mutation. The base
166+
* value is empty for idempotent mutations, as they can be re-played even if the backend has
167+
* already applied them.
168+
*
169+
* @return a base value to store along with the mutation, or empty for idempotent mutations.
165170
*/
166-
- (nullable const model::FieldMask *)fieldMask;
171+
- (absl::optional<model::ObjectValue>)extractBaseValue:(nullable FSTMaybeDocument *)maybeDoc;
172+
173+
- (const model::DocumentKey &)key;
167174

168-
/** Returns whether all operations in the mutation are idempotent. */
169-
@property(nonatomic, readonly) BOOL idempotent;
175+
- (const model::Precondition &)precondition;
170176

171177
@end
172178

0 commit comments

Comments
 (0)