Skip to content

Commit e03614c

Browse files
Drop SortedSet from FieldMask (#2999)
1 parent a6e9d5b commit e03614c

File tree

8 files changed

+79
-59
lines changed

8 files changed

+79
-59
lines changed

packages/firestore/src/api/user_data_reader.ts

+33-23
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import {
4444
ServerTimestampTransform
4545
} from '../model/transform_operation';
4646
import { JsonProtoSerializer } from '../remote/serializer';
47-
import { SortedSet } from '../util/sorted_set';
4847
import { Blob } from './blob';
4948
import {
5049
FieldPath as ExternalFieldPath,
@@ -337,10 +336,10 @@ export class UserDataReader {
337336
let fieldTransforms: FieldTransform[];
338337

339338
if (!fieldPaths) {
340-
fieldMask = FieldMask.fromArray(context.fieldMask);
339+
fieldMask = new FieldMask(context.fieldMask);
341340
fieldTransforms = context.fieldTransforms;
342341
} else {
343-
let validatedFieldPaths = new SortedSet<FieldPath>(FieldPath.comparator);
342+
const validatedFieldPaths: FieldPath[] = [];
344343

345344
for (const stringOrFieldPath of fieldPaths) {
346345
let fieldPath: FieldPath;
@@ -365,10 +364,12 @@ export class UserDataReader {
365364
);
366365
}
367366

368-
validatedFieldPaths = validatedFieldPaths.add(fieldPath);
367+
if (!fieldMaskContains(validatedFieldPaths, fieldPath)) {
368+
validatedFieldPaths.push(fieldPath);
369+
}
369370
}
370371

371-
fieldMask = FieldMask.fromSet(validatedFieldPaths);
372+
fieldMask = new FieldMask(validatedFieldPaths);
372373
fieldTransforms = context.fieldTransforms.filter(transform =>
373374
fieldMask.covers(transform.field)
374375
);
@@ -389,7 +390,7 @@ export class UserDataReader {
389390
);
390391
validatePlainObject('Data must be an object, but it was:', context, input);
391392

392-
let fieldMaskPaths = new SortedSet<FieldPath>(FieldPath.comparator);
393+
const fieldMaskPaths: FieldPath[] = [];
393394
const updateData = new ObjectValueBuilder();
394395
forEach(input as Dict<unknown>, (key, value) => {
395396
const path = fieldPathFromDotSeparatedString(methodName, key);
@@ -398,17 +399,17 @@ export class UserDataReader {
398399
value = this.runPreConverter(value, childContext);
399400
if (value instanceof DeleteFieldValueImpl) {
400401
// Add it to the field mask, but don't add anything to updateData.
401-
fieldMaskPaths = fieldMaskPaths.add(path);
402+
fieldMaskPaths.push(path);
402403
} else {
403404
const parsedValue = this.parseData(value, childContext);
404405
if (parsedValue != null) {
405-
fieldMaskPaths = fieldMaskPaths.add(path);
406+
fieldMaskPaths.push(path);
406407
updateData.set(path, parsedValue);
407408
}
408409
}
409410
});
410411

411-
const mask = FieldMask.fromSet(fieldMaskPaths);
412+
const mask = new FieldMask(fieldMaskPaths);
412413
return new ParsedUpdateData(
413414
updateData.build(),
414415
mask,
@@ -449,26 +450,30 @@ export class UserDataReader {
449450
values.push(moreFieldsAndValues[i + 1]);
450451
}
451452

452-
let fieldMaskPaths = new SortedSet<FieldPath>(FieldPath.comparator);
453+
const fieldMaskPaths: FieldPath[] = [];
453454
const updateData = new ObjectValueBuilder();
454455

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

471-
const mask = FieldMask.fromSet(fieldMaskPaths);
476+
const mask = new FieldMask(fieldMaskPaths);
472477
return new ParsedUpdateData(
473478
updateData.build(),
474479
mask,
@@ -841,3 +846,8 @@ function fieldPathFromDotSeparatedString(
841846
function errorMessage(error: Error | object): string {
842847
return error instanceof Error ? error.message : error.toString();
843848
}
849+
850+
/** Checks `haystack` if FieldPath `needle` is present. Runs in O(n). */
851+
function fieldMaskContains(haystack: FieldPath[], needle: FieldPath): boolean {
852+
return haystack.some(v => v.isEqual(needle));
853+
}

packages/firestore/src/model/mutation.ts

+13-18
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import * as api from '../protos/firestore_proto_api';
2020
import { Timestamp } from '../api/timestamp';
2121
import { SnapshotVersion } from '../core/snapshot_version';
2222
import { debugAssert, fail, hardAssert } from '../util/assert';
23-
import { SortedSet } from '../util/sorted_set';
2423

2524
import {
2625
Document,
@@ -45,18 +44,15 @@ import { arrayEquals } from '../util/misc';
4544
* containing foo
4645
*/
4746
export class FieldMask {
48-
constructor(readonly fields: SortedSet<FieldPath>) {
47+
constructor(readonly fields: FieldPath[]) {
4948
// 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);
49+
// Sort the field mask to support `FieldMask.isEqual()` and assert below.
50+
fields.sort(FieldPath.comparator);
51+
debugAssert(
52+
!fields.some((v, i) => i !== 0 && v.isEqual(fields[i - 1])),
53+
'FieldMask contains field that is not unique: ' +
54+
fields.find((v, i) => i !== 0 && v.isEqual(fields[i - 1]))!
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

+8-9
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { FieldPath } from './path';
2323
import { isServerTimestamp } from './server_timestamps';
2424
import { valueEquals, isMapValue, typeOrder } from './values';
2525
import { forEach } from '../util/obj';
26-
import { SortedSet } from '../util/sorted_set';
2726

2827
export interface JsonObject<T> {
2928
[name: string]: T;
@@ -243,27 +242,27 @@ export class ObjectValueBuilder {
243242
* Returns a FieldMask built from all fields in a MapValue.
244243
*/
245244
export function extractFieldMask(value: api.MapValue): FieldMask {
246-
let fields = new SortedSet<FieldPath>(FieldPath.comparator);
245+
const fields: FieldPath[] = [];
247246
forEach(value!.fields || {}, (key, value) => {
248247
const currentPath = new FieldPath([key]);
249248
if (isMapValue(value)) {
250249
const nestedMask = extractFieldMask(value.mapValue!);
251250
const nestedFields = nestedMask.fields;
252-
if (nestedFields.isEmpty()) {
251+
if (nestedFields.length === 0) {
253252
// Preserve the empty map by adding it to the FieldMask.
254-
fields = fields.add(currentPath);
253+
fields.push(currentPath);
255254
} else {
256255
// For nested and non-empty ObjectValues, add the FieldPath of the
257256
// leaf nodes.
258-
nestedFields.forEach(nestedPath => {
259-
fields = fields.add(currentPath.child(nestedPath));
260-
});
257+
for (const nestedPath of nestedFields) {
258+
fields.push(currentPath.child(nestedPath));
259+
}
261260
}
262261
} else {
263262
// For nested and non-empty ObjectValues, add the FieldPath of the leaf
264263
// nodes.
265-
fields = fields.add(currentPath);
264+
fields.push(currentPath);
266265
}
267266
});
268-
return FieldMask.fromSet(fields);
267+
return new FieldMask(fields);
269268
}

packages/firestore/src/remote/serializer.ts

+1-2
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

+12
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

+2-2
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

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import { expect } from 'chai';
1919
import { immediateSuccessor } from '../../../src/util/misc';
2020
import { debugCast } from '../../../src/util/assert';
21+
import { mask } from '../../util/helpers';
2122

2223
describe('immediateSuccessor', () => {
2324
it('generates the correct immediate successors', () => {
@@ -43,3 +44,11 @@ describe('typeCast', () => {
4344
);
4445
});
4546
});
47+
48+
describe('FieldMask', () => {
49+
it('cannot contain duplicate fields', () => {
50+
expect(() => mask('a', 'b', 'a')).to.throw(
51+
'FieldMask contains field that is not unique: a'
52+
);
53+
});
54+
});

packages/firestore/test/util/helpers.ts

+1-5
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)