aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore
diff options
context:
space:
mode:
Diffstat (limited to 'Firestore')
-rw-r--r--Firestore/CHANGELOG.md14
-rw-r--r--Firestore/Example/SwiftBuildTest/main.swift25
-rw-r--r--Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm83
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRQueryTests.mm26
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm58
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm15
-rw-r--r--Firestore/Example/Tests/Local/FSTLocalStoreTests.mm135
-rw-r--r--Firestore/Example/Tests/Model/FSTMutationTests.mm144
-rw-r--r--Firestore/Source/API/FIRDocumentChange+Internal.h1
-rw-r--r--Firestore/Source/API/FIRDocumentChange.mm25
-rw-r--r--Firestore/Source/API/FIRDocumentSnapshot.mm57
-rw-r--r--Firestore/Source/API/FIRQuery.mm62
-rw-r--r--Firestore/Source/API/FIRQuerySnapshot.mm15
-rw-r--r--Firestore/Source/API/FIRSnapshotOptions+Internal.h38
-rw-r--r--Firestore/Source/API/FIRSnapshotOptions.mm72
-rw-r--r--Firestore/Source/Model/FSTFieldValue.h5
-rw-r--r--Firestore/Source/Model/FSTFieldValue.mm24
-rw-r--r--Firestore/Source/Model/FSTMutation.mm144
-rw-r--r--Firestore/Source/Public/FIRDocumentSnapshot.h49
-rw-r--r--Firestore/Source/Public/FIRQuery.h51
-rw-r--r--Firestore/Source/Public/FIRQuerySnapshot.h10
-rw-r--r--Firestore/core/src/firebase/firestore/local/leveldb_transaction.cc2
-rw-r--r--Firestore/core/src/firebase/firestore/model/transform_operations.h7
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_;