Skip to content

Commit 33a0896

Browse files
committed
Support encoding and decoding Composite Filters
1 parent eba3b84 commit 33a0896

File tree

4 files changed

+471
-79
lines changed

4 files changed

+471
-79
lines changed

packages/firestore/src/core/target.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ export class CompositeFilter extends Filter {
729729
}
730730

731731
matches(doc: Document): boolean {
732-
if (this.isConjunction()) {
732+
if (compositeFilterIsConjunction(this)) {
733733
// For conjunctions, all filters must match, so return false if any filter doesn't match.
734734
return this.filters.find(filter => !filter.matches(doc)) === undefined;
735735
} else {
@@ -785,10 +785,29 @@ export class CompositeFilter extends Filter {
785785

786786
return null;
787787
}
788+
}
789+
790+
export function compositeFilterIsConjunction(compositeFilter: CompositeFilter): boolean {
791+
return compositeFilter.op === CompositeOperator.AND;
792+
}
793+
794+
/**
795+
* Returns true if this filter is a conjunction of field filters only. Returns false otherwise.
796+
*/
797+
export function compositeFilterIsFlatConjunction(compositeFilter: CompositeFilter): boolean {
798+
return compositeFilterIsFlat(compositeFilter) && compositeFilterIsConjunction(compositeFilter);
799+
}
788800

789-
isConjunction(): boolean {
790-
return this.op === CompositeOperator.AND;
801+
/**
802+
* Returns true if this filter does not contain any composite filters. Returns false otherwise.
803+
*/
804+
export function compositeFilterIsFlat(compositeFilter: CompositeFilter): boolean {
805+
for (const filter of compositeFilter.filters) {
806+
if (filter instanceof CompositeFilter) {
807+
return false;
808+
}
791809
}
810+
return true;
792811
}
793812

794813
export function canonifyFilter(filter: Filter): string {
@@ -811,24 +830,46 @@ export function canonifyFilter(filter: Filter): string {
811830
const canonicalIdsString = filter.filters
812831
.map(filter => canonifyFilter(filter))
813832
.join(',');
814-
const opString = filter.isConjunction() ? 'and' : 'or';
833+
const opString = compositeFilterIsConjunction(filter) ? 'and' : 'or';
815834
return `${opString}(${canonicalIdsString})`;
816835
}
817836
}
818837

819838
export function filterEquals(f1: Filter, f2: Filter): boolean {
820-
debugAssert(
821-
f1 instanceof FieldFilter && f2 instanceof FieldFilter,
822-
'Only FieldFilters can be compared'
823-
);
839+
if (f1 instanceof FieldFilter) {
840+
return fieldFilterEquals(f1, f2);
841+
} else if (f1 instanceof CompositeFilter) {
842+
return compositeFilterEquals(f1, f2);
843+
} else {
844+
fail('Only FieldFilters and CompositeFilters can be compared');
845+
}
846+
}
824847

848+
export function fieldFilterEquals(f1: FieldFilter, f2: Filter): boolean {
825849
return (
850+
f2 instanceof FieldFilter &&
826851
f1.op === f2.op &&
827852
f1.field.isEqual(f2.field) &&
828853
valueEquals(f1.value, f2.value)
829854
);
830855
}
831856

857+
export function compositeFilterEquals(f1: CompositeFilter, f2: Filter): boolean {
858+
if (
859+
f2 instanceof CompositeFilter &&
860+
f1.op === f2.op &&
861+
f1.filters.length === f2.filters.length) {
862+
const subFiltersMatch: boolean = f1.filters.reduce(
863+
(result: boolean, f1Filter: Filter, index: number): boolean =>
864+
(result && filterEquals(f1Filter, f2.filters[index])),
865+
true);
866+
867+
return subFiltersMatch;
868+
}
869+
870+
return false;
871+
}
872+
832873
/** Returns a debug description for `filter`. */
833874
export function stringifyFilter(filter: Filter): string {
834875
debugAssert(

packages/firestore/src/remote/serializer.ts

Lines changed: 96 additions & 32 deletions
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 = encodeFilters(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 = decodeFilters(query.where);
877889
}
878890

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

975-
function toFilter(filters: Filter[]): ProtoFilter | undefined {
987+
function encodeFilters(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 encodeFilter(CompositeFilter.create(filters, CompositeOperator.AND));
993+
}
994+
995+
function decodeFilters(filter: ProtoFilter): Filter[] {
996+
const result = decodeFilter(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 (result instanceof CompositeFilter && compositeFilterIsFlatConjunction(result)) {
1003+
return result.getFilters();
9881004
}
989-
return { compositeFilter: { op: 'AND', filters: protos } };
1005+
1006+
return [result];
9901007
}
9911008

992-
function fromFilter(filter: ProtoFilter | undefined): Filter[] {
993-
if (!filter) {
994-
return [];
995-
} else if (filter.unaryFilter !== undefined) {
996-
return [fromUnaryFilter(filter)];
1009+
function decodeFilter(filter: ProtoFilter): Filter {
1010+
if (filter.unaryFilter !== undefined) {
1011+
return decodeUnaryFilter(filter);
9971012
} else if (filter.fieldFilter !== undefined) {
998-
return [fromFieldFilter(filter)];
1013+
return decodeFieldFilter(filter);
9991014
} else if (filter.compositeFilter !== undefined) {
1000-
return filter.compositeFilter
1001-
.filters!.map(f => fromFilter(f))
1002-
.reduce((accum, current) => accum.concat(current));
1015+
return decodeCompositeFilter(filter);
10031016
} else {
10041017
return fail('Unknown filter: ' + JSON.stringify(filter));
10051018
}
@@ -1066,6 +1079,10 @@ export function toOperatorName(op: Operator): ProtoFieldFilterOp {
10661079
return OPERATORS[op];
10671080
}
10681081

1082+
export function toCompositeOperatorName(op: CompositeOperator): ProtoCompositeFilterOp {
1083+
return COMPOSITE_OPERATORS[op];
1084+
}
1085+
10691086
export function fromOperatorName(op: ProtoFieldFilterOp): Operator {
10701087
switch (op) {
10711088
case 'EQUAL':
@@ -1095,6 +1112,21 @@ export function fromOperatorName(op: ProtoFieldFilterOp): Operator {
10951112
}
10961113
}
10971114

1115+
export function fromCompositeOperatorName(op: ProtoCompositeFilterOp): CompositeOperator {
1116+
// TODO(orquery) support OR
1117+
switch (op) {
1118+
case 'AND':
1119+
return CompositeOperator.AND;
1120+
// TODO(orquery) update when OR operatore is supported in ProtoCompositeFilterOp
1121+
// case 'OPERATOR_UNSPECIFIED':
1122+
// return fail('Unspecified operator');
1123+
case 'OPERATOR_UNSPECIFIED':
1124+
return CompositeOperator.OR;
1125+
default:
1126+
return fail('Unknown operator');
1127+
}
1128+
}
1129+
10981130
export function toFieldPathReference(path: FieldPath): ProtoFieldReference {
10991131
return { fieldPath: path.canonicalString() };
11001132
}
@@ -1120,16 +1152,33 @@ export function fromPropertyOrder(orderBy: ProtoOrder): OrderBy {
11201152
);
11211153
}
11221154

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-
);
1155+
// visible for testing
1156+
export function encodeFilter(filter: Filter): ProtoFilter {
1157+
if (filter instanceof FieldFilter) {
1158+
return encodeUnaryOrFieldFilter(filter);
1159+
} else if (filter instanceof CompositeFilter) {
1160+
return encodeCompositeFilter(filter);
1161+
} else {
1162+
return fail('Unrecognized filter type ' + JSON.stringify(filter));
1163+
}
11291164
}
11301165

1131-
// visible for testing
1132-
export function toUnaryOrFieldFilter(filter: FieldFilter): ProtoFilter {
1166+
export function encodeCompositeFilter(filter: CompositeFilter): ProtoFilter {
1167+
const protos = filter.getFilters().map(filter => encodeFilter(filter));
1168+
1169+
if (protos.length === 1) {
1170+
return protos[0];
1171+
}
1172+
1173+
return {
1174+
compositeFilter: {
1175+
op: toCompositeOperatorName(filter.op),
1176+
filters: protos
1177+
}
1178+
};
1179+
}
1180+
1181+
export function encodeUnaryOrFieldFilter(filter: FieldFilter): ProtoFilter {
11331182
if (filter.op === Operator.EQUAL) {
11341183
if (isNanValue(filter.value)) {
11351184
return {
@@ -1172,7 +1221,7 @@ export function toUnaryOrFieldFilter(filter: FieldFilter): ProtoFilter {
11721221
};
11731222
}
11741223

1175-
export function fromUnaryFilter(filter: ProtoFilter): Filter {
1224+
export function decodeUnaryFilter(filter: ProtoFilter): Filter {
11761225
switch (filter.unaryFilter!.op!) {
11771226
case 'IS_NAN':
11781227
const nanField = fromFieldPathReference(filter.unaryFilter!.field!);
@@ -1201,6 +1250,21 @@ export function fromUnaryFilter(filter: ProtoFilter): Filter {
12011250
}
12021251
}
12031252

1253+
export function decodeFieldFilter(filter: ProtoFilter): FieldFilter {
1254+
return FieldFilter.create(
1255+
fromFieldPathReference(filter.fieldFilter!.field!),
1256+
fromOperatorName(filter.fieldFilter!.op!),
1257+
filter.fieldFilter!.value!
1258+
);
1259+
}
1260+
1261+
export function decodeCompositeFilter(filter: ProtoFilter): CompositeFilter {
1262+
return CompositeFilter.create(
1263+
filter.compositeFilter!.filters!.map(filter => decodeFilter(filter)),
1264+
fromCompositeOperatorName(filter.compositeFilter!.op!)
1265+
);
1266+
}
1267+
12041268
export function toDocumentMask(fieldMask: FieldMask): ProtoDocumentMask {
12051269
const canonicalFields: string[] = [];
12061270
fieldMask.fields.forEach(field =>

0 commit comments

Comments
 (0)