Skip to content

Commit 36cce3b

Browse files
Port QueryEngine
This ports the query engine changes for indexed query execution from Android
1 parent 95c59fe commit 36cce3b

14 files changed

+452
-82
lines changed

packages/firestore/src/core/query.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export function asCollectionQueryAtPath(
155155
* Returns true if this query does not specify any query constraints that
156156
* could remove results.
157157
*/
158-
export function matchesAllDocuments(query: Query): boolean {
158+
export function queryMatchesAllDocuments(query: Query): boolean {
159159
return (
160160
query.filters.length === 0 &&
161161
query.limit === null &&
@@ -393,7 +393,7 @@ export function queryWithAddedOrderBy(query: Query, orderBy: OrderBy): Query {
393393

394394
export function queryWithLimit(
395395
query: Query,
396-
limit: number,
396+
limit: number | null,
397397
limitType: LimitType
398398
): Query {
399399
return new QueryImpl(

packages/firestore/src/local/index_manager.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,14 @@ export interface IndexManager {
155155
transaction: PersistenceTransaction,
156156
documents: DocumentMap
157157
): PersistencePromise<void>;
158+
159+
/**
160+
* Iterates over all field indexes that are used to serve the given target,
161+
* and returns the minimum offset of them all. Asserts that the target can be
162+
* served from index.
163+
*/
164+
getMinOffset(
165+
transaction: PersistenceTransaction,
166+
target: Target
167+
): PersistencePromise<IndexOffset>;
158168
}

packages/firestore/src/local/indexeddb_index_manager.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,16 @@ export class IndexedDbIndexManager implements IndexManager {
972972
}
973973
return ranges;
974974
}
975+
976+
getMinOffset(
977+
transaction: PersistenceTransaction,
978+
target: Target
979+
): PersistencePromise<IndexOffset> {
980+
// TODO(orqueries): Get the minimum offset for all subqueries
981+
return this.getFieldIndex(transaction, target).next(index =>
982+
index ? index.indexState.offset : IndexOffset.min()
983+
);
984+
}
975985
}
976986

977987
/**

packages/firestore/src/local/indexeddb_schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { EncodedResourcePath } from './encoded_resource_path';
2929
import { DbTimestampKey } from './indexeddb_sentinels';
3030

3131
// TODO(indexing): Remove this constant
32-
const INDEXING_ENABLED = false;
32+
export const INDEXING_ENABLED = false;
3333

3434
export const INDEXING_SCHEMA_VERSION = 14;
3535

packages/firestore/src/local/local_documents_view.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class LocalDocumentsView {
5252
constructor(
5353
readonly remoteDocumentCache: RemoteDocumentCache,
5454
readonly mutationQueue: MutationQueue,
55-
readonly indexManager: IndexManager
55+
private readonly indexManager: IndexManager
5656
) {}
5757

5858
/**

packages/firestore/src/local/local_store_impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ class LocalStoreImpl implements LocalStore {
203203
this.indexManager
204204
);
205205
this.remoteDocuments.setIndexManager(this.indexManager);
206-
this.queryEngine.setLocalDocumentsView(this.localDocuments);
206+
this.queryEngine.initialize(this.localDocuments, this.indexManager);
207207
}
208208

209209
collectGarbage(garbageCollector: LruGarbageCollector): Promise<LruResults> {

packages/firestore/src/local/memory_index_manager.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ export class MemoryIndexManager implements IndexManager {
9797
return PersistencePromise.resolve<string | null>(null);
9898
}
9999

100+
getMinOffset(
101+
transaction: PersistenceTransaction,
102+
target: Target
103+
): PersistencePromise<IndexOffset> {
104+
return PersistencePromise.resolve(IndexOffset.min());
105+
}
106+
100107
updateCollectionGroup(
101108
transaction: PersistenceTransaction,
102109
collectionGroup: string,

packages/firestore/src/local/query_engine.ts

Lines changed: 189 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,62 @@ import {
1919
hasLimitToFirst,
2020
hasLimitToLast,
2121
LimitType,
22-
matchesAllDocuments,
2322
newQueryComparator,
2423
Query,
2524
queryMatches,
25+
queryMatchesAllDocuments,
26+
queryToTarget,
27+
queryWithLimit,
2628
stringifyQuery
2729
} from '../core/query';
2830
import { SnapshotVersion } from '../core/snapshot_version';
29-
import { DocumentKeySet, DocumentMap } from '../model/collections';
31+
import {
32+
documentKeySet,
33+
DocumentKeySet,
34+
DocumentMap
35+
} from '../model/collections';
3036
import { Document } from '../model/document';
3137
import {
3238
IndexOffset,
3339
INITIAL_LARGEST_BATCH_ID,
3440
newIndexOffsetSuccessorFromReadTime
3541
} from '../model/field_index';
3642
import { debugAssert } from '../util/assert';
37-
import { getLogLevel, LogLevel, logDebug } from '../util/log';
43+
import { getLogLevel, logDebug, LogLevel } from '../util/log';
44+
import { ValueIterable, values } from '../util/misc';
3845
import { SortedSet } from '../util/sorted_set';
3946

47+
import { IndexManager, IndexType } from './index_manager';
48+
import { INDEXING_ENABLED } from './indexeddb_schema';
4049
import { LocalDocumentsView } from './local_documents_view';
4150
import { PersistencePromise } from './persistence_promise';
4251
import { PersistenceTransaction } from './persistence_transaction';
4352

4453
/**
45-
* A query engine that takes advantage of the target document mapping in the
46-
* QueryCache. Query execution is optimized by only reading the documents that
47-
* previously matched a query plus any documents that were edited after the
48-
* query was last listened to.
54+
* The Firestore query engine.
55+
*
56+
* Firestore queries can be executed in three modes. The Query Engine determines
57+
* what mode to use based on what data is persisted. The mode only determines
58+
* the runtime complexity of the query - the result set is equivalent across all
59+
* implementations.
60+
*
61+
* The Query engine will use indexed-based execution if a user has configured
62+
* any index that can be used to execute query (via `setIndexConfiguration()`).
63+
* Otherwise, the engine will try to optimize the query by re-using a previously
64+
* persisted query result. If that is not possible, the query will be executed
65+
* via a full collection scan.
66+
*
67+
* Index-based execution is the default when available. The query engine
68+
* supports partial indexed execution and merges the result from the index
69+
* lookup with documents that have not yet been indexed. The index evaluation
70+
* matches the backend's format and as such, the SDK can use indexing for all
71+
* queries that the backend supports.
72+
*
73+
* If no index exists, the query engine tries to take advantage of the target
74+
* document mapping in the TargetCache. These mappings exists for all queries
75+
* that have been synced with the backend at least once and allow the query
76+
* engine to only read documents that previously matched a query plus any
77+
* documents that were edited after the query was last listened to.
4978
*
5079
* There are some cases when this optimization is not guaranteed to produce
5180
* the same results as full collection scans. In these cases, query
@@ -60,11 +89,18 @@ import { PersistenceTransaction } from './persistence_transaction';
6089
* - Queries that have never been CURRENT or free of limbo documents.
6190
*/
6291
export class QueryEngine {
63-
private localDocumentsView: LocalDocumentsView | undefined;
92+
private localDocumentsView!: LocalDocumentsView;
93+
private indexManager!: IndexManager;
94+
private initialized = false;
6495

6596
/** Sets the document view to query against. */
66-
setLocalDocumentsView(localDocuments: LocalDocumentsView): void {
97+
initialize(
98+
localDocuments: LocalDocumentsView,
99+
indexManager: IndexManager
100+
): void {
67101
this.localDocumentsView = localDocuments;
102+
this.indexManager = indexManager;
103+
this.initialized = true;
68104
}
69105

70106
/** Returns all local documents matching the specified query. */
@@ -74,15 +110,128 @@ export class QueryEngine {
74110
lastLimboFreeSnapshotVersion: SnapshotVersion,
75111
remoteKeys: DocumentKeySet
76112
): PersistencePromise<DocumentMap> {
77-
debugAssert(
78-
this.localDocumentsView !== undefined,
79-
'setLocalDocumentsView() not called'
80-
);
113+
debugAssert(this.initialized, 'initialize() not called');
114+
115+
return this.performQueryUsingIndex(transaction, query)
116+
.next(result => {
117+
if (result) {
118+
return result;
119+
} else {
120+
return this.performQueryUsingRemoteKeys(
121+
transaction,
122+
query,
123+
remoteKeys,
124+
lastLimboFreeSnapshotVersion
125+
);
126+
}
127+
})
128+
.next(result => {
129+
if (result) {
130+
return result;
131+
} else {
132+
return this.executeFullCollectionScan(transaction, query);
133+
}
134+
});
135+
}
136+
137+
/**
138+
* Performs an indexed query that evaluates the query based on a collection's persisted index
139+
* values. Returns {@code null} if an index is not available.
140+
*/
141+
private performQueryUsingIndex(
142+
transaction: PersistenceTransaction,
143+
query: Query
144+
): PersistencePromise<DocumentMap | null> {
145+
if (!INDEXING_ENABLED) {
146+
return PersistencePromise.resolve<DocumentMap | null>(null);
147+
}
148+
149+
if (queryMatchesAllDocuments(query)) {
150+
// Don't use indexes for queries that can be executed by scanning the
151+
// collection.
152+
return PersistencePromise.resolve<DocumentMap | null>(null);
153+
}
154+
155+
let target = queryToTarget(query);
156+
return this.indexManager
157+
.getIndexType(transaction, target)
158+
.next(indexType => {
159+
if (indexType === IndexType.NONE) {
160+
// The target cannot be served from any index.
161+
return null;
162+
}
81163

164+
if (indexType === IndexType.PARTIAL) {
165+
// We cannot apply a limit for targets that are served using a partial
166+
// index. If a partial index will be used to serve the target, the
167+
// query may return a superset of documents that match the target
168+
// (e.g. if the index doesn't include all the target's filters), or
169+
// may return the correct set of documents in the wrong order (e.g. if
170+
// the index doesn't include a segment for one of the orderBys).
171+
// Therefore, a limit should not be applied in such cases.
172+
query = queryWithLimit(query, null, LimitType.First);
173+
target = queryToTarget(query);
174+
}
175+
176+
return this.indexManager
177+
.getDocumentsMatchingTarget(transaction, target)
178+
.next(keys => {
179+
debugAssert(
180+
!!keys,
181+
'Index manager must return results for partial and full indexes.'
182+
);
183+
const sortedKeys = documentKeySet(...keys);
184+
return this.localDocumentsView
185+
.getDocuments(transaction, sortedKeys)
186+
.next(indexedDocuments => {
187+
return this.indexManager
188+
.getMinOffset(transaction, target)
189+
.next(offset => {
190+
const previousResults = this.applyQuery(
191+
query,
192+
indexedDocuments
193+
);
194+
195+
if (
196+
(hasLimitToFirst(query) || hasLimitToLast(query)) &&
197+
this.needsRefill(
198+
query.limitType,
199+
previousResults,
200+
sortedKeys,
201+
offset.readTime
202+
)
203+
) {
204+
return PersistencePromise.resolve<DocumentMap | null>(
205+
null
206+
);
207+
}
208+
209+
return this.appendRemainingResults(
210+
transaction,
211+
values(indexedDocuments),
212+
query,
213+
offset
214+
) as PersistencePromise<DocumentMap | null>;
215+
});
216+
});
217+
});
218+
});
219+
}
220+
221+
/**
222+
* Performs a query based on the target's persisted query mapping. Returns
223+
* `null` if the mapping is not available or cannot be used.
224+
*/
225+
private performQueryUsingRemoteKeys(
226+
transaction: PersistenceTransaction,
227+
query: Query,
228+
remoteKeys: DocumentKeySet,
229+
lastLimboFreeSnapshotVersion: SnapshotVersion
230+
): PersistencePromise<DocumentMap> {
82231
// Queries that match all documents don't benefit from using
83232
// key-based lookups. It is more efficient to scan all documents in a
84233
// collection, rather than to perform individual lookups.
85-
if (matchesAllDocuments(query)) {
234+
if (queryMatchesAllDocuments(query)) {
86235
return this.executeFullCollectionScan(transaction, query);
87236
}
88237

@@ -119,22 +268,15 @@ export class QueryEngine {
119268

120269
// Retrieve all results for documents that were updated since the last
121270
// limbo-document free remote snapshot.
122-
return this.localDocumentsView!.getDocumentsMatchingQuery(
271+
return this.appendRemainingResults(
123272
transaction,
273+
previousResults,
124274
query,
125275
newIndexOffsetSuccessorFromReadTime(
126276
lastLimboFreeSnapshotVersion,
127277
INITIAL_LARGEST_BATCH_ID
128278
)
129-
).next(updatedResults => {
130-
// We merge `previousResults` into `updateResults`, since
131-
// `updateResults` is already a DocumentMap. If a document is
132-
// contained in both lists, then its contents are the same.
133-
previousResults.forEach(doc => {
134-
updatedResults = updatedResults.insert(doc.key, doc);
135-
});
136-
return updatedResults;
137-
});
279+
);
138280
}
139281
);
140282
}
@@ -159,6 +301,7 @@ export class QueryEngine {
159301
* Determines if a limit query needs to be refilled from cache, making it
160302
* ineligible for index-free execution.
161303
*
304+
* @param limitType The limit type used by the query.
162305
* @param sortedPreviousResults - The documents that matched the query when it
163306
* was last synchronized, sorted by the query's comparator.
164307
* @param remoteKeys - The document keys that matched the query at the last
@@ -218,4 +361,26 @@ export class QueryEngine {
218361
IndexOffset.min()
219362
);
220363
}
364+
365+
/**
366+
* Combines the results from an indexed execution with the remaining documents
367+
* that have not yet been indexed.
368+
*/
369+
private appendRemainingResults(
370+
transaction: PersistenceTransaction,
371+
indexedResults: ValueIterable<Document>,
372+
query: Query,
373+
offset: IndexOffset
374+
): PersistencePromise<DocumentMap> {
375+
// Retrieve all results for documents that were updated since the offset.
376+
return this.localDocumentsView
377+
.getDocumentsMatchingQuery(transaction, query, offset)
378+
.next(remainingResults => {
379+
// Merge with existing results
380+
indexedResults.forEach(d => {
381+
remainingResults = remainingResults.insert(d.key, d);
382+
});
383+
return remainingResults;
384+
});
385+
}
221386
}

packages/firestore/src/model/collections.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ export type DocumentMap = SortedMap<DocumentKey, Document>;
4646
const EMPTY_DOCUMENT_MAP = new SortedMap<DocumentKey, Document>(
4747
DocumentKey.comparator
4848
);
49-
export function documentMap(): DocumentMap {
50-
return EMPTY_DOCUMENT_MAP;
49+
export function documentMap(...docs: Document[]): DocumentMap {
50+
let map = EMPTY_DOCUMENT_MAP;
51+
for (const doc of docs) {
52+
map = map.insert(doc.key, doc);
53+
}
54+
return map;
5155
}
5256

5357
export type OverlayMap = ObjectMap<DocumentKey, Overlay>;

0 commit comments

Comments
 (0)