Skip to content

Commit 2169123

Browse files
Merge fef4ed0 into a722abc
2 parents a722abc + fef4ed0 commit 2169123

File tree

4 files changed

+441
-40
lines changed

4 files changed

+441
-40
lines changed

packages/firestore/src/core/target.ts

+62-7
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ export class CompositeFilter extends Filter {
732732
}
733733

734734
matches(doc: Document): boolean {
735-
if (this.isConjunction()) {
735+
if (compositeFilterIsConjunction(this)) {
736736
// For conjunctions, all filters must match, so return false if any filter doesn't match.
737737
return this.filters.find(filter => !filter.matches(doc)) === undefined;
738738
} else {
@@ -780,10 +780,38 @@ export class CompositeFilter extends Filter {
780780

781781
return null;
782782
}
783+
}
784+
785+
export function compositeFilterIsConjunction(
786+
compositeFilter: CompositeFilter
787+
): boolean {
788+
return compositeFilter.op === CompositeOperator.AND;
789+
}
790+
791+
/**
792+
* Returns true if this filter is a conjunction of field filters only. Returns false otherwise.
793+
*/
794+
export function compositeFilterIsFlatConjunction(
795+
compositeFilter: CompositeFilter
796+
): boolean {
797+
return (
798+
compositeFilterIsFlat(compositeFilter) &&
799+
compositeFilterIsConjunction(compositeFilter)
800+
);
801+
}
783802

784-
isConjunction(): boolean {
785-
return this.op === CompositeOperator.AND;
803+
/**
804+
* Returns true if this filter does not contain any composite filters. Returns false otherwise.
805+
*/
806+
export function compositeFilterIsFlat(
807+
compositeFilter: CompositeFilter
808+
): boolean {
809+
for (const filter of compositeFilter.filters) {
810+
if (filter instanceof CompositeFilter) {
811+
return false;
812+
}
786813
}
814+
return true;
787815
}
788816

789817
export function canonifyFilter(filter: Filter): string {
@@ -811,18 +839,45 @@ export function canonifyFilter(filter: Filter): string {
811839
}
812840

813841
export function filterEquals(f1: Filter, f2: Filter): boolean {
814-
debugAssert(
815-
f1 instanceof FieldFilter && f2 instanceof FieldFilter,
816-
'Only FieldFilters can be compared'
817-
);
842+
if (f1 instanceof FieldFilter) {
843+
return fieldFilterEquals(f1, f2);
844+
} else if (f1 instanceof CompositeFilter) {
845+
return compositeFilterEquals(f1, f2);
846+
} else {
847+
fail('Only FieldFilters and CompositeFilters can be compared');
848+
}
849+
}
818850

851+
export function fieldFilterEquals(f1: FieldFilter, f2: Filter): boolean {
819852
return (
853+
f2 instanceof FieldFilter &&
820854
f1.op === f2.op &&
821855
f1.field.isEqual(f2.field) &&
822856
valueEquals(f1.value, f2.value)
823857
);
824858
}
825859

860+
export function compositeFilterEquals(
861+
f1: CompositeFilter,
862+
f2: Filter
863+
): boolean {
864+
if (
865+
f2 instanceof CompositeFilter &&
866+
f1.op === f2.op &&
867+
f1.filters.length === f2.filters.length
868+
) {
869+
const subFiltersMatch: boolean = f1.filters.reduce(
870+
(result: boolean, f1Filter: Filter, index: number): boolean =>
871+
result && filterEquals(f1Filter, f2.filters[index]),
872+
true
873+
);
874+
875+
return subFiltersMatch;
876+
}
877+
878+
return false;
879+
}
880+
826881
/** Returns a debug description for `filter`. */
827882
export function stringifyFilter(filter: Filter): string {
828883
debugAssert(

packages/firestore/src/remote/serializer.ts

+101-30
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ import {
3131
Filter,
3232
targetIsDocumentTarget,
3333
Operator,
34+
CompositeOperator,
3435
OrderBy,
35-
Target
36+
Target,
37+
CompositeFilter,
38+
compositeFilterIsFlatConjunction
3639
} from '../core/target';
3740
import { TargetId } from '../core/types';
3841
import { Timestamp } from '../lite-api/timestamp';
@@ -64,6 +67,7 @@ import { isNanValue, isNullValue } from '../model/values';
6467
import {
6568
ApiClientObjectMap as ProtoApiClientObjectMap,
6669
BatchGetDocumentsResponse as ProtoBatchGetDocumentsResponse,
70+
CompositeFilterOp as ProtoCompositeFilterOp,
6771
Cursor as ProtoCursor,
6872
Document as ProtoDocument,
6973
DocumentMask as ProtoDocumentMask,
@@ -122,6 +126,14 @@ const OPERATORS = (() => {
122126
return ops;
123127
})();
124128

129+
const COMPOSITE_OPERATORS = (() => {
130+
const ops: { [op: string]: ProtoCompositeFilterOp } = {};
131+
ops[CompositeOperator.AND] = 'AND';
132+
// TODO(orquery) change 'OPERATOR_UNSPECIFIED' to 'OR' when the updated protos are published
133+
ops[CompositeOperator.OR] = 'OPERATOR_UNSPECIFIED';
134+
return ops;
135+
})();
136+
125137
function assertPresent(value: unknown, description: string): asserts value {
126138
debugAssert(!isNullOrUndefined(value), description + ' is missing');
127139
}
@@ -827,7 +839,7 @@ export function toQueryTarget(
827839
result.structuredQuery!.from = [{ collectionId: path.lastSegment() }];
828840
}
829841

830-
const where = toFilter(target.filters);
842+
const where = toFilters(target.filters);
831843
if (where) {
832844
result.structuredQuery!.where = where;
833845
}
@@ -873,7 +885,7 @@ export function convertQueryTargetToQuery(target: ProtoQueryTarget): Query {
873885

874886
let filterBy: Filter[] = [];
875887
if (query.where) {
876-
filterBy = fromFilter(query.where);
888+
filterBy = fromFilters(query.where);
877889
}
878890

879891
let orderBy: OrderBy[] = [];
@@ -972,34 +984,39 @@ export function toTarget(
972984
return result;
973985
}
974986

975-
function toFilter(filters: Filter[]): ProtoFilter | undefined {
987+
function toFilters(filters: Filter[]): ProtoFilter | undefined {
976988
if (filters.length === 0) {
977989
return;
978990
}
979-
const protos = filters.map(filter => {
980-
debugAssert(
981-
filter instanceof FieldFilter,
982-
'Only FieldFilters are supported'
983-
);
984-
return toUnaryOrFieldFilter(filter);
985-
});
986-
if (protos.length === 1) {
987-
return protos[0];
991+
992+
return toFilter(CompositeFilter.create(filters, CompositeOperator.AND));
993+
}
994+
995+
function fromFilters(filter: ProtoFilter): Filter[] {
996+
const result = fromFilter(filter);
997+
998+
// Instead of a singletonList containing AND(F1, F2, ...), we can return a list containing F1,
999+
// F2, ...
1000+
// TODO(orquery): Once proper support for composite filters has been completed, we can remove
1001+
// this flattening from here.
1002+
if (
1003+
result instanceof CompositeFilter &&
1004+
compositeFilterIsFlatConjunction(result)
1005+
) {
1006+
// Copy the readonly array into a mutable array
1007+
return Object.assign([], result.getFilters());
9881008
}
989-
return { compositeFilter: { op: 'AND', filters: protos } };
1009+
1010+
return [result];
9901011
}
9911012

992-
function fromFilter(filter: ProtoFilter | undefined): Filter[] {
993-
if (!filter) {
994-
return [];
995-
} else if (filter.unaryFilter !== undefined) {
996-
return [fromUnaryFilter(filter)];
1013+
function fromFilter(filter: ProtoFilter): Filter {
1014+
if (filter.unaryFilter !== undefined) {
1015+
return fromUnaryFilter(filter);
9971016
} else if (filter.fieldFilter !== undefined) {
998-
return [fromFieldFilter(filter)];
1017+
return fromFieldFilter(filter);
9991018
} else if (filter.compositeFilter !== undefined) {
1000-
return filter.compositeFilter
1001-
.filters!.map(f => fromFilter(f))
1002-
.reduce((accum, current) => accum.concat(current));
1019+
return fromCompositeFilter(filter);
10031020
} else {
10041021
return fail('Unknown filter: ' + JSON.stringify(filter));
10051022
}
@@ -1066,6 +1083,12 @@ export function toOperatorName(op: Operator): ProtoFieldFilterOp {
10661083
return OPERATORS[op];
10671084
}
10681085

1086+
export function toCompositeOperatorName(
1087+
op: CompositeOperator
1088+
): ProtoCompositeFilterOp {
1089+
return COMPOSITE_OPERATORS[op];
1090+
}
1091+
10691092
export function fromOperatorName(op: ProtoFieldFilterOp): Operator {
10701093
switch (op) {
10711094
case 'EQUAL':
@@ -1095,6 +1118,22 @@ export function fromOperatorName(op: ProtoFieldFilterOp): Operator {
10951118
}
10961119
}
10971120

1121+
export function fromCompositeOperatorName(
1122+
op: ProtoCompositeFilterOp
1123+
): CompositeOperator {
1124+
// TODO(orquery) support OR
1125+
switch (op) {
1126+
case 'AND':
1127+
return CompositeOperator.AND;
1128+
// TODO(orquery) update when OR operator is supported in ProtoCompositeFilterOp
1129+
// OPERATOR_UNSPECIFIED should fail and OR should return OR
1130+
case 'OPERATOR_UNSPECIFIED':
1131+
return CompositeOperator.OR;
1132+
default:
1133+
return fail('Unknown operator');
1134+
}
1135+
}
1136+
10981137
export function toFieldPathReference(path: FieldPath): ProtoFieldReference {
10991138
return { fieldPath: path.canonicalString() };
11001139
}
@@ -1120,15 +1159,32 @@ export function fromPropertyOrder(orderBy: ProtoOrder): OrderBy {
11201159
);
11211160
}
11221161

1123-
export function fromFieldFilter(filter: ProtoFilter): Filter {
1124-
return FieldFilter.create(
1125-
fromFieldPathReference(filter.fieldFilter!.field!),
1126-
fromOperatorName(filter.fieldFilter!.op!),
1127-
filter.fieldFilter!.value!
1128-
);
1162+
// visible for testing
1163+
export function toFilter(filter: Filter): ProtoFilter {
1164+
if (filter instanceof FieldFilter) {
1165+
return toUnaryOrFieldFilter(filter);
1166+
} else if (filter instanceof CompositeFilter) {
1167+
return toCompositeFilter(filter);
1168+
} else {
1169+
return fail('Unrecognized filter type ' + JSON.stringify(filter));
1170+
}
1171+
}
1172+
1173+
export function toCompositeFilter(filter: CompositeFilter): ProtoFilter {
1174+
const protos = filter.getFilters().map(filter => toFilter(filter));
1175+
1176+
if (protos.length === 1) {
1177+
return protos[0];
1178+
}
1179+
1180+
return {
1181+
compositeFilter: {
1182+
op: toCompositeOperatorName(filter.op),
1183+
filters: protos
1184+
}
1185+
};
11291186
}
11301187

1131-
// visible for testing
11321188
export function toUnaryOrFieldFilter(filter: FieldFilter): ProtoFilter {
11331189
if (filter.op === Operator.EQUAL) {
11341190
if (isNanValue(filter.value)) {
@@ -1201,6 +1257,21 @@ export function fromUnaryFilter(filter: ProtoFilter): Filter {
12011257
}
12021258
}
12031259

1260+
export function fromFieldFilter(filter: ProtoFilter): FieldFilter {
1261+
return FieldFilter.create(
1262+
fromFieldPathReference(filter.fieldFilter!.field!),
1263+
fromOperatorName(filter.fieldFilter!.op!),
1264+
filter.fieldFilter!.value!
1265+
);
1266+
}
1267+
1268+
export function fromCompositeFilter(filter: ProtoFilter): CompositeFilter {
1269+
return CompositeFilter.create(
1270+
filter.compositeFilter!.filters!.map(filter => fromFilter(filter)),
1271+
fromCompositeOperatorName(filter.compositeFilter!.op!)
1272+
);
1273+
}
1274+
12041275
export function toDocumentMask(fieldMask: FieldMask): ProtoDocumentMask {
12051276
const canonicalFields: string[] = [];
12061277
fieldMask.fields.forEach(field =>

0 commit comments

Comments
 (0)