Skip to content

Commit cf3401d

Browse files
author
Brian Chen
authored
Add NOT_IN and != queries (not public) (#3557)
1 parent 2a0d254 commit cf3401d

File tree

9 files changed

+716
-84
lines changed

9 files changed

+716
-84
lines changed

packages/firestore/src/api/database.ts

+102-58
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+
/* allowArrays= */ op === Operator.IN || op === Operator.NOT_IN
14581462
);
14591463
}
14601464
const filter = FieldFilter.create(fieldPath, op, fieldValue);
@@ -1663,7 +1667,7 @@ function parseDocumentIdValue(
16631667
}
16641668

16651669
/**
1666-
* Validates that the value passed into a disjunctrive filter satisfies all
1670+
* Validates that the value passed into a disjunctive filter satisfies all
16671671
* array requirements.
16681672
*/
16691673
function validateDisjunctiveFilterElements(
@@ -1684,30 +1688,71 @@ function validateDisjunctiveFilterElements(
16841688
'maximum of 10 elements in the value array.'
16851689
);
16861690
}
1687-
if (value.indexOf(null) >= 0) {
1688-
throw new FirestoreError(
1689-
Code.INVALID_ARGUMENT,
1690-
`Invalid Query. '${operator.toString()}' filters cannot contain 'null' ` +
1691-
'in the value array.'
1692-
);
1691+
if (operator === Operator.IN || operator === Operator.ARRAY_CONTAINS_ANY) {
1692+
if (value.indexOf(null) >= 0) {
1693+
throw new FirestoreError(
1694+
Code.INVALID_ARGUMENT,
1695+
`Invalid Query. '${operator.toString()}' filters cannot contain 'null' ` +
1696+
'in the value array.'
1697+
);
1698+
}
1699+
if (value.filter(element => Number.isNaN(element)).length > 0) {
1700+
throw new FirestoreError(
1701+
Code.INVALID_ARGUMENT,
1702+
`Invalid Query. '${operator.toString()}' filters cannot contain 'NaN' ` +
1703+
'in the value array.'
1704+
);
1705+
}
16931706
}
1694-
if (value.filter(element => Number.isNaN(element)).length > 0) {
1695-
throw new FirestoreError(
1696-
Code.INVALID_ARGUMENT,
1697-
`Invalid Query. '${operator.toString()}' filters cannot contain 'NaN' ` +
1698-
'in the value array.'
1699-
);
1707+
}
1708+
1709+
/**
1710+
* Given an operator, returns the set of operators that cannot be used with it.
1711+
*
1712+
* Operators in a query must adhere to the following set of rules:
1713+
* 1. Only one array operator is allowed.
1714+
* 2. Only one disjunctive operator is allowed.
1715+
* 3. NOT_EQUAL cannot be used with another NOT_EQUAL operator.
1716+
* 4. NOT_IN cannot be used with array, disjunctive, or NOT_EQUAL operators.
1717+
*
1718+
* Array operators: ARRAY_CONTAINS, ARRAY_CONTAINS_ANY
1719+
* Disjunctive operators: IN, ARRAY_CONTAINS_ANY, NOT_IN
1720+
*/
1721+
function conflictingOps(op: Operator): Operator[] {
1722+
switch (op) {
1723+
case Operator.NOT_EQUAL:
1724+
return [Operator.NOT_EQUAL, Operator.NOT_IN];
1725+
case Operator.ARRAY_CONTAINS:
1726+
return [
1727+
Operator.ARRAY_CONTAINS,
1728+
Operator.ARRAY_CONTAINS_ANY,
1729+
Operator.NOT_IN
1730+
];
1731+
case Operator.IN:
1732+
return [Operator.ARRAY_CONTAINS_ANY, Operator.IN, Operator.NOT_IN];
1733+
case Operator.ARRAY_CONTAINS_ANY:
1734+
return [
1735+
Operator.ARRAY_CONTAINS,
1736+
Operator.ARRAY_CONTAINS_ANY,
1737+
Operator.IN,
1738+
Operator.NOT_IN
1739+
];
1740+
case Operator.NOT_IN:
1741+
return [
1742+
Operator.ARRAY_CONTAINS,
1743+
Operator.ARRAY_CONTAINS_ANY,
1744+
Operator.IN,
1745+
Operator.NOT_IN,
1746+
Operator.NOT_EQUAL
1747+
];
1748+
default:
1749+
return [];
17001750
}
17011751
}
17021752

17031753
function validateNewFilter(query: InternalQuery, filter: Filter): void {
17041754
debugAssert(filter instanceof FieldFilter, 'Only FieldFilters are supported');
17051755

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-
17111756
if (filter.isInequality()) {
17121757
const existingField = query.getInequalityFilterField();
17131758
if (existingField !== null && !existingField.isEqual(filter.field)) {
@@ -1724,31 +1769,23 @@ function validateNewFilter(query: InternalQuery, filter: Filter): void {
17241769
if (firstOrderByField !== null) {
17251770
validateOrderByAndInequalityMatch(query, filter.field, firstOrderByField);
17261771
}
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-
}
1772+
}
1773+
1774+
const conflictingOp = query.findFilterOperator(conflictingOps(filter.op));
1775+
if (conflictingOp !== null) {
1776+
// Special case when it's a duplicate op to give a slightly clearer error message.
1777+
if (conflictingOp === filter.op) {
1778+
throw new FirestoreError(
1779+
Code.INVALID_ARGUMENT,
1780+
'Invalid query. You cannot use more than one ' +
1781+
`'${filter.op.toString()}' filter.`
1782+
);
1783+
} else {
1784+
throw new FirestoreError(
1785+
Code.INVALID_ARGUMENT,
1786+
`Invalid query. You cannot use '${filter.op.toString()}' filters ` +
1787+
`with '${conflictingOp.toString()}' filters.`
1788+
);
17521789
}
17531790
}
17541791
}
@@ -1806,18 +1843,25 @@ export class Query<T = firestore.DocumentData> implements firestore.Query<T> {
18061843
validateExactNumberOfArgs('Query.where', arguments, 3);
18071844
validateDefined('Query.where', 3, value);
18081845

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);
1846+
// TODO(ne-queries): Add 'not-in' and '!=' to validation.
1847+
let op: Operator;
1848+
if ((opStr as unknown) === 'not-in' || (opStr as unknown) === '!=') {
1849+
op = opStr as Operator;
1850+
} else {
1851+
// Enumerated from the WhereFilterOp type in index.d.ts.
1852+
const whereFilterOpEnums = [
1853+
Operator.LESS_THAN,
1854+
Operator.LESS_THAN_OR_EQUAL,
1855+
Operator.EQUAL,
1856+
Operator.GREATER_THAN_OR_EQUAL,
1857+
Operator.GREATER_THAN,
1858+
Operator.ARRAY_CONTAINS,
1859+
Operator.IN,
1860+
Operator.ARRAY_CONTAINS_ANY
1861+
];
1862+
op = validateStringEnum('Query.where', whereFilterOpEnums, 2, opStr);
1863+
}
1864+
18211865
const fieldPath = fieldPathFromArgument('Query.where', field);
18221866
const filter = newQueryFilter(
18231867
this._query,

0 commit comments

Comments
 (0)