@@ -19,33 +19,62 @@ import {
19
19
hasLimitToFirst ,
20
20
hasLimitToLast ,
21
21
LimitType ,
22
- matchesAllDocuments ,
23
22
newQueryComparator ,
24
23
Query ,
25
24
queryMatches ,
25
+ queryMatchesAllDocuments ,
26
+ queryToTarget ,
27
+ queryWithLimit ,
26
28
stringifyQuery
27
29
} from '../core/query' ;
28
30
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' ;
30
36
import { Document } from '../model/document' ;
31
37
import {
32
38
IndexOffset ,
33
39
INITIAL_LARGEST_BATCH_ID ,
34
40
newIndexOffsetSuccessorFromReadTime
35
41
} from '../model/field_index' ;
36
42
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' ;
38
45
import { SortedSet } from '../util/sorted_set' ;
39
46
47
+ import { IndexManager , IndexType } from './index_manager' ;
48
+ import { INDEXING_ENABLED } from './indexeddb_schema' ;
40
49
import { LocalDocumentsView } from './local_documents_view' ;
41
50
import { PersistencePromise } from './persistence_promise' ;
42
51
import { PersistenceTransaction } from './persistence_transaction' ;
43
52
44
53
/**
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.
49
78
*
50
79
* There are some cases when this optimization is not guaranteed to produce
51
80
* the same results as full collection scans. In these cases, query
@@ -60,11 +89,18 @@ import { PersistenceTransaction } from './persistence_transaction';
60
89
* - Queries that have never been CURRENT or free of limbo documents.
61
90
*/
62
91
export class QueryEngine {
63
- private localDocumentsView : LocalDocumentsView | undefined ;
92
+ private localDocumentsView ! : LocalDocumentsView ;
93
+ private indexManager ! : IndexManager ;
94
+ private initialized = false ;
64
95
65
96
/** Sets the document view to query against. */
66
- setLocalDocumentsView ( localDocuments : LocalDocumentsView ) : void {
97
+ initialize (
98
+ localDocuments : LocalDocumentsView ,
99
+ indexManager : IndexManager
100
+ ) : void {
67
101
this . localDocumentsView = localDocuments ;
102
+ this . indexManager = indexManager ;
103
+ this . initialized = true ;
68
104
}
69
105
70
106
/** Returns all local documents matching the specified query. */
@@ -74,15 +110,128 @@ export class QueryEngine {
74
110
lastLimboFreeSnapshotVersion : SnapshotVersion ,
75
111
remoteKeys : DocumentKeySet
76
112
) : 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
+ }
81
163
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 > {
82
231
// Queries that match all documents don't benefit from using
83
232
// key-based lookups. It is more efficient to scan all documents in a
84
233
// collection, rather than to perform individual lookups.
85
- if ( matchesAllDocuments ( query ) ) {
234
+ if ( queryMatchesAllDocuments ( query ) ) {
86
235
return this . executeFullCollectionScan ( transaction , query ) ;
87
236
}
88
237
@@ -119,22 +268,15 @@ export class QueryEngine {
119
268
120
269
// Retrieve all results for documents that were updated since the last
121
270
// limbo-document free remote snapshot.
122
- return this . localDocumentsView ! . getDocumentsMatchingQuery (
271
+ return this . appendRemainingResults (
123
272
transaction ,
273
+ previousResults ,
124
274
query ,
125
275
newIndexOffsetSuccessorFromReadTime (
126
276
lastLimboFreeSnapshotVersion ,
127
277
INITIAL_LARGEST_BATCH_ID
128
278
)
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
+ ) ;
138
280
}
139
281
) ;
140
282
}
@@ -159,6 +301,7 @@ export class QueryEngine {
159
301
* Determines if a limit query needs to be refilled from cache, making it
160
302
* ineligible for index-free execution.
161
303
*
304
+ * @param limitType The limit type used by the query.
162
305
* @param sortedPreviousResults - The documents that matched the query when it
163
306
* was last synchronized, sorted by the query's comparator.
164
307
* @param remoteKeys - The document keys that matched the query at the last
@@ -218,4 +361,26 @@ export class QueryEngine {
218
361
IndexOffset . min ( )
219
362
) ;
220
363
}
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
+ }
221
386
}
0 commit comments