aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Source/Core/FSTTransaction.m
diff options
context:
space:
mode:
Diffstat (limited to 'Firestore/Source/Core/FSTTransaction.m')
-rw-r--r--Firestore/Source/Core/FSTTransaction.m250
1 files changed, 250 insertions, 0 deletions
diff --git a/Firestore/Source/Core/FSTTransaction.m b/Firestore/Source/Core/FSTTransaction.m
new file mode 100644
index 0000000..26c69e0
--- /dev/null
+++ b/Firestore/Source/Core/FSTTransaction.m
@@ -0,0 +1,250 @@
+/*
+ * 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 "FSTTransaction.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+#import "FIRFirestoreErrors.h"
+#import "FIRSetOptions.h"
+#import "FSTAssert.h"
+#import "FSTDatastore.h"
+#import "FSTDocument.h"
+#import "FSTDocumentKey.h"
+#import "FSTDocumentKeySet.h"
+#import "FSTMutation.h"
+#import "FSTSnapshotVersion.h"
+#import "FSTUsageValidation.h"
+#import "FSTUserDataConverter.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTTransaction
+
+@interface FSTTransaction ()
+@property(nonatomic, strong, readonly) FSTDatastore *datastore;
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTDocumentKey *, FSTSnapshotVersion *> *readVersions;
+@property(nonatomic, strong, readonly) NSMutableArray *mutations;
+@property(nonatomic, assign) BOOL commitCalled;
+/**
+ * An error that may have occurred as a consequence of a write. If set, needs to be raised in the
+ * completion handler instead of trying to commit.
+ */
+@property(nonatomic, strong, nullable) NSError *lastWriteError;
+@end
+
+@implementation FSTTransaction
+
++ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore {
+ return [[FSTTransaction alloc] initWithDatastore:datastore];
+}
+
+- (instancetype)initWithDatastore:(FSTDatastore *)datastore {
+ self = [super init];
+ if (self) {
+ _datastore = datastore;
+ _readVersions = [NSMutableDictionary dictionary];
+ _mutations = [NSMutableArray array];
+ _commitCalled = NO;
+ }
+ return self;
+}
+
+/**
+ * Every time a document is read, this should be called to record its version. If we read two
+ * different versions of the same document, this will return an error through its out parameter.
+ * When the transaction is committed, the versions recorded will be set as preconditions on the
+ * writes sent to the backend.
+ */
+- (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error {
+ FSTAssert(error != nil, @"nil error parameter");
+ *error = nil;
+ FSTSnapshotVersion *docVersion = doc.version;
+ if ([doc isKindOfClass:[FSTDeletedDocument class]]) {
+ // For deleted docs, we must record an explicit no version to build the right precondition
+ // when writing.
+ docVersion = [FSTSnapshotVersion noVersion];
+ }
+ FSTSnapshotVersion *existingVersion = self.readVersions[doc.key];
+ if (existingVersion) {
+ if (error) {
+ *error =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeFailedPrecondition
+ userInfo:@{
+ NSLocalizedDescriptionKey :
+ @"A document cannot be read twice within a single transaction."
+ }];
+ }
+ return NO;
+ } else {
+ self.readVersions[doc.key] = docVersion;
+ return YES;
+ }
+}
+
+- (void)lookupDocumentsForKeys:(NSArray<FSTDocumentKey *> *)keys
+ completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion {
+ [self ensureCommitNotCalled];
+ if (self.mutations.count) {
+ FSTThrowInvalidUsage(@"FIRIllegalStateException",
+ @"All reads in a transaction must be done before any writes.");
+ }
+ [self.datastore
+ lookupDocuments:keys
+ completion:^(NSArray<FSTDocument *> *_Nullable documents, NSError *_Nullable error) {
+ if (error) {
+ completion(nil, error);
+ return;
+ }
+ for (FSTMaybeDocument *doc in documents) {
+ NSError *recordError = nil;
+ if (![self recordVersionForDocument:doc error:&recordError]) {
+ completion(nil, recordError);
+ return;
+ }
+ }
+ completion(documents, nil);
+ }];
+}
+
+/** Stores mutations to be written when commitWithCompletion is called. */
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
+ [self ensureCommitNotCalled];
+ [self.mutations addObjectsFromArray:mutations];
+}
+
+/**
+ * Returns version of this doc when it was read in this transaction as a precondition, or no
+ * precondition if it was not read.
+ */
+- (FSTPrecondition *)preconditionForDocumentKey:(FSTDocumentKey *)key {
+ FSTSnapshotVersion *_Nullable snapshotVersion = self.readVersions[key];
+ if (snapshotVersion) {
+ return [FSTPrecondition preconditionWithUpdateTime:snapshotVersion];
+ } else {
+ return [FSTPrecondition none];
+ }
+}
+
+/**
+ * Returns the precondition for a document if the operation is an update, based on the provided
+ * UpdateOptions. Will return nil if an error occurred, in which case it sets the error parameter.
+ */
+- (nullable FSTPrecondition *)preconditionForUpdateWithDocumentKey:(FSTDocumentKey *)key
+ error:(NSError **)error {
+ FSTSnapshotVersion *_Nullable version = self.readVersions[key];
+ if (version && [version isEqual:[FSTSnapshotVersion noVersion]]) {
+ // The document was read, but doesn't exist.
+ // Return an error because the precondition is impossible
+ if (error) {
+ *error = [NSError
+ errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeAborted
+ userInfo:@{
+ NSLocalizedDescriptionKey : @"Can't update a document that doesn't exist."
+ }];
+ }
+ return nil;
+ } else if (version) {
+ // Document exists, just base precondition on document update time.
+ return [FSTPrecondition preconditionWithUpdateTime:version];
+ } else {
+ // Document was not read, so we just use the preconditions for an update.
+ return [FSTPrecondition preconditionWithExists:YES];
+ }
+}
+
+- (void)setData:(FSTParsedSetData *)data forDocument:(FSTDocumentKey *)key {
+ [self writeMutations:[data mutationsWithKey:key
+ precondition:[self preconditionForDocumentKey:key]]];
+}
+
+- (void)updateData:(FSTParsedUpdateData *)data forDocument:(FSTDocumentKey *)key {
+ NSError *error = nil;
+ FSTPrecondition *_Nullable precondition =
+ [self preconditionForUpdateWithDocumentKey:key error:&error];
+ if (precondition) {
+ [self writeMutations:[data mutationsWithKey:key precondition:precondition]];
+ } else {
+ FSTAssert(error, @"Got nil precondition, but error was not set");
+ self.lastWriteError = error;
+ }
+}
+
+- (void)deleteDocument:(FSTDocumentKey *)key {
+ [self writeMutations:@[ [[FSTDeleteMutation alloc]
+ initWithKey:key
+ precondition:[self preconditionForDocumentKey:key]] ]];
+ // Since the delete will be applied before all following writes, we need to ensure that the
+ // precondition for the next write will be exists: false.
+ self.readVersions[key] = [FSTSnapshotVersion noVersion];
+}
+
+- (void)commitWithCompletion:(FSTVoidErrorBlock)completion {
+ [self ensureCommitNotCalled];
+ // Once commitWithCompletion is called once, mark this object so it can't be used again.
+ self.commitCalled = YES;
+
+ // If there was an error writing, raise that error now
+ if (self.lastWriteError) {
+ completion(self.lastWriteError);
+ return;
+ }
+
+ // Make a list of read documents that haven't been written.
+ __block FSTDocumentKeySet *unwritten = [FSTDocumentKeySet keySet];
+ [self.readVersions enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key,
+ FSTSnapshotVersion *version, BOOL *stop) {
+ unwritten = [unwritten setByAddingObject:key];
+ }];
+ // For each mutation, note that the doc was written.
+ for (FSTMutation *mutation in self.mutations) {
+ unwritten = [unwritten setByRemovingObject:mutation.key];
+ }
+ if (unwritten.count) {
+ // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend.
+ completion([NSError
+ errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeFailedPrecondition
+ userInfo:@{
+ NSLocalizedDescriptionKey : @"Every document read in a transaction must also be "
+ @"written in that transaction."
+ }]);
+ } else {
+ [self.datastore commitMutations:self.mutations
+ completion:^(NSError *_Nullable error) {
+ if (error) {
+ completion(error);
+ } else {
+ completion(nil);
+ }
+ }];
+ }
+}
+
+- (void)ensureCommitNotCalled {
+ if (self.commitCalled) {
+ FSTThrowInvalidUsage(
+ @"FIRIllegalStateException",
+ @"A transaction object cannot be used after its update block has completed.");
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END