Skip to content

Commit 2fa771a

Browse files
Drop SortedSet from FieldMask
1 parent 04d4f97 commit 2fa771a

File tree

8 files changed

+79
-56
lines changed

8 files changed

+79
-56
lines changed

packages/firestore/src/api/user_data_reader.ts

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,10 @@ export class UserDataReader {
337337
let fieldTransforms: FieldTransform[];
338338

339339
if (!fieldPaths) {
340-
fieldMask = FieldMask.fromArray(context.fieldMask);
340+
fieldMask = new FieldMask(context.fieldMask);
341341
fieldTransforms = context.fieldTransforms;
342342
} else {
343-
let validatedFieldPaths = new SortedSet<FieldPath>(FieldPath.comparator);
343+
const validatedFieldPaths: FieldPath[] = [];
344344

345345
for (const stringOrFieldPath of fieldPaths) {
346346
let fieldPath: FieldPath;
@@ -365,10 +365,12 @@ export class UserDataReader {
365365
);
366366
}
367367

368-
validatedFieldPaths = validatedFieldPaths.add(fieldPath);
368+
if (!fieldMaskContains(validatedFieldPaths, fieldPath)) {
369+
validatedFieldPaths.push(fieldPath);
370+
}
369371
}
370372

371-
fieldMask = FieldMask.fromSet(validatedFieldPaths);
373+
fieldMask = new FieldMask(validatedFieldPaths);
372374
fieldTransforms = context.fieldTransforms.filter(transform =>
373375
fieldMask.covers(transform.field)
374376
);
@@ -389,7 +391,7 @@ export class UserDataReader {
389391
);
390392
validatePlainObject('Data must be an object, but it was:', context, input);
391393

392-
let fieldMaskPaths = new SortedSet<FieldPath>(FieldPath.comparator);
394+
const fieldMaskPaths: FieldPath[] = [];
393395
const updateData = new ObjectValueBuilder();
394396
forEach(input as Dict<unknown>, (key, value) => {
395397
const path = fieldPathFromDotSeparatedString(methodName, key);
@@ -398,17 +400,17 @@ export class UserDataReader {
398400
value = this.runPreConverter(value, childContext);
399401
if (value instanceof DeleteFieldValueImpl) {
400402
// Add it to the field mask, but don't add anything to updateData.
401-
fieldMaskPaths = fieldMaskPaths.add(path);
403+
fieldMaskPaths.push(path);
402404
} else {
403405
const parsedValue = this.parseData(value, childContext);
404406
if (parsedValue != null) {
405-
fieldMaskPaths = fieldMaskPaths.add(path);
407+
fieldMaskPaths.push(path);
406408
updateData.set(path, parsedValue);
407409
}
408410
}
409411
});
410412

411-
const mask = FieldMask.fromSet(fieldMaskPaths);
413+
const mask = new FieldMask(fieldMaskPaths);
412414
return new ParsedUpdateData(
413415
updateData.build(),
414416
mask,
@@ -449,26 +451,30 @@ export class UserDataReader {
449451
values.push(moreFieldsAndValues[i + 1]);
450452
}
451453

452-
let fieldMaskPaths = new SortedSet<FieldPath>(FieldPath.comparator);
454+
const fieldMaskPaths: FieldPath[] = [];
453455
const updateData = new ObjectValueBuilder();
454456

455-
for (let i = 0; i < keys.length; ++i) {
456-
const path = keys[i];
457-
const childContext = context.childContextForFieldPath(path);
458-
const value = this.runPreConverter(values[i], childContext);
459-
if (value instanceof DeleteFieldValueImpl) {
460-
// Add it to the field mask, but don't add anything to updateData.
461-
fieldMaskPaths = fieldMaskPaths.add(path);
462-
} else {
463-
const parsedValue = this.parseData(value, childContext);
464-
if (parsedValue != null) {
465-
fieldMaskPaths = fieldMaskPaths.add(path);
466-
updateData.set(path, parsedValue);
457+
// We iterate in reverse order to pick the last value for a field if the
458+
// user specified the field multiple times.
459+
for (let i = keys.length - 1; i >= 0; --i) {
460+
if (!fieldMaskContains(fieldMaskPaths, keys[i])) {
461+
const path = keys[i];
462+
const childContext = context.childContextForFieldPath(path);
463+
const value = this.runPreConverter(values[i], childContext);
464+
if (value instanceof DeleteFieldValueImpl) {
465+
// Add it to the field mask, but don't add anything to updateData.
466+
fieldMaskPaths.push(path);
467+
} else {
468+
const parsedValue = this.parseData(value, childContext);
469+
if (parsedValue != null) {
470+
fieldMaskPaths.push(path);
471+
updateData.set(path, parsedValue);
472+
}
467473
}
468474
}
469475
}
470476

471-
const mask = FieldMask.fromSet(fieldMaskPaths);
477+
const mask = new FieldMask(fieldMaskPaths);
472478
return new ParsedUpdateData(
473479
updateData.build(),
474480
mask,
@@ -841,3 +847,8 @@ function fieldPathFromDotSeparatedString(
841847
function errorMessage(error: Error | object): string {
842848
return error instanceof Error ? error.message : error.toString();
843849
}
850+
851+
/** Checks `haystack` if FieldPath `needle` is present. Runs in O(n). */
852+
function fieldMaskContains(haystack: FieldPath[], needle: FieldPath): boolean {
853+
return haystack.some(v => v.isEqual(needle));
854+
}

packages/firestore/src/model/mutation.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,14 @@ import { arrayEquals } from '../util/misc';
4545
* containing foo
4646
*/
4747
export class FieldMask {
48-
constructor(readonly fields: SortedSet<FieldPath>) {
48+
constructor(readonly fields: FieldPath[]) {
4949
// TODO(dimond): validation of FieldMask
50-
}
51-
52-
static fromSet(fields: SortedSet<FieldPath>): FieldMask {
53-
return new FieldMask(fields);
54-
}
55-
56-
static fromArray(fields: FieldPath[]): FieldMask {
57-
let fieldsAsSet = new SortedSet<FieldPath>(FieldPath.comparator);
58-
fields.forEach(fieldPath => (fieldsAsSet = fieldsAsSet.add(fieldPath)));
59-
return new FieldMask(fieldsAsSet);
50+
// Sort the field mask to support `FieldMask.isEqual()` and assert below.
51+
fields.sort(FieldPath.comparator);
52+
debugAssert(
53+
!fields.some((v, i) => i !== 0 && v.isEqual(fields[i - 1])),
54+
'FieldMask contains fields that are not unique'
55+
);
6056
}
6157

6258
/**
@@ -66,17 +62,16 @@ export class FieldMask {
6662
* This is an O(n) operation, where `n` is the size of the field mask.
6763
*/
6864
covers(fieldPath: FieldPath): boolean {
69-
let found = false;
70-
this.fields.forEach(fieldMaskPath => {
65+
for (const fieldMaskPath of this.fields) {
7166
if (fieldMaskPath.isPrefixOf(fieldPath)) {
72-
found = true;
67+
return true;
7368
}
74-
});
75-
return found;
69+
}
70+
return false;
7671
}
7772

7873
isEqual(other: FieldMask): boolean {
79-
return this.fields.isEqual(other.fields);
74+
return arrayEquals(this.fields, other.fields, (l, r) => l.isEqual(r));
8075
}
8176
}
8277

packages/firestore/src/model/object_value.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,27 +243,27 @@ export class ObjectValueBuilder {
243243
* Returns a FieldMask built from all fields in a MapValue.
244244
*/
245245
export function extractFieldMask(value: api.MapValue): FieldMask {
246-
let fields = new SortedSet<FieldPath>(FieldPath.comparator);
246+
const fields: FieldPath[] = [];
247247
forEach(value!.fields || {}, (key, value) => {
248248
const currentPath = new FieldPath([key]);
249249
if (isMapValue(value)) {
250250
const nestedMask = extractFieldMask(value.mapValue!);
251251
const nestedFields = nestedMask.fields;
252-
if (nestedFields.isEmpty()) {
252+
if (nestedFields.length === 0) {
253253
// Preserve the empty map by adding it to the FieldMask.
254-
fields = fields.add(currentPath);
254+
fields.push(currentPath);
255255
} else {
256256
// For nested and non-empty ObjectValues, add the FieldPath of the
257257
// leaf nodes.
258-
nestedFields.forEach(nestedPath => {
259-
fields = fields.add(currentPath.child(nestedPath));
260-
});
258+
for (const nestedPath of nestedFields) {
259+
fields.push(currentPath.child(nestedPath));
260+
}
261261
}
262262
} else {
263263
// For nested and non-empty ObjectValues, add the FieldPath of the leaf
264264
// nodes.
265-
fields = fields.add(currentPath);
265+
fields.push(currentPath);
266266
}
267267
});
268-
return FieldMask.fromSet(fields);
268+
return new FieldMask(fields);
269269
}

packages/firestore/src/remote/serializer.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,8 +1086,7 @@ export class JsonProtoSerializer {
10861086

10871087
fromDocumentMask(proto: api.DocumentMask): FieldMask {
10881088
const paths = proto.fieldPaths || [];
1089-
const fields = paths.map(path => FieldPath.fromServerFormat(path));
1090-
return FieldMask.fromArray(fields);
1089+
return new FieldMask(paths.map(path => FieldPath.fromServerFormat(path)));
10911090
}
10921091
}
10931092

packages/firestore/test/integration/api/database.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,18 @@ apiDescribe('Database', (persistence: boolean) => {
531531
});
532532
});
533533

534+
it('can specify updated field multiple times', () => {
535+
return withTestDoc(persistence, doc => {
536+
return doc
537+
.set({})
538+
.then(() => doc.update('field', 100, new FieldPath('field'), 200))
539+
.then(() => doc.get())
540+
.then(docSnap => {
541+
expect(docSnap.data()).to.deep.equal({ field: 200 });
542+
});
543+
});
544+
});
545+
534546
describe('documents: ', () => {
535547
const invalidDocValues = [undefined, null, 0, 'foo', ['a'], new Date()];
536548
for (const val of invalidDocValues) {

packages/firestore/test/unit/remote/serializer.helper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ export function serializerTest(
540540
const expected: api.DocumentMask = {
541541
fieldPaths: ['foo.`bar.baz\\qux`']
542542
};
543-
const mask = FieldMask.fromArray([
543+
const mask = new FieldMask([
544544
FieldPath.fromServerFormat('foo.bar\\.baz\\\\qux')
545545
]);
546546
const actual = s.toDocumentMask(mask);
@@ -554,7 +554,7 @@ export function serializerTest(
554554
// TODO(b/34988481): Implement correct escaping
555555
// eslint-disable-next-line no-restricted-properties
556556
it.skip('converts a weird path', () => {
557-
const expected = FieldMask.fromArray([
557+
const expected = new FieldMask([
558558
FieldPath.fromServerFormat('foo.bar\\.baz\\\\qux')
559559
]);
560560
const proto: api.DocumentMask = { fieldPaths: ['foo.`bar.baz\\qux`'] };

packages/firestore/test/unit/util/misc.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import { expect } from 'chai';
1919
import { immediateSuccessor } from '../../../src/util/misc';
2020
import { debugCast } from '../../../src/util/assert';
21+
import { FieldMask } from '../../../src/model/mutation';
22+
import { mask } from '../../util/helpers';
2123

2224
describe('immediateSuccessor', () => {
2325
it('generates the correct immediate successors', () => {
@@ -43,3 +45,11 @@ describe('typeCast', () => {
4345
);
4446
});
4547
});
48+
49+
describe('FieldMask', () => {
50+
it('cannot contain duplicate fields', () => {
51+
expect(() => mask('a', 'b', 'a')).to.throw(
52+
'FieldMask contains fields that are not unique'
53+
);
54+
});
55+
});

packages/firestore/test/util/helpers.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,7 @@ export function field(path: string): FieldPath {
212212
}
213213

214214
export function mask(...paths: string[]): FieldMask {
215-
let fieldPaths = new SortedSet<FieldPath>(FieldPath.comparator);
216-
for (const path of paths) {
217-
fieldPaths = fieldPaths.add(field(path));
218-
}
219-
return FieldMask.fromSet(fieldPaths);
215+
return new FieldMask(paths.map(v => field(v)));
220216
}
221217

222218
export function blob(...bytes: number[]): Blob {

0 commit comments

Comments
 (0)