@@ -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,122 @@ 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
+ 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
+ }
81
169
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 > {
82
225
// Queries that match all documents don't benefit from using
83
226
// key-based lookups. It is more efficient to scan all documents in a
84
227
// collection, rather than to perform individual lookups.
85
- if ( matchesAllDocuments ( query ) ) {
228
+ if ( queryMatchesAllDocuments ( query ) ) {
86
229
return this . executeFullCollectionScan ( transaction , query ) ;
87
230
}
88
231
@@ -119,22 +262,15 @@ export class QueryEngine {
119
262
120
263
// Retrieve all results for documents that were updated since the last
121
264
// limbo-document free remote snapshot.
122
- return this . localDocumentsView ! . getDocumentsMatchingQuery (
265
+ return this . appendRemainingResults (
123
266
transaction ,
267
+ previousResults ,
124
268
query ,
125
269
newIndexOffsetSuccessorFromReadTime (
126
270
lastLimboFreeSnapshotVersion ,
127
271
INITIAL_LARGEST_BATCH_ID
128
272
)
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
+ ) ;
138
274
}
139
275
) ;
140
276
}
@@ -159,6 +295,7 @@ export class QueryEngine {
159
295
* Determines if a limit query needs to be refilled from cache, making it
160
296
* ineligible for index-free execution.
161
297
*
298
+ * @param limitType The limit type used by the query.
162
299
* @param sortedPreviousResults - The documents that matched the query when it
163
300
* was last synchronized, sorted by the query's comparator.
164
301
* @param remoteKeys - The document keys that matched the query at the last
@@ -218,4 +355,26 @@ export class QueryEngine {
218
355
IndexOffset . min ( )
219
356
) ;
220
357
}
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
+ }
221
380
}
0 commit comments