diff options
Diffstat (limited to 'Firestore/Source/Model/FSTMutation.mm')
-rw-r--r-- | Firestore/Source/Model/FSTMutation.mm | 593 |
1 files changed, 593 insertions, 0 deletions
diff --git a/Firestore/Source/Model/FSTMutation.mm b/Firestore/Source/Model/FSTMutation.mm new file mode 100644 index 0000000..c249138 --- /dev/null +++ b/Firestore/Source/Model/FSTMutation.mm @@ -0,0 +1,593 @@ +/* + * 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 "Firestore/Source/Model/FSTMutation.h" + +#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Core/FSTTimestamp.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/Source/Util/FSTClasses.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTFieldMask + +@implementation FSTFieldMask + +- (instancetype)initWithFields:(NSArray<FSTFieldPath *> *)fields { + if (self = [super init]) { + _fields = fields; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTFieldMask class]]) { + return NO; + } + + FSTFieldMask *otherMask = (FSTFieldMask *)other; + return [self.fields isEqual:otherMask.fields]; +} + +- (NSUInteger)hash { + return self.fields.hash; +} +@end + +#pragma mark - FSTServerTimestampTransform + +@implementation FSTServerTimestampTransform + ++ (instancetype)serverTimestampTransform { + static FSTServerTimestampTransform *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTServerTimestampTransform alloc] init]; + }); + return sharedInstance; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + return [other isKindOfClass:[FSTServerTimestampTransform class]]; +} + +- (NSUInteger)hash { + // arbitrary number since all instances are equal. + return 37; +} + +@end + +#pragma mark - FSTFieldTransform + +@implementation FSTFieldTransform + +- (instancetype)initWithPath:(FSTFieldPath *)path transform:(id<FSTTransformOperation>)transform { + self = [super init]; + if (self) { + _path = path; + _transform = transform; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + FSTFieldTransform *otherFieldTransform = other; + return [self.path isEqual:otherFieldTransform.path] && + [self.transform isEqual:otherFieldTransform.transform]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.path hash]; + hash = hash * 31 + [self.transform hash]; + return hash; +} + +@end + +#pragma mark - FSTPrecondition + +@implementation FSTPrecondition + ++ (FSTPrecondition *)preconditionWithExists:(BOOL)exists { + FSTPreconditionExists existsEnum = exists ? FSTPreconditionExistsYes : FSTPreconditionExistsNo; + return [[FSTPrecondition alloc] initWithUpdateTime:nil exists:existsEnum]; +} + ++ (FSTPrecondition *)preconditionWithUpdateTime:(FSTSnapshotVersion *)updateTime { + return [[FSTPrecondition alloc] initWithUpdateTime:updateTime exists:FSTPreconditionExistsNotSet]; +} + ++ (FSTPrecondition *)none { + static dispatch_once_t onceToken; + static FSTPrecondition *noPrecondition; + dispatch_once(&onceToken, ^{ + noPrecondition = + [[FSTPrecondition alloc] initWithUpdateTime:nil exists:FSTPreconditionExistsNotSet]; + }); + return noPrecondition; +} + +- (instancetype)initWithUpdateTime:(FSTSnapshotVersion *_Nullable)updateTime + exists:(FSTPreconditionExists)exists { + if (self = [super init]) { + _updateTime = updateTime; + _exists = exists; + } + return self; +} + +- (BOOL)isValidForDocument:(FSTMaybeDocument *_Nullable)maybeDoc { + if (self.updateTime) { + return + [maybeDoc isKindOfClass:[FSTDocument class]] && [maybeDoc.version isEqual:self.updateTime]; + } else if (self.exists != FSTPreconditionExistsNotSet) { + if (self.exists == FSTPreconditionExistsYes) { + return [maybeDoc isKindOfClass:[FSTDocument class]]; + } else { + FSTAssert(self.exists == FSTPreconditionExistsNo, @"Invalid precondition"); + return maybeDoc == nil || [maybeDoc isKindOfClass:[FSTDeletedDocument class]]; + } + } else { + FSTAssert(self.isNone, @"Precondition should be empty"); + return YES; + } +} + +- (BOOL)isNone { + return self.updateTime == nil && self.exists == FSTPreconditionExistsNotSet; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + + if (![other isKindOfClass:[FSTPrecondition class]]) { + return NO; + } + + FSTPrecondition *otherPrecondition = (FSTPrecondition *)other; + // Compare references to cover nil equality + return (self.updateTime == otherPrecondition.updateTime || + [self.updateTime isEqual:otherPrecondition.updateTime]) && + self.exists == otherPrecondition.exists; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.updateTime hash]; + hash = hash * 31 + self.exists; + return hash; +} + +- (NSString *)description { + if (self.isNone) { + return @"<FSTPrecondition <none>>"; + } else { + NSString *existsString; + switch (self.exists) { + case FSTPreconditionExistsYes: + existsString = @"yes"; + break; + case FSTPreconditionExistsNo: + existsString = @"no"; + break; + default: + existsString = @"<not-set>"; + break; + } + return [NSString stringWithFormat:@"<FSTPrecondition updateTime=%@ exists=%@>", self.updateTime, + existsString]; + } +} + +@end + +#pragma mark - FSTMutationResult + +@implementation FSTMutationResult + +- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version + transformResults:(NSArray<FSTFieldValue *> *_Nullable)transformResults { + if (self = [super init]) { + _version = version; + _transformResults = transformResults; + } + return self; +} + +@end + +#pragma mark - FSTMutation + +@implementation FSTMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key precondition:(FSTPrecondition *)precondition { + if (self = [super init]) { + _key = key; + _precondition = precondition; + } + return self; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(nullable FSTTimestamp *)localWriteTime { + return + [self applyTo:maybeDoc baseDocument:baseDoc localWriteTime:localWriteTime mutationResult:nil]; +} + +@end + +#pragma mark - FSTSetMutation + +@implementation FSTSetMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition { + if (self = [super initWithKey:key precondition:precondition]) { + _value = value; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTSetMutation key=%@ value=%@ precondition=%@>", self.key, + self.value, self.precondition]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTSetMutation class]]) { + return NO; + } + + FSTSetMutation *otherMutation = (FSTSetMutation *)other; + return [self.key isEqual:otherMutation.key] && [self.value isEqual:otherMutation.value] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.value hash]; + return result; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTSetMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + BOOL hasLocalMutations = (mutationResult == nil); + if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { + // If the document didn't exist before, create it. + return [FSTDocument documentWithData:self.value + key:self.key + version:[FSTSnapshotVersion noVersion] + hasLocalMutations:hasLocalMutations]; + } + + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only set a document with the same key"); + return [FSTDocument documentWithData:self.value + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} +@end + +#pragma mark - FSTPatchMutation + +@implementation FSTPatchMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldMask:(FSTFieldMask *)fieldMask + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition { + self = [super initWithKey:key precondition:precondition]; + if (self) { + _fieldMask = fieldMask; + _value = value; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTPatchMutation class]]) { + return NO; + } + + FSTPatchMutation *otherMutation = (FSTPatchMutation *)other; + return [self.key isEqual:otherMutation.key] && [self.fieldMask isEqual:otherMutation.fieldMask] && + [self.value isEqual:otherMutation.value] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.fieldMask hash]; + result = 31 * result + [self.value hash]; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTPatchMutation key=%@ mask=%@ value=%@ precondition=%@>", + self.key, self.fieldMask, self.value, self.precondition]; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTPatchMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + BOOL hasLocalMutations = (mutationResult == nil); + if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { + // Precondition applied, so create the document if necessary + FSTDocumentKey *key = maybeDoc ? maybeDoc.key : self.key; + FSTSnapshotVersion *version = maybeDoc ? maybeDoc.version : [FSTSnapshotVersion noVersion]; + maybeDoc = [FSTDocument documentWithData:[FSTObjectValue objectValue] + key:key + version:version + hasLocalMutations:hasLocalMutations]; + } + + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); + + FSTObjectValue *newData = [self patchObjectValue:doc.data]; + return [FSTDocument documentWithData:newData + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} + +- (FSTObjectValue *)patchObjectValue:(FSTObjectValue *)objectValue { + FSTObjectValue *result = objectValue; + for (FSTFieldPath *fieldPath in self.fieldMask.fields) { + FSTFieldValue *newValue = [self.value valueForPath:fieldPath]; + if (newValue) { + result = [result objectBySettingValue:newValue forPath:fieldPath]; + } else { + result = [result objectByDeletingPath:fieldPath]; + } + } + return result; +} + +@end + +@implementation FSTTransformMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms { + // NOTE: We set a precondition of exists: true as a safety-check, since we always combine + // FSTTransformMutations with a FSTSetMutation or FSTPatchMutation which (if successful) should + // end up with an existing document. + if (self = [super initWithKey:key precondition:[FSTPrecondition preconditionWithExists:YES]]) { + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTTransformMutation class]]) { + return NO; + } + + FSTTransformMutation *otherMutation = (FSTTransformMutation *)other; + return [self.key isEqual:otherMutation.key] && + [self.fieldTransforms isEqual:otherMutation.fieldTransforms] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.fieldTransforms hash]; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTTransformMutation key=%@ transforms=%@ precondition=%@>", + self.key, self.fieldTransforms, self.precondition]; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + if (mutationResult) { + FSTAssert(mutationResult.transformResults, + @"Transform results missing for FSTTransformMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + // We only support transforms with precondition exists, so we can only apply it to an existing + // document + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); + + BOOL hasLocalMutations = (mutationResult == nil); + NSArray<FSTFieldValue *> *transformResults = + mutationResult + ? mutationResult.transformResults + : [self localTransformResultsWithBaseDocument:baseDoc writeTime:localWriteTime]; + FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults]; + return [FSTDocument documentWithData:newData + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} + +/** + * 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. + * @param localWriteTime The local time of the transform mutation (used to generate + * FSTServerTimestampValues). + * @return The transform results array. + */ +- (NSArray<FSTFieldValue *> *)localTransformResultsWithBaseDocument: + (FSTMaybeDocument *_Nullable)baseDocument + writeTime:(FSTTimestamp *)localWriteTime { + NSMutableArray<FSTFieldValue *> *transformResults = [NSMutableArray array]; + for (FSTFieldTransform *fieldTransform in self.fieldTransforms) { + if ([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]]) { + FSTFieldValue *previousValue = nil; + + if ([baseDocument isMemberOfClass:[FSTDocument class]]) { + previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path]; + } + + [transformResults + addObject:[FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:localWriteTime + previousValue:previousValue]]; + } else { + FSTFail(@"Encountered unknown transform: %@", fieldTransform); + } + } + return transformResults; +} + +- (FSTObjectValue *)transformObject:(FSTObjectValue *)objectValue + transformResults:(NSArray<FSTFieldValue *> *)transformResults { + FSTAssert(transformResults.count == self.fieldTransforms.count, + @"Transform results length mismatch."); + + for (NSUInteger i = 0; i < self.fieldTransforms.count; i++) { + FSTFieldTransform *fieldTransform = self.fieldTransforms[i]; + id<FSTTransformOperation> transform = fieldTransform.transform; + FSTFieldPath *fieldPath = fieldTransform.path; + if ([transform isKindOfClass:[FSTServerTimestampTransform class]]) { + objectValue = [objectValue objectBySettingValue:transformResults[i] forPath:fieldPath]; + } else { + FSTFail(@"Encountered unknown transform: %@", transform); + } + } + return objectValue; +} + +@end + +#pragma mark - FSTDeleteMutation + +@implementation FSTDeleteMutation + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDeleteMutation class]]) { + return NO; + } + + FSTDeleteMutation *otherMutation = (FSTDeleteMutation *)other; + return [self.key isEqual:otherMutation.key] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + return result; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"<FSTDeleteMutation key=%@ precondition=%@>", self.key, self.precondition]; +} + +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(nullable FSTMutationResult *)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, + @"Transform results received by FSTDeleteMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + if (maybeDoc) { + FSTAssert([maybeDoc.key isEqual:self.key], @"Can only delete a document with the same key"); + } + + return [FSTDeletedDocument documentWithKey:self.key version:[FSTSnapshotVersion noVersion]]; +} + +@end + +NS_ASSUME_NONNULL_END |