Skip to content

Drop SortedSet from FieldMask #2999

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 1, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions packages/firestore/src/api/user_data_reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import {
ServerTimestampTransform
} from '../model/transform_operation';
import { JsonProtoSerializer } from '../remote/serializer';
import { SortedSet } from '../util/sorted_set';
import { Blob } from './blob';
import {
FieldPath as ExternalFieldPath,
Expand Down Expand Up @@ -337,10 +336,10 @@ export class UserDataReader {
let fieldTransforms: FieldTransform[];

if (!fieldPaths) {
fieldMask = FieldMask.fromArray(context.fieldMask);
fieldMask = new FieldMask(context.fieldMask);
fieldTransforms = context.fieldTransforms;
} else {
let validatedFieldPaths = new SortedSet<FieldPath>(FieldPath.comparator);
const validatedFieldPaths: FieldPath[] = [];

for (const stringOrFieldPath of fieldPaths) {
let fieldPath: FieldPath;
Expand All @@ -365,10 +364,12 @@ export class UserDataReader {
);
}

validatedFieldPaths = validatedFieldPaths.add(fieldPath);
if (!fieldMaskContains(validatedFieldPaths, fieldPath)) {
validatedFieldPaths.push(fieldPath);
}
}

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

let fieldMaskPaths = new SortedSet<FieldPath>(FieldPath.comparator);
const fieldMaskPaths: FieldPath[] = [];
const updateData = new ObjectValueBuilder();
forEach(input as Dict<unknown>, (key, value) => {
const path = fieldPathFromDotSeparatedString(methodName, key);
Expand All @@ -398,17 +399,17 @@ export class UserDataReader {
value = this.runPreConverter(value, childContext);
if (value instanceof DeleteFieldValueImpl) {
// Add it to the field mask, but don't add anything to updateData.
fieldMaskPaths = fieldMaskPaths.add(path);
fieldMaskPaths.push(path);
} else {
const parsedValue = this.parseData(value, childContext);
if (parsedValue != null) {
fieldMaskPaths = fieldMaskPaths.add(path);
fieldMaskPaths.push(path);
updateData.set(path, parsedValue);
}
}
});

const mask = FieldMask.fromSet(fieldMaskPaths);
const mask = new FieldMask(fieldMaskPaths);
return new ParsedUpdateData(
updateData.build(),
mask,
Expand Down Expand Up @@ -449,26 +450,30 @@ export class UserDataReader {
values.push(moreFieldsAndValues[i + 1]);
}

let fieldMaskPaths = new SortedSet<FieldPath>(FieldPath.comparator);
const fieldMaskPaths: FieldPath[] = [];
const updateData = new ObjectValueBuilder();

for (let i = 0; i < keys.length; ++i) {
const path = keys[i];
const childContext = context.childContextForFieldPath(path);
const value = this.runPreConverter(values[i], childContext);
if (value instanceof DeleteFieldValueImpl) {
// Add it to the field mask, but don't add anything to updateData.
fieldMaskPaths = fieldMaskPaths.add(path);
} else {
const parsedValue = this.parseData(value, childContext);
if (parsedValue != null) {
fieldMaskPaths = fieldMaskPaths.add(path);
updateData.set(path, parsedValue);
// We iterate in reverse order to pick the last value for a field if the
// user specified the field multiple times.
for (let i = keys.length - 1; i >= 0; --i) {
if (!fieldMaskContains(fieldMaskPaths, keys[i])) {
const path = keys[i];
const childContext = context.childContextForFieldPath(path);
const value = this.runPreConverter(values[i], childContext);
if (value instanceof DeleteFieldValueImpl) {
// Add it to the field mask, but don't add anything to updateData.
fieldMaskPaths.push(path);
} else {
const parsedValue = this.parseData(value, childContext);
if (parsedValue != null) {
fieldMaskPaths.push(path);
updateData.set(path, parsedValue);
}
}
}
}

const mask = FieldMask.fromSet(fieldMaskPaths);
const mask = new FieldMask(fieldMaskPaths);
return new ParsedUpdateData(
updateData.build(),
mask,
Expand Down Expand Up @@ -841,3 +846,8 @@ function fieldPathFromDotSeparatedString(
function errorMessage(error: Error | object): string {
return error instanceof Error ? error.message : error.toString();
}

/** Checks `haystack` if FieldPath `needle` is present. Runs in O(n). */
function fieldMaskContains(haystack: FieldPath[], needle: FieldPath): boolean {
return haystack.some(v => v.isEqual(needle));
}
30 changes: 12 additions & 18 deletions packages/firestore/src/model/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import * as api from '../protos/firestore_proto_api';
import { Timestamp } from '../api/timestamp';
import { SnapshotVersion } from '../core/snapshot_version';
import { debugAssert, fail, hardAssert } from '../util/assert';
import { SortedSet } from '../util/sorted_set';

import {
Document,
Expand All @@ -45,18 +44,14 @@ import { arrayEquals } from '../util/misc';
* containing foo
*/
export class FieldMask {
constructor(readonly fields: SortedSet<FieldPath>) {
constructor(readonly fields: FieldPath[]) {
// TODO(dimond): validation of FieldMask
}

static fromSet(fields: SortedSet<FieldPath>): FieldMask {
return new FieldMask(fields);
}

static fromArray(fields: FieldPath[]): FieldMask {
let fieldsAsSet = new SortedSet<FieldPath>(FieldPath.comparator);
fields.forEach(fieldPath => (fieldsAsSet = fieldsAsSet.add(fieldPath)));
return new FieldMask(fieldsAsSet);
// Sort the field mask to support `FieldMask.isEqual()` and assert below.
fields.sort(FieldPath.comparator);
debugAssert(
!fields.some((v, i) => i !== 0 && v.isEqual(fields[i - 1])),
'FieldMask contains fields that are not unique'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be helpful to include the name of the repeated field in the message. Consider adding this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't think of a clean way to do this, so I added an ugly way. Done.

);
}

/**
Expand All @@ -66,17 +61,16 @@ export class FieldMask {
* This is an O(n) operation, where `n` is the size of the field mask.
*/
covers(fieldPath: FieldPath): boolean {
let found = false;
this.fields.forEach(fieldMaskPath => {
for (const fieldMaskPath of this.fields) {
if (fieldMaskPath.isPrefixOf(fieldPath)) {
found = true;
return true;
}
});
return found;
}
return false;
}

isEqual(other: FieldMask): boolean {
return this.fields.isEqual(other.fields);
return arrayEquals(this.fields, other.fields, (l, r) => l.isEqual(r));
}
}

Expand Down
17 changes: 8 additions & 9 deletions packages/firestore/src/model/object_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { FieldPath } from './path';
import { isServerTimestamp } from './server_timestamps';
import { valueEquals, isMapValue, typeOrder } from './values';
import { forEach } from '../util/obj';
import { SortedSet } from '../util/sorted_set';

export interface JsonObject<T> {
[name: string]: T;
Expand Down Expand Up @@ -243,27 +242,27 @@ export class ObjectValueBuilder {
* Returns a FieldMask built from all fields in a MapValue.
*/
export function extractFieldMask(value: api.MapValue): FieldMask {
let fields = new SortedSet<FieldPath>(FieldPath.comparator);
const fields: FieldPath[] = [];
forEach(value!.fields || {}, (key, value) => {
const currentPath = new FieldPath([key]);
if (isMapValue(value)) {
const nestedMask = extractFieldMask(value.mapValue!);
const nestedFields = nestedMask.fields;
if (nestedFields.isEmpty()) {
if (nestedFields.length === 0) {
// Preserve the empty map by adding it to the FieldMask.
fields = fields.add(currentPath);
fields.push(currentPath);
} else {
// For nested and non-empty ObjectValues, add the FieldPath of the
// leaf nodes.
nestedFields.forEach(nestedPath => {
fields = fields.add(currentPath.child(nestedPath));
});
for (const nestedPath of nestedFields) {
fields.push(currentPath.child(nestedPath));
}
}
} else {
// For nested and non-empty ObjectValues, add the FieldPath of the leaf
// nodes.
fields = fields.add(currentPath);
fields.push(currentPath);
}
});
return FieldMask.fromSet(fields);
return new FieldMask(fields);
}
3 changes: 1 addition & 2 deletions packages/firestore/src/remote/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1086,8 +1086,7 @@ export class JsonProtoSerializer {

fromDocumentMask(proto: api.DocumentMask): FieldMask {
const paths = proto.fieldPaths || [];
const fields = paths.map(path => FieldPath.fromServerFormat(path));
return FieldMask.fromArray(fields);
return new FieldMask(paths.map(path => FieldPath.fromServerFormat(path)));
}
}

Expand Down
12 changes: 12 additions & 0 deletions packages/firestore/test/integration/api/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,18 @@ apiDescribe('Database', (persistence: boolean) => {
});
});

it('can specify updated field multiple times', () => {
return withTestDoc(persistence, doc => {
return doc
.set({})
.then(() => doc.update('field', 100, new FieldPath('field'), 200))
.then(() => doc.get())
.then(docSnap => {
expect(docSnap.data()).to.deep.equal({ field: 200 });
});
});
});

describe('documents: ', () => {
const invalidDocValues = [undefined, null, 0, 'foo', ['a'], new Date()];
for (const val of invalidDocValues) {
Expand Down
4 changes: 2 additions & 2 deletions packages/firestore/test/unit/remote/serializer.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ export function serializerTest(
const expected: api.DocumentMask = {
fieldPaths: ['foo.`bar.baz\\qux`']
};
const mask = FieldMask.fromArray([
const mask = new FieldMask([
FieldPath.fromServerFormat('foo.bar\\.baz\\\\qux')
]);
const actual = s.toDocumentMask(mask);
Expand All @@ -554,7 +554,7 @@ export function serializerTest(
// TODO(b/34988481): Implement correct escaping
// eslint-disable-next-line no-restricted-properties
it.skip('converts a weird path', () => {
const expected = FieldMask.fromArray([
const expected = new FieldMask([
FieldPath.fromServerFormat('foo.bar\\.baz\\\\qux')
]);
const proto: api.DocumentMask = { fieldPaths: ['foo.`bar.baz\\qux`'] };
Expand Down
9 changes: 9 additions & 0 deletions packages/firestore/test/unit/util/misc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { expect } from 'chai';
import { immediateSuccessor } from '../../../src/util/misc';
import { debugCast } from '../../../src/util/assert';
import { mask } from '../../util/helpers';

describe('immediateSuccessor', () => {
it('generates the correct immediate successors', () => {
Expand All @@ -43,3 +44,11 @@ describe('typeCast', () => {
);
});
});

describe('FieldMask', () => {
it('cannot contain duplicate fields', () => {
expect(() => mask('a', 'b', 'a')).to.throw(
'FieldMask contains fields that are not unique'
);
});
});
6 changes: 1 addition & 5 deletions packages/firestore/test/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,7 @@ export function field(path: string): FieldPath {
}

export function mask(...paths: string[]): FieldMask {
let fieldPaths = new SortedSet<FieldPath>(FieldPath.comparator);
for (const path of paths) {
fieldPaths = fieldPaths.add(field(path));
}
return FieldMask.fromSet(fieldPaths);
return new FieldMask(paths.map(v => field(v)));
}

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