Skip to content

Commit e5b96d7

Browse files
authored
Multiple inequality support (#7453)
1 parent 98cfcbd commit e5b96d7

File tree

13 files changed

+1113
-322
lines changed

13 files changed

+1113
-322
lines changed

packages/firestore-compat/test/validation.test.ts

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -825,15 +825,11 @@ apiDescribe('Validation:', (persistence: boolean) => {
825825
}
826826
);
827827

828-
validationIt(persistence, 'with different inequality fields fail', db => {
828+
validationIt(persistence, 'can have different inequality fields', db => {
829829
const collection = db.collection('test');
830830
expect(() =>
831831
collection.where('x', '>=', 32).where('y', '<', 'cat')
832-
).to.throw(
833-
'Invalid query. All where filters with an ' +
834-
'inequality (<, <=, !=, not-in, >, or >=) must be on the same field.' +
835-
` But you have inequality filters on 'x' and 'y'`
836-
);
832+
).not.to.throw();
837833
});
838834

839835
validationIt(persistence, 'with more than one != query fail', db => {
@@ -845,59 +841,46 @@ apiDescribe('Validation:', (persistence: boolean) => {
845841

846842
validationIt(
847843
persistence,
848-
'with != and inequality queries on different fields fail',
844+
'can have != and inequality queries on different fields',
849845
db => {
850846
const collection = db.collection('test');
851847
expect(() =>
852848
collection.where('y', '>', 32).where('x', '!=', 33)
853-
).to.throw(
854-
'Invalid query. All where filters with an ' +
855-
'inequality (<, <=, !=, not-in, >, or >=) must be on the same field.' +
856-
` But you have inequality filters on 'y' and 'x`
857-
);
849+
).not.to.throw();
858850
}
859851
);
860852

861853
validationIt(
862854
persistence,
863-
'with != and inequality queries on different fields fail',
855+
'can have NOT-IN and inequality queries on different fields',
864856
db => {
865857
const collection = db.collection('test');
866858
expect(() =>
867859
collection.where('y', '>', 32).where('x', 'not-in', [33])
868-
).to.throw(
869-
'Invalid query. All where filters with an ' +
870-
'inequality (<, <=, !=, not-in, >, or >=) must be on the same field.' +
871-
` But you have inequality filters on 'y' and 'x`
872-
);
860+
).not.to.throw();
873861
}
874862
);
875863

876864
validationIt(
877865
persistence,
878-
'with inequality different than first orderBy fail.',
866+
'can have inequality different than first orderBy',
879867
db => {
880868
const collection = db.collection('test');
881-
const reason =
882-
`Invalid query. You have a where filter with an ` +
883-
`inequality (<, <=, !=, not-in, >, or >=) on field 'x' and so you must also ` +
884-
`use 'x' as your first argument to Query.orderBy(), but your first ` +
885-
`orderBy() is on field 'y' instead.`;
886-
expect(() => collection.where('x', '>', 32).orderBy('y')).to.throw(
887-
reason
888-
);
889-
expect(() => collection.orderBy('y').where('x', '>', 32)).to.throw(
890-
reason
891-
);
869+
expect(() =>
870+
collection.where('x', '>', 32).orderBy('y')
871+
).not.to.throw();
872+
expect(() =>
873+
collection.orderBy('y').where('x', '>', 32)
874+
).not.to.throw();
892875
expect(() =>
893876
collection.where('x', '>', 32).orderBy('y').orderBy('x')
894-
).to.throw(reason);
877+
).not.to.throw();
895878
expect(() =>
896879
collection.orderBy('y').orderBy('x').where('x', '>', 32)
897-
).to.throw(reason);
898-
expect(() => collection.where('x', '!=', 32).orderBy('y')).to.throw(
899-
reason
900-
);
880+
).not.to.throw();
881+
expect(() =>
882+
collection.where('x', '!=', 32).orderBy('y')
883+
).not.to.throw();
901884
}
902885
);
903886

packages/firestore/src/core/filter.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ export abstract class Filter {
5656
abstract getFlattenedFilters(): readonly FieldFilter[];
5757

5858
abstract getFilters(): Filter[];
59-
60-
abstract getFirstInequalityField(): FieldPath | null;
6159
}
6260

6361
export class FieldFilter extends Filter {
@@ -194,13 +192,6 @@ export class FieldFilter extends Filter {
194192
getFilters(): Filter[] {
195193
return [this];
196194
}
197-
198-
getFirstInequalityField(): FieldPath | null {
199-
if (this.isInequality()) {
200-
return this.field;
201-
}
202-
return null;
203-
}
204195
}
205196

206197
export class CompositeFilter extends Filter {
@@ -246,30 +237,6 @@ export class CompositeFilter extends Filter {
246237
getFilters(): Filter[] {
247238
return Object.assign([], this.filters);
248239
}
249-
250-
getFirstInequalityField(): FieldPath | null {
251-
const found = this.findFirstMatchingFilter(filter => filter.isInequality());
252-
253-
if (found !== null) {
254-
return found.field;
255-
}
256-
return null;
257-
}
258-
259-
// Performs a depth-first search to find and return the first FieldFilter in the composite filter
260-
// that satisfies the predicate. Returns `null` if none of the FieldFilters satisfy the
261-
// predicate.
262-
private findFirstMatchingFilter(
263-
predicate: (filter: FieldFilter) => boolean
264-
): FieldFilter | null {
265-
for (const fieldFilter of this.getFlattenedFilters()) {
266-
if (predicate(fieldFilter)) {
267-
return fieldFilter;
268-
}
269-
}
270-
271-
return null;
272-
}
273240
}
274241

275242
export function compositeFilterIsConjunction(

packages/firestore/src/core/query.ts

Lines changed: 47 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ import { compareDocumentsByField, Document } from '../model/document';
1919
import { DocumentKey } from '../model/document_key';
2020
import { FieldPath, ResourcePath } from '../model/path';
2121
import { debugAssert, debugCast, fail } from '../util/assert';
22+
import { SortedSet } from '../util/sorted_set';
2223

2324
import {
2425
Bound,
2526
boundSortsAfterDocument,
2627
boundSortsBeforeDocument
2728
} from './bound';
28-
import { Filter } from './filter';
29+
import { FieldFilter, Filter } from './filter';
2930
import { Direction, OrderBy } from './order_by';
3031
import {
3132
canonifyTarget,
@@ -172,21 +173,18 @@ export function queryMatchesAllDocuments(query: Query): boolean {
172173
);
173174
}
174175

175-
export function getFirstOrderByField(query: Query): FieldPath | null {
176-
return query.explicitOrderBy.length > 0
177-
? query.explicitOrderBy[0].field
178-
: null;
179-
}
180-
181-
export function getInequalityFilterField(query: Query): FieldPath | null {
182-
for (const filter of query.filters) {
183-
const result = filter.getFirstInequalityField();
184-
if (result !== null) {
185-
return result;
186-
}
187-
}
188-
189-
return null;
176+
// Returns the sorted set of inequality filter fields used in this query.
177+
export function getInequalityFilterFields(query: Query): SortedSet<FieldPath> {
178+
let result = new SortedSet<FieldPath>(FieldPath.comparator);
179+
query.filters.forEach((filter: Filter) => {
180+
const subFilters = filter.getFlattenedFilters();
181+
subFilters.forEach((filter: FieldFilter) => {
182+
if (filter.isInequality()) {
183+
result = result.add(filter.field);
184+
}
185+
});
186+
});
187+
return result;
190188
}
191189

192190
/**
@@ -228,45 +226,43 @@ export function queryNormalizedOrderBy(query: Query): OrderBy[] {
228226
const queryImpl = debugCast(query, QueryImpl);
229227
if (queryImpl.memoizedNormalizedOrderBy === null) {
230228
queryImpl.memoizedNormalizedOrderBy = [];
229+
const fieldsNormalized = new Set<string>();
231230

232-
const inequalityField = getInequalityFilterField(queryImpl);
233-
const firstOrderByField = getFirstOrderByField(queryImpl);
234-
if (inequalityField !== null && firstOrderByField === null) {
235-
// In order to implicitly add key ordering, we must also add the
236-
// inequality filter field for it to be a valid query.
237-
// Note that the default inequality field and key ordering is ascending.
238-
if (!inequalityField.isKeyField()) {
239-
queryImpl.memoizedNormalizedOrderBy.push(new OrderBy(inequalityField));
231+
// Any explicit order by fields should be added as is.
232+
for (const orderBy of queryImpl.explicitOrderBy) {
233+
queryImpl.memoizedNormalizedOrderBy.push(orderBy);
234+
fieldsNormalized.add(orderBy.field.canonicalString());
235+
}
236+
237+
// The order of the implicit ordering always matches the last explicit order by.
238+
const lastDirection =
239+
queryImpl.explicitOrderBy.length > 0
240+
? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1].dir
241+
: Direction.ASCENDING;
242+
243+
// Any inequality fields not explicitly ordered should be implicitly ordered in a lexicographical
244+
// order. When there are multiple inequality filters on the same field, the field should be added
245+
// only once.
246+
// Note: `SortedSet<FieldPath>` sorts the key field before other fields. However, we want the key
247+
// field to be sorted last.
248+
const inequalityFields: SortedSet<FieldPath> =
249+
getInequalityFilterFields(queryImpl);
250+
inequalityFields.forEach(field => {
251+
if (
252+
!fieldsNormalized.has(field.canonicalString()) &&
253+
!field.isKeyField()
254+
) {
255+
queryImpl.memoizedNormalizedOrderBy!.push(
256+
new OrderBy(field, lastDirection)
257+
);
240258
}
259+
});
260+
261+
// Add the document key field to the last if it is not explicitly ordered.
262+
if (!fieldsNormalized.has(FieldPath.keyField().canonicalString())) {
241263
queryImpl.memoizedNormalizedOrderBy.push(
242-
new OrderBy(FieldPath.keyField(), Direction.ASCENDING)
243-
);
244-
} else {
245-
debugAssert(
246-
inequalityField === null ||
247-
(firstOrderByField !== null &&
248-
inequalityField.isEqual(firstOrderByField)),
249-
'First orderBy should match inequality field.'
264+
new OrderBy(FieldPath.keyField(), lastDirection)
250265
);
251-
let foundKeyOrdering = false;
252-
for (const orderBy of queryImpl.explicitOrderBy) {
253-
queryImpl.memoizedNormalizedOrderBy.push(orderBy);
254-
if (orderBy.field.isKeyField()) {
255-
foundKeyOrdering = true;
256-
}
257-
}
258-
if (!foundKeyOrdering) {
259-
// The order of the implicit key ordering always matches the last
260-
// explicit order-by
261-
const lastDirection =
262-
queryImpl.explicitOrderBy.length > 0
263-
? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
264-
.dir
265-
: Direction.ASCENDING;
266-
queryImpl.memoizedNormalizedOrderBy.push(
267-
new OrderBy(FieldPath.keyField(), lastDirection)
268-
);
269-
}
270266
}
271267
}
272268
return queryImpl.memoizedNormalizedOrderBy;
@@ -350,16 +346,6 @@ function _queryToTarget(queryImpl: QueryImpl, orderBys: OrderBy[]): Target {
350346
}
351347

352348
export function queryWithAddedFilter(query: Query, filter: Filter): Query {
353-
const newInequalityField = filter.getFirstInequalityField();
354-
const queryInequalityField = getInequalityFilterField(query);
355-
356-
debugAssert(
357-
queryInequalityField == null ||
358-
newInequalityField == null ||
359-
newInequalityField.isEqual(queryInequalityField),
360-
'Query must only have one inequality field.'
361-
);
362-
363349
debugAssert(
364350
!isDocumentQuery(query),
365351
'No filtering allowed for document query'

packages/firestore/src/lite-api/query.ts

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ import {
2828
} from '../core/filter';
2929
import { Direction, OrderBy } from '../core/order_by';
3030
import {
31-
getFirstOrderByField,
32-
getInequalityFilterField,
3331
isCollectionGroupQuery,
3432
LimitType,
3533
Query as InternalQuery,
@@ -868,7 +866,6 @@ export function newQueryOrderBy(
868866
);
869867
}
870868
const orderBy = new OrderBy(fieldPath, direction);
871-
validateNewOrderBy(query, orderBy);
872869
return orderBy;
873870
}
874871

@@ -1100,33 +1097,6 @@ function validateNewFieldFilter(
11001097
query: InternalQuery,
11011098
fieldFilter: FieldFilter
11021099
): void {
1103-
if (fieldFilter.isInequality()) {
1104-
const existingInequality = getInequalityFilterField(query);
1105-
const newInequality = fieldFilter.field;
1106-
1107-
if (
1108-
existingInequality !== null &&
1109-
!existingInequality.isEqual(newInequality)
1110-
) {
1111-
throw new FirestoreError(
1112-
Code.INVALID_ARGUMENT,
1113-
'Invalid query. All where filters with an inequality' +
1114-
' (<, <=, !=, not-in, >, or >=) must be on the same field. But you have' +
1115-
` inequality filters on '${existingInequality.toString()}'` +
1116-
` and '${newInequality.toString()}'`
1117-
);
1118-
}
1119-
1120-
const firstOrderByField = getFirstOrderByField(query);
1121-
if (firstOrderByField !== null) {
1122-
validateOrderByAndInequalityMatch(
1123-
query,
1124-
newInequality,
1125-
firstOrderByField
1126-
);
1127-
}
1128-
}
1129-
11301100
const conflictingOp = findOpInsideFilters(
11311101
query.filters,
11321102
conflictingOps(fieldFilter.op)
@@ -1174,33 +1144,6 @@ function findOpInsideFilters(
11741144
return null;
11751145
}
11761146

1177-
function validateNewOrderBy(query: InternalQuery, orderBy: OrderBy): void {
1178-
if (getFirstOrderByField(query) === null) {
1179-
// This is the first order by. It must match any inequality.
1180-
const inequalityField = getInequalityFilterField(query);
1181-
if (inequalityField !== null) {
1182-
validateOrderByAndInequalityMatch(query, inequalityField, orderBy.field);
1183-
}
1184-
}
1185-
}
1186-
1187-
function validateOrderByAndInequalityMatch(
1188-
baseQuery: InternalQuery,
1189-
inequality: InternalFieldPath,
1190-
orderBy: InternalFieldPath
1191-
): void {
1192-
if (!orderBy.isEqual(inequality)) {
1193-
throw new FirestoreError(
1194-
Code.INVALID_ARGUMENT,
1195-
`Invalid query. You have a where filter with an inequality ` +
1196-
`(<, <=, !=, not-in, >, or >=) on field '${inequality.toString()}' ` +
1197-
`and so you must also use '${inequality.toString()}' ` +
1198-
`as your first argument to orderBy(), but your first orderBy() ` +
1199-
`is on field '${orderBy.toString()}' instead.`
1200-
);
1201-
}
1202-
}
1203-
12041147
export function validateQueryFilterConstraint(
12051148
functionName: string,
12061149
queryConstraint: AppliableConstraint

packages/firestore/src/local/indexeddb_index_manager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,10 @@ export class IndexedDbIndexManager implements IndexManager {
275275
return this.getIndexType(transaction, subTarget).next(type => {
276276
if (type === IndexType.NONE || type === IndexType.PARTIAL) {
277277
const targetIndexMatcher = new TargetIndexMatcher(subTarget);
278-
return this.addFieldIndex(
279-
transaction,
280-
targetIndexMatcher.buildTargetIndex()
281-
);
278+
const fieldIndex = targetIndexMatcher.buildTargetIndex();
279+
if (fieldIndex != null) {
280+
return this.addFieldIndex(transaction, fieldIndex);
281+
}
282282
}
283283
});
284284
}

0 commit comments

Comments
 (0)