Skip to content

Commit df56eb2

Browse files
author
Brian Chen
committed
add != and NOT_IN queries
1 parent cf9dd92 commit df56eb2

File tree

9 files changed

+673
-63
lines changed

9 files changed

+673
-63
lines changed

packages/firestore/src/api/database.ts

Lines changed: 80 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,7 +1436,7 @@ export function newQueryFilter(
14361436
`Invalid Query. You can't perform '${op}' ` +
14371437
'queries on FieldPath.documentId().'
14381438
);
1439-
} else if (op === Operator.IN) {
1439+
} else if (op === Operator.IN || op === Operator.NOT_IN) {
14401440
validateDisjunctiveFilterElements(value, op);
14411441
const referenceList: api.Value[] = [];
14421442
for (const arrayValue of value as api.Value[]) {
@@ -1447,14 +1447,18 @@ export function newQueryFilter(
14471447
fieldValue = parseDocumentIdValue(databaseId, query, value);
14481448
}
14491449
} else {
1450-
if (op === Operator.IN || op === Operator.ARRAY_CONTAINS_ANY) {
1450+
if (
1451+
op === Operator.IN ||
1452+
op === Operator.NOT_IN ||
1453+
op === Operator.ARRAY_CONTAINS_ANY
1454+
) {
14511455
validateDisjunctiveFilterElements(value, op);
14521456
}
14531457
fieldValue = parseQueryValue(
14541458
dataReader,
14551459
methodName,
14561460
value,
1457-
op === Operator.IN
1461+
op === Operator.IN || op === Operator.NOT_IN
14581462
);
14591463
}
14601464
const filter = FieldFilter.create(fieldPath, op, fieldValue);
@@ -1684,14 +1688,17 @@ function validateDisjunctiveFilterElements(
16841688
'maximum of 10 elements in the value array.'
16851689
);
16861690
}
1687-
if (value.indexOf(null) >= 0) {
1691+
if (value.indexOf(null) >= 0 && operator !== Operator.NOT_IN) {
16881692
throw new FirestoreError(
16891693
Code.INVALID_ARGUMENT,
16901694
`Invalid Query. '${operator.toString()}' filters cannot contain 'null' ` +
16911695
'in the value array.'
16921696
);
16931697
}
1694-
if (value.filter(element => Number.isNaN(element)).length > 0) {
1698+
if (
1699+
value.filter(element => Number.isNaN(element)).length > 0 &&
1700+
operator !== Operator.NOT_IN
1701+
) {
16951702
throw new FirestoreError(
16961703
Code.INVALID_ARGUMENT,
16971704
`Invalid Query. '${operator.toString()}' filters cannot contain 'NaN' ` +
@@ -1700,14 +1707,41 @@ function validateDisjunctiveFilterElements(
17001707
}
17011708
}
17021709

1710+
function conflictingOps(op: Operator): Operator[] {
1711+
switch (op) {
1712+
case Operator.NOT_EQUAL:
1713+
return [Operator.NOT_IN, Operator.NOT_EQUAL];
1714+
case Operator.ARRAY_CONTAINS:
1715+
return [
1716+
Operator.ARRAY_CONTAINS,
1717+
Operator.ARRAY_CONTAINS_ANY,
1718+
Operator.NOT_IN
1719+
];
1720+
case Operator.IN:
1721+
return [Operator.ARRAY_CONTAINS_ANY, Operator.IN, Operator.NOT_IN];
1722+
case Operator.ARRAY_CONTAINS_ANY:
1723+
return [
1724+
Operator.ARRAY_CONTAINS,
1725+
Operator.ARRAY_CONTAINS_ANY,
1726+
Operator.IN,
1727+
Operator.NOT_IN
1728+
];
1729+
case Operator.NOT_IN:
1730+
return [
1731+
Operator.ARRAY_CONTAINS,
1732+
Operator.ARRAY_CONTAINS_ANY,
1733+
Operator.IN,
1734+
Operator.NOT_IN,
1735+
Operator.NOT_EQUAL
1736+
];
1737+
default:
1738+
return [];
1739+
}
1740+
}
1741+
17031742
function validateNewFilter(query: InternalQuery, filter: Filter): void {
17041743
debugAssert(filter instanceof FieldFilter, 'Only FieldFilters are supported');
17051744

1706-
const arrayOps = [Operator.ARRAY_CONTAINS, Operator.ARRAY_CONTAINS_ANY];
1707-
const disjunctiveOps = [Operator.IN, Operator.ARRAY_CONTAINS_ANY];
1708-
const isArrayOp = arrayOps.indexOf(filter.op) >= 0;
1709-
const isDisjunctiveOp = disjunctiveOps.indexOf(filter.op) >= 0;
1710-
17111745
if (filter.isInequality()) {
17121746
const existingField = query.getInequalityFilterField();
17131747
if (existingField !== null && !existingField.isEqual(filter.field)) {
@@ -1724,31 +1758,23 @@ function validateNewFilter(query: InternalQuery, filter: Filter): void {
17241758
if (firstOrderByField !== null) {
17251759
validateOrderByAndInequalityMatch(query, filter.field, firstOrderByField);
17261760
}
1727-
} else if (isDisjunctiveOp || isArrayOp) {
1728-
// You can have at most 1 disjunctive filter and 1 array filter. Check if
1729-
// the new filter conflicts with an existing one.
1730-
let conflictingOp: Operator | null = null;
1731-
if (isDisjunctiveOp) {
1732-
conflictingOp = query.findFilterOperator(disjunctiveOps);
1733-
}
1734-
if (conflictingOp === null && isArrayOp) {
1735-
conflictingOp = query.findFilterOperator(arrayOps);
1736-
}
1737-
if (conflictingOp !== null) {
1738-
// We special case when it's a duplicate op to give a slightly clearer error message.
1739-
if (conflictingOp === filter.op) {
1740-
throw new FirestoreError(
1741-
Code.INVALID_ARGUMENT,
1742-
'Invalid query. You cannot use more than one ' +
1743-
`'${filter.op.toString()}' filter.`
1744-
);
1745-
} else {
1746-
throw new FirestoreError(
1747-
Code.INVALID_ARGUMENT,
1748-
`Invalid query. You cannot use '${filter.op.toString()}' filters ` +
1749-
`with '${conflictingOp.toString()}' filters.`
1750-
);
1751-
}
1761+
}
1762+
1763+
const conflictingOp = query.findFilterOperator(conflictingOps(filter.op));
1764+
if (conflictingOp !== null) {
1765+
// We special case when it's a duplicate op to give a slightly clearer error message.
1766+
if (conflictingOp === filter.op) {
1767+
throw new FirestoreError(
1768+
Code.INVALID_ARGUMENT,
1769+
'Invalid query. You cannot use more than one ' +
1770+
`'${filter.op.toString()}' filter.`
1771+
);
1772+
} else {
1773+
throw new FirestoreError(
1774+
Code.INVALID_ARGUMENT,
1775+
`Invalid query. You cannot use '${filter.op.toString()}' filters ` +
1776+
`with '${conflictingOp.toString()}' filters.`
1777+
);
17521778
}
17531779
}
17541780
}
@@ -1806,18 +1832,25 @@ export class Query<T = firestore.DocumentData> implements firestore.Query<T> {
18061832
validateExactNumberOfArgs('Query.where', arguments, 3);
18071833
validateDefined('Query.where', 3, value);
18081834

1809-
// Enumerated from the WhereFilterOp type in index.d.ts.
1810-
const whereFilterOpEnums = [
1811-
Operator.LESS_THAN,
1812-
Operator.LESS_THAN_OR_EQUAL,
1813-
Operator.EQUAL,
1814-
Operator.GREATER_THAN_OR_EQUAL,
1815-
Operator.GREATER_THAN,
1816-
Operator.ARRAY_CONTAINS,
1817-
Operator.IN,
1818-
Operator.ARRAY_CONTAINS_ANY
1819-
];
1820-
const op = validateStringEnum('Query.where', whereFilterOpEnums, 2, opStr);
1835+
// TODO(ne-queries): Add 'not-in' and '!=' to validation.
1836+
let op: Operator;
1837+
if ((opStr as unknown) === 'not-in' || (opStr as unknown) === '!=') {
1838+
op = opStr as Operator;
1839+
} else {
1840+
// Enumerated from the WhereFilterOp type in index.d.ts.
1841+
const whereFilterOpEnums = [
1842+
Operator.LESS_THAN,
1843+
Operator.LESS_THAN_OR_EQUAL,
1844+
Operator.EQUAL,
1845+
Operator.GREATER_THAN_OR_EQUAL,
1846+
Operator.GREATER_THAN,
1847+
Operator.ARRAY_CONTAINS,
1848+
Operator.IN,
1849+
Operator.ARRAY_CONTAINS_ANY
1850+
];
1851+
op = validateStringEnum('Query.where', whereFilterOpEnums, 2, opStr);
1852+
}
1853+
18211854
const fieldPath = fieldPathFromArgument('Query.where', field);
18221855
const filter = newQueryFilter(
18231856
this._query,

packages/firestore/src/core/query.ts

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -566,10 +566,12 @@ export const enum Operator {
566566
LESS_THAN = '<',
567567
LESS_THAN_OR_EQUAL = '<=',
568568
EQUAL = '==',
569+
NOT_EQUAL = '!=',
569570
GREATER_THAN = '>',
570571
GREATER_THAN_OR_EQUAL = '>=',
571572
ARRAY_CONTAINS = 'array-contains',
572573
IN = 'in',
574+
NOT_IN = 'not-in',
573575
ARRAY_CONTAINS_ANY = 'array-contains-any'
574576
}
575577

@@ -587,16 +589,8 @@ export class FieldFilter extends Filter {
587589
*/
588590
static create(field: FieldPath, op: Operator, value: api.Value): FieldFilter {
589591
if (field.isKeyField()) {
590-
if (op === Operator.IN) {
591-
debugAssert(
592-
isArray(value),
593-
'Comparing on key with IN, but filter value not an ArrayValue'
594-
);
595-
debugAssert(
596-
(value.arrayValue.values || []).every(elem => isReferenceValue(elem)),
597-
'Comparing on key with IN, but an array value was not a RefValue'
598-
);
599-
return new KeyFieldInFilter(field, value);
592+
if (op === Operator.IN || op === Operator.NOT_IN) {
593+
return this.createKeyFieldInFilter(field, op, value);
600594
} else {
601595
debugAssert(
602596
isReferenceValue(value),
@@ -609,15 +603,17 @@ export class FieldFilter extends Filter {
609603
return new KeyFieldFilter(field, op, value);
610604
}
611605
} else if (isNullValue(value)) {
612-
if (op !== Operator.EQUAL) {
606+
if (op !== Operator.EQUAL && op !== Operator.NOT_EQUAL) {
607+
// TODO(ne-queries): Update error message to include != comparison.
613608
throw new FirestoreError(
614609
Code.INVALID_ARGUMENT,
615610
'Invalid query. Null supports only equality comparisons.'
616611
);
617612
}
618613
return new FieldFilter(field, op, value);
619614
} else if (isNanValue(value)) {
620-
if (op !== Operator.EQUAL) {
615+
if (op !== Operator.EQUAL && op !== Operator.NOT_EQUAL) {
616+
// TODO(ne-queries): Update error message to include != comparison.
621617
throw new FirestoreError(
622618
Code.INVALID_ARGUMENT,
623619
'Invalid query. NaN supports only equality comparisons.'
@@ -632,6 +628,12 @@ export class FieldFilter extends Filter {
632628
'IN filter has invalid value: ' + value.toString()
633629
);
634630
return new InFilter(field, value);
631+
} else if (op === Operator.NOT_IN) {
632+
debugAssert(
633+
isArray(value),
634+
'NOT_IN filter has invalid value: ' + value.toString()
635+
);
636+
return new NotInFilter(field, value);
635637
} else if (op === Operator.ARRAY_CONTAINS_ANY) {
636638
debugAssert(
637639
isArray(value),
@@ -643,8 +645,40 @@ export class FieldFilter extends Filter {
643645
}
644646
}
645647

648+
private static createKeyFieldInFilter(
649+
field: FieldPath,
650+
op: Operator,
651+
value: api.Value
652+
): FieldFilter {
653+
debugAssert(
654+
op === Operator.IN || op === Operator.NOT_IN,
655+
'createKeyFieldInFilter requires an IN or NOT_IN operator'
656+
);
657+
debugAssert(
658+
isArray(value),
659+
`Comparing on key with ${op.toString()}` +
660+
', but filter value not an ArrayValue'
661+
);
662+
debugAssert(
663+
(value.arrayValue.values || []).every(elem => isReferenceValue(elem)),
664+
`Comparing on key with ${op.toString()}` +
665+
', but an array value was not a RefValue'
666+
);
667+
668+
return op === Operator.IN
669+
? new KeyFieldInFilter(field, value)
670+
: new KeyFieldNotInFilter(field, value);
671+
}
672+
646673
matches(doc: Document): boolean {
647674
const other = doc.field(this.field);
675+
// Types do not have to match in NOT_EQUAL filters.
676+
if (this.op === Operator.NOT_EQUAL) {
677+
return (
678+
other !== null &&
679+
this.matchesComparison(valueCompare(other!, this.value))
680+
);
681+
}
648682

649683
// Only compare types with matching backend order (such as double and int).
650684
return (
@@ -662,6 +696,8 @@ export class FieldFilter extends Filter {
662696
return comparison <= 0;
663697
case Operator.EQUAL:
664698
return comparison === 0;
699+
case Operator.NOT_EQUAL:
700+
return comparison !== 0;
665701
case Operator.GREATER_THAN:
666702
return comparison > 0;
667703
case Operator.GREATER_THAN_OR_EQUAL:
@@ -677,7 +713,8 @@ export class FieldFilter extends Filter {
677713
Operator.LESS_THAN,
678714
Operator.LESS_THAN_OR_EQUAL,
679715
Operator.GREATER_THAN,
680-
Operator.GREATER_THAN_OR_EQUAL
716+
Operator.GREATER_THAN_OR_EQUAL,
717+
Operator.NOT_EQUAL
681718
].indexOf(this.op) >= 0
682719
);
683720
}
@@ -762,6 +799,27 @@ export class KeyFieldInFilter extends FieldFilter {
762799
}
763800
}
764801

802+
/** Filter that matches on key fields not present within an array. */
803+
export class KeyFieldNotInFilter extends FieldFilter {
804+
private readonly keys: DocumentKey[];
805+
806+
constructor(field: FieldPath, value: api.Value) {
807+
super(field, Operator.NOT_IN, value);
808+
debugAssert(isArray(value), 'KeyFieldNotInFilter expects an ArrayValue');
809+
this.keys = (value.arrayValue.values || []).map(v => {
810+
debugAssert(
811+
isReferenceValue(v),
812+
'Comparing on key with NOT_IN, but an array value was not a ReferenceValue'
813+
);
814+
return DocumentKey.fromName(v.referenceValue);
815+
});
816+
}
817+
818+
matches(doc: Document): boolean {
819+
return !this.keys.some(key => key.isEqual(doc.key));
820+
}
821+
}
822+
765823
/** A Filter that implements the array-contains operator. */
766824
export class ArrayContainsFilter extends FieldFilter {
767825
constructor(field: FieldPath, value: api.Value) {
@@ -787,6 +845,19 @@ export class InFilter extends FieldFilter {
787845
}
788846
}
789847

848+
/** A Filter that implements the not-in operator. */
849+
export class NotInFilter extends FieldFilter {
850+
constructor(field: FieldPath, value: api.Value) {
851+
super(field, Operator.NOT_IN, value);
852+
debugAssert(isArray(value), 'NotInFilter expects an ArrayValue');
853+
}
854+
855+
matches(doc: Document): boolean {
856+
const other = doc.field(this.field);
857+
return other !== null && !arrayValueContains(this.value.arrayValue!, other);
858+
}
859+
}
860+
790861
/** A Filter that implements the array-contains-any operator. */
791862
export class ArrayContainsAnyFilter extends FieldFilter {
792863
constructor(field: FieldPath, value: api.Value) {

0 commit comments

Comments
 (0)