Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple inequality support #7453

Merged
merged 43 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
04358df
initial code
milaGGL Jul 4, 2023
f779fa1
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Jul 5, 2023
352cb38
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Jul 5, 2023
31d5c9d
remove validation in TargetIndexMatcher
milaGGL Jul 5, 2023
c5bf4c8
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Jul 5, 2023
8d15a7f
add order by rules
milaGGL Jul 12, 2023
7d17790
format
milaGGL Jul 12, 2023
5a29be4
add composite ineuquality field implicit orderby
milaGGL Jul 13, 2023
fac7443
fix lint
milaGGL Jul 13, 2023
15c60f1
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Jul 13, 2023
832b29a
add offline test and aggregation test
milaGGL Jul 13, 2023
b2cad6e
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Jul 13, 2023
9f4cef9
format
milaGGL Jul 13, 2023
74dc8af
add more tests
milaGGL Jul 14, 2023
d140edf
re-organize tests and format
milaGGL Jul 14, 2023
a63bca3
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Jul 18, 2023
c444386
resolve comments
milaGGL Jul 20, 2023
2783ef4
use sortedset
milaGGL Jul 20, 2023
5b1fdb5
replace test docs to ids
milaGGL Jul 20, 2023
936b00a
remove problematic use of andFilter, orFilter
milaGGL Jul 20, 2023
4e2262a
resolve comments
milaGGL Jul 21, 2023
5909a08
Update target_index_matcher.ts
milaGGL Jul 21, 2023
db3793e
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Jul 25, 2023
4fadda1
fix typo
milaGGL Jul 25, 2023
8ee0775
add changeset
milaGGL Jul 25, 2023
341d202
resolve comment
milaGGL Jul 25, 2023
a586432
change misnamed test case
milaGGL Jul 28, 2023
a69e458
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Jul 28, 2023
63698a7
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Aug 2, 2023
59796bc
Simplify code and improve test coverage (#7512)
milaGGL Aug 8, 2023
792e271
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Aug 11, 2023
0c0678e
format
milaGGL Aug 11, 2023
b26b2e9
update chengeset
milaGGL Aug 11, 2023
642186c
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Aug 17, 2023
226a754
add more unit tests for order by normalization
milaGGL Aug 17, 2023
2ed1a48
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Aug 29, 2023
bceea26
CSI compatibility
cherylEnkidu Aug 30, 2023
69947dc
Address feedback
cherylEnkidu Aug 31, 2023
feea4ad
Address feedback2
cherylEnkidu Aug 31, 2023
0d3c452
update test cooment
milaGGL Sep 7, 2023
f932645
Merge branch 'master' into mila/Multiple-Inequality-support
milaGGL Sep 12, 2023
31c2133
Update local_store_indexeddb.test.ts
milaGGL Sep 12, 2023
4c154aa
Delete changelog
milaGGL Sep 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/gold-ways-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@firebase/firestore-compat': minor
'@firebase/firestore': minor
'firebase': minor
---

Added support for multiple inequality in compound queries.
53 changes: 18 additions & 35 deletions packages/firestore-compat/test/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,15 +825,11 @@ apiDescribe('Validation:', (persistence: boolean) => {
}
);

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

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

validationIt(
persistence,
'with != and inequality queries on different fields fail',
'can have != and inequality queries on different fields',
db => {
const collection = db.collection('test');
expect(() =>
collection.where('y', '>', 32).where('x', '!=', 33)
).to.throw(
'Invalid query. All where filters with an ' +
'inequality (<, <=, !=, not-in, >, or >=) must be on the same field.' +
` But you have inequality filters on 'y' and 'x`
);
).not.to.throw();
}
);

validationIt(
persistence,
'with != and inequality queries on different fields fail',
'can have NOT-IN and inequality queries on different fields',
db => {
const collection = db.collection('test');
expect(() =>
collection.where('y', '>', 32).where('x', 'not-in', [33])
).to.throw(
'Invalid query. All where filters with an ' +
'inequality (<, <=, !=, not-in, >, or >=) must be on the same field.' +
` But you have inequality filters on 'y' and 'x`
);
).not.to.throw();
}
);

validationIt(
persistence,
'with inequality different than first orderBy fail.',
'can have inequality different than first orderBy',
db => {
const collection = db.collection('test');
const reason =
`Invalid query. You have a where filter with an ` +
`inequality (<, <=, !=, not-in, >, or >=) on field 'x' and so you must also ` +
`use 'x' as your first argument to Query.orderBy(), but your first ` +
`orderBy() is on field 'y' instead.`;
expect(() => collection.where('x', '>', 32).orderBy('y')).to.throw(
reason
);
expect(() => collection.orderBy('y').where('x', '>', 32)).to.throw(
reason
);
expect(() =>
collection.where('x', '>', 32).orderBy('y')
).not.to.throw();
expect(() =>
collection.orderBy('y').where('x', '>', 32)
).not.to.throw();
expect(() =>
collection.where('x', '>', 32).orderBy('y').orderBy('x')
).to.throw(reason);
).not.to.throw();
expect(() =>
collection.orderBy('y').orderBy('x').where('x', '>', 32)
).to.throw(reason);
expect(() => collection.where('x', '!=', 32).orderBy('y')).to.throw(
reason
);
).not.to.throw();
expect(() =>
collection.where('x', '!=', 32).orderBy('y')
).not.to.throw();
}
);

Expand Down
36 changes: 10 additions & 26 deletions packages/firestore/src/core/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export abstract class Filter {

abstract getFilters(): Filter[];

abstract getFirstInequalityField(): FieldPath | null;
abstract getInequalityFilters(): readonly FieldFilter[];
}

export class FieldFilter extends Filter {
Expand Down Expand Up @@ -195,11 +195,11 @@ export class FieldFilter extends Filter {
return [this];
}

getFirstInequalityField(): FieldPath | null {
getInequalityFilters(): readonly FieldFilter[] {
if (this.isInequality()) {
return this.field;
return [this];
}
return null;
return [];
}
}

Expand Down Expand Up @@ -247,28 +247,12 @@ export class CompositeFilter extends Filter {
return Object.assign([], this.filters);
}

getFirstInequalityField(): FieldPath | null {
const found = this.findFirstMatchingFilter(filter => filter.isInequality());

if (found !== null) {
return found.field;
}
return null;
}

// Performs a depth-first search to find and return the first FieldFilter in the composite filter
// that satisfies the predicate. Returns `null` if none of the FieldFilters satisfy the
// predicate.
private findFirstMatchingFilter(
predicate: (filter: FieldFilter) => boolean
): FieldFilter | null {
for (const fieldFilter of this.getFlattenedFilters()) {
if (predicate(fieldFilter)) {
return fieldFilter;
}
}

return null;
// Performs a depth-first search to find and return the inequality FieldFilters in the composite
// filter. Returns an empty array if none of the FieldFilters has inequality filters.
getInequalityFilters(): readonly FieldFilter[] {
return this.getFlattenedFilters().filter((filter: FieldFilter) =>
filter.isInequality()
);
}
}

Expand Down
110 changes: 47 additions & 63 deletions packages/firestore/src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { compareDocumentsByField, Document } from '../model/document';
import { DocumentKey } from '../model/document_key';
import { FieldPath, ResourcePath } from '../model/path';
import { debugAssert, debugCast, fail } from '../util/assert';
import { SortedSet } from '../util/sorted_set';

import {
Bound,
Expand Down Expand Up @@ -165,21 +166,18 @@ export function queryMatchesAllDocuments(query: Query): boolean {
);
}

export function getFirstOrderByField(query: Query): FieldPath | null {
return query.explicitOrderBy.length > 0
? query.explicitOrderBy[0].field
: null;
}

export function getInequalityFilterField(query: Query): FieldPath | null {
for (const filter of query.filters) {
const result = filter.getFirstInequalityField();
if (result !== null) {
return result;
}
}

return null;
// Returns the sorted set of inequality filter fields used in this query.
export function getInequalityFilterFields(query: Query): SortedSet<FieldPath> {
let result = new SortedSet<FieldPath>(FieldPath.comparator);
query.filters.forEach((filter: Filter) => {
const inequalityFields = filter
.getInequalityFilters()
.map(filter => filter.field);
inequalityFields.forEach((field: FieldPath) => {
result = result.add(field);
});
});
return result;
}

/**
Expand Down Expand Up @@ -211,53 +209,49 @@ export function isCollectionGroupQuery(query: Query): boolean {
}

/**
* Returns the implicit order by constraint that is used to execute the Query,
* which can be different from the order by constraints the user provided (e.g.
* the SDK and backend always orders by `__name__`).
* Returns the order by constraint that is used to execute the Query. Note that the implicit order
* by constraint can be different from the order by constraints the user provided (e.g. the SDK and
* backend always orders by `__name__`).
ehsannas marked this conversation as resolved.
Show resolved Hide resolved
*/
export function queryOrderBy(query: Query): OrderBy[] {
const queryImpl = debugCast(query, QueryImpl);
if (queryImpl.memoizedOrderBy === null) {
queryImpl.memoizedOrderBy = [];
const fieldsNormalized = new Set<string>();

// Any explicit order by fields should be added as is.
for (const orderBy of queryImpl.explicitOrderBy) {
queryImpl.memoizedOrderBy.push(orderBy);
fieldsNormalized.add(orderBy.field.canonicalString());
}

const inequalityField = getInequalityFilterField(queryImpl);
const firstOrderByField = getFirstOrderByField(queryImpl);
if (inequalityField !== null && firstOrderByField === null) {
// In order to implicitly add key ordering, we must also add the
// inequality filter field for it to be a valid query.
// Note that the default inequality field and key ordering is ascending.
if (!inequalityField.isKeyField()) {
queryImpl.memoizedOrderBy.push(new OrderBy(inequalityField));
// The order of the implicit ordering always matches the last explicit order by.
const lastDirection =
queryImpl.explicitOrderBy.length > 0
? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1].dir
: Direction.ASCENDING;

// Any inequality fields not explicitly ordered should be implicitly ordered in a lexicographical
// order. When there are multiple inequality filters on the same field, the field should be added
// only once.
// Note: `SortedSet<FieldPath>` sorts the key field before other fields. However, we want the key
// field to be sorted last.
const inequalityFields: SortedSet<FieldPath> =
getInequalityFilterFields(queryImpl);
inequalityFields.forEach(field => {
if (
!fieldsNormalized.has(field.canonicalString()) &&
!field.isKeyField()
) {
queryImpl.memoizedOrderBy!.push(new OrderBy(field, lastDirection));
}
});

// Add the document key field to the last if it is not explicitly ordered.
if (!fieldsNormalized.has(FieldPath.keyField().canonicalString())) {
queryImpl.memoizedOrderBy.push(
new OrderBy(FieldPath.keyField(), Direction.ASCENDING)
new OrderBy(FieldPath.keyField(), lastDirection)
);
} else {
debugAssert(
inequalityField === null ||
(firstOrderByField !== null &&
inequalityField.isEqual(firstOrderByField)),
'First orderBy should match inequality field.'
);
let foundKeyOrdering = false;
for (const orderBy of queryImpl.explicitOrderBy) {
queryImpl.memoizedOrderBy.push(orderBy);
if (orderBy.field.isKeyField()) {
foundKeyOrdering = true;
}
}
if (!foundKeyOrdering) {
// The order of the implicit key ordering always matches the last
// explicit order by
const lastDirection =
queryImpl.explicitOrderBy.length > 0
? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
.dir
: Direction.ASCENDING;
queryImpl.memoizedOrderBy.push(
new OrderBy(FieldPath.keyField(), lastDirection)
);
}
}
}
return queryImpl.memoizedOrderBy;
Expand Down Expand Up @@ -314,16 +308,6 @@ export function queryToTarget(query: Query): Target {
}

export function queryWithAddedFilter(query: Query, filter: Filter): Query {
const newInequalityField = filter.getFirstInequalityField();
const queryInequalityField = getInequalityFilterField(query);

debugAssert(
queryInequalityField == null ||
newInequalityField == null ||
newInequalityField.isEqual(queryInequalityField),
'Query must only have one inequality field.'
);

debugAssert(
!isDocumentQuery(query),
'No filtering allowed for document query'
Expand Down
57 changes: 0 additions & 57 deletions packages/firestore/src/lite-api/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import {
} from '../core/filter';
import { Direction, OrderBy } from '../core/order_by';
import {
getFirstOrderByField,
getInequalityFilterField,
isCollectionGroupQuery,
LimitType,
Query as InternalQuery,
Expand Down Expand Up @@ -868,7 +866,6 @@ export function newQueryOrderBy(
);
}
const orderBy = new OrderBy(fieldPath, direction);
validateNewOrderBy(query, orderBy);
return orderBy;
}

Expand Down Expand Up @@ -1100,33 +1097,6 @@ function validateNewFieldFilter(
query: InternalQuery,
fieldFilter: FieldFilter
): void {
if (fieldFilter.isInequality()) {
const existingInequality = getInequalityFilterField(query);
const newInequality = fieldFilter.field;

if (
existingInequality !== null &&
!existingInequality.isEqual(newInequality)
) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'Invalid query. All where filters with an inequality' +
' (<, <=, !=, not-in, >, or >=) must be on the same field. But you have' +
` inequality filters on '${existingInequality.toString()}'` +
` and '${newInequality.toString()}'`
);
}

const firstOrderByField = getFirstOrderByField(query);
if (firstOrderByField !== null) {
validateOrderByAndInequalityMatch(
query,
newInequality,
firstOrderByField
);
}
}

ehsannas marked this conversation as resolved.
Show resolved Hide resolved
const conflictingOp = findOpInsideFilters(
query.filters,
conflictingOps(fieldFilter.op)
Expand Down Expand Up @@ -1174,33 +1144,6 @@ function findOpInsideFilters(
return null;
}

function validateNewOrderBy(query: InternalQuery, orderBy: OrderBy): void {
if (getFirstOrderByField(query) === null) {
// This is the first order by. It must match any inequality.
const inequalityField = getInequalityFilterField(query);
if (inequalityField !== null) {
validateOrderByAndInequalityMatch(query, inequalityField, orderBy.field);
}
}
}

function validateOrderByAndInequalityMatch(
baseQuery: InternalQuery,
inequality: InternalFieldPath,
orderBy: InternalFieldPath
): void {
if (!orderBy.isEqual(inequality)) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
`Invalid query. You have a where filter with an inequality ` +
`(<, <=, !=, not-in, >, or >=) on field '${inequality.toString()}' ` +
`and so you must also use '${inequality.toString()}' ` +
`as your first argument to orderBy(), but your first orderBy() ` +
`is on field '${orderBy.toString()}' instead.`
);
}
}

ehsannas marked this conversation as resolved.
Show resolved Hide resolved
export function validateQueryFilterConstraint(
functionName: string,
queryConstraint: AppliableConstraint
Expand Down
Loading