Skip to content

Commit b082bdd

Browse files
Allowing field deletes with merge-enabled sets
1 parent 4ed1221 commit b082bdd

File tree

7 files changed

+96
-36
lines changed

7 files changed

+96
-36
lines changed

Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,40 @@ - (void)testCanMergeServerTimestamps {
176176
XCTAssertTrue([document[@"time"] isKindOfClass:[NSDate class]]);
177177
}
178178

179+
- (void)testCanDeleteFieldUsingMerge {
180+
FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
181+
182+
NSDictionary<NSString *, id> *initialData = @{
183+
@"untouched" : @YES,
184+
@"foo" : @"bar",
185+
@"nested" : @{@"untouched" : @YES, @"foo" : @"bar"}
186+
};
187+
NSDictionary<NSString *, id> *mergeData = @{
188+
@"foo" : [FIRFieldValue fieldValueForDelete],
189+
@"nested" : @{@"foo" : [FIRFieldValue fieldValueForDelete]}
190+
};
191+
192+
[self writeDocumentRef:doc data:initialData];
193+
194+
XCTestExpectation *completed =
195+
[self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"];
196+
197+
[doc setData:mergeData
198+
options:[FIRSetOptions merge]
199+
completion:^(NSError *error) {
200+
XCTAssertNil(error);
201+
[completed fulfill];
202+
}];
203+
204+
[self awaitExpectations];
205+
206+
FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
207+
XCTAssertEqual(document[@"untouched"], @YES);
208+
XCTAssertNil(document[@"foo"]);
209+
XCTAssertEqual(document[@"nested.untouched"], @YES);
210+
XCTAssertNil(document[@"nested.foo"]);
211+
}
212+
179213
- (void)testMergeReplacesArrays {
180214
FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
181215

Firestore/Example/Tests/Integration/API/FIRValidationTests.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,9 @@ - (void)testWritesWithReservedFieldsFail {
280280

281281
- (void)testSetsWithFieldValueDeleteFail {
282282
[self expectSet:@{@"foo" : [FIRFieldValue fieldValueForDelete]}
283-
toFailWithReason:@"FieldValue.delete() can only be used with updateData()."];
283+
toFailWithReason:
284+
@"FieldValue.delete() can only be used with updateData() and setData() with "
285+
@"SetOptions.merge()."];
284286
}
285287

286288
- (void)testUpdatesWithNestedFieldValueDeleteFail {

Firestore/Source/API/FIRDocumentReference.m

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,9 @@ - (void)setData:(NSDictionary<NSString *, id> *)documentData
174174
- (void)setData:(NSDictionary<NSString *, id> *)documentData
175175
options:(FIRSetOptions *)options
176176
completion:(nullable void (^)(NSError *_Nullable error))completion {
177-
FSTParsedSetData *parsed =
178-
[self.firestore.dataConverter parsedSetData:documentData options:options];
177+
FSTParsedSetData *parsed = options.isMerge
178+
? [self.firestore.dataConverter parsedMergeData:documentData]
179+
: [self.firestore.dataConverter parsedSetData:documentData];
179180
return [self.firestore.client
180181
writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]]
181182
completion:completion];

Firestore/Source/API/FIRTransaction.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ - (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data
6969
forDocument:(FIRDocumentReference *)document
7070
options:(FIRSetOptions *)options {
7171
[self validateReference:document];
72-
FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options];
72+
FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data]
73+
: [self.firestore.dataConverter parsedSetData:data];
7374
[self.internalTransaction setData:parsed forDocument:document.key];
7475
return self;
7576
}

Firestore/Source/API/FIRWriteBatch.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ - (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data
6767
options:(FIRSetOptions *)options {
6868
[self verifyNotCommitted];
6969
[self validateReference:document];
70-
FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options];
70+
FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:data]
71+
: [self.firestore.dataConverter parsedSetData:data];
7172
[self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key
7273
precondition:[FSTPrecondition none]]];
7374
return self;

Firestore/Source/API/FSTUserDataConverter.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,13 @@ typedef id _Nullable (^FSTPreConverterBlock)(id _Nullable);
110110
- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID
111111
preConverter:(FSTPreConverterBlock)preConverter NS_DESIGNATED_INITIALIZER;
112112

113-
/** Parse document data (e.g. from a setData call). */
114-
- (FSTParsedSetData *)parsedSetData:(id)input options:(FIRSetOptions *)options;
113+
/** Parse document data from a non-merge setData call.*/
114+
- (FSTParsedSetData *)parsedMergeData:(id)input;
115115

116-
/** Parse "update" data (i.e. from an updateData call). */
116+
/** Parse document data from a setData call with '[FIRSetOptions merge]'. */
117+
- (FSTParsedSetData *)parsedSetData:(id)input;
118+
119+
/** Parse update data from an updateData call. */
117120
- (FSTParsedUpdateData *)parsedUpdateData:(id)input;
118121

119122
/** Parse a "query value" (e.g. value in a where filter or a value in a cursor bound). */

Firestore/Source/API/FSTUserDataConverter.m

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ - (instancetype)initWithData:(FSTObjectValue *)data
109109
*/
110110
typedef NS_ENUM(NSInteger, FSTUserDataSource) {
111111
FSTUserDataSourceSet,
112+
FSTUserDataSourceMergeSet,
112113
FSTUserDataSourceUpdate,
113114
FSTUserDataSourceQueryValue, // from a where clause or cursor bound.
114115
};
@@ -158,6 +159,9 @@ - (instancetype)initWithSource:(FSTUserDataSource)dataSource
158159
- (instancetype)contextForField:(NSString *)fieldName;
159160
- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath;
160161
- (instancetype)contextForArrayIndex:(NSUInteger)index;
162+
163+
/** Returns true for the non-query parse contexts (Set, MergeSet and Update) */
164+
- (BOOL)isWrite;
161165
@end
162166

163167
@implementation FSTParseContext
@@ -230,10 +234,6 @@ - (NSString *)fieldDescription {
230234
}
231235
}
232236

233-
- (BOOL)isWrite {
234-
return _dataSource == FSTUserDataSourceSet || _dataSource == FSTUserDataSourceUpdate;
235-
}
236-
237237
- (void)validatePath {
238238
// TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays.
239239
if (self.path == nil) {
@@ -244,6 +244,19 @@ - (void)validatePath {
244244
}
245245
}
246246

247+
- (BOOL)isWrite {
248+
switch (self.dataSource) {
249+
case FSTUserDataSourceSet: // Falls through.
250+
case FSTUserDataSourceMergeSet: // Falls through.
251+
case FSTUserDataSourceUpdate:
252+
return YES;
253+
case FSTUserDataSourceQueryValue:
254+
return NO;
255+
default:
256+
FSTThrowInvalidArgument(@"Unexpected case for FSTUserDataSource: %d", self.dataSource);
257+
}
258+
}
259+
247260
- (void)validatePathSegment:(NSString *)segment {
248261
if ([self isWrite] && [segment hasPrefix:RESERVED_FIELD_DESIGNATOR] &&
249262
[segment hasSuffix:RESERVED_FIELD_DESIGNATOR]) {
@@ -288,35 +301,37 @@ - (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID
288301
return self;
289302
}
290303

291-
- (FSTParsedSetData *)parsedSetData:(id)input options:(FIRSetOptions *)options {
304+
- (FSTParsedSetData *)parsedMergeData:(id)input {
292305
// NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust
293306
// Obj-C to verify the type for us.
294307
if (![input isKindOfClass:[NSDictionary class]]) {
295308
FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary.");
296309
}
297310

298311
FSTParseContext *context =
299-
[FSTParseContext contextWithSource:FSTUserDataSourceSet path:[FSTFieldPath emptyPath]];
300-
301-
__block FSTObjectValue *updateData = [FSTObjectValue objectValue];
312+
[FSTParseContext contextWithSource:FSTUserDataSourceMergeSet path:[FSTFieldPath emptyPath]];
313+
FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context];
302314

303-
[input enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
304-
// Treat key as a complete field name (don't split on dots, etc.)
305-
FSTFieldPath *path = [[FIRFieldPath alloc] initWithFields:@[ key ]].internalValue;
315+
return
316+
[[FSTParsedSetData alloc] initWithData:updateData
317+
fieldMask:[[FSTFieldMask alloc] initWithFields:context.fieldMask]
318+
fieldTransforms:context.fieldTransforms];
319+
}
306320

307-
value = self.preConverter(value);
321+
- (FSTParsedSetData *)parsedSetData:(id)input {
322+
// NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust
323+
// Obj-C to verify the type for us.
324+
if (![input isKindOfClass:[NSDictionary class]]) {
325+
FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary.");
326+
}
308327

309-
FSTFieldValue *_Nullable parsedValue =
310-
[self parseData:value context:[context contextForFieldPath:path]];
311-
if (parsedValue) {
312-
updateData = [updateData objectBySettingValue:parsedValue forPath:path];
313-
}
314-
}];
328+
FSTParseContext *context =
329+
[FSTParseContext contextWithSource:FSTUserDataSourceSet path:[FSTFieldPath emptyPath]];
330+
FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context];
315331

316-
return [[FSTParsedSetData alloc]
317-
initWithData:updateData
318-
fieldMask:options.merge ? [[FSTFieldMask alloc] initWithFields:context.fieldMask] : nil
319-
fieldTransforms:context.fieldTransforms];
332+
return [[FSTParsedSetData alloc] initWithData:updateData
333+
fieldMask:nil
334+
fieldTransforms:context.fieldTransforms];
320335
}
321336

322337
- (FSTParsedUpdateData *)parsedUpdateData:(id)input {
@@ -536,20 +551,23 @@ - (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTPars
536551

537552
} else if ([input isKindOfClass:[FIRFieldValue class]]) {
538553
if ([input isKindOfClass:[FSTDeleteFieldValue class]]) {
539-
// We shouldn't encounter delete sentinels here. Provide a good error.
540-
if (context.dataSource != FSTUserDataSourceUpdate) {
541-
FSTThrowInvalidArgument(@"FieldValue.delete() can only be used with updateData().");
542-
} else {
554+
if (context.dataSource == FSTUserDataSourceMergeSet) {
555+
return nil;
556+
} else if (context.dataSource == FSTUserDataSourceUpdate) {
543557
FSTAssert(context.path.length > 0,
544558
@"FieldValue.delete() at the top level should have already been handled.");
545559
FSTThrowInvalidArgument(
546560
@"FieldValue.delete() can only appear at the top level of your "
547561
"update data%@",
548562
[context fieldDescription]);
563+
} else {
564+
// We shouldn't encounter delete sentinels for queries or non-merge setData calls.
565+
FSTThrowInvalidArgument(
566+
@"FieldValue.delete() can only be used with updateData() and setData() with "
567+
@"SetOptions.merge().");
549568
}
550569
} else if ([input isKindOfClass:[FSTServerTimestampFieldValue class]]) {
551-
if (context.dataSource != FSTUserDataSourceSet &&
552-
context.dataSource != FSTUserDataSourceUpdate) {
570+
if (![context isWrite]) {
553571
FSTThrowInvalidArgument(
554572
@"FieldValue.serverTimestamp() can only be used with setData() and updateData().");
555573
}

0 commit comments

Comments
 (0)