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

Markduckworth/or queries pr 4 #6527

Merged
merged 35 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
effced9
Add composite filters. Port of https://github.com/firebase/firebase-a…
MarkDuckworth Jun 24, 2022
eba3b84
Cleanup: lint
MarkDuckworth Jun 24, 2022
33a0896
Support encoding and decoding Composite Filters
MarkDuckworth Jun 29, 2022
9f6e216
Formatting
MarkDuckworth Jun 30, 2022
6556240
Merge remote-tracking branch 'origin/markduckworth/or-queries' into m…
MarkDuckworth Jul 26, 2022
78a8900
Fix issue with readonly array.
MarkDuckworth Jul 26, 2022
80fc05d
WIP: serving results from index.
MarkDuckworth Aug 5, 2022
8b6f05f
Perform DNF transform
MarkDuckworth Aug 10, 2022
152fc18
Cleanup and tests from https://github.com/firebase/firebase-android-s…
MarkDuckworth Aug 10, 2022
8b0bfbd
Merge branch 'markduckworth/or-queries' of github.com:firebase/fireba…
MarkDuckworth Aug 12, 2022
1c948cb
use index if we have composite filters
MarkDuckworth Aug 12, 2022
2cad46c
Merge branch 'markduckworth/or-queries' into markduckworth/or-queries…
MarkDuckworth Aug 12, 2022
266f7f6
Fix bad import
MarkDuckworth Aug 12, 2022
c2d7cd2
Update the orderBy filtering mechanism for OR queries and steps to se…
MarkDuckworth Aug 15, 2022
41d0e29
Adding tests
MarkDuckworth Aug 15, 2022
90f7c50
Test for DNF computation.
MarkDuckworth Aug 16, 2022
ff52ecc
Fix implementation of isFlatConjunction
MarkDuckworth Aug 16, 2022
dfda9f8
Adding integration tests for validation of composite queries.
MarkDuckworth Aug 17, 2022
9bc3336
IndexDb manager tests for partial and full index.
MarkDuckworth Aug 17, 2022
d66abd4
Test query does not include documents with missing fields
MarkDuckworth Aug 17, 2022
e33263d
Run prettier
MarkDuckworth Aug 17, 2022
01c3487
Add missing awaits
MarkDuckworth Aug 17, 2022
7d56b57
Fixing argument validation for and . Updating tests for asserted err…
MarkDuckworth Aug 17, 2022
d01ff74
Prettier
MarkDuckworth Aug 18, 2022
186fd92
Removing duplicate test cases.
MarkDuckworth Aug 18, 2022
409b19c
Update proto with OR operator.
MarkDuckworth Aug 22, 2022
131f4be
Update protos with OR operator.
MarkDuckworth Aug 23, 2022
a514358
Testing query with indexes
MarkDuckworth Aug 23, 2022
0ab215b
Add compile.sh to compile proto updates.
MarkDuckworth Aug 23, 2022
638b0ad
Adding new script to run browser tests against the emulator.
MarkDuckworth Aug 23, 2022
86c9057
Update stringifyFilter to support CompositeFilters
MarkDuckworth Aug 23, 2022
3501135
Lint and formatting
MarkDuckworth Aug 23, 2022
a962368
Merge branch 'markduckworth/or-queries' into markduckworth/or-queries…
MarkDuckworth Sep 22, 2022
66b16cb
Addressing code comments.
MarkDuckworth Sep 22, 2022
2ddc068
Corrected the implementation and documentation of
MarkDuckworth Oct 25, 2022
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
6 changes: 6 additions & 0 deletions common/api-review/firestore.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export type AddPrefixToKeys<Prefix extends string, T extends Record<string, unkn
[K in keyof T & string as `${Prefix}.${K}`]+?: T[K];
};

// @public
export function and(...queryConstraints: QueryConstraint[]): QueryConstraint;

// @public
export function arrayRemove(...elements: unknown[]): FieldValue;

Expand Down Expand Up @@ -324,6 +327,9 @@ export function onSnapshotsInSync(firestore: Firestore, observer: {
// @public
export function onSnapshotsInSync(firestore: Firestore, onSync: () => void): Unsubscribe;

// @public
export function or(...queryConstraints: QueryConstraint[]): QueryConstraint;

// @public
export function orderBy(fieldPath: string | FieldPath, directionStr?: OrderByDirection): QueryConstraint;

Expand Down
1 change: 1 addition & 0 deletions packages/firestore/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"test:all:ci": "run-p test:browser test:lite:browser test:travis",
"test:all": "run-p test:browser test:lite:browser test:travis test:minified",
"test:browser": "karma start --single-run",
"test:browser:emulator": "karma start --single-run --local",
"test:browser:unit": "karma start --single-run --unit",
"test:browser:debug": "karma start --browsers=Chrome --auto-watch",
"test:node": "node ./scripts/run-tests.js --main=test/register.ts --emulator 'test/{,!(browser|lite)/**/}*.test.ts'",
Expand Down
2 changes: 2 additions & 0 deletions packages/firestore/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ export {
} from './api/reference';

export {
and,
endAt,
endBefore,
startAt,
startAfter,
limit,
limitToLast,
where,
or,
orderBy,
query,
QueryConstraint,
Expand Down
2 changes: 2 additions & 0 deletions packages/firestore/src/api/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
*/

export {
and,
endAt,
endBefore,
startAfter,
startAt,
limitToLast,
limit,
or,
orderBy,
OrderByDirection,
where,
Expand Down
8 changes: 7 additions & 1 deletion packages/firestore/src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,13 @@ function queryMatchesPathAndCollectionGroup(
* in the results.
*/
function queryMatchesOrderBy(query: Query, doc: Document): boolean {
for (const orderBy of query.explicitOrderBy) {
// We must use `queryOrderBy()` to get the list of all orderBys (both implicit and explicit).
// Note that for OR queries, orderBy applies to all disjunction terms and implicit orderBys must
// be taken into account. For example, the query "a > 1 || b==1" has an implicit "orderBy a" due
// to the inequality, and is evaluated as "a > 1 orderBy a || b==1 orderBy a".
// A document with content of {b:1} matches the filters, but does not match the orderBy because
// it's missing the field 'a'.
for (const orderBy of queryOrderBy(query)) {
MarkDuckworth marked this conversation as resolved.
Show resolved Hide resolved
// order by key always matches
if (!orderBy.field.isKeyField() && doc.data.field(orderBy.field) === null) {
return false;
Expand Down
88 changes: 63 additions & 25 deletions packages/firestore/src/core/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,26 +500,25 @@ export function targetGetSegmentCount(target: Target): number {
let hasArraySegment = false;

for (const filter of target.filters) {
// TODO(orquery): Use the flattened filters here
const fieldFilter = filter as FieldFilter;

// __name__ is not an explicit segment of any index, so we don't need to
// count it.
if (fieldFilter.field.isKeyField()) {
continue;
}
for (const subFilter of filter.getFlattenedFilters()) {
// __name__ is not an explicit segment of any index, so we don't need to
// count it.
if (subFilter.field.isKeyField()) {
continue;
}

// ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filters must be counted separately.
// For instance, it is possible to have an index for "a ARRAY a ASC". Even
// though these are on the same field, they should be counted as two
// separate segments in an index.
if (
fieldFilter.op === Operator.ARRAY_CONTAINS ||
fieldFilter.op === Operator.ARRAY_CONTAINS_ANY
) {
hasArraySegment = true;
} else {
fields = fields.add(fieldFilter.field);
// ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filters must be counted separately.
// For instance, it is possible to have an index for "a ARRAY a ASC". Even
// though these are on the same field, they should be counted as two
// separate segments in an index.
if (
subFilter.op === Operator.ARRAY_CONTAINS ||
subFilter.op === Operator.ARRAY_CONTAINS_ANY
) {
hasArraySegment = true;
} else {
fields = fields.add(subFilter.field);
}
}
}

Expand All @@ -534,12 +533,16 @@ export function targetGetSegmentCount(target: Target): number {
return fields.size + (hasArraySegment ? 1 : 0);
}

export function targetHasLimit(target: Target): boolean {
return target.limit !== null;
}

export abstract class Filter {
abstract matches(doc: Document): boolean;

abstract getFlattenedFilters(): readonly FieldFilter[];

abstract getFilters(): readonly Filter[];
abstract getFilters(): Filter[];

abstract getFirstInequalityField(): FieldPath | null;
}
Expand Down Expand Up @@ -702,7 +705,7 @@ export class FieldFilter extends Filter {
return [this];
}

getFilters(): readonly Filter[] {
getFilters(): Filter[] {
return [this];
}

Expand Down Expand Up @@ -753,8 +756,9 @@ export class CompositeFilter extends Filter {
return this.memoizedFlattenedFilters;
}

getFilters(): readonly Filter[] {
return this.filters;
// Returns a mutable copy of `this.filters`
getFilters(): Filter[] {
return Object.assign([], this.filters);
}

getFirstInequalityField(): FieldPath | null {
Expand Down Expand Up @@ -782,12 +786,27 @@ export class CompositeFilter extends Filter {
}
}

// TODO(orquery) move compositeFilterWithAddedFilters to filter.ts in future refactor
export function compositeFilterWithAddedFilters(
MarkDuckworth marked this conversation as resolved.
Show resolved Hide resolved
compositeFilter: CompositeFilter,
otherFilters: Filter[]
): CompositeFilter {
const mergedFilters = compositeFilter.filters.concat(otherFilters);
return CompositeFilter.create(mergedFilters, CompositeOperator.AND);
}

export function compositeFilterIsConjunction(
compositeFilter: CompositeFilter
): boolean {
return compositeFilter.op === CompositeOperator.AND;
}

export function compositeFilterIsDisjunction(
compositeFilter: CompositeFilter
): boolean {
return compositeFilter.op === CompositeOperator.OR;
}

/**
* Returns true if this filter is a conjunction of field filters only. Returns false otherwise.
*/
Expand Down Expand Up @@ -881,9 +900,28 @@ export function compositeFilterEquals(
/** Returns a debug description for `filter`. */
export function stringifyFilter(filter: Filter): string {
debugAssert(
filter instanceof FieldFilter,
'stringifyFilter() only supports FieldFilters'
filter instanceof FieldFilter || filter instanceof CompositeFilter,
'stringifyFilter() only supports FieldFilters and CompositeFilters'
);
if (filter instanceof FieldFilter) {
return stringifyFieldFilter(filter);
} else if (filter instanceof CompositeFilter) {
return stringifyCompositeFilter(filter);
} else {
return 'Filter';
}
}

export function stringifyCompositeFilter(filter: CompositeFilter): string {
return (
filter.op.toString() +
` {` +
filter.getFilters().map(stringifyFilter).join(' ,') +
'}'
);
}

export function stringifyFieldFilter(filter: FieldFilter): string {
return `${filter.field.canonicalString()} ${filter.op} ${canonicalId(
filter.value
)}`;
Expand Down
5 changes: 4 additions & 1 deletion packages/firestore/src/lite-api/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,10 @@ export function validateQueryFilterConstraint(
functionName: string,
queryConstraint: QueryConstraint
): void {
if (!(queryConstraint instanceof QueryFilterConstraint)) {
if (
!(queryConstraint instanceof QueryFilterConstraint) &&
!(queryConstraint instanceof QueryCompositeFilterConstraint)
) {
throw new FirestoreError(
Code.INVALID_ARGUMENT,
`Function ${functionName}() requires QueryContraints created with a call to 'where(...)'.`
Expand Down
79 changes: 58 additions & 21 deletions packages/firestore/src/local/indexeddb_index_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@ import { DatabaseId } from '../core/database_info';
import {
Bound,
canonifyTarget,
CompositeFilter,
CompositeOperator,
FieldFilter,
Filter,
newTarget,
Operator,
Target,
targetEquals,
targetGetArrayValues,
targetGetLowerBound,
targetGetNotInValues,
targetGetSegmentCount,
targetGetUpperBound
targetGetUpperBound,
targetHasLimit
} from '../core/target';
import { FirestoreIndexValueWriter } from '../index/firestore_index_value_writer';
import { IndexByteEncoder } from '../index/index_byte_encoder';
Expand All @@ -53,6 +58,7 @@ import { isArray, refValue } from '../model/values';
import { Value as ProtoValue } from '../protos/firestore_proto_api';
import { debugAssert, fail, hardAssert } from '../util/assert';
import { logDebug } from '../util/log';
import { getDnfTerms } from '../util/logic_utils';
import { immediateSuccessor, primitiveComparator } from '../util/misc';
import { ObjectMap } from '../util/obj_map';
import { diffSortedSets, SortedSet } from '../util/sorted_set';
Expand Down Expand Up @@ -333,8 +339,28 @@ export class IndexedDbIndexManager implements IndexManager {
if (subTargets) {
return subTargets;
}
// TODO(orquery): Implement DNF transform
subTargets = [target];

if (target.filters.length === 0) {
subTargets = [target];
} else {
// There is an implicit AND operation between all the filters stored in the target
const dnf: Filter[] = getDnfTerms(
CompositeFilter.create(target.filters, CompositeOperator.AND)
);

subTargets = dnf.map(term =>
newTarget(
target.path,
target.collectionGroup,
target.orderBy,
term.getFilters(),
target.limit,
target.startAt,
target.endAt
)
);
}

this.targetToDnfSubTargets.set(target, subTargets);
return subTargets;
}
Expand Down Expand Up @@ -459,21 +485,32 @@ export class IndexedDbIndexManager implements IndexManager {
target: Target
): PersistencePromise<IndexType> {
let indexType = IndexType.FULL;
return PersistencePromise.forEach(
this.getSubTargets(target),
(target: Target) => {
return this.getFieldIndex(transaction, target).next(index => {
if (!index) {
indexType = IndexType.NONE;
} else if (
indexType !== IndexType.NONE &&
index.fields.length < targetGetSegmentCount(target)
) {
indexType = IndexType.PARTIAL;
}
});
const subTargets = this.getSubTargets(target);
return PersistencePromise.forEach(subTargets, (target: Target) => {
return this.getFieldIndex(transaction, target).next(index => {
if (!index) {
indexType = IndexType.NONE;
} else if (
indexType !== IndexType.NONE &&
index.fields.length < targetGetSegmentCount(target)
) {
indexType = IndexType.PARTIAL;
}
});
}).next(() => {
// OR queries have more than one sub-target (one sub-target per DNF term). We currently consider
// OR queries that have a `limit` to have a partial index. For such queries we perform sorting
// and apply the limit in memory as a post-processing step.
if (
targetHasLimit(target) &&
subTargets.length > 1 &&
indexType === IndexType.FULL
) {
return IndexType.PARTIAL;
}
).next(() => indexType);

return indexType;
});
}

/**
Expand Down Expand Up @@ -533,18 +570,18 @@ export class IndexedDbIndexManager implements IndexManager {
private encodeValues(
fieldIndex: FieldIndex,
target: Target,
bound: ProtoValue[] | null
values: ProtoValue[] | null
): Uint8Array[] {
if (bound === null) {
if (values === null) {
return [];
}

let encoders: IndexByteEncoder[] = [];
encoders.push(new IndexByteEncoder());

let boundIdx = 0;
let valueIdx = 0;
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
const value = bound[boundIdx++];
const value = values[valueIdx++];
for (const encoder of encoders) {
if (this.isInFilter(target, segment.fieldPath) && isArray(value)) {
encoders = this.expandIndexValues(encoders, segment, value);
Expand Down
11 changes: 2 additions & 9 deletions packages/firestore/src/local/query_engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
LimitType,
newQueryComparator,
Query,
queryContainsCompositeFilters,
queryMatches,
queryMatchesAllDocuments,
queryToTarget,
Expand Down Expand Up @@ -134,10 +133,7 @@ export class QueryEngine {
transaction: PersistenceTransaction,
query: Query
): PersistencePromise<DocumentMap | null> {

if (
queryMatchesAllDocuments(query)
) {
if (queryMatchesAllDocuments(query)) {
// Queries that match all documents don't benefit from using
// key-based lookups. It is more efficient to scan all documents in a
// collection, rather than to perform individual lookups.
Expand Down Expand Up @@ -226,10 +222,7 @@ export class QueryEngine {
remoteKeys: DocumentKeySet,
lastLimboFreeSnapshotVersion: SnapshotVersion
): PersistencePromise<DocumentMap> {
if (
queryMatchesAllDocuments(query) ||
queryContainsCompositeFilters(query)
) {
if (queryMatchesAllDocuments(query)) {
// Queries that match all documents don't benefit from using
// key-based lookups. It is more efficient to scan all documents in a
// collection, rather than to perform individual lookups.
Expand Down
Loading