@@ -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' ;
2830import { SnapshotVersion } from '../core/snapshot_version' ;
29- import { DocumentKeySet , DocumentMap } from '../model/collections' ;
31+ import {
32+ documentKeySet ,
33+ DocumentKeySet ,
34+ DocumentMap
35+ } from '../model/collections' ;
3036import { Document } from '../model/document' ;
3137import {
3238 IndexOffset ,
3339 INITIAL_LARGEST_BATCH_ID ,
3440 newIndexOffsetSuccessorFromReadTime
3541} from '../model/field_index' ;
3642import { 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' ;
3845import { SortedSet } from '../util/sorted_set' ;
3946
47+ import { IndexManager , IndexType } from './index_manager' ;
48+ import { INDEXING_ENABLED } from './indexeddb_schema' ;
4049import { LocalDocumentsView } from './local_documents_view' ;
4150import { PersistencePromise } from './persistence_promise' ;
4251import { 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 */
6291export 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,122 @@ 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+ result
118+ ? result
119+ : this . performQueryUsingRemoteKeys (
120+ transaction ,
121+ query ,
122+ remoteKeys ,
123+ lastLimboFreeSnapshotVersion
124+ )
125+ )
126+ . next ( result =>
127+ result ? result : this . executeFullCollectionScan ( transaction , query )
128+ ) ;
129+ }
130+
131+ /**
132+ * Performs an indexed query that evaluates the query based on a collection's
133+ * persisted index values. Returns `null` if an index is not available.
134+ */
135+ private performQueryUsingIndex (
136+ transaction : PersistenceTransaction ,
137+ query : Query
138+ ) : PersistencePromise < DocumentMap | null > {
139+ if ( ! INDEXING_ENABLED ) {
140+ return PersistencePromise . resolve < DocumentMap | null > ( null ) ;
141+ }
142+
143+ if ( queryMatchesAllDocuments ( query ) ) {
144+ // Don't use indexes for queries that can be executed by scanning the
145+ // collection.
146+ return PersistencePromise . resolve < DocumentMap | null > ( null ) ;
147+ }
148+
149+ let target = queryToTarget ( query ) ;
150+ return this . indexManager
151+ . getIndexType ( transaction , target )
152+ . next ( indexType => {
153+ if ( indexType === IndexType . NONE ) {
154+ // The target cannot be served from any index.
155+ return null ;
156+ }
157+
158+ if ( indexType === IndexType . PARTIAL ) {
159+ // We cannot apply a limit for targets that are served using a partial
160+ // index. If a partial index will be used to serve the target, the
161+ // query may return a superset of documents that match the target
162+ // (e.g. if the index doesn't include all the target's filters), or
163+ // may return the correct set of documents in the wrong order (e.g. if
164+ // the index doesn't include a segment for one of the orderBys).
165+ // Therefore, a limit should not be applied in such cases.
166+ query = queryWithLimit ( query , null , LimitType . First ) ;
167+ target = queryToTarget ( query ) ;
168+ }
81169
170+ return this . indexManager
171+ . getDocumentsMatchingTarget ( transaction , target )
172+ . next ( keys => {
173+ debugAssert (
174+ ! ! keys ,
175+ 'Index manager must return results for partial and full indexes.'
176+ ) ;
177+ const sortedKeys = documentKeySet ( ...keys ) ;
178+ return this . localDocumentsView
179+ . getDocuments ( transaction , sortedKeys )
180+ . next ( indexedDocuments => {
181+ return this . indexManager
182+ . getMinOffset ( transaction , target )
183+ . next ( offset => {
184+ const previousResults = this . applyQuery (
185+ query ,
186+ indexedDocuments
187+ ) ;
188+
189+ if (
190+ ( hasLimitToFirst ( query ) || hasLimitToLast ( query ) ) &&
191+ this . needsRefill (
192+ query . limitType ,
193+ previousResults ,
194+ sortedKeys ,
195+ offset . readTime
196+ )
197+ ) {
198+ return PersistencePromise . resolve < DocumentMap | null > (
199+ null
200+ ) ;
201+ }
202+
203+ return this . appendRemainingResults (
204+ transaction ,
205+ values ( indexedDocuments ) ,
206+ query ,
207+ offset
208+ ) as PersistencePromise < DocumentMap | null > ;
209+ } ) ;
210+ } ) ;
211+ } ) ;
212+ } ) ;
213+ }
214+
215+ /**
216+ * Performs a query based on the target's persisted query mapping. Returns
217+ * `null` if the mapping is not available or cannot be used.
218+ */
219+ private performQueryUsingRemoteKeys (
220+ transaction : PersistenceTransaction ,
221+ query : Query ,
222+ remoteKeys : DocumentKeySet ,
223+ lastLimboFreeSnapshotVersion : SnapshotVersion
224+ ) : PersistencePromise < DocumentMap > {
82225 // Queries that match all documents don't benefit from using
83226 // key-based lookups. It is more efficient to scan all documents in a
84227 // collection, rather than to perform individual lookups.
85- if ( matchesAllDocuments ( query ) ) {
228+ if ( queryMatchesAllDocuments ( query ) ) {
86229 return this . executeFullCollectionScan ( transaction , query ) ;
87230 }
88231
@@ -119,22 +262,15 @@ export class QueryEngine {
119262
120263 // Retrieve all results for documents that were updated since the last
121264 // limbo-document free remote snapshot.
122- return this . localDocumentsView ! . getDocumentsMatchingQuery (
265+ return this . appendRemainingResults (
123266 transaction ,
267+ previousResults ,
124268 query ,
125269 newIndexOffsetSuccessorFromReadTime (
126270 lastLimboFreeSnapshotVersion ,
127271 INITIAL_LARGEST_BATCH_ID
128272 )
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- } ) ;
273+ ) ;
138274 }
139275 ) ;
140276 }
@@ -159,6 +295,7 @@ export class QueryEngine {
159295 * Determines if a limit query needs to be refilled from cache, making it
160296 * ineligible for index-free execution.
161297 *
298+ * @param limitType The limit type used by the query.
162299 * @param sortedPreviousResults - The documents that matched the query when it
163300 * was last synchronized, sorted by the query's comparator.
164301 * @param remoteKeys - The document keys that matched the query at the last
@@ -218,4 +355,26 @@ export class QueryEngine {
218355 IndexOffset . min ( )
219356 ) ;
220357 }
358+
359+ /**
360+ * Combines the results from an indexed execution with the remaining documents
361+ * that have not yet been indexed.
362+ */
363+ private appendRemainingResults (
364+ transaction : PersistenceTransaction ,
365+ indexedResults : ValueIterable < Document > ,
366+ query : Query ,
367+ offset : IndexOffset
368+ ) : PersistencePromise < DocumentMap > {
369+ // Retrieve all results for documents that were updated since the offset.
370+ return this . localDocumentsView
371+ . getDocumentsMatchingQuery ( transaction , query , offset )
372+ . next ( remainingResults => {
373+ // Merge with existing results
374+ indexedResults . forEach ( d => {
375+ remainingResults = remainingResults . insert ( d . key , d ) ;
376+ } ) ;
377+ return remainingResults ;
378+ } ) ;
379+ }
221380}
0 commit comments