diff options
Diffstat (limited to 'Firestore')
23 files changed, 661 insertions, 401 deletions
diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 00e16f5..e17cf56 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -3,9 +3,23 @@ Instead of calling `addSnapshotListener(options: DocumentListenOptions.includeMetadataChanges(true))` call `addSnapshotListener(includeMetadataChanges:true)`. +- [changed] Replaced the `QueryListenOptions` object with simple booleans. + Instead of calling + `addSnapshotListener(options: + QueryListenOptions.includeQueryMetadataChanges(true) + .includeDocumentMetadataChanges(true))` + call `addSnapshotListener(includeMetadataChanges:true)`. +- [changed] `QuerySnapshot.documentChanges()` is now a method which optionally + takes `includeMetadataChanges:true`. By default even when listening to a + query with `includeMetadataChanges:true` metadata-only document changes are + suppressed in `documentChanges()`. - [changed] Replaced the `SetOptions` object with a simple boolean. Instead of calling `setData(["a": "b"], options: SetOptions.merge())` call `setData(["a": "b"], merge: true)`. +- [changed] Replaced the `SnapshotOptions` object with direct use of the + `FIRServerTimestampBehavior` on `DocumentSnapshot`. Instead of calling + `data(SnapshotOptions.serverTimestampBehavior(.estimate))` call + `data(serverTimestampBehavior: .estimate)`. Changed `get` similarly. # v0.11.0 - [fixed] Fixed a regression in the Firebase iOS SDK release 4.11.0 that could diff --git a/Firestore/Example/SwiftBuildTest/main.swift b/Firestore/Example/SwiftBuildTest/main.swift index ad6c0f6..00839c4 100644 --- a/Firestore/Example/SwiftBuildTest/main.swift +++ b/Firestore/Example/SwiftBuildTest/main.swift @@ -198,13 +198,13 @@ func readDocument(at docRef: DocumentReference) { if let data = document.data() { print("Read document: \(data)") } - if let data = document.data(with: SnapshotOptions.serverTimestampBehavior(.estimate)) { + if let data = document.data(with: .estimate) { print("Read document: \(data)") } if let foo = document.get("foo") { print("Field: \(foo)") } - if let foo = document.get("foo", options: SnapshotOptions.serverTimestampBehavior(.previous)) { + if let foo = document.get("foo", serverTimestampBehavior: .previous) { print("Field: \(foo)") } // Fields can also be read via subscript notation. @@ -321,6 +321,26 @@ func listenToQueryDiffs(onQuery query: Query) { listener.remove() } +func listenToQueryDiffsWithMetadata(onQuery query: Query) { + let listener = query.addSnapshotListener(includeMetadataChanges: true) { snap, error in + if let snap = snap { + for change in snap.documentChanges(includeMetadataChanges: true) { + switch change.type { + case .added: + print("New document: \(change.document.data())") + case .modified: + print("Modified document: \(change.document.data())") + case .removed: + print("Removed document: \(change.document.data())") + } + } + } + } + + // Unsubscribe + listener.remove() +} + func transactions() { let db = Firestore.firestore() @@ -361,7 +381,6 @@ func types() { let _: GeoPoint let _: Timestamp let _: ListenerRegistration - let _: QueryListenOptions let _: Query let _: QuerySnapshot let _: SnapshotMetadata diff --git a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm index f8c7d60..746c107 100644 --- a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm +++ b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm @@ -19,9 +19,31 @@ #import <XCTest/XCTest.h> #import "Firestore/Example/Tests/API/FSTAPIHelpers.h" +#import "Firestore/Example/Tests/Util/FSTHelpers.h" +#import "Firestore/Source/API/FIRDocumentChange+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" + +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" + +namespace util = firebase::firestore::util; NS_ASSUME_NONNULL_BEGIN +@interface FIRDocumentChange () + +// Expose initializer for testing. +- (instancetype)initWithType:(FIRDocumentChangeType)type + document:(FIRQueryDocumentSnapshot *)document + oldIndex:(NSUInteger)oldIndex + newIndex:(NSUInteger)newIndex; + +@end + @interface FIRQuerySnapshotTests : XCTestCase @end @@ -51,6 +73,67 @@ NS_ASSUME_NONNULL_BEGIN XCTAssertNotEqual([foo hash], [fromCache hash]); } +- (void)testIncludeMetadataChanges { + FSTDocument *doc1Old = FSTTestDoc("foo/bar", 1, @{@"a" : @"b"}, YES); + FSTDocument *doc1New = FSTTestDoc("foo/bar", 1, @{@"a" : @"b"}, NO); + + FSTDocument *doc2Old = FSTTestDoc("foo/baz", 1, @{@"a" : @"b"}, NO); + FSTDocument *doc2New = FSTTestDoc("foo/baz", 1, @{@"a" : @"c"}, NO); + + FSTDocumentSet *oldDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[ doc1Old, doc2Old ]); + FSTDocumentSet *newDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[ doc2New, doc2New ]); + NSArray<FSTDocumentViewChange *> *documentChanges = @[ + [FSTDocumentViewChange changeWithDocument:doc1New type:FSTDocumentViewChangeTypeMetadata], + [FSTDocumentViewChange changeWithDocument:doc2New type:FSTDocumentViewChangeTypeModified], + ]; + + FIRFirestore *firestore = FSTTestFirestore(); + FSTQuery *query = FSTTestQuery("foo"); + FSTViewSnapshot *viewSnapshot = [[FSTViewSnapshot alloc] initWithQuery:query + documents:newDocuments + oldDocuments:oldDocuments + documentChanges:documentChanges + fromCache:NO + hasPendingWrites:NO + syncStateChanged:YES]; + FIRSnapshotMetadata *metadata = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:NO fromCache:NO]; + FIRQuerySnapshot *snapshot = [FIRQuerySnapshot snapshotWithFirestore:firestore + originalQuery:query + snapshot:viewSnapshot + metadata:metadata]; + + FIRQueryDocumentSnapshot *doc1Snap = [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:doc1New.key + document:doc1New + fromCache:NO]; + FIRQueryDocumentSnapshot *doc2Snap = [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:doc2New.key + document:doc2New + fromCache:NO]; + + NSArray<FIRDocumentChange *> *changesWithoutMetadata = @[ + [[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeModified + document:doc2Snap + oldIndex:1 + newIndex:1], + ]; + XCTAssertEqualObjects(snapshot.documentChanges, changesWithoutMetadata); + + NSArray<FIRDocumentChange *> *changesWithMetadata = @[ + [[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeModified + document:doc1Snap + oldIndex:0 + newIndex:0], + [[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeModified + document:doc2Snap + oldIndex:1 + newIndex:1], + ]; + XCTAssertEqualObjects([snapshot documentChangesWithIncludeMetadataChanges:YES], + changesWithMetadata); +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm b/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm index 32d746e..d1c0d75 100644 --- a/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm @@ -221,16 +221,14 @@ FIRFirestore *firestore = collectionRef.firestore; - FIRQueryListenOptions *options = [[[FIRQueryListenOptions options] - includeDocumentMetadataChanges:YES] includeQueryMetadataChanges:YES]; - - [collectionRef addSnapshotListenerWithOptions:options - listener:^(FIRQuerySnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - if (!snapshot.empty && !snapshot.metadata.fromCache) { - [testExpectiation fulfill]; - } - }]; + [collectionRef + addSnapshotListenerWithIncludeMetadataChanges:YES + listener:^(FIRQuerySnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + if (!snapshot.empty && !snapshot.metadata.fromCache) { + [testExpectiation fulfill]; + } + }]; [firestore disableNetworkWithCompletion:^(NSError *error) { XCTAssertNil(error); @@ -249,11 +247,9 @@ }; FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; - FIRQueryListenOptions *options = [[[FIRQueryListenOptions options] - includeDocumentMetadataChanges:YES] includeQueryMetadataChanges:YES]; - id<FIRListenerRegistration> registration = - [collection addSnapshotListenerWithOptions:options - listener:self.eventAccumulator.valueEventHandler]; + id<FIRListenerRegistration> registration = [collection + addSnapshotListenerWithIncludeMetadataChanges:YES + listener:self.eventAccumulator.valueEventHandler]; FIRQuerySnapshot *querySnap = [self.eventAccumulator awaitEventWithName:@"initial event"]; XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnap), @[ @{ @"foo" : @1 } ]); diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm index 4d51434..e1ad3d2 100644 --- a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm @@ -42,20 +42,11 @@ // Listener registration for a listener maintained during the course of the test. id<FIRListenerRegistration> _listenerRegistration; - - // Snapshot options that return the previous value for pending server timestamps. - FIRSnapshotOptions *_returnPreviousValue; - FIRSnapshotOptions *_returnEstimatedValue; } - (void)setUp { [super setUp]; - _returnPreviousValue = - [FIRSnapshotOptions serverTimestampBehavior:FIRServerTimestampBehaviorPrevious]; - _returnEstimatedValue = - [FIRSnapshotOptions serverTimestampBehavior:FIRServerTimestampBehaviorEstimate]; - // Data written in tests via set. _setData = @{ @"a" : @42, @@ -124,10 +115,12 @@ /** Verifies a snapshot containing _setData but with a local estimate for the timestamps. */ - (void)verifyTimestampsAreEstimatedInSnapshot:(FIRDocumentSnapshot *)snapshot { - id timestamp = [snapshot valueForField:@"when" options:_returnEstimatedValue]; + id timestamp = + [snapshot valueForField:@"when" serverTimestampBehavior:FIRServerTimestampBehaviorEstimate]; XCTAssertTrue([timestamp isKindOfClass:[FIRTimestamp class]]); - XCTAssertEqualObjects([snapshot dataWithOptions:_returnEstimatedValue], - [self expectedDataWithTimestamp:timestamp]); + XCTAssertEqualObjects( + [snapshot dataWithServerTimestampBehavior:FIRServerTimestampBehaviorEstimate], + [self expectedDataWithTimestamp:timestamp]); } /** @@ -137,11 +130,13 @@ - (void)verifyTimestampsInSnapshot:(FIRDocumentSnapshot *)snapshot fromPreviousSnapshot:(nullable FIRDocumentSnapshot *)previousSnapshot { if (previousSnapshot == nil) { - XCTAssertEqualObjects([snapshot dataWithOptions:_returnPreviousValue], - [self expectedDataWithTimestamp:[NSNull null]]); + XCTAssertEqualObjects( + [snapshot dataWithServerTimestampBehavior:FIRServerTimestampBehaviorPrevious], + [self expectedDataWithTimestamp:[NSNull null]]); } else { - XCTAssertEqualObjects([snapshot dataWithOptions:_returnPreviousValue], - [self expectedDataWithTimestamp:previousSnapshot[@"when"]]); + XCTAssertEqualObjects( + [snapshot dataWithServerTimestampBehavior:FIRServerTimestampBehaviorPrevious], + [self expectedDataWithTimestamp:previousSnapshot[@"when"]]); } } @@ -211,15 +206,20 @@ [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; XCTAssertEqualObjects([localSnapshot valueForField:@"a"], [NSNull null]); - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); - XCTAssertTrue([[localSnapshot valueForField:@"a" options:_returnEstimatedValue] - isKindOfClass:[FIRTimestamp class]]); + XCTAssertEqualObjects( + [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], + @42); + XCTAssertTrue( + [[localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorEstimate] + isKindOfClass:[FIRTimestamp class]]); FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[FIRTimestamp class]]); - XCTAssertTrue([[remoteSnapshot valueForField:@"a" options:_returnPreviousValue] + XCTAssertTrue([ + [remoteSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious] isKindOfClass:[FIRTimestamp class]]); - XCTAssertTrue([[remoteSnapshot valueForField:@"a" options:_returnEstimatedValue] + XCTAssertTrue([ + [remoteSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorEstimate] isKindOfClass:[FIRTimestamp class]]); } @@ -232,11 +232,15 @@ [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + XCTAssertEqualObjects( + [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], + @42); [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + XCTAssertEqualObjects( + [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], + @42); [self enableNetwork]; @@ -253,7 +257,9 @@ [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + XCTAssertEqualObjects( + [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], + @42); [_docRef updateData:@{ @"a" : @1337 }]; localSnapshot = [self waitForLocalEvent]; @@ -261,7 +267,9 @@ [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; localSnapshot = [self waitForLocalEvent]; - XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @1337); + XCTAssertEqualObjects( + [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], + @1337); [self enableNetwork]; diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm index 3f2d64b..5340873 100644 --- a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm @@ -147,9 +147,8 @@ FIRDocumentReference *docA = [collection documentWithPath:@"a"]; FIRDocumentReference *docB = [collection documentWithPath:@"b"]; FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; - [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] - includeQueryMetadataChanges:YES] - listener:accumulator.valueEventHandler]; + [collection addSnapshotListenerWithIncludeMetadataChanges:YES + listener:accumulator.valueEventHandler]; FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; XCTAssertEqual(initialSnap.count, 0); @@ -177,9 +176,8 @@ FIRDocumentReference *docA = [collection documentWithPath:@"a"]; FIRDocumentReference *docB = [collection documentWithPath:@"b"]; FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; - [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] - includeQueryMetadataChanges:YES] - listener:accumulator.valueEventHandler]; + [collection addSnapshotListenerWithIncludeMetadataChanges:YES + listener:accumulator.valueEventHandler]; FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; XCTAssertEqual(initialSnap.count, 0); @@ -211,9 +209,8 @@ FIRDocumentReference *docA = [collection documentWithPath:@"a"]; FIRDocumentReference *docB = [collection documentWithPath:@"b"]; FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; - [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] - includeQueryMetadataChanges:YES] - listener:accumulator.valueEventHandler]; + [collection addSnapshotListenerWithIncludeMetadataChanges:YES + listener:accumulator.valueEventHandler]; FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; XCTAssertEqual(initialSnap.count, 0); diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index 2ef3acf..3565e2e 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -156,9 +156,10 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, self.lastChanges = [self.localStore rejectBatchID:batch.batchID]; } -- (void)allocateQuery:(FSTQuery *)query { +- (FSTTargetID)allocateQuery:(FSTQuery *)query { FSTQueryData *queryData = [self.localStore allocateQuery:query]; self.lastTargetID = queryData.targetID; + return queryData.targetID; } - (void)collectGarbage { @@ -249,8 +250,12 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES)); - [self applyRemoteEvent:FSTTestUpdateRemoteEvent( - FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + + [self + applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO), + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, YES) ]); FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, YES)); } @@ -260,7 +265,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, // Start a query that requires acks to be held. FSTQuery *query = FSTTestQuery("foo"); - [self allocateQuery:query]; + FSTTargetID targetID = [self allocateQuery:query]; [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); @@ -279,8 +284,9 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTAssertRemoved(@[ @"bar/baz" ]); FSTAssertNotContains(@"bar/baz"); - [self applyRemoteEvent:FSTTestUpdateRemoteEvent( - FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + [self + applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO), + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO) ]); FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO)); FSTAssertNotContains(@"bar/baz"); @@ -289,13 +295,19 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testHandlesDeletedDocumentThenSetMutationThenAck { if ([self isTestBaseClass]) return; - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @1 ], @[])]; + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @(targetID) ], + @[])]; FSTAssertRemoved(@[ @"foo/bar" ]); FSTAssertContains(FSTTestDeletedDoc("foo/bar", 2)); [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES)); + // Can now remove the target, since we have a mutation pinning the document + [self.localStore releaseQuery:query]; [self acknowledgeMutationWithVersion:3]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, NO) ]); @@ -305,10 +317,14 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testHandlesSetMutationThenDeletedDocument { if ([self isTestBaseClass]) return; + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @1 ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @(targetID) ], + @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES)); } @@ -316,8 +332,12 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testHandlesDocumentThenSetMutationThenAckThenDocument { if ([self isTestBaseClass]) return; + // Start a query that requires acks to be held. + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"it" : @"base"}, NO) ]); FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"it" : @"base"}, NO)); @@ -326,11 +346,13 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, YES)); [self acknowledgeMutationWithVersion:3]; - FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO) ]); - FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO)); + // we haven't seen the remote event yet, so the write is still held. + FSTAssertChanged(@[]); + FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, YES)); - [self applyRemoteEvent:FSTTestUpdateRemoteEvent( - FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + [self + applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO), + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO) ]); FSTAssertContains(FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO)); } @@ -354,12 +376,24 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTAssertRemoved(@[ @"foo/bar" ]); FSTAssertNotContains(@"foo/bar"); + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES) ]); FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES)); [self acknowledgeMutationWithVersion:2]; + // We still haven't seen the remote events for the patch, so the local changes remain, and there + // are no changes + FSTAssertChanged(@[]); + FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES)); + + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar", @"it" : @"base"}, NO), @[], + @[])]; + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO) ]); FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO)); } @@ -375,8 +409,11 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTAssertRemoved(@[ @"foo/bar" ]); FSTAssertNotContains(@"foo/bar"); + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO) ]); FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO)); } @@ -396,8 +433,11 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testHandlesDocumentThenDeleteMutationThenAck { if ([self isTestBaseClass]) return; + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO) ]); FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO)); @@ -405,6 +445,9 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTAssertRemoved(@[ @"foo/bar" ]); FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0)); + // Remove the target so only the mutation is pinning the document + [self.localStore releaseQuery:query]; + [self acknowledgeMutationWithVersion:2]; FSTAssertRemoved(@[ @"foo/bar" ]); FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0)); @@ -413,15 +456,21 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testHandlesDeleteMutationThenDocumentThenAck { if ([self isTestBaseClass]) return; + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self writeMutation:FSTTestDeleteMutation(@"foo/bar")]; FSTAssertRemoved(@[ @"foo/bar" ]); FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0)); [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; FSTAssertRemoved(@[ @"foo/bar" ]); FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0)); + // Don't need to keep it pinned anymore + [self.localStore releaseQuery:query]; + [self acknowledgeMutationWithVersion:2]; FSTAssertRemoved(@[ @"foo/bar" ]); FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0)); @@ -430,17 +479,22 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testHandlesDocumentThenDeletedDocumentThenDocument { if ([self isTestBaseClass]) return; + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO) ]); FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO)); - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @1 ], @[])]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @(targetID) ], + @[])]; FSTAssertRemoved(@[ @"foo/bar" ]); FSTAssertContains(FSTTestDeletedDoc("foo/bar", 2)); - [self applyRemoteEvent:FSTTestUpdateRemoteEvent( - FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])]; + [self + applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO), + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO) ]); FSTAssertContains(FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO)); } @@ -456,11 +510,15 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES)); + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, YES)); + [self.localStore releaseQuery:query]; [self acknowledgeMutationWithVersion:2]; // delete mutation FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, YES) ]); FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, YES)); @@ -553,16 +611,15 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, if ([self isTestBaseClass]) return; FSTQuery *query = FSTTestQuery("foo"); - [self allocateQuery:query]; - FSTAssertTargetID(2); + FSTTargetID targetID = [self allocateQuery:query]; [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO), - @[ @2 ], @[])]; + @[ @(targetID) ], @[])]; [self collectGarbage]; FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO)); [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"baz"}, NO), - @[], @[ @2 ])]; + @[], @[ @(targetID) ])]; [self collectGarbage]; FSTAssertNotContains(@"foo/bar"); @@ -571,9 +628,15 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testCollectsGarbageAfterAcknowledgedMutation { if ([self isTestBaseClass]) return; + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 0, @{@"foo" : @"old"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})]; + // Release the query so that our target count goes back to 0 and we are considered up-to-date. + [self.localStore releaseQuery:query]; + [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; [self collectGarbage]; @@ -603,9 +666,15 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testCollectsGarbageAfterRejectedMutation { if ([self isTestBaseClass]) return; + FSTQuery *query = FSTTestQuery("foo"); + FSTTargetID targetID = [self allocateQuery:query]; + [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 0, @{@"foo" : @"old"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})]; + // Release the query so that our target count goes back to 0 and we are considered up-to-date. + [self.localStore releaseQuery:query]; + [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; [self collectGarbage]; @@ -636,11 +705,10 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, if ([self isTestBaseClass]) return; FSTQuery *query = FSTTestQuery("foo"); - [self allocateQuery:query]; - FSTAssertTargetID(2); + FSTTargetID targetID = [self allocateQuery:query]; [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO), - @[ @2 ], @[])]; + @[ @(targetID) ], @[])]; [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})]; [self collectGarbage]; FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO)); @@ -648,15 +716,16 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, [self notifyLocalViewChanges:FSTTestViewChanges(query, @[ @"foo/bar", @"foo/baz" ], @[])]; [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO), - @[], @[ @2 ])]; + @[], @[ @(targetID) ])]; [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/baz", 2, @{@"foo" : @"baz"}, NO), - @[ @1 ], @[])]; + @[ @(targetID) ], @[])]; [self acknowledgeMutationWithVersion:2]; [self collectGarbage]; FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO)); FSTAssertContains(FSTTestDoc("foo/baz", 2, @{@"foo" : @"baz"}, NO)); [self notifyLocalViewChanges:FSTTestViewChanges(query, @[], @[ @"foo/bar", @"foo/baz" ])]; + [self.localStore releaseQuery:query]; [self collectGarbage]; FSTAssertNotContains(@"foo/bar"); diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.mm b/Firestore/Example/Tests/Model/FSTMutationTests.mm index 56bf1c2..0bb7518 100644 --- a/Firestore/Example/Tests/Model/FSTMutationTests.mm +++ b/Firestore/Example/Tests/Model/FSTMutationTests.mm @@ -185,7 +185,126 @@ using firebase::firestore::model::TransformOperation; } } -- (void)testAppliesServerAckedTransformsToDocuments { +- (void)testAppliesLocalArrayUnionTransformToMissingField { + auto baseDoc = @{}; + auto transform = @{ @"missing" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]] }; + auto expected = @{ @"missing" : @[ @1, @2 ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayUnionTransformToNonArrayField { + auto baseDoc = @{ @"non-array" : @42 }; + auto transform = @{ @"non-array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]] }; + auto expected = @{ @"non-array" : @[ @1, @2 ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayUnionTransformWithNonExistingElements { + auto baseDoc = @{ @"array" : @[ @1, @3 ] }; + auto transform = @{ @"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @4 ]] }; + auto expected = @{ @"array" : @[ @1, @3, @2, @4 ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayUnionTransformWithExistingElements { + auto baseDoc = @{ @"array" : @[ @1, @3 ] }; + auto transform = @{ @"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @3 ]] }; + auto expected = @{ @"array" : @[ @1, @3 ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayUnionTransformWithDuplicateExistingElements { + // Duplicate entries in your existing array should be preserved. + auto baseDoc = @{ @"array" : @[ @1, @2, @2, @3 ] }; + auto transform = @{ @"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2 ]] }; + auto expected = @{ @"array" : @[ @1, @2, @2, @3 ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayUnionTransformWithDuplicateUnionElements { + // Duplicate entries in your union array should only be added once. + auto baseDoc = @{ @"array" : @[ @1, @3 ] }; + auto transform = @{ @"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @2 ]] }; + auto expected = @{ @"array" : @[ @1, @3, @2 ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayUnionTransformWithNonPrimitiveElements { + // Union nested object values (one existing, one not). + auto baseDoc = @{ @"array" : @[ @1, @{@"a" : @"b"} ] }; + auto transform = + @{ @"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @{@"a" : @"b"}, @{@"c" : @"d"} ]] }; + auto expected = @{ @"array" : @[ @1, @{@"a" : @"b"}, @{@"c" : @"d"} ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayUnionTransformWithPartiallyOverlappingElements { + // Union objects that partially overlap an existing object. + auto baseDoc = @{ @"array" : @[ @1, @{@"a" : @"b", @"c" : @"d"} ] }; + auto transform = + @{ @"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @{@"a" : @"b"}, @{@"c" : @"d"} ]] }; + auto expected = + @{ @"array" : @[ @1, @{@"a" : @"b", @"c" : @"d"}, @{@"a" : @"b"}, @{@"c" : @"d"} ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayRemoveTransformToMissingField { + auto baseDoc = @{}; + auto transform = @{ @"missing" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @2 ]] }; + auto expected = @{ @"missing" : @[] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayRemoveTransformToNonArrayField { + auto baseDoc = @{ @"non-array" : @42 }; + auto transform = @{ @"non-array" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @2 ]] }; + auto expected = @{ @"non-array" : @[] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayRemoveTransformWithNonExistingElements { + auto baseDoc = @{ @"array" : @[ @1, @3 ] }; + auto transform = @{ @"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @2, @4 ]] }; + auto expected = @{ @"array" : @[ @1, @3 ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayRemoveTransformWithExistingElements { + auto baseDoc = @{ @"array" : @[ @1, @2, @3, @4 ] }; + auto transform = @{ @"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @3 ]] }; + auto expected = @{ @"array" : @[ @2, @4 ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +- (void)testAppliesLocalArrayRemoveTransformWithNonPrimitiveElements { + // Remove nested object values (one existing, one not). + auto baseDoc = @{ @"array" : @[ @1, @{@"a" : @"b"} ] }; + auto transform = + @{ @"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @{@"a" : @"b"}, @{@"c" : @"d"} ]] }; + auto expected = @{ @"array" : @[ @1 ] }; + [self transformBaseDoc:baseDoc with:transform expecting:expected]; +} + +// Helper to test a particular transform scenario. +- (void)transformBaseDoc:(NSDictionary<NSString *, id> *)baseData + with:(NSDictionary<NSString *, id> *)transformData + expecting:(NSDictionary<NSString *, id> *)expectedData { + FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, baseData, NO); + + FSTMutation *transform = FSTTestTransformMutation(@"collection/key", transformData); + + FSTMaybeDocument *transformedDoc = + [transform applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; + + FSTDocument *expectedDoc = [FSTDocument documentWithData:FSTTestObjectValue(expectedData) + key:FSTTestDocKey(@"collection/key") + version:FSTTestVersion(0) + hasLocalMutations:YES]; + + XCTAssertEqualObjects(transformedDoc, expectedDoc); +} + +- (void)testAppliesServerAckedServerTimestampTransformToDocuments { NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, NO); @@ -207,6 +326,29 @@ using firebase::firestore::model::TransformOperation; XCTAssertEqualObjects(transformedDoc, FSTTestDoc("collection/key", 0, expectedData, NO)); } +- (void)testAppliesServerAckedArrayTransformsToDocuments { + NSDictionary *docData = @{ @"array_1" : @[ @1, @2 ], @"array_2" : @[ @"a", @"b" ] }; + FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, NO); + + FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @{ + @"array_1" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @3 ]], + @"array_2" : [FIRFieldValue fieldValueForArrayRemove:@[ @"a", @"c" ]] + }); + + // Server just sends null transform results for array operations. + FSTMutationResult *mutationResult = [[FSTMutationResult alloc] + initWithVersion:FSTTestVersion(1) + transformResults:@[ [FSTNullValue nullValue], [FSTNullValue nullValue] ]]; + + FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc + baseDocument:baseDoc + localWriteTime:_timestamp + mutationResult:mutationResult]; + + NSDictionary *expectedData = @{ @"array_1" : @[ @1, @2, @3 ], @"array_2" : @[ @"b" ] }; + XCTAssertEqualObjects(transformedDoc, FSTTestDoc("collection/key", 0, expectedData, NO)); +} + - (void)testDeleteDeletes { NSDictionary *docData = @{@"foo" : @"bar"}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, NO); diff --git a/Firestore/Source/API/FIRDocumentChange+Internal.h b/Firestore/Source/API/FIRDocumentChange+Internal.h index 7c9723c..2aaced1 100644 --- a/Firestore/Source/API/FIRDocumentChange+Internal.h +++ b/Firestore/Source/API/FIRDocumentChange+Internal.h @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN /** Calculates the array of FIRDocumentChange's based on the given FSTViewSnapshot. */ + (NSArray<FIRDocumentChange *> *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot + includeMetadataChanges:(BOOL)includeMetadataChanges firestore:(FIRFirestore *)firestore; @end diff --git a/Firestore/Source/API/FIRDocumentChange.mm b/Firestore/Source/API/FIRDocumentChange.mm index d1d9999..7bb24d2 100644 --- a/Firestore/Source/API/FIRDocumentChange.mm +++ b/Firestore/Source/API/FIRDocumentChange.mm @@ -50,9 +50,11 @@ NS_ASSUME_NONNULL_BEGIN } + (NSArray<FIRDocumentChange *> *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot + includeMetadataChanges:(BOOL)includeMetadataChanges firestore:(FIRFirestore *)firestore { if (snapshot.oldDocuments.isEmpty) { - // Special case the first snapshot because index calculation is easy and fast + // Special case the first snapshot because index calculation is easy and fast. Also all changes + // on the first snapshot are adds so there are also no metadata-only changes to filter out. FSTDocument *_Nullable lastDocument = nil; NSUInteger index = 0; NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array]; @@ -79,6 +81,10 @@ NS_ASSUME_NONNULL_BEGIN FSTDocumentSet *indexTracker = snapshot.oldDocuments; NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array]; for (FSTDocumentViewChange *change in snapshot.documentChanges) { + if (!includeMetadataChanges && change.type == FSTDocumentViewChangeTypeMetadata) { + continue; + } + FIRQueryDocumentSnapshot *document = [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore documentKey:change.document.key @@ -124,6 +130,23 @@ NS_ASSUME_NONNULL_BEGIN return self; } +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![other isKindOfClass:[FIRDocumentChange class]]) return NO; + + FIRDocumentChange *change = (FIRDocumentChange *)other; + return self.type == change.type && [self.document isEqual:change.document] && + self.oldIndex == change.oldIndex && self.newIndex == change.newIndex; +} + +- (NSUInteger)hash { + NSUInteger result = (NSUInteger)self.type; + result = result * 31u + [self.document hash]; + result = result * 31u + (NSUInteger)self.oldIndex; + result = result * 31u + (NSUInteger)self.newIndex; + return result; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentSnapshot.mm b/Firestore/Source/API/FIRDocumentSnapshot.mm index 0fd59f4..614982b 100644 --- a/Firestore/Source/API/FIRDocumentSnapshot.mm +++ b/Firestore/Source/API/FIRDocumentSnapshot.mm @@ -24,7 +24,6 @@ #import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" -#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Util/FSTAssert.h" @@ -40,6 +39,21 @@ using firebase::firestore::model::DocumentKey; NS_ASSUME_NONNULL_BEGIN +/** Converts a public FIRServerTimestampBehavior into its internal equivalent. */ +static FSTServerTimestampBehavior InternalServerTimestampBehavor( + FIRServerTimestampBehavior behavior) { + switch (behavior) { + case FIRServerTimestampBehaviorNone: + return FSTServerTimestampBehaviorNone; + case FIRServerTimestampBehaviorEstimate: + return FSTServerTimestampBehaviorEstimate; + case FIRServerTimestampBehaviorPrevious: + return FSTServerTimestampBehaviorPrevious; + default: + FIREBASE_ASSERT_MESSAGE(false, "Unexpected server timestamp option: %ld", (long)behavior); + } +} + @interface FIRDocumentSnapshot () - (instancetype)initWithFirestore:(FIRFirestore *)firestore @@ -144,24 +158,23 @@ NS_ASSUME_NONNULL_BEGIN } - (nullable NSDictionary<NSString *, id> *)data { - return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; + return [self dataWithServerTimestampBehavior:FIRServerTimestampBehaviorNone]; } -- (nullable NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options { +- (nullable NSDictionary<NSString *, id> *)dataWithServerTimestampBehavior: + (FIRServerTimestampBehavior)serverTimestampBehavior { + FSTFieldValueOptions *options = [self optionsForServerTimestampBehavior:serverTimestampBehavior]; return self.internalDocument == nil ? nil - : [self convertedObject:[self.internalDocument data] - options:[FSTFieldValueOptions - optionsForSnapshotOptions:options - timestampsInSnapshotsEnabled: - self.firestore.settings.timestampsInSnapshotsEnabled]]; + : [self convertedObject:[self.internalDocument data] options:options]; } - (nullable id)valueForField:(id)field { - return [self valueForField:field options:[FIRSnapshotOptions defaultOptions]]; + return [self valueForField:field serverTimestampBehavior:FIRServerTimestampBehaviorNone]; } -- (nullable id)valueForField:(id)field options:(FIRSnapshotOptions *)options { +- (nullable id)valueForField:(id)field + serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { FIRFieldPath *fieldPath; if ([field isKindOfClass:[NSString class]]) { @@ -173,13 +186,17 @@ NS_ASSUME_NONNULL_BEGIN } FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; - return fieldValue == nil - ? nil - : [self convertedValue:fieldValue - options:[FSTFieldValueOptions - optionsForSnapshotOptions:options - timestampsInSnapshotsEnabled: - self.firestore.settings.timestampsInSnapshotsEnabled]]; + FSTFieldValueOptions *options = [self optionsForServerTimestampBehavior:serverTimestampBehavior]; + return fieldValue == nil ? nil : [self convertedValue:fieldValue options:options]; +} + +- (FSTFieldValueOptions *)optionsForServerTimestampBehavior: + (FIRServerTimestampBehavior)serverTimestampBehavior { + FSTServerTimestampBehavior internalBehavior = + InternalServerTimestampBehavor(serverTimestampBehavior); + return [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:internalBehavior + timestampsInSnapshotsEnabled:self.firestore.settings.timestampsInSnapshotsEnabled]; } - (nullable id)objectForKeyedSubscript:(id)key { @@ -262,8 +279,10 @@ NS_ASSUME_NONNULL_BEGIN return data; } -- (NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options { - NSDictionary<NSString *, id> *data = [super dataWithOptions:options]; +- (NSDictionary<NSString *, id> *)dataWithServerTimestampBehavior: + (FIRServerTimestampBehavior)serverTimestampBehavior { + NSDictionary<NSString *, id> *data = + [super dataWithServerTimestampBehavior:serverTimestampBehavior]; FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); return data; } diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm index 9cdc572..14dcaef 100644 --- a/Firestore/Source/API/FIRQuery.mm +++ b/Firestore/Source/API/FIRQuery.mm @@ -48,47 +48,6 @@ using firebase::firestore::model::ResourcePath; NS_ASSUME_NONNULL_BEGIN -@interface FIRQueryListenOptions () - -- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges - NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FIRQueryListenOptions - -+ (instancetype)options { - return [[FIRQueryListenOptions alloc] init]; -} - -- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { - if (self = [super init]) { - _includeQueryMetadataChanges = includeQueryMetadataChanges; - _includeDocumentMetadataChanges = includeDocumentMetadataChanges; - } - return self; -} - -- (instancetype)init { - return [self initWithIncludeQueryMetadataChanges:NO includeDocumentMetadataChanges:NO]; -} - -- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges { - return [[FIRQueryListenOptions alloc] - initWithIncludeQueryMetadataChanges:includeQueryMetadataChanges - includeDocumentMetadataChanges:_includeDocumentMetadataChanges]; -} - -- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { - return [[FIRQueryListenOptions alloc] - initWithIncludeQueryMetadataChanges:_includeQueryMetadataChanges - includeDocumentMetadataChanges:includeDocumentMetadataChanges]; -} - -@end - @interface FIRQuery () @property(nonatomic, strong, readonly) FSTQuery *query; @end @@ -162,14 +121,14 @@ NS_ASSUME_NONNULL_BEGIN } - (id<FIRListenerRegistration>)addSnapshotListener:(FIRQuerySnapshotBlock)listener { - return [self addSnapshotListenerWithOptions:nil listener:listener]; + return [self addSnapshotListenerWithIncludeMetadataChanges:NO listener:listener]; } -- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions: - (nullable FIRQueryListenOptions *)options - listener:(FIRQuerySnapshotBlock)listener { - return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] - listener:listener]; +- (id<FIRListenerRegistration>) +addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges + listener:(FIRQuerySnapshotBlock)listener { + auto options = [self internalOptionsForIncludeMetadataChanges:includeMetadataChanges]; + return [self addSnapshotListenerInternalWithOptions:options listener:listener]; } - (id<FIRListenerRegistration>) @@ -629,11 +588,10 @@ addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions } /** Converts the public API options object to the internal options object. */ -- (FSTListenOptions *)internalOptions:(nullable FIRQueryListenOptions *)options { - return [[FSTListenOptions alloc] - initWithIncludeQueryMetadataChanges:options.includeQueryMetadataChanges - includeDocumentMetadataChanges:options.includeDocumentMetadataChanges - waitForSyncWhenOnline:NO]; +- (FSTListenOptions *)internalOptionsForIncludeMetadataChanges:(BOOL)includeMetadataChanges { + return [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:includeMetadataChanges + includeDocumentMetadataChanges:includeMetadataChanges + waitForSyncWhenOnline:NO]; } @end diff --git a/Firestore/Source/API/FIRQuerySnapshot.mm b/Firestore/Source/API/FIRQuerySnapshot.mm index abee84c..fb7a849 100644 --- a/Firestore/Source/API/FIRQuerySnapshot.mm +++ b/Firestore/Source/API/FIRQuerySnapshot.mm @@ -62,6 +62,7 @@ NS_ASSUME_NONNULL_BEGIN // Cached value of the documentChanges property. NSArray<FIRDocumentChange *> *_documentChanges; + BOOL _documentChangesIncludeMetadataChanges; } - (instancetype)initWithFirestore:(FIRFirestore *)firestore @@ -73,6 +74,7 @@ NS_ASSUME_NONNULL_BEGIN _originalQuery = query; _snapshot = snapshot; _metadata = metadata; + _documentChangesIncludeMetadataChanges = NO; } return self; } @@ -139,9 +141,16 @@ NS_ASSUME_NONNULL_BEGIN } - (NSArray<FIRDocumentChange *> *)documentChanges { - if (!_documentChanges) { - _documentChanges = - [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore]; + return [self documentChangesWithIncludeMetadataChanges:NO]; +} + +- (NSArray<FIRDocumentChange *> *)documentChangesWithIncludeMetadataChanges: + (BOOL)includeMetadataChanges { + if (!_documentChanges || _documentChangesIncludeMetadataChanges != includeMetadataChanges) { + _documentChanges = [FIRDocumentChange documentChangesForSnapshot:self.snapshot + includeMetadataChanges:includeMetadataChanges + firestore:self.firestore]; + _documentChangesIncludeMetadataChanges = includeMetadataChanges; } return _documentChanges; } diff --git a/Firestore/Source/API/FIRSnapshotOptions+Internal.h b/Firestore/Source/API/FIRSnapshotOptions+Internal.h deleted file mode 100644 index 64e7dbc..0000000 --- a/Firestore/Source/API/FIRSnapshotOptions+Internal.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "FIRDocumentSnapshot.h" - -#import <Foundation/Foundation.h> - -#import "Firestore/Source/Model/FSTFieldValue.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRSnapshotOptions (Internal) - -/** Returns a default instance of FIRSnapshotOptions that specifies no options. */ -+ (instancetype)defaultOptions; - -/* Initializes a new instance with the specified server timestamp behavior. */ -- (instancetype)initWithServerTimestampBehavior:(FSTServerTimestampBehavior)serverTimestampBehavior; - -/* Returns the server timestamp behavior. Returns -1 if no behavior is specified. */ -- (FSTServerTimestampBehavior)serverTimestampBehavior; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotOptions.mm b/Firestore/Source/API/FIRSnapshotOptions.mm deleted file mode 100644 index 72ea3cc..0000000 --- a/Firestore/Source/API/FIRSnapshotOptions.mm +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "FIRDocumentSnapshot.h" - -#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" -#import "Firestore/Source/Util/FSTAssert.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRSnapshotOptions () - -@property(nonatomic) FSTServerTimestampBehavior serverTimestampBehavior; - -@end - -@implementation FIRSnapshotOptions - -- (instancetype)initWithServerTimestampBehavior: - (FSTServerTimestampBehavior)serverTimestampBehavior { - self = [super init]; - - if (self) { - _serverTimestampBehavior = serverTimestampBehavior; - } - - return self; -} - -+ (instancetype)defaultOptions { - static FIRSnapshotOptions *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = - [[FIRSnapshotOptions alloc] initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; - }); - - return sharedInstance; -} - -+ (instancetype)serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { - switch (serverTimestampBehavior) { - case FIRServerTimestampBehaviorEstimate: - return [[FIRSnapshotOptions alloc] - initWithServerTimestampBehavior:FSTServerTimestampBehaviorEstimate]; - case FIRServerTimestampBehaviorPrevious: - return [[FIRSnapshotOptions alloc] - initWithServerTimestampBehavior:FSTServerTimestampBehaviorPrevious]; - case FIRServerTimestampBehaviorNone: - return [FIRSnapshotOptions defaultOptions]; - default: - FSTFail(@"Encountered unknown server timestamp behavior: %d", (int)serverTimestampBehavior); - } -} - -@end - -NS_ASSUME_NONNULL_END
\ No newline at end of file diff --git a/Firestore/Source/Model/FSTFieldValue.h b/Firestore/Source/Model/FSTFieldValue.h index 6914f4d..6f9798a 100644 --- a/Firestore/Source/Model/FSTFieldValue.h +++ b/Firestore/Source/Model/FSTFieldValue.h @@ -25,7 +25,6 @@ @class FIRTimestamp; @class FSTFieldValueOptions; @class FIRGeoPoint; -@class FIRSnapshotOptions; NS_ASSUME_NONNULL_BEGIN @@ -67,10 +66,6 @@ typedef NS_ENUM(NSInteger, FSTServerTimestampBehavior) { timestampsInSnapshotsEnabled:(BOOL)timestampsInSnapshotsEnabled NS_DESIGNATED_INITIALIZER; -/** Creates an FSTFieldValueOptions instance from FIRSnapshotOptions. */ -+ (instancetype)optionsForSnapshotOptions:(FIRSnapshotOptions *)value - timestampsInSnapshotsEnabled:(BOOL)timestampsInSnapshotsEnabled; - @end /** diff --git a/Firestore/Source/Model/FSTFieldValue.mm b/Firestore/Source/Model/FSTFieldValue.mm index 0d7c649..9e77d39 100644 --- a/Firestore/Source/Model/FSTFieldValue.mm +++ b/Firestore/Source/Model/FSTFieldValue.mm @@ -16,10 +16,10 @@ #import "Firestore/Source/Model/FSTFieldValue.h" +#import "FIRDocumentSnapshot.h" #import "FIRTimestamp.h" #import "Firestore/Source/API/FIRGeoPoint+Internal.h" -#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" #import "Firestore/Source/Model/FSTDocumentKey.h" #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTClasses.h" @@ -46,28 +46,6 @@ NS_ASSUME_NONNULL_BEGIN @implementation FSTFieldValueOptions -+ (instancetype)optionsForSnapshotOptions:(FIRSnapshotOptions *)options - timestampsInSnapshotsEnabled:(BOOL)timestampsInSnapshotsEnabled { - FSTServerTimestampBehavior convertedServerTimestampBehavior = FSTServerTimestampBehaviorNone; - switch (options.serverTimestampBehavior) { - case FIRServerTimestampBehaviorNone: - convertedServerTimestampBehavior = FSTServerTimestampBehaviorNone; - break; - case FIRServerTimestampBehaviorEstimate: - convertedServerTimestampBehavior = FSTServerTimestampBehaviorEstimate; - break; - case FIRServerTimestampBehaviorPrevious: - convertedServerTimestampBehavior = FSTServerTimestampBehaviorPrevious; - break; - default: - FSTFail(@"Unexpected server timestamp option: %ld", (long)options.serverTimestampBehavior); - } - - return - [[FSTFieldValueOptions alloc] initWithServerTimestampBehavior:convertedServerTimestampBehavior - timestampsInSnapshotsEnabled:timestampsInSnapshotsEnabled]; -} - - (instancetype)initWithServerTimestampBehavior:(FSTServerTimestampBehavior)serverTimestampBehavior timestampsInSnapshotsEnabled:(BOOL)timestampsInSnapshotsEnabled { self = [super init]; diff --git a/Firestore/Source/Model/FSTMutation.mm b/Firestore/Source/Model/FSTMutation.mm index 99d2e51..47e34da 100644 --- a/Firestore/Source/Model/FSTMutation.mm +++ b/Firestore/Source/Model/FSTMutation.mm @@ -36,6 +36,7 @@ #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" +using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldPath; @@ -50,8 +51,8 @@ NS_ASSUME_NONNULL_BEGIN @implementation FSTMutationResult -- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version - transformResults:(NSArray<FSTFieldValue *> *_Nullable)transformResults { +- (instancetype)initWithVersion:(nullable FSTSnapshotVersion *)version + transformResults:(nullable NSArray<FSTFieldValue *> *)transformResults { if (self = [super init]) { _version = version; _transformResults = transformResults; @@ -345,13 +346,18 @@ NS_ASSUME_NONNULL_BEGIN [maybeDoc class]); FSTDocument *doc = (FSTDocument *)maybeDoc; - FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); + FSTAssert([doc.key isEqual:self.key], @"Can only transform a document with the same key"); BOOL hasLocalMutations = (mutationResult == nil); - NSArray<FSTFieldValue *> *transformResults = - mutationResult - ? mutationResult.transformResults - : [self localTransformResultsWithBaseDocument:baseDoc writeTime:localWriteTime]; + NSArray<FSTFieldValue *> *transformResults; + if (mutationResult) { + transformResults = + [self serverTransformResultsWithBaseDocument:baseDoc + serverTransformResults:mutationResult.transformResults]; + } else { + transformResults = + [self localTransformResultsWithBaseDocument:baseDoc writeTime:localWriteTime]; + } FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults]; return [FSTDocument documentWithData:newData key:doc.key @@ -361,6 +367,53 @@ NS_ASSUME_NONNULL_BEGIN /** * Creates an array of "transform results" (a transform result is a field value representing the + * result of applying a transform) for use after a FSTTransformMutation has been acknowledged by + * the server. + * + * @param baseDocument The document prior to applying this mutation batch. + * @param serverTransformResults The transform results received by the server. + * @return The transform results array. + */ +- (NSArray<FSTFieldValue *> *) +serverTransformResultsWithBaseDocument:(nullable FSTMaybeDocument *)baseDocument + serverTransformResults:(NSArray<FSTFieldValue *> *)serverTransformResults { + NSMutableArray<FSTFieldValue *> *transformResults = [NSMutableArray array]; + FSTAssert(self.fieldTransforms.size() == serverTransformResults.count, + @"server transform result count (%ld) should match field transforms count (%ld)", + serverTransformResults.count, self.fieldTransforms.size()); + + for (NSUInteger i = 0; i < serverTransformResults.count; i++) { + const FieldTransform &fieldTransform = self.fieldTransforms[i]; + FSTFieldValue *previousValue = nil; + if ([baseDocument isMemberOfClass:[FSTDocument class]]) { + previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path()]; + } + + FSTFieldValue *transformResult; + // The server just sends null as the transform result for array union / remove operations, so + // we have to calculate a result the same as we do for local applications. + if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayUnion) { + transformResult = [self + arrayUnionResultWithElements:ArrayTransform::Elements(fieldTransform.transformation()) + previousValue:previousValue]; + + } else if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayRemove) { + transformResult = [self + arrayRemoveResultWithElements:ArrayTransform::Elements(fieldTransform.transformation()) + previousValue:previousValue]; + + } else { + // Just use the server-supplied result. + transformResult = serverTransformResults[i]; + } + + [transformResults addObject:transformResult]; + } + return transformResults; +} + +/** + * Creates an array of "transform results" (a transform result is a field value representing the * result of applying a transform) for use when applying an FSTTransformMutation locally. * * @param baseDocument The document prior to applying this mutation batch. @@ -369,27 +422,81 @@ NS_ASSUME_NONNULL_BEGIN * @return The transform results array. */ - (NSArray<FSTFieldValue *> *)localTransformResultsWithBaseDocument: - (FSTMaybeDocument *_Nullable)baseDocument + (nullable FSTMaybeDocument *)baseDocument writeTime:(FIRTimestamp *)localWriteTime { NSMutableArray<FSTFieldValue *> *transformResults = [NSMutableArray array]; for (const FieldTransform &fieldTransform : self.fieldTransforms) { + FSTFieldValue *previousValue = nil; + if ([baseDocument isMemberOfClass:[FSTDocument class]]) { + previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path()]; + } + + FSTFieldValue *transformResult; if (fieldTransform.transformation().type() == TransformOperation::Type::ServerTimestamp) { - FSTFieldValue *previousValue = nil; + transformResult = + [FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:localWriteTime + previousValue:previousValue]; + + } else if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayUnion) { + transformResult = [self + arrayUnionResultWithElements:ArrayTransform::Elements(fieldTransform.transformation()) + previousValue:previousValue]; - if ([baseDocument isMemberOfClass:[FSTDocument class]]) { - previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path()]; - } + } else if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayRemove) { + transformResult = [self + arrayRemoveResultWithElements:ArrayTransform::Elements(fieldTransform.transformation()) + previousValue:previousValue]; - [transformResults - addObject:[FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:localWriteTime - previousValue:previousValue]]; } else { FSTFail(@"Encountered unknown transform: %d type", fieldTransform.transformation().type()); } + + [transformResults addObject:transformResult]; } return transformResults; } +/** + * Transforms the provided `previousValue` via the provided `elements`. Used both for local + * application and after server acknowledgement. + */ +- (FSTFieldValue *)arrayUnionResultWithElements:(const std::vector<FSTFieldValue *> &)elements + previousValue:(FSTFieldValue *)previousValue { + NSMutableArray<FSTFieldValue *> *result = [self coercedFieldValuesArray:previousValue]; + for (FSTFieldValue *element : elements) { + if (![result containsObject:element]) { + [result addObject:element]; + } + } + return [[FSTArrayValue alloc] initWithValueNoCopy:result]; +} + +/** + * Transforms the provided `previousValue` via the provided `elements`. Used both for local + * application and after server acknowledgement. + */ +- (FSTFieldValue *)arrayRemoveResultWithElements:(const std::vector<FSTFieldValue *> &)elements + previousValue:(FSTFieldValue *)previousValue { + NSMutableArray<FSTFieldValue *> *result = [self coercedFieldValuesArray:previousValue]; + for (FSTFieldValue *element : elements) { + [result removeObject:element]; + } + return [[FSTArrayValue alloc] initWithValueNoCopy:result]; +} + +/** + * Inspects the provided value, returning a mutable copy of the internal array if it's an + * FSTArrayValue and an empty mutable array if it's nil or any other type of FSTFieldValue. + */ +- (NSMutableArray<FSTFieldValue *> *)coercedFieldValuesArray:(nullable FSTFieldValue *)value { + if ([value isMemberOfClass:[FSTArrayValue class]]) { + return [NSMutableArray arrayWithArray:((FSTArrayValue *)value).internalValue]; + } else { + // coerce to empty array. + return [NSMutableArray array]; + } +} + - (FSTObjectValue *)transformObject:(FSTObjectValue *)objectValue transformResults:(NSArray<FSTFieldValue *> *)transformResults { FSTAssert(transformResults.count == self.fieldTransforms.size(), @@ -397,13 +504,8 @@ NS_ASSUME_NONNULL_BEGIN for (size_t i = 0; i < self.fieldTransforms.size(); i++) { const FieldTransform &fieldTransform = self.fieldTransforms[i]; - const TransformOperation &transform = fieldTransform.transformation(); const FieldPath &fieldPath = fieldTransform.path(); - if (transform.type() == TransformOperation::Type::ServerTimestamp) { - objectValue = [objectValue objectBySettingValue:transformResults[i] forPath:fieldPath]; - } else { - FSTFail(@"Encountered unknown transform: %d type", transform.type()); - } + objectValue = [objectValue objectBySettingValue:transformResults[i] forPath:fieldPath]; } return objectValue; } diff --git a/Firestore/Source/Public/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h index 6e79a7f..669fe07 100644 --- a/Firestore/Source/Public/FIRDocumentSnapshot.h +++ b/Firestore/Source/Public/FIRDocumentSnapshot.h @@ -48,29 +48,6 @@ typedef NS_ENUM(NSInteger, FIRServerTimestampBehavior) { } NS_SWIFT_NAME(ServerTimestampBehavior); /** - * Options that configure how data is retrieved from a `DocumentSnapshot` - * (e.g. the desired behavior for server timestamps that have not yet been set - * to their final value). - */ -NS_SWIFT_NAME(SnapshotOptions) -@interface FIRSnapshotOptions : NSObject - -/** */ -- (instancetype)init __attribute__((unavailable("FIRSnapshotOptions cannot be created directly."))); - -/** - * If set, controls the return value for `FieldValue.serverTimestamp()` - * fields that have not yet been set to their final value. - * - * If omitted, `NSNull` will be returned by default. - * - * @return The created `FIRSnapshotOptions` object. - */ -+ (instancetype)serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior; - -@end - -/** * A `FIRDocumentSnapshot` contains data read from a document in your Firestore database. The data * can be extracted with the `data` property or by using subscript syntax to access a specific * field. @@ -105,7 +82,7 @@ NS_SWIFT_NAME(DocumentSnapshot) * `NSNull`. You can use `dataWithOptions()` to configure this behavior. * * @return An `NSDictionary` containing all fields in the document or `nil` if the document doesn't - * exist. + * exist. */ - (nullable NSDictionary<NSString *, id> *)data; @@ -113,12 +90,13 @@ NS_SWIFT_NAME(DocumentSnapshot) * Retrieves all fields in the document as a `Dictionary`. Returns `nil` if the document doesn't * exist. * - * @param options `SnapshotOptions` to configure how data is returned from the snapshot (e.g. the - * desired behavior for server timestamps that have not yet been set to their final value). + * @param serverTimestampBehavior Configures how server timestamps that have not yet been set to + * their final value are returned from the snapshot. * @return A `Dictionary` containing all fields in the document or `nil` if the document doesn't - * exist. + * exist. */ -- (nullable NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options; +- (nullable NSDictionary<NSString *, id> *)dataWithServerTimestampBehavior: + (FIRServerTimestampBehavior)serverTimestampBehavior; /** * Retrieves a specific field from the document. Returns `nil` if the document or the field doesn't @@ -140,14 +118,14 @@ NS_SWIFT_NAME(DocumentSnapshot) * can use `get(_:options:)` to configure this behavior. * * @param field The field to retrieve. - * @param options `SnapshotOptions` to configure how data is returned from the snapshot (e.g. the - * desired behavior for server timestamps that have not yet been set to their final value). + * @param serverTimestampBehavior Configures how server timestamps that have not yet been set to + * their final value are returned from the snapshot. * @return The value contained in the field or `nil` if the document or field doesn't exist. */ // clang-format off - (nullable id)valueForField:(id)field - options:(FIRSnapshotOptions *)options - NS_SWIFT_NAME(get(_:options:)); + serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior + NS_SWIFT_NAME(get(_:serverTimestampBehavior:)); // clang-format on /** @@ -190,11 +168,12 @@ NS_SWIFT_NAME(QueryDocumentSnapshot) /** * Retrieves all fields in the document as a `Dictionary`. * - * @param options `SnapshotOptions` to configure how data is returned from the snapshot (e.g. the - * desired behavior for server timestamps that have not yet been set to their final value). + * @param serverTimestampBehavior Configures how server timestamps that have not yet been set to + * their final value are returned from the snapshot. * @return A `Dictionary` containing all fields in the document. */ -- (NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options; +- (NSDictionary<NSString *, id> *)dataWithServerTimestampBehavior: + (FIRServerTimestampBehavior)serverTimestampBehavior; @end diff --git a/Firestore/Source/Public/FIRQuery.h b/Firestore/Source/Public/FIRQuery.h index ff15ac6..a28af39 100644 --- a/Firestore/Source/Public/FIRQuery.h +++ b/Firestore/Source/Public/FIRQuery.h @@ -25,46 +25,6 @@ NS_ASSUME_NONNULL_BEGIN -/** - * Options for use with `[FIRQuery addSnapshotListener]` to control the behavior of the snapshot - * listener. - */ -NS_SWIFT_NAME(QueryListenOptions) -@interface FIRQueryListenOptions : NSObject - -+ (instancetype)options NS_SWIFT_UNAVAILABLE("Use initializer"); - -- (instancetype)init; - -@property(nonatomic, assign, readonly) BOOL includeQueryMetadataChanges; - -/** - * Sets the includeQueryMetadataChanges option which controls whether metadata-only changes on the - * query (i.e. only `FIRQuerySnapshot.metadata` changed) should trigger snapshot events. Default is - * NO. - * - * @param includeQueryMetadataChanges Whether to raise events for metadata-only changes on the - * query. - * @return The receiver is returned for optional method chaining. - */ -- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - NS_SWIFT_NAME(includeQueryMetadataChanges(_:)); - -@property(nonatomic, assign, readonly) BOOL includeDocumentMetadataChanges; - -/** - * Sets the includeDocumentMetadataChanges option which controls whether document metadata-only - * changes (i.e. only `FIRDocumentSnapshot.metadata` on a document contained in the query - * changed) should trigger snapshot events. Default is NO. - * - * @param includeDocumentMetadataChanges Whether to raise events for document metadata-only changes. - * @return The receiver is returned for optional method chaining. - */ -- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges - NS_SWIFT_NAME(includeDocumentMetadataChanges(_:)); - -@end - typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error); @@ -103,16 +63,17 @@ NS_SWIFT_NAME(Query) /** * Attaches a listener for QuerySnapshot events. * - * @param options Options controlling the listener behavior. + * @param includeMetadataChanges Whether metadata-only changes (i.e. only + * `FIRDocumentSnapshot.metadata` changed) should trigger snapshot events. * @param listener The listener to attach. * * @return A FIRListenerRegistration that can be used to remove this listener. */ // clang-format off -- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions: - (nullable FIRQueryListenOptions *)options - listener:(FIRQuerySnapshotBlock)listener - NS_SWIFT_NAME(addSnapshotListener(options:listener:)); +- (id<FIRListenerRegistration>) +addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges + listener:(FIRQuerySnapshotBlock)listener + NS_SWIFT_NAME(addSnapshotListener(includeMetadataChanges:listener:)); // clang-format on #pragma mark - Filtering Data diff --git a/Firestore/Source/Public/FIRQuerySnapshot.h b/Firestore/Source/Public/FIRQuerySnapshot.h index 1266832..6a7e60d 100644 --- a/Firestore/Source/Public/FIRQuerySnapshot.h +++ b/Firestore/Source/Public/FIRQuerySnapshot.h @@ -58,6 +58,16 @@ NS_SWIFT_NAME(QuerySnapshot) */ @property(nonatomic, strong, readonly) NSArray<FIRDocumentChange *> *documentChanges; +/** + * Returns an array of the documents that changed since the last snapshot. If this is the first + * snapshot, all documents will be in the list as Added changes. + * + * @param includeMetadataChanges Whether metadata-only changes (i.e. only + * `FIRDocumentSnapshot.metadata` changed) should be included. + */ +- (NSArray<FIRDocumentChange *> *)documentChangesWithIncludeMetadataChanges: + (BOOL)includeMetadataChanges NS_SWIFT_NAME(documentChanges(includeMetadataChanges:)); + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_transaction.cc b/Firestore/core/src/firebase/firestore/local/leveldb_transaction.cc index f998550..561d1e2 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_transaction.cc +++ b/Firestore/core/src/firebase/firestore/local/leveldb_transaction.cc @@ -123,7 +123,7 @@ void LevelDbTransaction::Iterator::AdvanceLDB() { void LevelDbTransaction::Iterator::Next() { FIREBASE_ASSERT_MESSAGE(Valid(), "Next() called on invalid iterator"); bool advanced = SyncToTransaction(); - if (!advanced) { + if (!advanced && is_valid_) { if (is_mutation_) { // A mutation might be shadowing leveldb. If so, advance both. if (db_iter_->Valid() && db_iter_->key() == mutations_iter_->first) { diff --git a/Firestore/core/src/firebase/firestore/model/transform_operations.h b/Firestore/core/src/firebase/firestore/model/transform_operations.h index aad5a9b..2943ea0 100644 --- a/Firestore/core/src/firebase/firestore/model/transform_operations.h +++ b/Firestore/core/src/firebase/firestore/model/transform_operations.h @@ -151,6 +151,13 @@ class ArrayTransform : public TransformOperation { } #endif // defined(__OBJC__) + static const std::vector<FSTFieldValue*>& Elements( + const TransformOperation& op) { + FIREBASE_ASSERT(op.type() == Type::ArrayUnion || + op.type() == Type::ArrayRemove); + return static_cast<const ArrayTransform&>(op).elements(); + } + private: Type type_; std::vector<FSTFieldValue*> elements_; |