@firebase/firestore 3.4.7 → 3.4.8-canary.44948ac91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/firestore/src/core/query.d.ts +2 -4
- package/dist/firestore/src/core/target.d.ts +2 -0
- package/dist/firestore/src/local/index_manager.d.ts +27 -5
- package/dist/firestore/src/local/indexeddb_index_manager.d.ts +15 -5
- package/dist/firestore/src/local/indexeddb_remote_document_cache.d.ts +9 -0
- package/dist/firestore/src/local/indexeddb_schema.d.ts +8 -2
- package/dist/firestore/src/local/indexeddb_sentinels.d.ts +9 -2
- package/dist/firestore/src/local/local_documents_view.d.ts +1 -1
- package/dist/firestore/src/local/memory_index_manager.d.ts +6 -4
- package/dist/firestore/src/local/query_engine.d.ts +44 -5
- package/dist/firestore/src/local/simple_db.d.ts +2 -0
- package/dist/firestore/src/model/collections.d.ts +1 -1
- package/dist/firestore/src/model/field_index.d.ts +6 -0
- package/dist/firestore/src/util/misc.d.ts +3 -0
- package/dist/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
- package/dist/firestore/test/unit/local/test_index_manager.d.ts +5 -4
- package/dist/firestore/test/unit/local/test_remote_document_cache.d.ts +0 -4
- package/dist/index.esm2017.js +4632 -4498
- package/dist/index.esm2017.js.map +1 -1
- package/dist/index.esm5.js +4989 -4864
- package/dist/index.esm5.js.map +1 -1
- package/dist/index.node.cjs.js +1743 -1547
- package/dist/index.node.cjs.js.map +1 -1
- package/dist/index.node.mjs +1743 -1547
- package/dist/index.node.mjs.map +1 -1
- package/dist/index.rn.js +4461 -4327
- package/dist/index.rn.js.map +1 -1
- package/dist/internal.d.ts +26 -4
- package/dist/lite/firestore/src/core/query.d.ts +2 -4
- package/dist/lite/firestore/src/core/target.d.ts +2 -0
- package/dist/lite/firestore/src/local/index_manager.d.ts +27 -5
- package/dist/lite/firestore/src/local/indexeddb_index_manager.d.ts +15 -5
- package/dist/lite/firestore/src/local/indexeddb_remote_document_cache.d.ts +9 -0
- package/dist/lite/firestore/src/local/indexeddb_schema.d.ts +8 -2
- package/dist/lite/firestore/src/local/indexeddb_sentinels.d.ts +9 -2
- package/dist/lite/firestore/src/local/local_documents_view.d.ts +1 -1
- package/dist/lite/firestore/src/local/memory_index_manager.d.ts +6 -4
- package/dist/lite/firestore/src/local/query_engine.d.ts +44 -5
- package/dist/lite/firestore/src/local/simple_db.d.ts +2 -0
- package/dist/lite/firestore/src/model/collections.d.ts +1 -1
- package/dist/lite/firestore/src/model/field_index.d.ts +6 -0
- package/dist/lite/firestore/src/util/misc.d.ts +3 -0
- package/dist/lite/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
- package/dist/lite/firestore/test/unit/local/test_index_manager.d.ts +5 -4
- package/dist/lite/firestore/test/unit/local/test_remote_document_cache.d.ts +0 -4
- package/dist/lite/index.browser.esm2017.js +385 -389
- package/dist/lite/index.browser.esm2017.js.map +1 -1
- package/dist/lite/index.browser.esm5.js +377 -381
- package/dist/lite/index.browser.esm5.js.map +1 -1
- package/dist/lite/index.node.cjs.js +7 -9
- package/dist/lite/index.node.cjs.js.map +1 -1
- package/dist/lite/index.node.mjs +7 -9
- package/dist/lite/index.node.mjs.map +1 -1
- package/dist/lite/index.rn.esm2017.js +385 -389
- package/dist/lite/index.rn.esm2017.js.map +1 -1
- package/dist/lite/packages/firestore/dist/lite/index.browser.esm2017.d.ts +95 -95
- package/dist/lite/packages/firestore/src/core/query.d.ts +2 -4
- package/dist/lite/packages/firestore/src/core/target.d.ts +2 -0
- package/dist/lite/packages/firestore/src/local/index_manager.d.ts +27 -5
- package/dist/lite/packages/firestore/src/local/indexeddb_index_manager.d.ts +15 -5
- package/dist/lite/packages/firestore/src/local/indexeddb_remote_document_cache.d.ts +9 -0
- package/dist/lite/packages/firestore/src/local/indexeddb_schema.d.ts +8 -2
- package/dist/lite/packages/firestore/src/local/indexeddb_sentinels.d.ts +9 -2
- package/dist/lite/packages/firestore/src/local/local_documents_view.d.ts +1 -1
- package/dist/lite/packages/firestore/src/local/memory_index_manager.d.ts +6 -4
- package/dist/lite/packages/firestore/src/local/query_engine.d.ts +44 -5
- package/dist/lite/packages/firestore/src/local/simple_db.d.ts +2 -0
- package/dist/lite/packages/firestore/src/model/collections.d.ts +1 -1
- package/dist/lite/packages/firestore/src/model/field_index.d.ts +6 -0
- package/dist/lite/packages/firestore/src/util/misc.d.ts +3 -0
- package/dist/lite/packages/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
- package/dist/lite/packages/firestore/test/unit/local/test_index_manager.d.ts +5 -4
- package/dist/lite/packages/firestore/test/unit/local/test_remote_document_cache.d.ts +0 -4
- package/dist/packages/firestore/dist/index.esm2017.d.ts +173 -173
- package/dist/packages/firestore/src/core/query.d.ts +2 -4
- package/dist/packages/firestore/src/core/target.d.ts +2 -0
- package/dist/packages/firestore/src/local/index_manager.d.ts +27 -5
- package/dist/packages/firestore/src/local/indexeddb_index_manager.d.ts +15 -5
- package/dist/packages/firestore/src/local/indexeddb_remote_document_cache.d.ts +9 -0
- package/dist/packages/firestore/src/local/indexeddb_schema.d.ts +8 -2
- package/dist/packages/firestore/src/local/indexeddb_sentinels.d.ts +9 -2
- package/dist/packages/firestore/src/local/local_documents_view.d.ts +1 -1
- package/dist/packages/firestore/src/local/memory_index_manager.d.ts +6 -4
- package/dist/packages/firestore/src/local/query_engine.d.ts +44 -5
- package/dist/packages/firestore/src/local/simple_db.d.ts +2 -0
- package/dist/packages/firestore/src/model/collections.d.ts +1 -1
- package/dist/packages/firestore/src/model/field_index.d.ts +6 -0
- package/dist/packages/firestore/src/util/misc.d.ts +3 -0
- package/dist/packages/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
- package/dist/packages/firestore/test/unit/local/test_index_manager.d.ts +5 -4
- package/dist/packages/firestore/test/unit/local/test_remote_document_cache.d.ts +0 -4
- package/dist/private.d.ts +26 -4
- package/package.json +9 -9
package/dist/index.node.cjs.js
CHANGED
|
@@ -33,7 +33,7 @@ var grpc__namespace = /*#__PURE__*/_interopNamespace(grpc);
|
|
|
33
33
|
var protoLoader__namespace = /*#__PURE__*/_interopNamespace(protoLoader);
|
|
34
34
|
|
|
35
35
|
const name = "@firebase/firestore";
|
|
36
|
-
const version$1 = "3.4.
|
|
36
|
+
const version$1 = "3.4.8-canary.44948ac91";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* @license
|
|
@@ -86,7 +86,7 @@ User.GOOGLE_CREDENTIALS = new User('google-credentials-uid');
|
|
|
86
86
|
User.FIRST_PARTY = new User('first-party-uid');
|
|
87
87
|
User.MOCK_USER = new User('mock-user');
|
|
88
88
|
|
|
89
|
-
const version = "9.6.
|
|
89
|
+
const version = "9.6.11-canary.44948ac91";
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* @license
|
|
@@ -1418,13 +1418,14 @@ const DbIndexEntryKeyPath = [
|
|
|
1418
1418
|
'uid',
|
|
1419
1419
|
'arrayValue',
|
|
1420
1420
|
'directionalValue',
|
|
1421
|
+
'orderedDocumentKey',
|
|
1421
1422
|
'documentKey'
|
|
1422
1423
|
];
|
|
1423
1424
|
const DbIndexEntryDocumentKeyIndex = 'documentKeyIndex';
|
|
1424
1425
|
const DbIndexEntryDocumentKeyIndexPath = [
|
|
1425
1426
|
'indexId',
|
|
1426
1427
|
'uid',
|
|
1427
|
-
'
|
|
1428
|
+
'orderedDocumentKey'
|
|
1428
1429
|
];
|
|
1429
1430
|
/** Name of the IndexedDb object store. */
|
|
1430
1431
|
const DbDocumentOverlayStore = 'documentOverlays';
|
|
@@ -2310,9 +2311,7 @@ class SimpleDbStore {
|
|
|
2310
2311
|
cursor.continue(controller.skipToKey);
|
|
2311
2312
|
}
|
|
2312
2313
|
};
|
|
2313
|
-
}).next(() =>
|
|
2314
|
-
return PersistencePromise.waitFor(results);
|
|
2315
|
-
});
|
|
2314
|
+
}).next(() => PersistencePromise.waitFor(results));
|
|
2316
2315
|
}
|
|
2317
2316
|
options(indexOrRange, range) {
|
|
2318
2317
|
let indexName = undefined;
|
|
@@ -4221,6 +4220,17 @@ function fieldIndexGetArraySegment(fieldIndex) {
|
|
|
4221
4220
|
function fieldIndexGetDirectionalSegments(fieldIndex) {
|
|
4222
4221
|
return fieldIndex.fields.filter(s => s.kind !== 2 /* CONTAINS */);
|
|
4223
4222
|
}
|
|
4223
|
+
/**
|
|
4224
|
+
* Returns the order of the document key component for the given index.
|
|
4225
|
+
*
|
|
4226
|
+
* PORTING NOTE: This is only used in the Web IndexedDb implementation.
|
|
4227
|
+
*/
|
|
4228
|
+
function fieldIndexGetKeyOrder(fieldIndex) {
|
|
4229
|
+
const directionalSegments = fieldIndexGetDirectionalSegments(fieldIndex);
|
|
4230
|
+
return directionalSegments.length === 0
|
|
4231
|
+
? 0 /* ASCENDING */
|
|
4232
|
+
: directionalSegments[directionalSegments.length - 1].kind;
|
|
4233
|
+
}
|
|
4224
4234
|
/** Returns a debug representation of the field index */
|
|
4225
4235
|
function fieldIndexToString(fieldIndex) {
|
|
4226
4236
|
return `id=${fieldIndex.indexId}|cg=${fieldIndex.collectionGroup}|f=${fieldIndex.fields.map(f => `${f.fieldPath}:${f.kind}`).join(',')}`;
|
|
@@ -4325,7 +4335,7 @@ function indexOffsetComparator(left, right) {
|
|
|
4325
4335
|
|
|
4326
4336
|
/**
|
|
4327
4337
|
* @license
|
|
4328
|
-
* Copyright
|
|
4338
|
+
* Copyright 2017 Google LLC
|
|
4329
4339
|
*
|
|
4330
4340
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4331
4341
|
* you may not use this file except in compliance with the License.
|
|
@@ -4339,1488 +4349,1656 @@ function indexOffsetComparator(left, right) {
|
|
|
4339
4349
|
* See the License for the specific language governing permissions and
|
|
4340
4350
|
* limitations under the License.
|
|
4341
4351
|
*/
|
|
4342
|
-
//
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
this.
|
|
4347
|
-
this.
|
|
4348
|
-
this.filters = filters;
|
|
4349
|
-
this.limit = limit;
|
|
4350
|
-
this.startAt = startAt;
|
|
4351
|
-
this.endAt = endAt;
|
|
4352
|
-
this.memoizedCanonicalId = null;
|
|
4352
|
+
// An immutable sorted map implementation, based on a Left-leaning Red-Black
|
|
4353
|
+
// tree.
|
|
4354
|
+
class SortedMap {
|
|
4355
|
+
constructor(comparator, root) {
|
|
4356
|
+
this.comparator = comparator;
|
|
4357
|
+
this.root = root ? root : LLRBNode.EMPTY;
|
|
4353
4358
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
* NOTE: you should always construct `Target` from `Query.toTarget` instead of
|
|
4360
|
-
* using this factory method, because `Query` provides an implicit `orderBy`
|
|
4361
|
-
* property.
|
|
4362
|
-
*/
|
|
4363
|
-
function newTarget(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
|
|
4364
|
-
return new TargetImpl(path, collectionGroup, orderBy, filters, limit, startAt, endAt);
|
|
4365
|
-
}
|
|
4366
|
-
function canonifyTarget(target) {
|
|
4367
|
-
const targetImpl = debugCast(target);
|
|
4368
|
-
if (targetImpl.memoizedCanonicalId === null) {
|
|
4369
|
-
let str = targetImpl.path.canonicalString();
|
|
4370
|
-
if (targetImpl.collectionGroup !== null) {
|
|
4371
|
-
str += '|cg:' + targetImpl.collectionGroup;
|
|
4372
|
-
}
|
|
4373
|
-
str += '|f:';
|
|
4374
|
-
str += targetImpl.filters.map(f => canonifyFilter(f)).join(',');
|
|
4375
|
-
str += '|ob:';
|
|
4376
|
-
str += targetImpl.orderBy.map(o => canonifyOrderBy(o)).join(',');
|
|
4377
|
-
if (!isNullOrUndefined(targetImpl.limit)) {
|
|
4378
|
-
str += '|l:';
|
|
4379
|
-
str += targetImpl.limit;
|
|
4380
|
-
}
|
|
4381
|
-
if (targetImpl.startAt) {
|
|
4382
|
-
str += '|lb:';
|
|
4383
|
-
str += targetImpl.startAt.inclusive ? 'b:' : 'a:';
|
|
4384
|
-
str += targetImpl.startAt.position.map(p => canonicalId(p)).join(',');
|
|
4385
|
-
}
|
|
4386
|
-
if (targetImpl.endAt) {
|
|
4387
|
-
str += '|ub:';
|
|
4388
|
-
str += targetImpl.endAt.inclusive ? 'a:' : 'b:';
|
|
4389
|
-
str += targetImpl.endAt.position.map(p => canonicalId(p)).join(',');
|
|
4390
|
-
}
|
|
4391
|
-
targetImpl.memoizedCanonicalId = str;
|
|
4359
|
+
// Returns a copy of the map, with the specified key/value added or replaced.
|
|
4360
|
+
insert(key, value) {
|
|
4361
|
+
return new SortedMap(this.comparator, this.root
|
|
4362
|
+
.insert(key, value, this.comparator)
|
|
4363
|
+
.copy(null, null, LLRBNode.BLACK, null, null));
|
|
4392
4364
|
}
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
str += ' collectionGroup=' + target.collectionGroup;
|
|
4365
|
+
// Returns a copy of the map, with the specified key removed.
|
|
4366
|
+
remove(key) {
|
|
4367
|
+
return new SortedMap(this.comparator, this.root
|
|
4368
|
+
.remove(key, this.comparator)
|
|
4369
|
+
.copy(null, null, LLRBNode.BLACK, null, null));
|
|
4399
4370
|
}
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4371
|
+
// Returns the value of the node with the given key, or null.
|
|
4372
|
+
get(key) {
|
|
4373
|
+
let node = this.root;
|
|
4374
|
+
while (!node.isEmpty()) {
|
|
4375
|
+
const cmp = this.comparator(key, node.key);
|
|
4376
|
+
if (cmp === 0) {
|
|
4377
|
+
return node.value;
|
|
4378
|
+
}
|
|
4379
|
+
else if (cmp < 0) {
|
|
4380
|
+
node = node.left;
|
|
4381
|
+
}
|
|
4382
|
+
else if (cmp > 0) {
|
|
4383
|
+
node = node.right;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
return null;
|
|
4404
4387
|
}
|
|
4405
|
-
if
|
|
4406
|
-
|
|
4388
|
+
// Returns the index of the element in this sorted map, or -1 if it doesn't
|
|
4389
|
+
// exist.
|
|
4390
|
+
indexOf(key) {
|
|
4391
|
+
// Number of nodes that were pruned when descending right
|
|
4392
|
+
let prunedNodes = 0;
|
|
4393
|
+
let node = this.root;
|
|
4394
|
+
while (!node.isEmpty()) {
|
|
4395
|
+
const cmp = this.comparator(key, node.key);
|
|
4396
|
+
if (cmp === 0) {
|
|
4397
|
+
return prunedNodes + node.left.size;
|
|
4398
|
+
}
|
|
4399
|
+
else if (cmp < 0) {
|
|
4400
|
+
node = node.left;
|
|
4401
|
+
}
|
|
4402
|
+
else {
|
|
4403
|
+
// Count all nodes left of the node plus the node itself
|
|
4404
|
+
prunedNodes += node.left.size + 1;
|
|
4405
|
+
node = node.right;
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
// Node not found
|
|
4409
|
+
return -1;
|
|
4407
4410
|
}
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
.map(o => stringifyOrderBy(o))
|
|
4411
|
-
.join(', ')}]`;
|
|
4411
|
+
isEmpty() {
|
|
4412
|
+
return this.root.isEmpty();
|
|
4412
4413
|
}
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
str += target.startAt.position.map(p => canonicalId(p)).join(',');
|
|
4414
|
+
// Returns the total number of nodes in the map.
|
|
4415
|
+
get size() {
|
|
4416
|
+
return this.root.size;
|
|
4417
4417
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
str += target.endAt.position.map(p => canonicalId(p)).join(',');
|
|
4418
|
+
// Returns the minimum key in the map.
|
|
4419
|
+
minKey() {
|
|
4420
|
+
return this.root.minKey();
|
|
4422
4421
|
}
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
if (left.limit !== right.limit) {
|
|
4427
|
-
return false;
|
|
4422
|
+
// Returns the maximum key in the map.
|
|
4423
|
+
maxKey() {
|
|
4424
|
+
return this.root.maxKey();
|
|
4428
4425
|
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4426
|
+
// Traverses the map in key order and calls the specified action function
|
|
4427
|
+
// for each key/value pair. If action returns true, traversal is aborted.
|
|
4428
|
+
// Returns the first truthy value returned by action, or the last falsey
|
|
4429
|
+
// value returned by action.
|
|
4430
|
+
inorderTraversal(action) {
|
|
4431
|
+
return this.root.inorderTraversal(action);
|
|
4431
4432
|
}
|
|
4432
|
-
|
|
4433
|
-
|
|
4433
|
+
forEach(fn) {
|
|
4434
|
+
this.inorderTraversal((k, v) => {
|
|
4435
|
+
fn(k, v);
|
|
4434
4436
|
return false;
|
|
4435
|
-
}
|
|
4436
|
-
}
|
|
4437
|
-
if (left.filters.length !== right.filters.length) {
|
|
4438
|
-
return false;
|
|
4437
|
+
});
|
|
4439
4438
|
}
|
|
4440
|
-
|
|
4441
|
-
|
|
4439
|
+
toString() {
|
|
4440
|
+
const descriptions = [];
|
|
4441
|
+
this.inorderTraversal((k, v) => {
|
|
4442
|
+
descriptions.push(`${k}:${v}`);
|
|
4442
4443
|
return false;
|
|
4443
|
-
}
|
|
4444
|
-
|
|
4445
|
-
if (left.collectionGroup !== right.collectionGroup) {
|
|
4446
|
-
return false;
|
|
4444
|
+
});
|
|
4445
|
+
return `{${descriptions.join(', ')}}`;
|
|
4447
4446
|
}
|
|
4448
|
-
|
|
4449
|
-
|
|
4447
|
+
// Traverses the map in reverse key order and calls the specified action
|
|
4448
|
+
// function for each key/value pair. If action returns true, traversal is
|
|
4449
|
+
// aborted.
|
|
4450
|
+
// Returns the first truthy value returned by action, or the last falsey
|
|
4451
|
+
// value returned by action.
|
|
4452
|
+
reverseTraversal(action) {
|
|
4453
|
+
return this.root.reverseTraversal(action);
|
|
4450
4454
|
}
|
|
4451
|
-
|
|
4452
|
-
|
|
4455
|
+
// Returns an iterator over the SortedMap.
|
|
4456
|
+
getIterator() {
|
|
4457
|
+
return new SortedMapIterator(this.root, null, this.comparator, false);
|
|
4453
4458
|
}
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
function targetIsDocumentTarget(target) {
|
|
4457
|
-
return (DocumentKey.isDocumentKey(target.path) &&
|
|
4458
|
-
target.collectionGroup === null &&
|
|
4459
|
-
target.filters.length === 0);
|
|
4460
|
-
}
|
|
4461
|
-
/** Returns the field filters that target the given field path. */
|
|
4462
|
-
function targetGetFieldFiltersForPath(target, path) {
|
|
4463
|
-
return target.filters.filter(f => f instanceof FieldFilter && f.field.isEqual(path));
|
|
4464
|
-
}
|
|
4465
|
-
/**
|
|
4466
|
-
* Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY
|
|
4467
|
-
* filters. Returns `null` if there are no such filters.
|
|
4468
|
-
*/
|
|
4469
|
-
function targetGetArrayValues(target, fieldIndex) {
|
|
4470
|
-
const segment = fieldIndexGetArraySegment(fieldIndex);
|
|
4471
|
-
if (segment === undefined) {
|
|
4472
|
-
return null;
|
|
4459
|
+
getIteratorFrom(key) {
|
|
4460
|
+
return new SortedMapIterator(this.root, key, this.comparator, false);
|
|
4473
4461
|
}
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
case "array-contains-any" /* ARRAY_CONTAINS_ANY */:
|
|
4477
|
-
return fieldFilter.value.arrayValue.values || [];
|
|
4478
|
-
case "array-contains" /* ARRAY_CONTAINS */:
|
|
4479
|
-
return [fieldFilter.value];
|
|
4480
|
-
// Remaining filters are not array filters.
|
|
4481
|
-
}
|
|
4462
|
+
getReverseIterator() {
|
|
4463
|
+
return new SortedMapIterator(this.root, null, this.comparator, true);
|
|
4482
4464
|
}
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
/**
|
|
4486
|
-
* Returns the list of values that are used in != or NOT_IN filters. Returns
|
|
4487
|
-
* `null` if there are no such filters.
|
|
4488
|
-
*/
|
|
4489
|
-
function targetGetNotInValues(target, fieldIndex) {
|
|
4490
|
-
const values = new Map();
|
|
4491
|
-
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
4492
|
-
for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
|
|
4493
|
-
switch (fieldFilter.op) {
|
|
4494
|
-
case "==" /* EQUAL */:
|
|
4495
|
-
case "in" /* IN */:
|
|
4496
|
-
// Encode equality prefix, which is encoded in the index value before
|
|
4497
|
-
// the inequality (e.g. `a == 'a' && b != 'b'` is encoded to
|
|
4498
|
-
// `value != 'ab'`).
|
|
4499
|
-
values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
|
|
4500
|
-
break;
|
|
4501
|
-
case "not-in" /* NOT_IN */:
|
|
4502
|
-
case "!=" /* NOT_EQUAL */:
|
|
4503
|
-
// NotIn/NotEqual is always a suffix. There cannot be any remaining
|
|
4504
|
-
// segments and hence we can return early here.
|
|
4505
|
-
values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
|
|
4506
|
-
return Array.from(values.values());
|
|
4507
|
-
// Remaining filters cannot be used as notIn bounds.
|
|
4508
|
-
}
|
|
4509
|
-
}
|
|
4465
|
+
getReverseIteratorFrom(key) {
|
|
4466
|
+
return new SortedMapIterator(this.root, key, this.comparator, true);
|
|
4510
4467
|
}
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
// Remaining filters cannot be used as lower bounds.
|
|
4549
|
-
}
|
|
4550
|
-
if (valuesMax(segmentValue, filterValue) === filterValue) {
|
|
4551
|
-
segmentValue = filterValue;
|
|
4552
|
-
segmentInclusive = filterInclusive;
|
|
4553
|
-
}
|
|
4554
|
-
}
|
|
4555
|
-
// If there is a startAt bound, compare the values against the existing
|
|
4556
|
-
// boundary to see if we can narrow the scope.
|
|
4557
|
-
if (target.startAt !== null) {
|
|
4558
|
-
for (let i = 0; i < target.orderBy.length; ++i) {
|
|
4559
|
-
const orderBy = target.orderBy[i];
|
|
4560
|
-
if (orderBy.field.isEqual(segment.fieldPath)) {
|
|
4561
|
-
const cursorValue = target.startAt.position[i];
|
|
4562
|
-
if (valuesMax(segmentValue, cursorValue) === cursorValue) {
|
|
4563
|
-
segmentValue = cursorValue;
|
|
4564
|
-
segmentInclusive = target.startAt.inclusive;
|
|
4565
|
-
}
|
|
4566
|
-
break;
|
|
4468
|
+
} // end SortedMap
|
|
4469
|
+
// An iterator over an LLRBNode.
|
|
4470
|
+
class SortedMapIterator {
|
|
4471
|
+
constructor(node, startKey, comparator, isReverse) {
|
|
4472
|
+
this.isReverse = isReverse;
|
|
4473
|
+
this.nodeStack = [];
|
|
4474
|
+
let cmp = 1;
|
|
4475
|
+
while (!node.isEmpty()) {
|
|
4476
|
+
cmp = startKey ? comparator(node.key, startKey) : 1;
|
|
4477
|
+
// flip the comparison if we're going in reverse
|
|
4478
|
+
if (startKey && isReverse) {
|
|
4479
|
+
cmp *= -1;
|
|
4480
|
+
}
|
|
4481
|
+
if (cmp < 0) {
|
|
4482
|
+
// This node is less than our start key. ignore it
|
|
4483
|
+
if (this.isReverse) {
|
|
4484
|
+
node = node.left;
|
|
4485
|
+
}
|
|
4486
|
+
else {
|
|
4487
|
+
node = node.right;
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
else if (cmp === 0) {
|
|
4491
|
+
// This node is exactly equal to our start key. Push it on the stack,
|
|
4492
|
+
// but stop iterating;
|
|
4493
|
+
this.nodeStack.push(node);
|
|
4494
|
+
break;
|
|
4495
|
+
}
|
|
4496
|
+
else {
|
|
4497
|
+
// This node is greater than our start key, add it to the stack and move
|
|
4498
|
+
// to the next one
|
|
4499
|
+
this.nodeStack.push(node);
|
|
4500
|
+
if (this.isReverse) {
|
|
4501
|
+
node = node.right;
|
|
4502
|
+
}
|
|
4503
|
+
else {
|
|
4504
|
+
node = node.left;
|
|
4567
4505
|
}
|
|
4568
4506
|
}
|
|
4569
4507
|
}
|
|
4570
|
-
if (segmentValue === undefined) {
|
|
4571
|
-
// No lower bound exists
|
|
4572
|
-
return null;
|
|
4573
|
-
}
|
|
4574
|
-
values.push(segmentValue);
|
|
4575
|
-
inclusive && (inclusive = segmentInclusive);
|
|
4576
4508
|
}
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
const values = [];
|
|
4586
|
-
let inclusive = true;
|
|
4587
|
-
// For each segment, retrieve an upper bound if there is a suitable filter or
|
|
4588
|
-
// endAt.
|
|
4589
|
-
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
4590
|
-
let segmentValue = undefined;
|
|
4591
|
-
let segmentInclusive = true;
|
|
4592
|
-
// Process all filters to find a value for the current field segment
|
|
4593
|
-
for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
|
|
4594
|
-
let filterValue = undefined;
|
|
4595
|
-
let filterInclusive = true;
|
|
4596
|
-
switch (fieldFilter.op) {
|
|
4597
|
-
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
4598
|
-
case ">" /* GREATER_THAN */:
|
|
4599
|
-
filterValue = valuesGetUpperBound(fieldFilter.value);
|
|
4600
|
-
filterInclusive = false;
|
|
4601
|
-
break;
|
|
4602
|
-
case "==" /* EQUAL */:
|
|
4603
|
-
case "in" /* IN */:
|
|
4604
|
-
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
4605
|
-
filterValue = fieldFilter.value;
|
|
4606
|
-
break;
|
|
4607
|
-
case "<" /* LESS_THAN */:
|
|
4608
|
-
filterValue = fieldFilter.value;
|
|
4609
|
-
filterInclusive = false;
|
|
4610
|
-
break;
|
|
4611
|
-
case "!=" /* NOT_EQUAL */:
|
|
4612
|
-
case "not-in" /* NOT_IN */:
|
|
4613
|
-
filterValue = MAX_VALUE;
|
|
4614
|
-
break;
|
|
4615
|
-
// Remaining filters cannot be used as upper bounds.
|
|
4616
|
-
}
|
|
4617
|
-
if (valuesMin(segmentValue, filterValue) === filterValue) {
|
|
4618
|
-
segmentValue = filterValue;
|
|
4619
|
-
segmentInclusive = filterInclusive;
|
|
4620
|
-
}
|
|
4621
|
-
}
|
|
4622
|
-
// If there is a endAt bound, compare the values against the existing
|
|
4623
|
-
// boundary to see if we can narrow the scope.
|
|
4624
|
-
if (target.endAt !== null) {
|
|
4625
|
-
for (let i = 0; i < target.orderBy.length; ++i) {
|
|
4626
|
-
const orderBy = target.orderBy[i];
|
|
4627
|
-
if (orderBy.field.isEqual(segment.fieldPath)) {
|
|
4628
|
-
const cursorValue = target.endAt.position[i];
|
|
4629
|
-
if (valuesMin(segmentValue, cursorValue) === cursorValue) {
|
|
4630
|
-
segmentValue = cursorValue;
|
|
4631
|
-
segmentInclusive = target.endAt.inclusive;
|
|
4632
|
-
}
|
|
4633
|
-
break;
|
|
4634
|
-
}
|
|
4509
|
+
getNext() {
|
|
4510
|
+
let node = this.nodeStack.pop();
|
|
4511
|
+
const result = { key: node.key, value: node.value };
|
|
4512
|
+
if (this.isReverse) {
|
|
4513
|
+
node = node.left;
|
|
4514
|
+
while (!node.isEmpty()) {
|
|
4515
|
+
this.nodeStack.push(node);
|
|
4516
|
+
node = node.right;
|
|
4635
4517
|
}
|
|
4636
4518
|
}
|
|
4637
|
-
|
|
4638
|
-
|
|
4519
|
+
else {
|
|
4520
|
+
node = node.right;
|
|
4521
|
+
while (!node.isEmpty()) {
|
|
4522
|
+
this.nodeStack.push(node);
|
|
4523
|
+
node = node.left;
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
return result;
|
|
4527
|
+
}
|
|
4528
|
+
hasNext() {
|
|
4529
|
+
return this.nodeStack.length > 0;
|
|
4530
|
+
}
|
|
4531
|
+
peek() {
|
|
4532
|
+
if (this.nodeStack.length === 0) {
|
|
4639
4533
|
return null;
|
|
4640
4534
|
}
|
|
4641
|
-
|
|
4642
|
-
|
|
4535
|
+
const node = this.nodeStack[this.nodeStack.length - 1];
|
|
4536
|
+
return { key: node.key, value: node.value };
|
|
4643
4537
|
}
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
class
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
constructor(field, op, value) {
|
|
4650
|
-
super();
|
|
4651
|
-
this.field = field;
|
|
4652
|
-
this.op = op;
|
|
4538
|
+
} // end SortedMapIterator
|
|
4539
|
+
// Represents a node in a Left-leaning Red-Black tree.
|
|
4540
|
+
class LLRBNode {
|
|
4541
|
+
constructor(key, value, color, left, right) {
|
|
4542
|
+
this.key = key;
|
|
4653
4543
|
this.value = value;
|
|
4544
|
+
this.color = color != null ? color : LLRBNode.RED;
|
|
4545
|
+
this.left = left != null ? left : LLRBNode.EMPTY;
|
|
4546
|
+
this.right = right != null ? right : LLRBNode.EMPTY;
|
|
4547
|
+
this.size = this.left.size + 1 + this.right.size;
|
|
4654
4548
|
}
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4549
|
+
// Returns a copy of the current node, optionally replacing pieces of it.
|
|
4550
|
+
copy(key, value, color, left, right) {
|
|
4551
|
+
return new LLRBNode(key != null ? key : this.key, value != null ? value : this.value, color != null ? color : this.color, left != null ? left : this.left, right != null ? right : this.right);
|
|
4552
|
+
}
|
|
4553
|
+
isEmpty() {
|
|
4554
|
+
return false;
|
|
4555
|
+
}
|
|
4556
|
+
// Traverses the tree in key order and calls the specified action function
|
|
4557
|
+
// for each node. If action returns true, traversal is aborted.
|
|
4558
|
+
// Returns the first truthy value returned by action, or the last falsey
|
|
4559
|
+
// value returned by action.
|
|
4560
|
+
inorderTraversal(action) {
|
|
4561
|
+
return (this.left.inorderTraversal(action) ||
|
|
4562
|
+
action(this.key, this.value) ||
|
|
4563
|
+
this.right.inorderTraversal(action));
|
|
4564
|
+
}
|
|
4565
|
+
// Traverses the tree in reverse key order and calls the specified action
|
|
4566
|
+
// function for each node. If action returns true, traversal is aborted.
|
|
4567
|
+
// Returns the first truthy value returned by action, or the last falsey
|
|
4568
|
+
// value returned by action.
|
|
4569
|
+
reverseTraversal(action) {
|
|
4570
|
+
return (this.right.reverseTraversal(action) ||
|
|
4571
|
+
action(this.key, this.value) ||
|
|
4572
|
+
this.left.reverseTraversal(action));
|
|
4573
|
+
}
|
|
4574
|
+
// Returns the minimum node in the tree.
|
|
4575
|
+
min() {
|
|
4576
|
+
if (this.left.isEmpty()) {
|
|
4577
|
+
return this;
|
|
4666
4578
|
}
|
|
4667
|
-
else
|
|
4668
|
-
return
|
|
4579
|
+
else {
|
|
4580
|
+
return this.left.min();
|
|
4669
4581
|
}
|
|
4670
|
-
|
|
4671
|
-
|
|
4582
|
+
}
|
|
4583
|
+
// Returns the maximum key in the tree.
|
|
4584
|
+
minKey() {
|
|
4585
|
+
return this.min().key;
|
|
4586
|
+
}
|
|
4587
|
+
// Returns the maximum key in the tree.
|
|
4588
|
+
maxKey() {
|
|
4589
|
+
if (this.right.isEmpty()) {
|
|
4590
|
+
return this.key;
|
|
4672
4591
|
}
|
|
4673
|
-
else
|
|
4674
|
-
return
|
|
4592
|
+
else {
|
|
4593
|
+
return this.right.maxKey();
|
|
4675
4594
|
}
|
|
4676
|
-
|
|
4677
|
-
|
|
4595
|
+
}
|
|
4596
|
+
// Returns new tree, with the key/value added.
|
|
4597
|
+
insert(key, value, comparator) {
|
|
4598
|
+
let n = this;
|
|
4599
|
+
const cmp = comparator(key, n.key);
|
|
4600
|
+
if (cmp < 0) {
|
|
4601
|
+
n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
|
|
4602
|
+
}
|
|
4603
|
+
else if (cmp === 0) {
|
|
4604
|
+
n = n.copy(null, value, null, null, null);
|
|
4678
4605
|
}
|
|
4679
4606
|
else {
|
|
4680
|
-
|
|
4607
|
+
n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
|
|
4681
4608
|
}
|
|
4609
|
+
return n.fixUp();
|
|
4682
4610
|
}
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
: new KeyFieldNotInFilter(field, value);
|
|
4687
|
-
}
|
|
4688
|
-
matches(doc) {
|
|
4689
|
-
const other = doc.data.field(this.field);
|
|
4690
|
-
// Types do not have to match in NOT_EQUAL filters.
|
|
4691
|
-
if (this.op === "!=" /* NOT_EQUAL */) {
|
|
4692
|
-
return (other !== null &&
|
|
4693
|
-
this.matchesComparison(valueCompare(other, this.value)));
|
|
4611
|
+
removeMin() {
|
|
4612
|
+
if (this.left.isEmpty()) {
|
|
4613
|
+
return LLRBNode.EMPTY;
|
|
4694
4614
|
}
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
this.matchesComparison(valueCompare(other, this.value)));
|
|
4699
|
-
}
|
|
4700
|
-
matchesComparison(comparison) {
|
|
4701
|
-
switch (this.op) {
|
|
4702
|
-
case "<" /* LESS_THAN */:
|
|
4703
|
-
return comparison < 0;
|
|
4704
|
-
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
4705
|
-
return comparison <= 0;
|
|
4706
|
-
case "==" /* EQUAL */:
|
|
4707
|
-
return comparison === 0;
|
|
4708
|
-
case "!=" /* NOT_EQUAL */:
|
|
4709
|
-
return comparison !== 0;
|
|
4710
|
-
case ">" /* GREATER_THAN */:
|
|
4711
|
-
return comparison > 0;
|
|
4712
|
-
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
4713
|
-
return comparison >= 0;
|
|
4714
|
-
default:
|
|
4715
|
-
return fail();
|
|
4615
|
+
let n = this;
|
|
4616
|
+
if (!n.left.isRed() && !n.left.left.isRed()) {
|
|
4617
|
+
n = n.moveRedLeft();
|
|
4716
4618
|
}
|
|
4619
|
+
n = n.copy(null, null, null, n.left.removeMin(), null);
|
|
4620
|
+
return n.fixUp();
|
|
4717
4621
|
}
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4622
|
+
// Returns new tree, with the specified item removed.
|
|
4623
|
+
remove(key, comparator) {
|
|
4624
|
+
let smallest;
|
|
4625
|
+
let n = this;
|
|
4626
|
+
if (comparator(key, n.key) < 0) {
|
|
4627
|
+
if (!n.left.isEmpty() && !n.left.isRed() && !n.left.left.isRed()) {
|
|
4628
|
+
n = n.moveRedLeft();
|
|
4629
|
+
}
|
|
4630
|
+
n = n.copy(null, null, null, n.left.remove(key, comparator), null);
|
|
4631
|
+
}
|
|
4632
|
+
else {
|
|
4633
|
+
if (n.left.isRed()) {
|
|
4634
|
+
n = n.rotateRight();
|
|
4635
|
+
}
|
|
4636
|
+
if (!n.right.isEmpty() && !n.right.isRed() && !n.right.left.isRed()) {
|
|
4637
|
+
n = n.moveRedRight();
|
|
4638
|
+
}
|
|
4639
|
+
if (comparator(key, n.key) === 0) {
|
|
4640
|
+
if (n.right.isEmpty()) {
|
|
4641
|
+
return LLRBNode.EMPTY;
|
|
4642
|
+
}
|
|
4643
|
+
else {
|
|
4644
|
+
smallest = n.right.min();
|
|
4645
|
+
n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin());
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
n = n.copy(null, null, null, null, n.right.remove(key, comparator));
|
|
4649
|
+
}
|
|
4650
|
+
return n.fixUp();
|
|
4727
4651
|
}
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
// TODO(b/29183165): Technically, this won't be unique if two values have
|
|
4731
|
-
// the same description, such as the int 3 and the string "3". So we should
|
|
4732
|
-
// add the types in here somehow, too.
|
|
4733
|
-
return (filter.field.canonicalString() +
|
|
4734
|
-
filter.op.toString() +
|
|
4735
|
-
canonicalId(filter.value));
|
|
4736
|
-
}
|
|
4737
|
-
function filterEquals(f1, f2) {
|
|
4738
|
-
return (f1.op === f2.op &&
|
|
4739
|
-
f1.field.isEqual(f2.field) &&
|
|
4740
|
-
valueEquals(f1.value, f2.value));
|
|
4741
|
-
}
|
|
4742
|
-
/** Returns a debug description for `filter`. */
|
|
4743
|
-
function stringifyFilter(filter) {
|
|
4744
|
-
return `${filter.field.canonicalString()} ${filter.op} ${canonicalId(filter.value)}`;
|
|
4745
|
-
}
|
|
4746
|
-
/** Filter that matches on key fields (i.e. '__name__'). */
|
|
4747
|
-
class KeyFieldFilter extends FieldFilter {
|
|
4748
|
-
constructor(field, op, value) {
|
|
4749
|
-
super(field, op, value);
|
|
4750
|
-
this.key = DocumentKey.fromName(value.referenceValue);
|
|
4652
|
+
isRed() {
|
|
4653
|
+
return this.color;
|
|
4751
4654
|
}
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4655
|
+
// Returns new tree after performing any needed rotations.
|
|
4656
|
+
fixUp() {
|
|
4657
|
+
let n = this;
|
|
4658
|
+
if (n.right.isRed() && !n.left.isRed()) {
|
|
4659
|
+
n = n.rotateLeft();
|
|
4660
|
+
}
|
|
4661
|
+
if (n.left.isRed() && n.left.left.isRed()) {
|
|
4662
|
+
n = n.rotateRight();
|
|
4663
|
+
}
|
|
4664
|
+
if (n.left.isRed() && n.right.isRed()) {
|
|
4665
|
+
n = n.colorFlip();
|
|
4666
|
+
}
|
|
4667
|
+
return n;
|
|
4755
4668
|
}
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4669
|
+
moveRedLeft() {
|
|
4670
|
+
let n = this.colorFlip();
|
|
4671
|
+
if (n.right.left.isRed()) {
|
|
4672
|
+
n = n.copy(null, null, null, null, n.right.rotateRight());
|
|
4673
|
+
n = n.rotateLeft();
|
|
4674
|
+
n = n.colorFlip();
|
|
4675
|
+
}
|
|
4676
|
+
return n;
|
|
4762
4677
|
}
|
|
4763
|
-
|
|
4764
|
-
|
|
4678
|
+
moveRedRight() {
|
|
4679
|
+
let n = this.colorFlip();
|
|
4680
|
+
if (n.left.left.isRed()) {
|
|
4681
|
+
n = n.rotateRight();
|
|
4682
|
+
n = n.colorFlip();
|
|
4683
|
+
}
|
|
4684
|
+
return n;
|
|
4765
4685
|
}
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
constructor(field, value) {
|
|
4770
|
-
super(field, "not-in" /* NOT_IN */, value);
|
|
4771
|
-
this.keys = extractDocumentKeysFromArrayValue("not-in" /* NOT_IN */, value);
|
|
4686
|
+
rotateLeft() {
|
|
4687
|
+
const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
|
|
4688
|
+
return this.right.copy(null, null, this.color, nl, null);
|
|
4772
4689
|
}
|
|
4773
|
-
|
|
4774
|
-
|
|
4690
|
+
rotateRight() {
|
|
4691
|
+
const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
|
|
4692
|
+
return this.left.copy(null, null, this.color, null, nr);
|
|
4775
4693
|
}
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
return DocumentKey.fromName(v.referenceValue);
|
|
4781
|
-
});
|
|
4782
|
-
}
|
|
4783
|
-
/** A Filter that implements the array-contains operator. */
|
|
4784
|
-
class ArrayContainsFilter extends FieldFilter {
|
|
4785
|
-
constructor(field, value) {
|
|
4786
|
-
super(field, "array-contains" /* ARRAY_CONTAINS */, value);
|
|
4694
|
+
colorFlip() {
|
|
4695
|
+
const left = this.left.copy(null, null, !this.left.color, null, null);
|
|
4696
|
+
const right = this.right.copy(null, null, !this.right.color, null, null);
|
|
4697
|
+
return this.copy(null, null, !this.color, left, right);
|
|
4787
4698
|
}
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4699
|
+
// For testing.
|
|
4700
|
+
checkMaxDepth() {
|
|
4701
|
+
const blackDepth = this.check();
|
|
4702
|
+
if (Math.pow(2.0, blackDepth) <= this.size + 1) {
|
|
4703
|
+
return true;
|
|
4704
|
+
}
|
|
4705
|
+
else {
|
|
4706
|
+
return false;
|
|
4707
|
+
}
|
|
4791
4708
|
}
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4709
|
+
// In a balanced RB tree, the black-depth (number of black nodes) from root to
|
|
4710
|
+
// leaves is equal on both sides. This function verifies that or asserts.
|
|
4711
|
+
check() {
|
|
4712
|
+
if (this.isRed() && this.left.isRed()) {
|
|
4713
|
+
throw fail();
|
|
4714
|
+
}
|
|
4715
|
+
if (this.right.isRed()) {
|
|
4716
|
+
throw fail();
|
|
4717
|
+
}
|
|
4718
|
+
const blackDepth = this.left.check();
|
|
4719
|
+
if (blackDepth !== this.right.check()) {
|
|
4720
|
+
throw fail();
|
|
4721
|
+
}
|
|
4722
|
+
else {
|
|
4723
|
+
return blackDepth + (this.isRed() ? 0 : 1);
|
|
4724
|
+
}
|
|
4797
4725
|
}
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4726
|
+
} // end LLRBNode
|
|
4727
|
+
// Empty node is shared between all LLRB trees.
|
|
4728
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4729
|
+
LLRBNode.EMPTY = null;
|
|
4730
|
+
LLRBNode.RED = true;
|
|
4731
|
+
LLRBNode.BLACK = false;
|
|
4732
|
+
// Represents an empty node (a leaf node in the Red-Black Tree).
|
|
4733
|
+
class LLRBEmptyNode {
|
|
4734
|
+
constructor() {
|
|
4735
|
+
this.size = 0;
|
|
4801
4736
|
}
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
class NotInFilter extends FieldFilter {
|
|
4805
|
-
constructor(field, value) {
|
|
4806
|
-
super(field, "not-in" /* NOT_IN */, value);
|
|
4737
|
+
get key() {
|
|
4738
|
+
throw fail();
|
|
4807
4739
|
}
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
return false;
|
|
4811
|
-
}
|
|
4812
|
-
const other = doc.data.field(this.field);
|
|
4813
|
-
return other !== null && !arrayValueContains(this.value.arrayValue, other);
|
|
4740
|
+
get value() {
|
|
4741
|
+
throw fail();
|
|
4814
4742
|
}
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
class ArrayContainsAnyFilter extends FieldFilter {
|
|
4818
|
-
constructor(field, value) {
|
|
4819
|
-
super(field, "array-contains-any" /* ARRAY_CONTAINS_ANY */, value);
|
|
4743
|
+
get color() {
|
|
4744
|
+
throw fail();
|
|
4820
4745
|
}
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
if (!isArray(other) || !other.arrayValue.values) {
|
|
4824
|
-
return false;
|
|
4825
|
-
}
|
|
4826
|
-
return other.arrayValue.values.some(val => arrayValueContains(this.value.arrayValue, val));
|
|
4746
|
+
get left() {
|
|
4747
|
+
throw fail();
|
|
4827
4748
|
}
|
|
4828
|
-
|
|
4749
|
+
get right() {
|
|
4750
|
+
throw fail();
|
|
4751
|
+
}
|
|
4752
|
+
// Returns a copy of the current node.
|
|
4753
|
+
copy(key, value, color, left, right) {
|
|
4754
|
+
return this;
|
|
4755
|
+
}
|
|
4756
|
+
// Returns a copy of the tree, with the specified key/value added.
|
|
4757
|
+
insert(key, value, comparator) {
|
|
4758
|
+
return new LLRBNode(key, value);
|
|
4759
|
+
}
|
|
4760
|
+
// Returns a copy of the tree, with the specified key removed.
|
|
4761
|
+
remove(key, comparator) {
|
|
4762
|
+
return this;
|
|
4763
|
+
}
|
|
4764
|
+
isEmpty() {
|
|
4765
|
+
return true;
|
|
4766
|
+
}
|
|
4767
|
+
inorderTraversal(action) {
|
|
4768
|
+
return false;
|
|
4769
|
+
}
|
|
4770
|
+
reverseTraversal(action) {
|
|
4771
|
+
return false;
|
|
4772
|
+
}
|
|
4773
|
+
minKey() {
|
|
4774
|
+
return null;
|
|
4775
|
+
}
|
|
4776
|
+
maxKey() {
|
|
4777
|
+
return null;
|
|
4778
|
+
}
|
|
4779
|
+
isRed() {
|
|
4780
|
+
return false;
|
|
4781
|
+
}
|
|
4782
|
+
// For testing.
|
|
4783
|
+
checkMaxDepth() {
|
|
4784
|
+
return true;
|
|
4785
|
+
}
|
|
4786
|
+
check() {
|
|
4787
|
+
return 0;
|
|
4788
|
+
}
|
|
4789
|
+
} // end LLRBEmptyNode
|
|
4790
|
+
LLRBNode.EMPTY = new LLRBEmptyNode();
|
|
4791
|
+
|
|
4829
4792
|
/**
|
|
4830
|
-
*
|
|
4793
|
+
* @license
|
|
4794
|
+
* Copyright 2017 Google LLC
|
|
4831
4795
|
*
|
|
4832
|
-
*
|
|
4833
|
-
*
|
|
4834
|
-
*
|
|
4796
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4797
|
+
* you may not use this file except in compliance with the License.
|
|
4798
|
+
* You may obtain a copy of the License at
|
|
4835
4799
|
*
|
|
4836
|
-
*
|
|
4837
|
-
* of values for the (potentially implicit) order by clauses of a query.
|
|
4800
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
4838
4801
|
*
|
|
4839
|
-
*
|
|
4840
|
-
*
|
|
4841
|
-
*
|
|
4802
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
4803
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
4804
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
4805
|
+
* See the License for the specific language governing permissions and
|
|
4806
|
+
* limitations under the License.
|
|
4842
4807
|
*/
|
|
4843
|
-
class Bound {
|
|
4844
|
-
constructor(position, inclusive) {
|
|
4845
|
-
this.position = position;
|
|
4846
|
-
this.inclusive = inclusive;
|
|
4847
|
-
}
|
|
4848
|
-
}
|
|
4849
4808
|
/**
|
|
4850
|
-
*
|
|
4809
|
+
* SortedSet is an immutable (copy-on-write) collection that holds elements
|
|
4810
|
+
* in order specified by the provided comparator.
|
|
4811
|
+
*
|
|
4812
|
+
* NOTE: if provided comparator returns 0 for two elements, we consider them to
|
|
4813
|
+
* be equal!
|
|
4851
4814
|
*/
|
|
4852
|
-
class
|
|
4853
|
-
constructor(
|
|
4854
|
-
this.
|
|
4855
|
-
this.
|
|
4815
|
+
class SortedSet {
|
|
4816
|
+
constructor(comparator) {
|
|
4817
|
+
this.comparator = comparator;
|
|
4818
|
+
this.data = new SortedMap(this.comparator);
|
|
4856
4819
|
}
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4820
|
+
has(elem) {
|
|
4821
|
+
return this.data.get(elem) !== null;
|
|
4822
|
+
}
|
|
4823
|
+
first() {
|
|
4824
|
+
return this.data.minKey();
|
|
4825
|
+
}
|
|
4826
|
+
last() {
|
|
4827
|
+
return this.data.maxKey();
|
|
4828
|
+
}
|
|
4829
|
+
get size() {
|
|
4830
|
+
return this.data.size;
|
|
4831
|
+
}
|
|
4832
|
+
indexOf(elem) {
|
|
4833
|
+
return this.data.indexOf(elem);
|
|
4834
|
+
}
|
|
4835
|
+
/** Iterates elements in order defined by "comparator" */
|
|
4836
|
+
forEach(cb) {
|
|
4837
|
+
this.data.inorderTraversal((k, v) => {
|
|
4838
|
+
cb(k);
|
|
4839
|
+
return false;
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
/** Iterates over `elem`s such that: range[0] <= elem < range[1]. */
|
|
4843
|
+
forEachInRange(range, cb) {
|
|
4844
|
+
const iter = this.data.getIteratorFrom(range[0]);
|
|
4845
|
+
while (iter.hasNext()) {
|
|
4846
|
+
const elem = iter.getNext();
|
|
4847
|
+
if (this.comparator(elem.key, range[1]) >= 0) {
|
|
4848
|
+
return;
|
|
4849
|
+
}
|
|
4850
|
+
cb(elem.key);
|
|
4875
4851
|
}
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4852
|
+
}
|
|
4853
|
+
/**
|
|
4854
|
+
* Iterates over `elem`s such that: start <= elem until false is returned.
|
|
4855
|
+
*/
|
|
4856
|
+
forEachWhile(cb, start) {
|
|
4857
|
+
let iter;
|
|
4858
|
+
if (start !== undefined) {
|
|
4859
|
+
iter = this.data.getIteratorFrom(start);
|
|
4879
4860
|
}
|
|
4880
|
-
|
|
4881
|
-
|
|
4861
|
+
else {
|
|
4862
|
+
iter = this.data.getIterator();
|
|
4882
4863
|
}
|
|
4883
|
-
|
|
4884
|
-
|
|
4864
|
+
while (iter.hasNext()) {
|
|
4865
|
+
const elem = iter.getNext();
|
|
4866
|
+
const result = cb(elem.key);
|
|
4867
|
+
if (!result) {
|
|
4868
|
+
return;
|
|
4869
|
+
}
|
|
4885
4870
|
}
|
|
4886
4871
|
}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
* order.
|
|
4892
|
-
*/
|
|
4893
|
-
function boundSortsAfterDocument(bound, orderBy, doc) {
|
|
4894
|
-
const comparison = boundCompareToDocument(bound, orderBy, doc);
|
|
4895
|
-
return bound.inclusive ? comparison >= 0 : comparison > 0;
|
|
4896
|
-
}
|
|
4897
|
-
/**
|
|
4898
|
-
* Returns true if a document sorts before a bound using the provided sort
|
|
4899
|
-
* order.
|
|
4900
|
-
*/
|
|
4901
|
-
function boundSortsBeforeDocument(bound, orderBy, doc) {
|
|
4902
|
-
const comparison = boundCompareToDocument(bound, orderBy, doc);
|
|
4903
|
-
return bound.inclusive ? comparison <= 0 : comparison < 0;
|
|
4904
|
-
}
|
|
4905
|
-
function boundEquals(left, right) {
|
|
4906
|
-
if (left === null) {
|
|
4907
|
-
return right === null;
|
|
4872
|
+
/** Finds the least element greater than or equal to `elem`. */
|
|
4873
|
+
firstAfterOrEqual(elem) {
|
|
4874
|
+
const iter = this.data.getIteratorFrom(elem);
|
|
4875
|
+
return iter.hasNext() ? iter.getNext().key : null;
|
|
4908
4876
|
}
|
|
4909
|
-
|
|
4910
|
-
return
|
|
4877
|
+
getIterator() {
|
|
4878
|
+
return new SortedSetIterator(this.data.getIterator());
|
|
4911
4879
|
}
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
return false;
|
|
4880
|
+
getIteratorFrom(key) {
|
|
4881
|
+
return new SortedSetIterator(this.data.getIteratorFrom(key));
|
|
4915
4882
|
}
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4883
|
+
/** Inserts or updates an element */
|
|
4884
|
+
add(elem) {
|
|
4885
|
+
return this.copy(this.data.remove(elem).insert(elem, true));
|
|
4886
|
+
}
|
|
4887
|
+
/** Deletes an element */
|
|
4888
|
+
delete(elem) {
|
|
4889
|
+
if (!this.has(elem)) {
|
|
4890
|
+
return this;
|
|
4891
|
+
}
|
|
4892
|
+
return this.copy(this.data.remove(elem));
|
|
4893
|
+
}
|
|
4894
|
+
isEmpty() {
|
|
4895
|
+
return this.data.isEmpty();
|
|
4896
|
+
}
|
|
4897
|
+
unionWith(other) {
|
|
4898
|
+
let result = this;
|
|
4899
|
+
// Make sure `result` always refers to the larger one of the two sets.
|
|
4900
|
+
if (result.size < other.size) {
|
|
4901
|
+
result = other;
|
|
4902
|
+
other = this;
|
|
4903
|
+
}
|
|
4904
|
+
other.forEach(elem => {
|
|
4905
|
+
result = result.add(elem);
|
|
4906
|
+
});
|
|
4907
|
+
return result;
|
|
4908
|
+
}
|
|
4909
|
+
isEqual(other) {
|
|
4910
|
+
if (!(other instanceof SortedSet)) {
|
|
4911
|
+
return false;
|
|
4912
|
+
}
|
|
4913
|
+
if (this.size !== other.size) {
|
|
4920
4914
|
return false;
|
|
4921
4915
|
}
|
|
4916
|
+
const thisIt = this.data.getIterator();
|
|
4917
|
+
const otherIt = other.data.getIterator();
|
|
4918
|
+
while (thisIt.hasNext()) {
|
|
4919
|
+
const thisElem = thisIt.getNext().key;
|
|
4920
|
+
const otherElem = otherIt.getNext().key;
|
|
4921
|
+
if (this.comparator(thisElem, otherElem) !== 0) {
|
|
4922
|
+
return false;
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
return true;
|
|
4922
4926
|
}
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
* See the License for the specific language governing permissions and
|
|
4940
|
-
* limitations under the License.
|
|
4941
|
-
*/
|
|
4942
|
-
/**
|
|
4943
|
-
* Query encapsulates all the query attributes we support in the SDK. It can
|
|
4944
|
-
* be run against the LocalStore, as well as be converted to a `Target` to
|
|
4945
|
-
* query the RemoteStore results.
|
|
4946
|
-
*
|
|
4947
|
-
* Visible for testing.
|
|
4948
|
-
*/
|
|
4949
|
-
class QueryImpl {
|
|
4950
|
-
/**
|
|
4951
|
-
* Initializes a Query with a path and optional additional query constraints.
|
|
4952
|
-
* Path must currently be empty if this is a collection group query.
|
|
4953
|
-
*/
|
|
4954
|
-
constructor(path, collectionGroup = null, explicitOrderBy = [], filters = [], limit = null, limitType = "F" /* First */, startAt = null, endAt = null) {
|
|
4955
|
-
this.path = path;
|
|
4956
|
-
this.collectionGroup = collectionGroup;
|
|
4957
|
-
this.explicitOrderBy = explicitOrderBy;
|
|
4958
|
-
this.filters = filters;
|
|
4959
|
-
this.limit = limit;
|
|
4960
|
-
this.limitType = limitType;
|
|
4961
|
-
this.startAt = startAt;
|
|
4962
|
-
this.endAt = endAt;
|
|
4963
|
-
this.memoizedOrderBy = null;
|
|
4964
|
-
// The corresponding `Target` of this `Query` instance.
|
|
4965
|
-
this.memoizedTarget = null;
|
|
4966
|
-
if (this.startAt) ;
|
|
4967
|
-
if (this.endAt) ;
|
|
4927
|
+
toArray() {
|
|
4928
|
+
const res = [];
|
|
4929
|
+
this.forEach(targetId => {
|
|
4930
|
+
res.push(targetId);
|
|
4931
|
+
});
|
|
4932
|
+
return res;
|
|
4933
|
+
}
|
|
4934
|
+
toString() {
|
|
4935
|
+
const result = [];
|
|
4936
|
+
this.forEach(elem => result.push(elem));
|
|
4937
|
+
return 'SortedSet(' + result.toString() + ')';
|
|
4938
|
+
}
|
|
4939
|
+
copy(data) {
|
|
4940
|
+
const result = new SortedSet(this.comparator);
|
|
4941
|
+
result.data = data;
|
|
4942
|
+
return result;
|
|
4968
4943
|
}
|
|
4969
4944
|
}
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
}
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4945
|
+
class SortedSetIterator {
|
|
4946
|
+
constructor(iter) {
|
|
4947
|
+
this.iter = iter;
|
|
4948
|
+
}
|
|
4949
|
+
getNext() {
|
|
4950
|
+
return this.iter.getNext().key;
|
|
4951
|
+
}
|
|
4952
|
+
hasNext() {
|
|
4953
|
+
return this.iter.hasNext();
|
|
4954
|
+
}
|
|
4977
4955
|
}
|
|
4978
4956
|
/**
|
|
4979
|
-
*
|
|
4980
|
-
*
|
|
4981
|
-
*
|
|
4982
|
-
*
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
*
|
|
4990
|
-
*
|
|
4957
|
+
* Compares two sorted sets for equality using their natural ordering. The
|
|
4958
|
+
* method computes the intersection and invokes `onAdd` for every element that
|
|
4959
|
+
* is in `after` but not `before`. `onRemove` is invoked for every element in
|
|
4960
|
+
* `before` but missing from `after`.
|
|
4961
|
+
*
|
|
4962
|
+
* The method creates a copy of both `before` and `after` and runs in O(n log
|
|
4963
|
+
* n), where n is the size of the two lists.
|
|
4964
|
+
*
|
|
4965
|
+
* @param before - The elements that exist in the original set.
|
|
4966
|
+
* @param after - The elements to diff against the original set.
|
|
4967
|
+
* @param comparator - The comparator for the elements in before and after.
|
|
4968
|
+
* @param onAdd - A function to invoke for every element that is part of `
|
|
4969
|
+
* after` but not `before`.
|
|
4970
|
+
* @param onRemove - A function to invoke for every element that is part of
|
|
4971
|
+
* `before` but not `after`.
|
|
4991
4972
|
*/
|
|
4992
|
-
function
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
if (filter.isInequality()) {
|
|
5015
|
-
return filter.field;
|
|
4973
|
+
function diffSortedSets(before, after, comparator, onAdd, onRemove) {
|
|
4974
|
+
const beforeIt = before.getIterator();
|
|
4975
|
+
const afterIt = after.getIterator();
|
|
4976
|
+
let beforeValue = advanceIterator(beforeIt);
|
|
4977
|
+
let afterValue = advanceIterator(afterIt);
|
|
4978
|
+
// Walk through the two sets at the same time, using the ordering defined by
|
|
4979
|
+
// `comparator`.
|
|
4980
|
+
while (beforeValue || afterValue) {
|
|
4981
|
+
let added = false;
|
|
4982
|
+
let removed = false;
|
|
4983
|
+
if (beforeValue && afterValue) {
|
|
4984
|
+
const cmp = comparator(beforeValue, afterValue);
|
|
4985
|
+
if (cmp < 0) {
|
|
4986
|
+
// The element was removed if the next element in our ordered
|
|
4987
|
+
// walkthrough is only in `before`.
|
|
4988
|
+
removed = true;
|
|
4989
|
+
}
|
|
4990
|
+
else if (cmp > 0) {
|
|
4991
|
+
// The element was added if the next element in our ordered walkthrough
|
|
4992
|
+
// is only in `after`.
|
|
4993
|
+
added = true;
|
|
4994
|
+
}
|
|
5016
4995
|
}
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
}
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
4996
|
+
else if (beforeValue != null) {
|
|
4997
|
+
removed = true;
|
|
4998
|
+
}
|
|
4999
|
+
else {
|
|
5000
|
+
added = true;
|
|
5001
|
+
}
|
|
5002
|
+
if (added) {
|
|
5003
|
+
onAdd(afterValue);
|
|
5004
|
+
afterValue = advanceIterator(afterIt);
|
|
5005
|
+
}
|
|
5006
|
+
else if (removed) {
|
|
5007
|
+
onRemove(beforeValue);
|
|
5008
|
+
beforeValue = advanceIterator(beforeIt);
|
|
5009
|
+
}
|
|
5010
|
+
else {
|
|
5011
|
+
beforeValue = advanceIterator(beforeIt);
|
|
5012
|
+
afterValue = advanceIterator(afterIt);
|
|
5028
5013
|
}
|
|
5029
5014
|
}
|
|
5030
|
-
return null;
|
|
5031
5015
|
}
|
|
5032
5016
|
/**
|
|
5033
|
-
*
|
|
5034
|
-
* within the provided collection group.
|
|
5017
|
+
* Returns the next element from the iterator or `undefined` if none available.
|
|
5035
5018
|
*/
|
|
5036
|
-
function
|
|
5037
|
-
return
|
|
5038
|
-
}
|
|
5019
|
+
function advanceIterator(it) {
|
|
5020
|
+
return it.hasNext() ? it.getNext() : undefined;
|
|
5021
|
+
}
|
|
5022
|
+
|
|
5039
5023
|
/**
|
|
5040
|
-
*
|
|
5041
|
-
*
|
|
5024
|
+
* @license
|
|
5025
|
+
* Copyright 2019 Google LLC
|
|
5026
|
+
*
|
|
5027
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5028
|
+
* you may not use this file except in compliance with the License.
|
|
5029
|
+
* You may obtain a copy of the License at
|
|
5030
|
+
*
|
|
5031
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
5032
|
+
*
|
|
5033
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
5034
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
5035
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
5036
|
+
* See the License for the specific language governing permissions and
|
|
5037
|
+
* limitations under the License.
|
|
5042
5038
|
*/
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5039
|
+
// Visible for testing
|
|
5040
|
+
class TargetImpl {
|
|
5041
|
+
constructor(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
|
|
5042
|
+
this.path = path;
|
|
5043
|
+
this.collectionGroup = collectionGroup;
|
|
5044
|
+
this.orderBy = orderBy;
|
|
5045
|
+
this.filters = filters;
|
|
5046
|
+
this.limit = limit;
|
|
5047
|
+
this.startAt = startAt;
|
|
5048
|
+
this.endAt = endAt;
|
|
5049
|
+
this.memoizedCanonicalId = null;
|
|
5050
|
+
}
|
|
5047
5051
|
}
|
|
5048
5052
|
/**
|
|
5049
|
-
*
|
|
5050
|
-
* collection.
|
|
5053
|
+
* Initializes a Target with a path and optional additional query constraints.
|
|
5054
|
+
* Path must currently be empty if this is a collection group query.
|
|
5055
|
+
*
|
|
5056
|
+
* NOTE: you should always construct `Target` from `Query.toTarget` instead of
|
|
5057
|
+
* using this factory method, because `Query` provides an implicit `orderBy`
|
|
5058
|
+
* property.
|
|
5051
5059
|
*/
|
|
5052
|
-
function
|
|
5053
|
-
return
|
|
5060
|
+
function newTarget(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
|
|
5061
|
+
return new TargetImpl(path, collectionGroup, orderBy, filters, limit, startAt, endAt);
|
|
5054
5062
|
}
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
const queryImpl = debugCast(query);
|
|
5062
|
-
if (queryImpl.memoizedOrderBy === null) {
|
|
5063
|
-
queryImpl.memoizedOrderBy = [];
|
|
5064
|
-
const inequalityField = getInequalityFilterField(queryImpl);
|
|
5065
|
-
const firstOrderByField = getFirstOrderByField(queryImpl);
|
|
5066
|
-
if (inequalityField !== null && firstOrderByField === null) {
|
|
5067
|
-
// In order to implicitly add key ordering, we must also add the
|
|
5068
|
-
// inequality filter field for it to be a valid query.
|
|
5069
|
-
// Note that the default inequality field and key ordering is ascending.
|
|
5070
|
-
if (!inequalityField.isKeyField()) {
|
|
5071
|
-
queryImpl.memoizedOrderBy.push(new OrderBy(inequalityField));
|
|
5072
|
-
}
|
|
5073
|
-
queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), "asc" /* ASCENDING */));
|
|
5063
|
+
function canonifyTarget(target) {
|
|
5064
|
+
const targetImpl = debugCast(target);
|
|
5065
|
+
if (targetImpl.memoizedCanonicalId === null) {
|
|
5066
|
+
let str = targetImpl.path.canonicalString();
|
|
5067
|
+
if (targetImpl.collectionGroup !== null) {
|
|
5068
|
+
str += '|cg:' + targetImpl.collectionGroup;
|
|
5074
5069
|
}
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
}
|
|
5083
|
-
if (!foundKeyOrdering) {
|
|
5084
|
-
// The order of the implicit key ordering always matches the last
|
|
5085
|
-
// explicit order by
|
|
5086
|
-
const lastDirection = queryImpl.explicitOrderBy.length > 0
|
|
5087
|
-
? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
|
|
5088
|
-
.dir
|
|
5089
|
-
: "asc" /* ASCENDING */;
|
|
5090
|
-
queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), lastDirection));
|
|
5091
|
-
}
|
|
5070
|
+
str += '|f:';
|
|
5071
|
+
str += targetImpl.filters.map(f => canonifyFilter(f)).join(',');
|
|
5072
|
+
str += '|ob:';
|
|
5073
|
+
str += targetImpl.orderBy.map(o => canonifyOrderBy(o)).join(',');
|
|
5074
|
+
if (!isNullOrUndefined(targetImpl.limit)) {
|
|
5075
|
+
str += '|l:';
|
|
5076
|
+
str += targetImpl.limit;
|
|
5092
5077
|
}
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
* Converts this `Query` instance to it's corresponding `Target` representation.
|
|
5098
|
-
*/
|
|
5099
|
-
function queryToTarget(query) {
|
|
5100
|
-
const queryImpl = debugCast(query);
|
|
5101
|
-
if (!queryImpl.memoizedTarget) {
|
|
5102
|
-
if (queryImpl.limitType === "F" /* First */) {
|
|
5103
|
-
queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, queryOrderBy(queryImpl), queryImpl.filters, queryImpl.limit, queryImpl.startAt, queryImpl.endAt);
|
|
5078
|
+
if (targetImpl.startAt) {
|
|
5079
|
+
str += '|lb:';
|
|
5080
|
+
str += targetImpl.startAt.inclusive ? 'b:' : 'a:';
|
|
5081
|
+
str += targetImpl.startAt.position.map(p => canonicalId(p)).join(',');
|
|
5104
5082
|
}
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
const dir = orderBy.dir === "desc" /* DESCENDING */
|
|
5110
|
-
? "asc" /* ASCENDING */
|
|
5111
|
-
: "desc" /* DESCENDING */;
|
|
5112
|
-
orderBys.push(new OrderBy(orderBy.field, dir));
|
|
5113
|
-
}
|
|
5114
|
-
// We need to swap the cursors to match the now-flipped query ordering.
|
|
5115
|
-
const startAt = queryImpl.endAt
|
|
5116
|
-
? new Bound(queryImpl.endAt.position, !queryImpl.endAt.inclusive)
|
|
5117
|
-
: null;
|
|
5118
|
-
const endAt = queryImpl.startAt
|
|
5119
|
-
? new Bound(queryImpl.startAt.position, !queryImpl.startAt.inclusive)
|
|
5120
|
-
: null;
|
|
5121
|
-
// Now return as a LimitType.First query.
|
|
5122
|
-
queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, orderBys, queryImpl.filters, queryImpl.limit, startAt, endAt);
|
|
5083
|
+
if (targetImpl.endAt) {
|
|
5084
|
+
str += '|ub:';
|
|
5085
|
+
str += targetImpl.endAt.inclusive ? 'a:' : 'b:';
|
|
5086
|
+
str += targetImpl.endAt.position.map(p => canonicalId(p)).join(',');
|
|
5123
5087
|
}
|
|
5088
|
+
targetImpl.memoizedCanonicalId = str;
|
|
5124
5089
|
}
|
|
5125
|
-
return
|
|
5126
|
-
}
|
|
5127
|
-
function queryWithAddedFilter(query, filter) {
|
|
5128
|
-
const newFilters = query.filters.concat([filter]);
|
|
5129
|
-
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), newFilters, query.limit, query.limitType, query.startAt, query.endAt);
|
|
5130
|
-
}
|
|
5131
|
-
function queryWithAddedOrderBy(query, orderBy) {
|
|
5132
|
-
// TODO(dimond): validate that orderBy does not list the same key twice.
|
|
5133
|
-
const newOrderBy = query.explicitOrderBy.concat([orderBy]);
|
|
5134
|
-
return new QueryImpl(query.path, query.collectionGroup, newOrderBy, query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
|
|
5135
|
-
}
|
|
5136
|
-
function queryWithLimit(query, limit, limitType) {
|
|
5137
|
-
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), limit, limitType, query.startAt, query.endAt);
|
|
5138
|
-
}
|
|
5139
|
-
function queryWithStartAt(query, bound) {
|
|
5140
|
-
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, bound, query.endAt);
|
|
5141
|
-
}
|
|
5142
|
-
function queryWithEndAt(query, bound) {
|
|
5143
|
-
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, bound);
|
|
5090
|
+
return targetImpl.memoizedCanonicalId;
|
|
5144
5091
|
}
|
|
5145
|
-
function
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
// TODO(b/29183165): This is used to get a unique string from a query to, for
|
|
5150
|
-
// example, use as a dictionary key, but the implementation is subject to
|
|
5151
|
-
// collisions. Make it collision-free.
|
|
5152
|
-
function canonifyQuery(query) {
|
|
5153
|
-
return `${canonifyTarget(queryToTarget(query))}|lt:${query.limitType}`;
|
|
5154
|
-
}
|
|
5155
|
-
function stringifyQuery(query) {
|
|
5156
|
-
return `Query(target=${stringifyTarget(queryToTarget(query))}; limitType=${query.limitType})`;
|
|
5157
|
-
}
|
|
5158
|
-
/** Returns whether `doc` matches the constraints of `query`. */
|
|
5159
|
-
function queryMatches(query, doc) {
|
|
5160
|
-
return (doc.isFoundDocument() &&
|
|
5161
|
-
queryMatchesPathAndCollectionGroup(query, doc) &&
|
|
5162
|
-
queryMatchesOrderBy(query, doc) &&
|
|
5163
|
-
queryMatchesFilters(query, doc) &&
|
|
5164
|
-
queryMatchesBounds(query, doc));
|
|
5165
|
-
}
|
|
5166
|
-
function queryMatchesPathAndCollectionGroup(query, doc) {
|
|
5167
|
-
const docPath = doc.key.path;
|
|
5168
|
-
if (query.collectionGroup !== null) {
|
|
5169
|
-
// NOTE: this.path is currently always empty since we don't expose Collection
|
|
5170
|
-
// Group queries rooted at a document path yet.
|
|
5171
|
-
return (doc.key.hasCollectionId(query.collectionGroup) &&
|
|
5172
|
-
query.path.isPrefixOf(docPath));
|
|
5092
|
+
function stringifyTarget(target) {
|
|
5093
|
+
let str = target.path.canonicalString();
|
|
5094
|
+
if (target.collectionGroup !== null) {
|
|
5095
|
+
str += ' collectionGroup=' + target.collectionGroup;
|
|
5173
5096
|
}
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5097
|
+
if (target.filters.length > 0) {
|
|
5098
|
+
str += `, filters: [${target.filters
|
|
5099
|
+
.map(f => stringifyFilter(f))
|
|
5100
|
+
.join(', ')}]`;
|
|
5177
5101
|
}
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5102
|
+
if (!isNullOrUndefined(target.limit)) {
|
|
5103
|
+
str += ', limit: ' + target.limit;
|
|
5104
|
+
}
|
|
5105
|
+
if (target.orderBy.length > 0) {
|
|
5106
|
+
str += `, orderBy: [${target.orderBy
|
|
5107
|
+
.map(o => stringifyOrderBy(o))
|
|
5108
|
+
.join(', ')}]`;
|
|
5109
|
+
}
|
|
5110
|
+
if (target.startAt) {
|
|
5111
|
+
str += ', startAt: ';
|
|
5112
|
+
str += target.startAt.inclusive ? 'b:' : 'a:';
|
|
5113
|
+
str += target.startAt.position.map(p => canonicalId(p)).join(',');
|
|
5114
|
+
}
|
|
5115
|
+
if (target.endAt) {
|
|
5116
|
+
str += ', endAt: ';
|
|
5117
|
+
str += target.endAt.inclusive ? 'a:' : 'b:';
|
|
5118
|
+
str += target.endAt.position.map(p => canonicalId(p)).join(',');
|
|
5181
5119
|
}
|
|
5120
|
+
return `Target(${str})`;
|
|
5182
5121
|
}
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5122
|
+
function targetEquals(left, right) {
|
|
5123
|
+
if (left.limit !== right.limit) {
|
|
5124
|
+
return false;
|
|
5125
|
+
}
|
|
5126
|
+
if (left.orderBy.length !== right.orderBy.length) {
|
|
5127
|
+
return false;
|
|
5128
|
+
}
|
|
5129
|
+
for (let i = 0; i < left.orderBy.length; i++) {
|
|
5130
|
+
if (!orderByEquals(left.orderBy[i], right.orderBy[i])) {
|
|
5191
5131
|
return false;
|
|
5192
5132
|
}
|
|
5193
5133
|
}
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
for (
|
|
5198
|
-
if (!
|
|
5134
|
+
if (left.filters.length !== right.filters.length) {
|
|
5135
|
+
return false;
|
|
5136
|
+
}
|
|
5137
|
+
for (let i = 0; i < left.filters.length; i++) {
|
|
5138
|
+
if (!filterEquals(left.filters[i], right.filters[i])) {
|
|
5199
5139
|
return false;
|
|
5200
5140
|
}
|
|
5201
5141
|
}
|
|
5202
|
-
|
|
5203
|
-
}
|
|
5204
|
-
/** Makes sure a document is within the bounds, if provided. */
|
|
5205
|
-
function queryMatchesBounds(query, doc) {
|
|
5206
|
-
if (query.startAt &&
|
|
5207
|
-
!boundSortsBeforeDocument(query.startAt, queryOrderBy(query), doc)) {
|
|
5142
|
+
if (left.collectionGroup !== right.collectionGroup) {
|
|
5208
5143
|
return false;
|
|
5209
5144
|
}
|
|
5210
|
-
if (
|
|
5211
|
-
!boundSortsAfterDocument(query.endAt, queryOrderBy(query), doc)) {
|
|
5145
|
+
if (!left.path.isEqual(right.path)) {
|
|
5212
5146
|
return false;
|
|
5213
5147
|
}
|
|
5214
|
-
|
|
5148
|
+
if (!boundEquals(left.startAt, right.startAt)) {
|
|
5149
|
+
return false;
|
|
5150
|
+
}
|
|
5151
|
+
return boundEquals(left.endAt, right.endAt);
|
|
5152
|
+
}
|
|
5153
|
+
function targetIsDocumentTarget(target) {
|
|
5154
|
+
return (DocumentKey.isDocumentKey(target.path) &&
|
|
5155
|
+
target.collectionGroup === null &&
|
|
5156
|
+
target.filters.length === 0);
|
|
5157
|
+
}
|
|
5158
|
+
/** Returns the field filters that target the given field path. */
|
|
5159
|
+
function targetGetFieldFiltersForPath(target, path) {
|
|
5160
|
+
return target.filters.filter(f => f instanceof FieldFilter && f.field.isEqual(path));
|
|
5215
5161
|
}
|
|
5216
5162
|
/**
|
|
5217
|
-
* Returns the
|
|
5218
|
-
*
|
|
5219
|
-
* PORTING NOTE: This is only used in the Web SDK to facilitate multi-tab
|
|
5220
|
-
* synchronization for query results.
|
|
5163
|
+
* Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY
|
|
5164
|
+
* filters. Returns `null` if there are no such filters.
|
|
5221
5165
|
*/
|
|
5222
|
-
function
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5166
|
+
function targetGetArrayValues(target, fieldIndex) {
|
|
5167
|
+
const segment = fieldIndexGetArraySegment(fieldIndex);
|
|
5168
|
+
if (segment === undefined) {
|
|
5169
|
+
return null;
|
|
5170
|
+
}
|
|
5171
|
+
for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
|
|
5172
|
+
switch (fieldFilter.op) {
|
|
5173
|
+
case "array-contains-any" /* ARRAY_CONTAINS_ANY */:
|
|
5174
|
+
return fieldFilter.value.arrayValue.values || [];
|
|
5175
|
+
case "array-contains" /* ARRAY_CONTAINS */:
|
|
5176
|
+
return [fieldFilter.value];
|
|
5177
|
+
// Remaining filters are not array filters.
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
return null;
|
|
5227
5181
|
}
|
|
5228
5182
|
/**
|
|
5229
|
-
* Returns
|
|
5230
|
-
*
|
|
5183
|
+
* Returns the list of values that are used in != or NOT_IN filters. Returns
|
|
5184
|
+
* `null` if there are no such filters.
|
|
5231
5185
|
*/
|
|
5232
|
-
function
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
for (const
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5186
|
+
function targetGetNotInValues(target, fieldIndex) {
|
|
5187
|
+
const values = new Map();
|
|
5188
|
+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
5189
|
+
for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
|
|
5190
|
+
switch (fieldFilter.op) {
|
|
5191
|
+
case "==" /* EQUAL */:
|
|
5192
|
+
case "in" /* IN */:
|
|
5193
|
+
// Encode equality prefix, which is encoded in the index value before
|
|
5194
|
+
// the inequality (e.g. `a == 'a' && b != 'b'` is encoded to
|
|
5195
|
+
// `value != 'ab'`).
|
|
5196
|
+
values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
|
|
5197
|
+
break;
|
|
5198
|
+
case "not-in" /* NOT_IN */:
|
|
5199
|
+
case "!=" /* NOT_EQUAL */:
|
|
5200
|
+
// NotIn/NotEqual is always a suffix. There cannot be any remaining
|
|
5201
|
+
// segments and hence we can return early here.
|
|
5202
|
+
values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
|
|
5203
|
+
return Array.from(values.values());
|
|
5204
|
+
// Remaining filters cannot be used as notIn bounds.
|
|
5239
5205
|
}
|
|
5240
|
-
comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
|
|
5241
5206
|
}
|
|
5242
|
-
return 0;
|
|
5243
|
-
};
|
|
5244
|
-
}
|
|
5245
|
-
function compareDocs(orderBy, d1, d2) {
|
|
5246
|
-
const comparison = orderBy.field.isKeyField()
|
|
5247
|
-
? DocumentKey.comparator(d1.key, d2.key)
|
|
5248
|
-
: compareDocumentsByField(orderBy.field, d1, d2);
|
|
5249
|
-
switch (orderBy.dir) {
|
|
5250
|
-
case "asc" /* ASCENDING */:
|
|
5251
|
-
return comparison;
|
|
5252
|
-
case "desc" /* DESCENDING */:
|
|
5253
|
-
return -1 * comparison;
|
|
5254
|
-
default:
|
|
5255
|
-
return fail();
|
|
5256
5207
|
}
|
|
5257
|
-
|
|
5258
|
-
|
|
5208
|
+
return null;
|
|
5209
|
+
}
|
|
5259
5210
|
/**
|
|
5260
|
-
*
|
|
5261
|
-
*
|
|
5262
|
-
*
|
|
5263
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5264
|
-
* you may not use this file except in compliance with the License.
|
|
5265
|
-
* You may obtain a copy of the License at
|
|
5266
|
-
*
|
|
5267
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
5268
|
-
*
|
|
5269
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
5270
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
5271
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
5272
|
-
* See the License for the specific language governing permissions and
|
|
5273
|
-
* limitations under the License.
|
|
5211
|
+
* Returns a lower bound of field values that can be used as a starting point to
|
|
5212
|
+
* scan the index defined by `fieldIndex`. Returns `null` if no lower bound
|
|
5213
|
+
* exists.
|
|
5274
5214
|
*/
|
|
5215
|
+
function targetGetLowerBound(target, fieldIndex) {
|
|
5216
|
+
const values = [];
|
|
5217
|
+
let inclusive = true;
|
|
5218
|
+
// For each segment, retrieve a lower bound if there is a suitable filter or
|
|
5219
|
+
// startAt.
|
|
5220
|
+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
5221
|
+
const segmentBound = segment.kind === 0 /* ASCENDING */
|
|
5222
|
+
? targetGetAscendingBound(target, segment.fieldPath, target.startAt)
|
|
5223
|
+
: targetGetDescendingBound(target, segment.fieldPath, target.startAt);
|
|
5224
|
+
if (!segmentBound.value) {
|
|
5225
|
+
// No lower bound exists
|
|
5226
|
+
return null;
|
|
5227
|
+
}
|
|
5228
|
+
values.push(segmentBound.value);
|
|
5229
|
+
inclusive && (inclusive = segmentBound.inclusive);
|
|
5230
|
+
}
|
|
5231
|
+
return new Bound(values, inclusive);
|
|
5232
|
+
}
|
|
5275
5233
|
/**
|
|
5276
|
-
*
|
|
5277
|
-
*
|
|
5278
|
-
*
|
|
5279
|
-
* automatically handles collisions of keys.
|
|
5234
|
+
* Returns an upper bound of field values that can be used as an ending point
|
|
5235
|
+
* when scanning the index defined by `fieldIndex`. Returns `null` if no
|
|
5236
|
+
* upper bound exists.
|
|
5280
5237
|
*/
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
this.innerSize = 0;
|
|
5294
|
-
}
|
|
5295
|
-
/** Get a value for this key, or undefined if it does not exist. */
|
|
5296
|
-
get(key) {
|
|
5297
|
-
const id = this.mapKeyFn(key);
|
|
5298
|
-
const matches = this.inner[id];
|
|
5299
|
-
if (matches === undefined) {
|
|
5300
|
-
return undefined;
|
|
5301
|
-
}
|
|
5302
|
-
for (const [otherKey, value] of matches) {
|
|
5303
|
-
if (this.equalsFn(otherKey, key)) {
|
|
5304
|
-
return value;
|
|
5305
|
-
}
|
|
5238
|
+
function targetGetUpperBound(target, fieldIndex) {
|
|
5239
|
+
const values = [];
|
|
5240
|
+
let inclusive = true;
|
|
5241
|
+
// For each segment, retrieve an upper bound if there is a suitable filter or
|
|
5242
|
+
// endAt.
|
|
5243
|
+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
5244
|
+
const segmentBound = segment.kind === 0 /* ASCENDING */
|
|
5245
|
+
? targetGetDescendingBound(target, segment.fieldPath, target.endAt)
|
|
5246
|
+
: targetGetAscendingBound(target, segment.fieldPath, target.endAt);
|
|
5247
|
+
if (!segmentBound.value) {
|
|
5248
|
+
// No upper bound exists
|
|
5249
|
+
return null;
|
|
5306
5250
|
}
|
|
5307
|
-
|
|
5251
|
+
values.push(segmentBound.value);
|
|
5252
|
+
inclusive && (inclusive = segmentBound.inclusive);
|
|
5308
5253
|
}
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5254
|
+
return new Bound(values, inclusive);
|
|
5255
|
+
}
|
|
5256
|
+
/**
|
|
5257
|
+
* Returns the value to use as the lower bound for ascending index segment at
|
|
5258
|
+
* the provided `fieldPath` (or the upper bound for an descending segment).
|
|
5259
|
+
*/
|
|
5260
|
+
function targetGetAscendingBound(target, fieldPath, bound) {
|
|
5261
|
+
let value = undefined;
|
|
5262
|
+
let inclusive = true;
|
|
5263
|
+
// Process all filters to find a value for the current field segment
|
|
5264
|
+
for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
|
|
5265
|
+
let filterValue = undefined;
|
|
5266
|
+
let filterInclusive = true;
|
|
5267
|
+
switch (fieldFilter.op) {
|
|
5268
|
+
case "<" /* LESS_THAN */:
|
|
5269
|
+
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
5270
|
+
filterValue = valuesGetLowerBound(fieldFilter.value);
|
|
5271
|
+
break;
|
|
5272
|
+
case "==" /* EQUAL */:
|
|
5273
|
+
case "in" /* IN */:
|
|
5274
|
+
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
5275
|
+
filterValue = fieldFilter.value;
|
|
5276
|
+
break;
|
|
5277
|
+
case ">" /* GREATER_THAN */:
|
|
5278
|
+
filterValue = fieldFilter.value;
|
|
5279
|
+
filterInclusive = false;
|
|
5280
|
+
break;
|
|
5281
|
+
case "!=" /* NOT_EQUAL */:
|
|
5282
|
+
case "not-in" /* NOT_IN */:
|
|
5283
|
+
filterValue = MIN_VALUE;
|
|
5284
|
+
break;
|
|
5285
|
+
// Remaining filters cannot be used as lower bounds.
|
|
5286
|
+
}
|
|
5287
|
+
if (valuesMax(value, filterValue) === filterValue) {
|
|
5288
|
+
value = filterValue;
|
|
5289
|
+
inclusive = filterInclusive;
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
// If there is an additional bound, compare the values against the existing
|
|
5293
|
+
// range to see if we can narrow the scope.
|
|
5294
|
+
if (bound !== null) {
|
|
5295
|
+
for (let i = 0; i < target.orderBy.length; ++i) {
|
|
5296
|
+
const orderBy = target.orderBy[i];
|
|
5297
|
+
if (orderBy.field.isEqual(fieldPath)) {
|
|
5298
|
+
const cursorValue = bound.position[i];
|
|
5299
|
+
if (valuesMax(value, cursorValue) === cursorValue) {
|
|
5300
|
+
value = cursorValue;
|
|
5301
|
+
inclusive = bound.inclusive;
|
|
5347
5302
|
}
|
|
5348
|
-
|
|
5349
|
-
return true;
|
|
5303
|
+
break;
|
|
5350
5304
|
}
|
|
5351
5305
|
}
|
|
5352
|
-
return false;
|
|
5353
|
-
}
|
|
5354
|
-
forEach(fn) {
|
|
5355
|
-
forEach(this.inner, (_, entries) => {
|
|
5356
|
-
for (const [k, v] of entries) {
|
|
5357
|
-
fn(k, v);
|
|
5358
|
-
}
|
|
5359
|
-
});
|
|
5360
|
-
}
|
|
5361
|
-
isEmpty() {
|
|
5362
|
-
return isEmpty(this.inner);
|
|
5363
5306
|
}
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
}
|
|
5367
|
-
}
|
|
5368
|
-
|
|
5307
|
+
return { value, inclusive };
|
|
5308
|
+
}
|
|
5369
5309
|
/**
|
|
5370
|
-
*
|
|
5371
|
-
*
|
|
5372
|
-
*
|
|
5373
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5374
|
-
* you may not use this file except in compliance with the License.
|
|
5375
|
-
* You may obtain a copy of the License at
|
|
5376
|
-
*
|
|
5377
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
5378
|
-
*
|
|
5379
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
5380
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
5381
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
5382
|
-
* See the License for the specific language governing permissions and
|
|
5383
|
-
* limitations under the License.
|
|
5310
|
+
* Returns the value to use as the upper bound for ascending index segment at
|
|
5311
|
+
* the provided `fieldPath` (or the lower bound for a descending segment).
|
|
5384
5312
|
*/
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5313
|
+
function targetGetDescendingBound(target, fieldPath, bound) {
|
|
5314
|
+
let value = undefined;
|
|
5315
|
+
let inclusive = true;
|
|
5316
|
+
// Process all filters to find a value for the current field segment
|
|
5317
|
+
for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
|
|
5318
|
+
let filterValue = undefined;
|
|
5319
|
+
let filterInclusive = true;
|
|
5320
|
+
switch (fieldFilter.op) {
|
|
5321
|
+
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
5322
|
+
case ">" /* GREATER_THAN */:
|
|
5323
|
+
filterValue = valuesGetUpperBound(fieldFilter.value);
|
|
5324
|
+
filterInclusive = false;
|
|
5325
|
+
break;
|
|
5326
|
+
case "==" /* EQUAL */:
|
|
5327
|
+
case "in" /* IN */:
|
|
5328
|
+
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
5329
|
+
filterValue = fieldFilter.value;
|
|
5330
|
+
break;
|
|
5331
|
+
case "<" /* LESS_THAN */:
|
|
5332
|
+
filterValue = fieldFilter.value;
|
|
5333
|
+
filterInclusive = false;
|
|
5334
|
+
break;
|
|
5335
|
+
case "!=" /* NOT_EQUAL */:
|
|
5336
|
+
case "not-in" /* NOT_IN */:
|
|
5337
|
+
filterValue = MAX_VALUE;
|
|
5338
|
+
break;
|
|
5339
|
+
// Remaining filters cannot be used as upper bounds.
|
|
5340
|
+
}
|
|
5341
|
+
if (valuesMin(value, filterValue) === filterValue) {
|
|
5342
|
+
value = filterValue;
|
|
5343
|
+
inclusive = filterInclusive;
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
// If there is an additional bound, compare the values against the existing
|
|
5347
|
+
// range to see if we can narrow the scope.
|
|
5348
|
+
if (bound !== null) {
|
|
5349
|
+
for (let i = 0; i < target.orderBy.length; ++i) {
|
|
5350
|
+
const orderBy = target.orderBy[i];
|
|
5351
|
+
if (orderBy.field.isEqual(fieldPath)) {
|
|
5352
|
+
const cursorValue = bound.position[i];
|
|
5353
|
+
if (valuesMin(value, cursorValue) === cursorValue) {
|
|
5354
|
+
value = cursorValue;
|
|
5355
|
+
inclusive = bound.inclusive;
|
|
5356
|
+
}
|
|
5357
|
+
break;
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5397
5360
|
}
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5361
|
+
return { value, inclusive };
|
|
5362
|
+
}
|
|
5363
|
+
/** Returns the number of segments of a perfect index for this target. */
|
|
5364
|
+
function targetGetSegmentCount(target) {
|
|
5365
|
+
let fields = new SortedSet(FieldPath$1.comparator);
|
|
5366
|
+
let hasArraySegment = false;
|
|
5367
|
+
for (const filter of target.filters) {
|
|
5368
|
+
// TODO(orquery): Use the flattened filters here
|
|
5369
|
+
const fieldFilter = filter;
|
|
5370
|
+
// __name__ is not an explicit segment of any index, so we don't need to
|
|
5371
|
+
// count it.
|
|
5372
|
+
if (fieldFilter.field.isKeyField()) {
|
|
5373
|
+
continue;
|
|
5374
|
+
}
|
|
5375
|
+
// ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filters must be counted separately.
|
|
5376
|
+
// For instance, it is possible to have an index for "a ARRAY a ASC". Even
|
|
5377
|
+
// though these are on the same field, they should be counted as two
|
|
5378
|
+
// separate segments in an index.
|
|
5379
|
+
if (fieldFilter.op === "array-contains" /* ARRAY_CONTAINS */ ||
|
|
5380
|
+
fieldFilter.op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
|
|
5381
|
+
hasArraySegment = true;
|
|
5382
|
+
}
|
|
5383
|
+
else {
|
|
5384
|
+
fields = fields.add(fieldFilter.field);
|
|
5385
|
+
}
|
|
5403
5386
|
}
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
if (cmp === 0) {
|
|
5410
|
-
return node.value;
|
|
5411
|
-
}
|
|
5412
|
-
else if (cmp < 0) {
|
|
5413
|
-
node = node.left;
|
|
5414
|
-
}
|
|
5415
|
-
else if (cmp > 0) {
|
|
5416
|
-
node = node.right;
|
|
5417
|
-
}
|
|
5387
|
+
for (const orderBy of target.orderBy) {
|
|
5388
|
+
// __name__ is not an explicit segment of any index, so we don't need to
|
|
5389
|
+
// count it.
|
|
5390
|
+
if (!orderBy.field.isKeyField()) {
|
|
5391
|
+
fields = fields.add(orderBy.field);
|
|
5418
5392
|
}
|
|
5419
|
-
return null;
|
|
5420
5393
|
}
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5394
|
+
return fields.size + (hasArraySegment ? 1 : 0);
|
|
5395
|
+
}
|
|
5396
|
+
class Filter {
|
|
5397
|
+
}
|
|
5398
|
+
class FieldFilter extends Filter {
|
|
5399
|
+
constructor(field, op, value) {
|
|
5400
|
+
super();
|
|
5401
|
+
this.field = field;
|
|
5402
|
+
this.op = op;
|
|
5403
|
+
this.value = value;
|
|
5404
|
+
}
|
|
5405
|
+
/**
|
|
5406
|
+
* Creates a filter based on the provided arguments.
|
|
5407
|
+
*/
|
|
5408
|
+
static create(field, op, value) {
|
|
5409
|
+
if (field.isKeyField()) {
|
|
5410
|
+
if (op === "in" /* IN */ || op === "not-in" /* NOT_IN */) {
|
|
5411
|
+
return this.createKeyFieldInFilter(field, op, value);
|
|
5434
5412
|
}
|
|
5435
5413
|
else {
|
|
5436
|
-
|
|
5437
|
-
prunedNodes += node.left.size + 1;
|
|
5438
|
-
node = node.right;
|
|
5414
|
+
return new KeyFieldFilter(field, op, value);
|
|
5439
5415
|
}
|
|
5440
5416
|
}
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5417
|
+
else if (op === "array-contains" /* ARRAY_CONTAINS */) {
|
|
5418
|
+
return new ArrayContainsFilter(field, value);
|
|
5419
|
+
}
|
|
5420
|
+
else if (op === "in" /* IN */) {
|
|
5421
|
+
return new InFilter(field, value);
|
|
5422
|
+
}
|
|
5423
|
+
else if (op === "not-in" /* NOT_IN */) {
|
|
5424
|
+
return new NotInFilter(field, value);
|
|
5425
|
+
}
|
|
5426
|
+
else if (op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
|
|
5427
|
+
return new ArrayContainsAnyFilter(field, value);
|
|
5428
|
+
}
|
|
5429
|
+
else {
|
|
5430
|
+
return new FieldFilter(field, op, value);
|
|
5431
|
+
}
|
|
5446
5432
|
}
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5433
|
+
static createKeyFieldInFilter(field, op, value) {
|
|
5434
|
+
return op === "in" /* IN */
|
|
5435
|
+
? new KeyFieldInFilter(field, value)
|
|
5436
|
+
: new KeyFieldNotInFilter(field, value);
|
|
5450
5437
|
}
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5438
|
+
matches(doc) {
|
|
5439
|
+
const other = doc.data.field(this.field);
|
|
5440
|
+
// Types do not have to match in NOT_EQUAL filters.
|
|
5441
|
+
if (this.op === "!=" /* NOT_EQUAL */) {
|
|
5442
|
+
return (other !== null &&
|
|
5443
|
+
this.matchesComparison(valueCompare(other, this.value)));
|
|
5444
|
+
}
|
|
5445
|
+
// Only compare types with matching backend order (such as double and int).
|
|
5446
|
+
return (other !== null &&
|
|
5447
|
+
typeOrder(this.value) === typeOrder(other) &&
|
|
5448
|
+
this.matchesComparison(valueCompare(other, this.value)));
|
|
5454
5449
|
}
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5450
|
+
matchesComparison(comparison) {
|
|
5451
|
+
switch (this.op) {
|
|
5452
|
+
case "<" /* LESS_THAN */:
|
|
5453
|
+
return comparison < 0;
|
|
5454
|
+
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
5455
|
+
return comparison <= 0;
|
|
5456
|
+
case "==" /* EQUAL */:
|
|
5457
|
+
return comparison === 0;
|
|
5458
|
+
case "!=" /* NOT_EQUAL */:
|
|
5459
|
+
return comparison !== 0;
|
|
5460
|
+
case ">" /* GREATER_THAN */:
|
|
5461
|
+
return comparison > 0;
|
|
5462
|
+
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
5463
|
+
return comparison >= 0;
|
|
5464
|
+
default:
|
|
5465
|
+
return fail();
|
|
5466
|
+
}
|
|
5458
5467
|
}
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5468
|
+
isInequality() {
|
|
5469
|
+
return ([
|
|
5470
|
+
"<" /* LESS_THAN */,
|
|
5471
|
+
"<=" /* LESS_THAN_OR_EQUAL */,
|
|
5472
|
+
">" /* GREATER_THAN */,
|
|
5473
|
+
">=" /* GREATER_THAN_OR_EQUAL */,
|
|
5474
|
+
"!=" /* NOT_EQUAL */,
|
|
5475
|
+
"not-in" /* NOT_IN */
|
|
5476
|
+
].indexOf(this.op) >= 0);
|
|
5465
5477
|
}
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5478
|
+
}
|
|
5479
|
+
function canonifyFilter(filter) {
|
|
5480
|
+
// TODO(b/29183165): Technically, this won't be unique if two values have
|
|
5481
|
+
// the same description, such as the int 3 and the string "3". So we should
|
|
5482
|
+
// add the types in here somehow, too.
|
|
5483
|
+
return (filter.field.canonicalString() +
|
|
5484
|
+
filter.op.toString() +
|
|
5485
|
+
canonicalId(filter.value));
|
|
5486
|
+
}
|
|
5487
|
+
function filterEquals(f1, f2) {
|
|
5488
|
+
return (f1.op === f2.op &&
|
|
5489
|
+
f1.field.isEqual(f2.field) &&
|
|
5490
|
+
valueEquals(f1.value, f2.value));
|
|
5491
|
+
}
|
|
5492
|
+
/** Returns a debug description for `filter`. */
|
|
5493
|
+
function stringifyFilter(filter) {
|
|
5494
|
+
return `${filter.field.canonicalString()} ${filter.op} ${canonicalId(filter.value)}`;
|
|
5495
|
+
}
|
|
5496
|
+
/** Filter that matches on key fields (i.e. '__name__'). */
|
|
5497
|
+
class KeyFieldFilter extends FieldFilter {
|
|
5498
|
+
constructor(field, op, value) {
|
|
5499
|
+
super(field, op, value);
|
|
5500
|
+
this.key = DocumentKey.fromName(value.referenceValue);
|
|
5471
5501
|
}
|
|
5472
|
-
|
|
5473
|
-
const
|
|
5474
|
-
this.
|
|
5475
|
-
descriptions.push(`${k}:${v}`);
|
|
5476
|
-
return false;
|
|
5477
|
-
});
|
|
5478
|
-
return `{${descriptions.join(', ')}}`;
|
|
5502
|
+
matches(doc) {
|
|
5503
|
+
const comparison = DocumentKey.comparator(doc.key, this.key);
|
|
5504
|
+
return this.matchesComparison(comparison);
|
|
5479
5505
|
}
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
return this.root.reverseTraversal(action);
|
|
5506
|
+
}
|
|
5507
|
+
/** Filter that matches on key fields within an array. */
|
|
5508
|
+
class KeyFieldInFilter extends FieldFilter {
|
|
5509
|
+
constructor(field, value) {
|
|
5510
|
+
super(field, "in" /* IN */, value);
|
|
5511
|
+
this.keys = extractDocumentKeysFromArrayValue("in" /* IN */, value);
|
|
5487
5512
|
}
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
return new SortedMapIterator(this.root, null, this.comparator, false);
|
|
5513
|
+
matches(doc) {
|
|
5514
|
+
return this.keys.some(key => key.isEqual(doc.key));
|
|
5491
5515
|
}
|
|
5492
|
-
|
|
5493
|
-
|
|
5516
|
+
}
|
|
5517
|
+
/** Filter that matches on key fields not present within an array. */
|
|
5518
|
+
class KeyFieldNotInFilter extends FieldFilter {
|
|
5519
|
+
constructor(field, value) {
|
|
5520
|
+
super(field, "not-in" /* NOT_IN */, value);
|
|
5521
|
+
this.keys = extractDocumentKeysFromArrayValue("not-in" /* NOT_IN */, value);
|
|
5494
5522
|
}
|
|
5495
|
-
|
|
5496
|
-
return
|
|
5523
|
+
matches(doc) {
|
|
5524
|
+
return !this.keys.some(key => key.isEqual(doc.key));
|
|
5497
5525
|
}
|
|
5498
|
-
|
|
5499
|
-
|
|
5526
|
+
}
|
|
5527
|
+
function extractDocumentKeysFromArrayValue(op, value) {
|
|
5528
|
+
var _a;
|
|
5529
|
+
return (((_a = value.arrayValue) === null || _a === void 0 ? void 0 : _a.values) || []).map(v => {
|
|
5530
|
+
return DocumentKey.fromName(v.referenceValue);
|
|
5531
|
+
});
|
|
5532
|
+
}
|
|
5533
|
+
/** A Filter that implements the array-contains operator. */
|
|
5534
|
+
class ArrayContainsFilter extends FieldFilter {
|
|
5535
|
+
constructor(field, value) {
|
|
5536
|
+
super(field, "array-contains" /* ARRAY_CONTAINS */, value);
|
|
5500
5537
|
}
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
constructor(node, startKey, comparator, isReverse) {
|
|
5505
|
-
this.isReverse = isReverse;
|
|
5506
|
-
this.nodeStack = [];
|
|
5507
|
-
let cmp = 1;
|
|
5508
|
-
while (!node.isEmpty()) {
|
|
5509
|
-
cmp = startKey ? comparator(node.key, startKey) : 1;
|
|
5510
|
-
// flip the comparison if we're going in reverse
|
|
5511
|
-
if (startKey && isReverse) {
|
|
5512
|
-
cmp *= -1;
|
|
5513
|
-
}
|
|
5514
|
-
if (cmp < 0) {
|
|
5515
|
-
// This node is less than our start key. ignore it
|
|
5516
|
-
if (this.isReverse) {
|
|
5517
|
-
node = node.left;
|
|
5518
|
-
}
|
|
5519
|
-
else {
|
|
5520
|
-
node = node.right;
|
|
5521
|
-
}
|
|
5522
|
-
}
|
|
5523
|
-
else if (cmp === 0) {
|
|
5524
|
-
// This node is exactly equal to our start key. Push it on the stack,
|
|
5525
|
-
// but stop iterating;
|
|
5526
|
-
this.nodeStack.push(node);
|
|
5527
|
-
break;
|
|
5528
|
-
}
|
|
5529
|
-
else {
|
|
5530
|
-
// This node is greater than our start key, add it to the stack and move
|
|
5531
|
-
// to the next one
|
|
5532
|
-
this.nodeStack.push(node);
|
|
5533
|
-
if (this.isReverse) {
|
|
5534
|
-
node = node.right;
|
|
5535
|
-
}
|
|
5536
|
-
else {
|
|
5537
|
-
node = node.left;
|
|
5538
|
-
}
|
|
5539
|
-
}
|
|
5540
|
-
}
|
|
5538
|
+
matches(doc) {
|
|
5539
|
+
const other = doc.data.field(this.field);
|
|
5540
|
+
return isArray(other) && arrayValueContains(other.arrayValue, this.value);
|
|
5541
5541
|
}
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
while (!node.isEmpty()) {
|
|
5548
|
-
this.nodeStack.push(node);
|
|
5549
|
-
node = node.right;
|
|
5550
|
-
}
|
|
5551
|
-
}
|
|
5552
|
-
else {
|
|
5553
|
-
node = node.right;
|
|
5554
|
-
while (!node.isEmpty()) {
|
|
5555
|
-
this.nodeStack.push(node);
|
|
5556
|
-
node = node.left;
|
|
5557
|
-
}
|
|
5558
|
-
}
|
|
5559
|
-
return result;
|
|
5542
|
+
}
|
|
5543
|
+
/** A Filter that implements the IN operator. */
|
|
5544
|
+
class InFilter extends FieldFilter {
|
|
5545
|
+
constructor(field, value) {
|
|
5546
|
+
super(field, "in" /* IN */, value);
|
|
5560
5547
|
}
|
|
5561
|
-
|
|
5562
|
-
|
|
5548
|
+
matches(doc) {
|
|
5549
|
+
const other = doc.data.field(this.field);
|
|
5550
|
+
return other !== null && arrayValueContains(this.value.arrayValue, other);
|
|
5563
5551
|
}
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
return { key: node.key, value: node.value };
|
|
5552
|
+
}
|
|
5553
|
+
/** A Filter that implements the not-in operator. */
|
|
5554
|
+
class NotInFilter extends FieldFilter {
|
|
5555
|
+
constructor(field, value) {
|
|
5556
|
+
super(field, "not-in" /* NOT_IN */, value);
|
|
5570
5557
|
}
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
this.value
|
|
5577
|
-
this.color = color != null ? color : LLRBNode.RED;
|
|
5578
|
-
this.left = left != null ? left : LLRBNode.EMPTY;
|
|
5579
|
-
this.right = right != null ? right : LLRBNode.EMPTY;
|
|
5580
|
-
this.size = this.left.size + 1 + this.right.size;
|
|
5558
|
+
matches(doc) {
|
|
5559
|
+
if (arrayValueContains(this.value.arrayValue, { nullValue: 'NULL_VALUE' })) {
|
|
5560
|
+
return false;
|
|
5561
|
+
}
|
|
5562
|
+
const other = doc.data.field(this.field);
|
|
5563
|
+
return other !== null && !arrayValueContains(this.value.arrayValue, other);
|
|
5581
5564
|
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5565
|
+
}
|
|
5566
|
+
/** A Filter that implements the array-contains-any operator. */
|
|
5567
|
+
class ArrayContainsAnyFilter extends FieldFilter {
|
|
5568
|
+
constructor(field, value) {
|
|
5569
|
+
super(field, "array-contains-any" /* ARRAY_CONTAINS_ANY */, value);
|
|
5585
5570
|
}
|
|
5586
|
-
|
|
5587
|
-
|
|
5571
|
+
matches(doc) {
|
|
5572
|
+
const other = doc.data.field(this.field);
|
|
5573
|
+
if (!isArray(other) || !other.arrayValue.values) {
|
|
5574
|
+
return false;
|
|
5575
|
+
}
|
|
5576
|
+
return other.arrayValue.values.some(val => arrayValueContains(this.value.arrayValue, val));
|
|
5588
5577
|
}
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5578
|
+
}
|
|
5579
|
+
/**
|
|
5580
|
+
* Represents a bound of a query.
|
|
5581
|
+
*
|
|
5582
|
+
* The bound is specified with the given components representing a position and
|
|
5583
|
+
* whether it's just before or just after the position (relative to whatever the
|
|
5584
|
+
* query order is).
|
|
5585
|
+
*
|
|
5586
|
+
* The position represents a logical index position for a query. It's a prefix
|
|
5587
|
+
* of values for the (potentially implicit) order by clauses of a query.
|
|
5588
|
+
*
|
|
5589
|
+
* Bound provides a function to determine whether a document comes before or
|
|
5590
|
+
* after a bound. This is influenced by whether the position is just before or
|
|
5591
|
+
* just after the provided values.
|
|
5592
|
+
*/
|
|
5593
|
+
class Bound {
|
|
5594
|
+
constructor(position, inclusive) {
|
|
5595
|
+
this.position = position;
|
|
5596
|
+
this.inclusive = inclusive;
|
|
5597
5597
|
}
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5598
|
+
}
|
|
5599
|
+
/**
|
|
5600
|
+
* An ordering on a field, in some Direction. Direction defaults to ASCENDING.
|
|
5601
|
+
*/
|
|
5602
|
+
class OrderBy {
|
|
5603
|
+
constructor(field, dir = "asc" /* ASCENDING */) {
|
|
5604
|
+
this.field = field;
|
|
5605
|
+
this.dir = dir;
|
|
5606
5606
|
}
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5607
|
+
}
|
|
5608
|
+
function canonifyOrderBy(orderBy) {
|
|
5609
|
+
// TODO(b/29183165): Make this collision robust.
|
|
5610
|
+
return orderBy.field.canonicalString() + orderBy.dir;
|
|
5611
|
+
}
|
|
5612
|
+
function stringifyOrderBy(orderBy) {
|
|
5613
|
+
return `${orderBy.field.canonicalString()} (${orderBy.dir})`;
|
|
5614
|
+
}
|
|
5615
|
+
function orderByEquals(left, right) {
|
|
5616
|
+
return left.dir === right.dir && left.field.isEqual(right.field);
|
|
5617
|
+
}
|
|
5618
|
+
function boundCompareToDocument(bound, orderBy, doc) {
|
|
5619
|
+
let comparison = 0;
|
|
5620
|
+
for (let i = 0; i < bound.position.length; i++) {
|
|
5621
|
+
const orderByComponent = orderBy[i];
|
|
5622
|
+
const component = bound.position[i];
|
|
5623
|
+
if (orderByComponent.field.isKeyField()) {
|
|
5624
|
+
comparison = DocumentKey.comparator(DocumentKey.fromName(component.referenceValue), doc.key);
|
|
5611
5625
|
}
|
|
5612
5626
|
else {
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
}
|
|
5616
|
-
// Returns the maximum key in the tree.
|
|
5617
|
-
minKey() {
|
|
5618
|
-
return this.min().key;
|
|
5619
|
-
}
|
|
5620
|
-
// Returns the maximum key in the tree.
|
|
5621
|
-
maxKey() {
|
|
5622
|
-
if (this.right.isEmpty()) {
|
|
5623
|
-
return this.key;
|
|
5624
|
-
}
|
|
5625
|
-
else {
|
|
5626
|
-
return this.right.maxKey();
|
|
5627
|
-
}
|
|
5628
|
-
}
|
|
5629
|
-
// Returns new tree, with the key/value added.
|
|
5630
|
-
insert(key, value, comparator) {
|
|
5631
|
-
let n = this;
|
|
5632
|
-
const cmp = comparator(key, n.key);
|
|
5633
|
-
if (cmp < 0) {
|
|
5634
|
-
n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
|
|
5627
|
+
const docValue = doc.data.field(orderByComponent.field);
|
|
5628
|
+
comparison = valueCompare(component, docValue);
|
|
5635
5629
|
}
|
|
5636
|
-
|
|
5637
|
-
|
|
5630
|
+
if (orderByComponent.dir === "desc" /* DESCENDING */) {
|
|
5631
|
+
comparison = comparison * -1;
|
|
5638
5632
|
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5633
|
+
if (comparison !== 0) {
|
|
5634
|
+
break;
|
|
5641
5635
|
}
|
|
5642
|
-
return n.fixUp();
|
|
5643
5636
|
}
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5637
|
+
return comparison;
|
|
5638
|
+
}
|
|
5639
|
+
/**
|
|
5640
|
+
* Returns true if a document sorts after a bound using the provided sort
|
|
5641
|
+
* order.
|
|
5642
|
+
*/
|
|
5643
|
+
function boundSortsAfterDocument(bound, orderBy, doc) {
|
|
5644
|
+
const comparison = boundCompareToDocument(bound, orderBy, doc);
|
|
5645
|
+
return bound.inclusive ? comparison >= 0 : comparison > 0;
|
|
5646
|
+
}
|
|
5647
|
+
/**
|
|
5648
|
+
* Returns true if a document sorts before a bound using the provided sort
|
|
5649
|
+
* order.
|
|
5650
|
+
*/
|
|
5651
|
+
function boundSortsBeforeDocument(bound, orderBy, doc) {
|
|
5652
|
+
const comparison = boundCompareToDocument(bound, orderBy, doc);
|
|
5653
|
+
return bound.inclusive ? comparison <= 0 : comparison < 0;
|
|
5654
|
+
}
|
|
5655
|
+
function boundEquals(left, right) {
|
|
5656
|
+
if (left === null) {
|
|
5657
|
+
return right === null;
|
|
5654
5658
|
}
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
let smallest;
|
|
5658
|
-
let n = this;
|
|
5659
|
-
if (comparator(key, n.key) < 0) {
|
|
5660
|
-
if (!n.left.isEmpty() && !n.left.isRed() && !n.left.left.isRed()) {
|
|
5661
|
-
n = n.moveRedLeft();
|
|
5662
|
-
}
|
|
5663
|
-
n = n.copy(null, null, null, n.left.remove(key, comparator), null);
|
|
5664
|
-
}
|
|
5665
|
-
else {
|
|
5666
|
-
if (n.left.isRed()) {
|
|
5667
|
-
n = n.rotateRight();
|
|
5668
|
-
}
|
|
5669
|
-
if (!n.right.isEmpty() && !n.right.isRed() && !n.right.left.isRed()) {
|
|
5670
|
-
n = n.moveRedRight();
|
|
5671
|
-
}
|
|
5672
|
-
if (comparator(key, n.key) === 0) {
|
|
5673
|
-
if (n.right.isEmpty()) {
|
|
5674
|
-
return LLRBNode.EMPTY;
|
|
5675
|
-
}
|
|
5676
|
-
else {
|
|
5677
|
-
smallest = n.right.min();
|
|
5678
|
-
n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin());
|
|
5679
|
-
}
|
|
5680
|
-
}
|
|
5681
|
-
n = n.copy(null, null, null, null, n.right.remove(key, comparator));
|
|
5682
|
-
}
|
|
5683
|
-
return n.fixUp();
|
|
5659
|
+
else if (right === null) {
|
|
5660
|
+
return false;
|
|
5684
5661
|
}
|
|
5685
|
-
|
|
5686
|
-
|
|
5662
|
+
if (left.inclusive !== right.inclusive ||
|
|
5663
|
+
left.position.length !== right.position.length) {
|
|
5664
|
+
return false;
|
|
5687
5665
|
}
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
if (
|
|
5692
|
-
|
|
5693
|
-
}
|
|
5694
|
-
if (n.left.isRed() && n.left.left.isRed()) {
|
|
5695
|
-
n = n.rotateRight();
|
|
5696
|
-
}
|
|
5697
|
-
if (n.left.isRed() && n.right.isRed()) {
|
|
5698
|
-
n = n.colorFlip();
|
|
5666
|
+
for (let i = 0; i < left.position.length; i++) {
|
|
5667
|
+
const leftPosition = left.position[i];
|
|
5668
|
+
const rightPosition = right.position[i];
|
|
5669
|
+
if (!valueEquals(leftPosition, rightPosition)) {
|
|
5670
|
+
return false;
|
|
5699
5671
|
}
|
|
5700
|
-
return n;
|
|
5701
5672
|
}
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5673
|
+
return true;
|
|
5674
|
+
}
|
|
5675
|
+
|
|
5676
|
+
/**
|
|
5677
|
+
* @license
|
|
5678
|
+
* Copyright 2017 Google LLC
|
|
5679
|
+
*
|
|
5680
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5681
|
+
* you may not use this file except in compliance with the License.
|
|
5682
|
+
* You may obtain a copy of the License at
|
|
5683
|
+
*
|
|
5684
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
5685
|
+
*
|
|
5686
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
5687
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
5688
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
5689
|
+
* See the License for the specific language governing permissions and
|
|
5690
|
+
* limitations under the License.
|
|
5691
|
+
*/
|
|
5692
|
+
/**
|
|
5693
|
+
* Query encapsulates all the query attributes we support in the SDK. It can
|
|
5694
|
+
* be run against the LocalStore, as well as be converted to a `Target` to
|
|
5695
|
+
* query the RemoteStore results.
|
|
5696
|
+
*
|
|
5697
|
+
* Visible for testing.
|
|
5698
|
+
*/
|
|
5699
|
+
class QueryImpl {
|
|
5700
|
+
/**
|
|
5701
|
+
* Initializes a Query with a path and optional additional query constraints.
|
|
5702
|
+
* Path must currently be empty if this is a collection group query.
|
|
5703
|
+
*/
|
|
5704
|
+
constructor(path, collectionGroup = null, explicitOrderBy = [], filters = [], limit = null, limitType = "F" /* First */, startAt = null, endAt = null) {
|
|
5705
|
+
this.path = path;
|
|
5706
|
+
this.collectionGroup = collectionGroup;
|
|
5707
|
+
this.explicitOrderBy = explicitOrderBy;
|
|
5708
|
+
this.filters = filters;
|
|
5709
|
+
this.limit = limit;
|
|
5710
|
+
this.limitType = limitType;
|
|
5711
|
+
this.startAt = startAt;
|
|
5712
|
+
this.endAt = endAt;
|
|
5713
|
+
this.memoizedOrderBy = null;
|
|
5714
|
+
// The corresponding `Target` of this `Query` instance.
|
|
5715
|
+
this.memoizedTarget = null;
|
|
5716
|
+
if (this.startAt) ;
|
|
5717
|
+
if (this.endAt) ;
|
|
5710
5718
|
}
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5719
|
+
}
|
|
5720
|
+
/** Creates a new Query instance with the options provided. */
|
|
5721
|
+
function newQuery(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt) {
|
|
5722
|
+
return new QueryImpl(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt);
|
|
5723
|
+
}
|
|
5724
|
+
/** Creates a new Query for a query that matches all documents at `path` */
|
|
5725
|
+
function newQueryForPath(path) {
|
|
5726
|
+
return new QueryImpl(path);
|
|
5727
|
+
}
|
|
5728
|
+
/**
|
|
5729
|
+
* Helper to convert a collection group query into a collection query at a
|
|
5730
|
+
* specific path. This is used when executing collection group queries, since
|
|
5731
|
+
* we have to split the query into a set of collection queries at multiple
|
|
5732
|
+
* paths.
|
|
5733
|
+
*/
|
|
5734
|
+
function asCollectionQueryAtPath(query, path) {
|
|
5735
|
+
return new QueryImpl(path,
|
|
5736
|
+
/*collectionGroup=*/ null, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
|
|
5737
|
+
}
|
|
5738
|
+
/**
|
|
5739
|
+
* Returns true if this query does not specify any query constraints that
|
|
5740
|
+
* could remove results.
|
|
5741
|
+
*/
|
|
5742
|
+
function queryMatchesAllDocuments(query) {
|
|
5743
|
+
return (query.filters.length === 0 &&
|
|
5744
|
+
query.limit === null &&
|
|
5745
|
+
query.startAt == null &&
|
|
5746
|
+
query.endAt == null &&
|
|
5747
|
+
(query.explicitOrderBy.length === 0 ||
|
|
5748
|
+
(query.explicitOrderBy.length === 1 &&
|
|
5749
|
+
query.explicitOrderBy[0].field.isKeyField())));
|
|
5750
|
+
}
|
|
5751
|
+
function getFirstOrderByField(query) {
|
|
5752
|
+
return query.explicitOrderBy.length > 0
|
|
5753
|
+
? query.explicitOrderBy[0].field
|
|
5754
|
+
: null;
|
|
5755
|
+
}
|
|
5756
|
+
function getInequalityFilterField(query) {
|
|
5757
|
+
for (const filter of query.filters) {
|
|
5758
|
+
if (filter.isInequality()) {
|
|
5759
|
+
return filter.field;
|
|
5716
5760
|
}
|
|
5717
|
-
return n;
|
|
5718
|
-
}
|
|
5719
|
-
rotateLeft() {
|
|
5720
|
-
const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
|
|
5721
|
-
return this.right.copy(null, null, this.color, nl, null);
|
|
5722
|
-
}
|
|
5723
|
-
rotateRight() {
|
|
5724
|
-
const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
|
|
5725
|
-
return this.left.copy(null, null, this.color, null, nr);
|
|
5726
5761
|
}
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5762
|
+
return null;
|
|
5763
|
+
}
|
|
5764
|
+
/**
|
|
5765
|
+
* Checks if any of the provided Operators are included in the query and
|
|
5766
|
+
* returns the first one that is, or null if none are.
|
|
5767
|
+
*/
|
|
5768
|
+
function findFilterOperator(query, operators) {
|
|
5769
|
+
for (const filter of query.filters) {
|
|
5770
|
+
if (operators.indexOf(filter.op) >= 0) {
|
|
5771
|
+
return filter.op;
|
|
5772
|
+
}
|
|
5731
5773
|
}
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5774
|
+
return null;
|
|
5775
|
+
}
|
|
5776
|
+
/**
|
|
5777
|
+
* Creates a new Query for a collection group query that matches all documents
|
|
5778
|
+
* within the provided collection group.
|
|
5779
|
+
*/
|
|
5780
|
+
function newQueryForCollectionGroup(collectionId) {
|
|
5781
|
+
return new QueryImpl(ResourcePath.emptyPath(), collectionId);
|
|
5782
|
+
}
|
|
5783
|
+
/**
|
|
5784
|
+
* Returns whether the query matches a single document by path (rather than a
|
|
5785
|
+
* collection).
|
|
5786
|
+
*/
|
|
5787
|
+
function isDocumentQuery$1(query) {
|
|
5788
|
+
return (DocumentKey.isDocumentKey(query.path) &&
|
|
5789
|
+
query.collectionGroup === null &&
|
|
5790
|
+
query.filters.length === 0);
|
|
5791
|
+
}
|
|
5792
|
+
/**
|
|
5793
|
+
* Returns whether the query matches a collection group rather than a specific
|
|
5794
|
+
* collection.
|
|
5795
|
+
*/
|
|
5796
|
+
function isCollectionGroupQuery(query) {
|
|
5797
|
+
return query.collectionGroup !== null;
|
|
5798
|
+
}
|
|
5799
|
+
/**
|
|
5800
|
+
* Returns the implicit order by constraint that is used to execute the Query,
|
|
5801
|
+
* which can be different from the order by constraints the user provided (e.g.
|
|
5802
|
+
* the SDK and backend always orders by `__name__`).
|
|
5803
|
+
*/
|
|
5804
|
+
function queryOrderBy(query) {
|
|
5805
|
+
const queryImpl = debugCast(query);
|
|
5806
|
+
if (queryImpl.memoizedOrderBy === null) {
|
|
5807
|
+
queryImpl.memoizedOrderBy = [];
|
|
5808
|
+
const inequalityField = getInequalityFilterField(queryImpl);
|
|
5809
|
+
const firstOrderByField = getFirstOrderByField(queryImpl);
|
|
5810
|
+
if (inequalityField !== null && firstOrderByField === null) {
|
|
5811
|
+
// In order to implicitly add key ordering, we must also add the
|
|
5812
|
+
// inequality filter field for it to be a valid query.
|
|
5813
|
+
// Note that the default inequality field and key ordering is ascending.
|
|
5814
|
+
if (!inequalityField.isKeyField()) {
|
|
5815
|
+
queryImpl.memoizedOrderBy.push(new OrderBy(inequalityField));
|
|
5816
|
+
}
|
|
5817
|
+
queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), "asc" /* ASCENDING */));
|
|
5737
5818
|
}
|
|
5738
5819
|
else {
|
|
5739
|
-
|
|
5820
|
+
let foundKeyOrdering = false;
|
|
5821
|
+
for (const orderBy of queryImpl.explicitOrderBy) {
|
|
5822
|
+
queryImpl.memoizedOrderBy.push(orderBy);
|
|
5823
|
+
if (orderBy.field.isKeyField()) {
|
|
5824
|
+
foundKeyOrdering = true;
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
if (!foundKeyOrdering) {
|
|
5828
|
+
// The order of the implicit key ordering always matches the last
|
|
5829
|
+
// explicit order by
|
|
5830
|
+
const lastDirection = queryImpl.explicitOrderBy.length > 0
|
|
5831
|
+
? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
|
|
5832
|
+
.dir
|
|
5833
|
+
: "asc" /* ASCENDING */;
|
|
5834
|
+
queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), lastDirection));
|
|
5835
|
+
}
|
|
5740
5836
|
}
|
|
5741
5837
|
}
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
if (blackDepth !== this.right.check()) {
|
|
5753
|
-
throw fail();
|
|
5838
|
+
return queryImpl.memoizedOrderBy;
|
|
5839
|
+
}
|
|
5840
|
+
/**
|
|
5841
|
+
* Converts this `Query` instance to it's corresponding `Target` representation.
|
|
5842
|
+
*/
|
|
5843
|
+
function queryToTarget(query) {
|
|
5844
|
+
const queryImpl = debugCast(query);
|
|
5845
|
+
if (!queryImpl.memoizedTarget) {
|
|
5846
|
+
if (queryImpl.limitType === "F" /* First */) {
|
|
5847
|
+
queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, queryOrderBy(queryImpl), queryImpl.filters, queryImpl.limit, queryImpl.startAt, queryImpl.endAt);
|
|
5754
5848
|
}
|
|
5755
5849
|
else {
|
|
5756
|
-
|
|
5850
|
+
// Flip the orderBy directions since we want the last results
|
|
5851
|
+
const orderBys = [];
|
|
5852
|
+
for (const orderBy of queryOrderBy(queryImpl)) {
|
|
5853
|
+
const dir = orderBy.dir === "desc" /* DESCENDING */
|
|
5854
|
+
? "asc" /* ASCENDING */
|
|
5855
|
+
: "desc" /* DESCENDING */;
|
|
5856
|
+
orderBys.push(new OrderBy(orderBy.field, dir));
|
|
5857
|
+
}
|
|
5858
|
+
// We need to swap the cursors to match the now-flipped query ordering.
|
|
5859
|
+
const startAt = queryImpl.endAt
|
|
5860
|
+
? new Bound(queryImpl.endAt.position, queryImpl.endAt.inclusive)
|
|
5861
|
+
: null;
|
|
5862
|
+
const endAt = queryImpl.startAt
|
|
5863
|
+
? new Bound(queryImpl.startAt.position, queryImpl.startAt.inclusive)
|
|
5864
|
+
: null;
|
|
5865
|
+
// Now return as a LimitType.First query.
|
|
5866
|
+
queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, orderBys, queryImpl.filters, queryImpl.limit, startAt, endAt);
|
|
5757
5867
|
}
|
|
5758
5868
|
}
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5869
|
+
return queryImpl.memoizedTarget;
|
|
5870
|
+
}
|
|
5871
|
+
function queryWithAddedFilter(query, filter) {
|
|
5872
|
+
const newFilters = query.filters.concat([filter]);
|
|
5873
|
+
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), newFilters, query.limit, query.limitType, query.startAt, query.endAt);
|
|
5874
|
+
}
|
|
5875
|
+
function queryWithAddedOrderBy(query, orderBy) {
|
|
5876
|
+
// TODO(dimond): validate that orderBy does not list the same key twice.
|
|
5877
|
+
const newOrderBy = query.explicitOrderBy.concat([orderBy]);
|
|
5878
|
+
return new QueryImpl(query.path, query.collectionGroup, newOrderBy, query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
|
|
5879
|
+
}
|
|
5880
|
+
function queryWithLimit(query, limit, limitType) {
|
|
5881
|
+
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), limit, limitType, query.startAt, query.endAt);
|
|
5882
|
+
}
|
|
5883
|
+
function queryWithStartAt(query, bound) {
|
|
5884
|
+
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, bound, query.endAt);
|
|
5885
|
+
}
|
|
5886
|
+
function queryWithEndAt(query, bound) {
|
|
5887
|
+
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, bound);
|
|
5888
|
+
}
|
|
5889
|
+
function queryEquals(left, right) {
|
|
5890
|
+
return (targetEquals(queryToTarget(left), queryToTarget(right)) &&
|
|
5891
|
+
left.limitType === right.limitType);
|
|
5892
|
+
}
|
|
5893
|
+
// TODO(b/29183165): This is used to get a unique string from a query to, for
|
|
5894
|
+
// example, use as a dictionary key, but the implementation is subject to
|
|
5895
|
+
// collisions. Make it collision-free.
|
|
5896
|
+
function canonifyQuery(query) {
|
|
5897
|
+
return `${canonifyTarget(queryToTarget(query))}|lt:${query.limitType}`;
|
|
5898
|
+
}
|
|
5899
|
+
function stringifyQuery(query) {
|
|
5900
|
+
return `Query(target=${stringifyTarget(queryToTarget(query))}; limitType=${query.limitType})`;
|
|
5901
|
+
}
|
|
5902
|
+
/** Returns whether `doc` matches the constraints of `query`. */
|
|
5903
|
+
function queryMatches(query, doc) {
|
|
5904
|
+
return (doc.isFoundDocument() &&
|
|
5905
|
+
queryMatchesPathAndCollectionGroup(query, doc) &&
|
|
5906
|
+
queryMatchesOrderBy(query, doc) &&
|
|
5907
|
+
queryMatchesFilters(query, doc) &&
|
|
5908
|
+
queryMatchesBounds(query, doc));
|
|
5909
|
+
}
|
|
5910
|
+
function queryMatchesPathAndCollectionGroup(query, doc) {
|
|
5911
|
+
const docPath = doc.key.path;
|
|
5912
|
+
if (query.collectionGroup !== null) {
|
|
5913
|
+
// NOTE: this.path is currently always empty since we don't expose Collection
|
|
5914
|
+
// Group queries rooted at a document path yet.
|
|
5915
|
+
return (doc.key.hasCollectionId(query.collectionGroup) &&
|
|
5916
|
+
query.path.isPrefixOf(docPath));
|
|
5788
5917
|
}
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
return
|
|
5918
|
+
else if (DocumentKey.isDocumentKey(query.path)) {
|
|
5919
|
+
// exact match for document queries
|
|
5920
|
+
return query.path.isEqual(docPath);
|
|
5792
5921
|
}
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
return
|
|
5922
|
+
else {
|
|
5923
|
+
// shallow ancestor queries by default
|
|
5924
|
+
return query.path.isImmediateParentOf(docPath);
|
|
5796
5925
|
}
|
|
5797
|
-
|
|
5798
|
-
|
|
5926
|
+
}
|
|
5927
|
+
/**
|
|
5928
|
+
* A document must have a value for every ordering clause in order to show up
|
|
5929
|
+
* in the results.
|
|
5930
|
+
*/
|
|
5931
|
+
function queryMatchesOrderBy(query, doc) {
|
|
5932
|
+
for (const orderBy of query.explicitOrderBy) {
|
|
5933
|
+
// order by key always matches
|
|
5934
|
+
if (!orderBy.field.isKeyField() && doc.data.field(orderBy.field) === null) {
|
|
5935
|
+
return false;
|
|
5936
|
+
}
|
|
5799
5937
|
}
|
|
5800
|
-
|
|
5801
|
-
|
|
5938
|
+
return true;
|
|
5939
|
+
}
|
|
5940
|
+
function queryMatchesFilters(query, doc) {
|
|
5941
|
+
for (const filter of query.filters) {
|
|
5942
|
+
if (!filter.matches(doc)) {
|
|
5943
|
+
return false;
|
|
5944
|
+
}
|
|
5802
5945
|
}
|
|
5803
|
-
|
|
5946
|
+
return true;
|
|
5947
|
+
}
|
|
5948
|
+
/** Makes sure a document is within the bounds, if provided. */
|
|
5949
|
+
function queryMatchesBounds(query, doc) {
|
|
5950
|
+
if (query.startAt &&
|
|
5951
|
+
!boundSortsBeforeDocument(query.startAt, queryOrderBy(query), doc)) {
|
|
5804
5952
|
return false;
|
|
5805
5953
|
}
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
}
|
|
5809
|
-
maxKey() {
|
|
5810
|
-
return null;
|
|
5811
|
-
}
|
|
5812
|
-
isRed() {
|
|
5954
|
+
if (query.endAt &&
|
|
5955
|
+
!boundSortsAfterDocument(query.endAt, queryOrderBy(query), doc)) {
|
|
5813
5956
|
return false;
|
|
5814
5957
|
}
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5958
|
+
return true;
|
|
5959
|
+
}
|
|
5960
|
+
/**
|
|
5961
|
+
* Returns the collection group that this query targets.
|
|
5962
|
+
*
|
|
5963
|
+
* PORTING NOTE: This is only used in the Web SDK to facilitate multi-tab
|
|
5964
|
+
* synchronization for query results.
|
|
5965
|
+
*/
|
|
5966
|
+
function queryCollectionGroup(query) {
|
|
5967
|
+
return (query.collectionGroup ||
|
|
5968
|
+
(query.path.length % 2 === 1
|
|
5969
|
+
? query.path.lastSegment()
|
|
5970
|
+
: query.path.get(query.path.length - 2)));
|
|
5971
|
+
}
|
|
5972
|
+
/**
|
|
5973
|
+
* Returns a new comparator function that can be used to compare two documents
|
|
5974
|
+
* based on the Query's ordering constraint.
|
|
5975
|
+
*/
|
|
5976
|
+
function newQueryComparator(query) {
|
|
5977
|
+
return (d1, d2) => {
|
|
5978
|
+
let comparedOnKeyField = false;
|
|
5979
|
+
for (const orderBy of queryOrderBy(query)) {
|
|
5980
|
+
const comp = compareDocs(orderBy, d1, d2);
|
|
5981
|
+
if (comp !== 0) {
|
|
5982
|
+
return comp;
|
|
5983
|
+
}
|
|
5984
|
+
comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
|
|
5985
|
+
}
|
|
5820
5986
|
return 0;
|
|
5987
|
+
};
|
|
5988
|
+
}
|
|
5989
|
+
function compareDocs(orderBy, d1, d2) {
|
|
5990
|
+
const comparison = orderBy.field.isKeyField()
|
|
5991
|
+
? DocumentKey.comparator(d1.key, d2.key)
|
|
5992
|
+
: compareDocumentsByField(orderBy.field, d1, d2);
|
|
5993
|
+
switch (orderBy.dir) {
|
|
5994
|
+
case "asc" /* ASCENDING */:
|
|
5995
|
+
return comparison;
|
|
5996
|
+
case "desc" /* DESCENDING */:
|
|
5997
|
+
return -1 * comparison;
|
|
5998
|
+
default:
|
|
5999
|
+
return fail();
|
|
5821
6000
|
}
|
|
5822
|
-
}
|
|
5823
|
-
LLRBNode.EMPTY = new LLRBEmptyNode();
|
|
6001
|
+
}
|
|
5824
6002
|
|
|
5825
6003
|
/**
|
|
5826
6004
|
* @license
|
|
@@ -5839,218 +6017,97 @@ LLRBNode.EMPTY = new LLRBEmptyNode();
|
|
|
5839
6017
|
* limitations under the License.
|
|
5840
6018
|
*/
|
|
5841
6019
|
/**
|
|
5842
|
-
*
|
|
5843
|
-
*
|
|
5844
|
-
*
|
|
5845
|
-
*
|
|
5846
|
-
* be equal!
|
|
6020
|
+
* A map implementation that uses objects as keys. Objects must have an
|
|
6021
|
+
* associated equals function and must be immutable. Entries in the map are
|
|
6022
|
+
* stored together with the key being produced from the mapKeyFn. This map
|
|
6023
|
+
* automatically handles collisions of keys.
|
|
5847
6024
|
*/
|
|
5848
|
-
class
|
|
5849
|
-
constructor(
|
|
5850
|
-
this.
|
|
5851
|
-
this.
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
}
|
|
5862
|
-
get size() {
|
|
5863
|
-
return this.data.size;
|
|
5864
|
-
}
|
|
5865
|
-
indexOf(elem) {
|
|
5866
|
-
return this.data.indexOf(elem);
|
|
5867
|
-
}
|
|
5868
|
-
/** Iterates elements in order defined by "comparator" */
|
|
5869
|
-
forEach(cb) {
|
|
5870
|
-
this.data.inorderTraversal((k, v) => {
|
|
5871
|
-
cb(k);
|
|
5872
|
-
return false;
|
|
5873
|
-
});
|
|
5874
|
-
}
|
|
5875
|
-
/** Iterates over `elem`s such that: range[0] <= elem < range[1]. */
|
|
5876
|
-
forEachInRange(range, cb) {
|
|
5877
|
-
const iter = this.data.getIteratorFrom(range[0]);
|
|
5878
|
-
while (iter.hasNext()) {
|
|
5879
|
-
const elem = iter.getNext();
|
|
5880
|
-
if (this.comparator(elem.key, range[1]) >= 0) {
|
|
5881
|
-
return;
|
|
5882
|
-
}
|
|
5883
|
-
cb(elem.key);
|
|
5884
|
-
}
|
|
6025
|
+
class ObjectMap {
|
|
6026
|
+
constructor(mapKeyFn, equalsFn) {
|
|
6027
|
+
this.mapKeyFn = mapKeyFn;
|
|
6028
|
+
this.equalsFn = equalsFn;
|
|
6029
|
+
/**
|
|
6030
|
+
* The inner map for a key/value pair. Due to the possibility of collisions we
|
|
6031
|
+
* keep a list of entries that we do a linear search through to find an actual
|
|
6032
|
+
* match. Note that collisions should be rare, so we still expect near
|
|
6033
|
+
* constant time lookups in practice.
|
|
6034
|
+
*/
|
|
6035
|
+
this.inner = {};
|
|
6036
|
+
/** The number of entries stored in the map */
|
|
6037
|
+
this.innerSize = 0;
|
|
5885
6038
|
}
|
|
5886
|
-
/**
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
iter = this.data.getIteratorFrom(start);
|
|
5893
|
-
}
|
|
5894
|
-
else {
|
|
5895
|
-
iter = this.data.getIterator();
|
|
6039
|
+
/** Get a value for this key, or undefined if it does not exist. */
|
|
6040
|
+
get(key) {
|
|
6041
|
+
const id = this.mapKeyFn(key);
|
|
6042
|
+
const matches = this.inner[id];
|
|
6043
|
+
if (matches === undefined) {
|
|
6044
|
+
return undefined;
|
|
5896
6045
|
}
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
if (!result) {
|
|
5901
|
-
return;
|
|
6046
|
+
for (const [otherKey, value] of matches) {
|
|
6047
|
+
if (this.equalsFn(otherKey, key)) {
|
|
6048
|
+
return value;
|
|
5902
6049
|
}
|
|
5903
6050
|
}
|
|
6051
|
+
return undefined;
|
|
5904
6052
|
}
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
const iter = this.data.getIteratorFrom(elem);
|
|
5908
|
-
return iter.hasNext() ? iter.getNext().key : null;
|
|
5909
|
-
}
|
|
5910
|
-
getIterator() {
|
|
5911
|
-
return new SortedSetIterator(this.data.getIterator());
|
|
5912
|
-
}
|
|
5913
|
-
getIteratorFrom(key) {
|
|
5914
|
-
return new SortedSetIterator(this.data.getIteratorFrom(key));
|
|
5915
|
-
}
|
|
5916
|
-
/** Inserts or updates an element */
|
|
5917
|
-
add(elem) {
|
|
5918
|
-
return this.copy(this.data.remove(elem).insert(elem, true));
|
|
6053
|
+
has(key) {
|
|
6054
|
+
return this.get(key) !== undefined;
|
|
5919
6055
|
}
|
|
5920
|
-
/**
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
6056
|
+
/** Put this key and value in the map. */
|
|
6057
|
+
set(key, value) {
|
|
6058
|
+
const id = this.mapKeyFn(key);
|
|
6059
|
+
const matches = this.inner[id];
|
|
6060
|
+
if (matches === undefined) {
|
|
6061
|
+
this.inner[id] = [[key, value]];
|
|
6062
|
+
this.innerSize++;
|
|
6063
|
+
return;
|
|
5924
6064
|
}
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
let result = this;
|
|
5932
|
-
// Make sure `result` always refers to the larger one of the two sets.
|
|
5933
|
-
if (result.size < other.size) {
|
|
5934
|
-
result = other;
|
|
5935
|
-
other = this;
|
|
6065
|
+
for (let i = 0; i < matches.length; i++) {
|
|
6066
|
+
if (this.equalsFn(matches[i][0], key)) {
|
|
6067
|
+
// This is updating an existing entry and does not increase `innerSize`.
|
|
6068
|
+
matches[i] = [key, value];
|
|
6069
|
+
return;
|
|
6070
|
+
}
|
|
5936
6071
|
}
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
});
|
|
5940
|
-
return result;
|
|
6072
|
+
matches.push([key, value]);
|
|
6073
|
+
this.innerSize++;
|
|
5941
6074
|
}
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
6075
|
+
/**
|
|
6076
|
+
* Remove this key from the map. Returns a boolean if anything was deleted.
|
|
6077
|
+
*/
|
|
6078
|
+
delete(key) {
|
|
6079
|
+
const id = this.mapKeyFn(key);
|
|
6080
|
+
const matches = this.inner[id];
|
|
6081
|
+
if (matches === undefined) {
|
|
5947
6082
|
return false;
|
|
5948
6083
|
}
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
6084
|
+
for (let i = 0; i < matches.length; i++) {
|
|
6085
|
+
if (this.equalsFn(matches[i][0], key)) {
|
|
6086
|
+
if (matches.length === 1) {
|
|
6087
|
+
delete this.inner[id];
|
|
6088
|
+
}
|
|
6089
|
+
else {
|
|
6090
|
+
matches.splice(i, 1);
|
|
6091
|
+
}
|
|
6092
|
+
this.innerSize--;
|
|
6093
|
+
return true;
|
|
5956
6094
|
}
|
|
5957
6095
|
}
|
|
5958
|
-
return
|
|
6096
|
+
return false;
|
|
5959
6097
|
}
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
6098
|
+
forEach(fn) {
|
|
6099
|
+
forEach(this.inner, (_, entries) => {
|
|
6100
|
+
for (const [k, v] of entries) {
|
|
6101
|
+
fn(k, v);
|
|
6102
|
+
}
|
|
5964
6103
|
});
|
|
5965
|
-
return res;
|
|
5966
|
-
}
|
|
5967
|
-
toString() {
|
|
5968
|
-
const result = [];
|
|
5969
|
-
this.forEach(elem => result.push(elem));
|
|
5970
|
-
return 'SortedSet(' + result.toString() + ')';
|
|
5971
|
-
}
|
|
5972
|
-
copy(data) {
|
|
5973
|
-
const result = new SortedSet(this.comparator);
|
|
5974
|
-
result.data = data;
|
|
5975
|
-
return result;
|
|
5976
6104
|
}
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
constructor(iter) {
|
|
5980
|
-
this.iter = iter;
|
|
5981
|
-
}
|
|
5982
|
-
getNext() {
|
|
5983
|
-
return this.iter.getNext().key;
|
|
5984
|
-
}
|
|
5985
|
-
hasNext() {
|
|
5986
|
-
return this.iter.hasNext();
|
|
6105
|
+
isEmpty() {
|
|
6106
|
+
return isEmpty(this.inner);
|
|
5987
6107
|
}
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
* Compares two sorted sets for equality using their natural ordering. The
|
|
5991
|
-
* method computes the intersection and invokes `onAdd` for every element that
|
|
5992
|
-
* is in `after` but not `before`. `onRemove` is invoked for every element in
|
|
5993
|
-
* `before` but missing from `after`.
|
|
5994
|
-
*
|
|
5995
|
-
* The method creates a copy of both `before` and `after` and runs in O(n log
|
|
5996
|
-
* n), where n is the size of the two lists.
|
|
5997
|
-
*
|
|
5998
|
-
* @param before - The elements that exist in the original set.
|
|
5999
|
-
* @param after - The elements to diff against the original set.
|
|
6000
|
-
* @param comparator - The comparator for the elements in before and after.
|
|
6001
|
-
* @param onAdd - A function to invoke for every element that is part of `
|
|
6002
|
-
* after` but not `before`.
|
|
6003
|
-
* @param onRemove - A function to invoke for every element that is part of
|
|
6004
|
-
* `before` but not `after`.
|
|
6005
|
-
*/
|
|
6006
|
-
function diffSortedSets(before, after, comparator, onAdd, onRemove) {
|
|
6007
|
-
const beforeIt = before.getIterator();
|
|
6008
|
-
const afterIt = after.getIterator();
|
|
6009
|
-
let beforeValue = advanceIterator(beforeIt);
|
|
6010
|
-
let afterValue = advanceIterator(afterIt);
|
|
6011
|
-
// Walk through the two sets at the same time, using the ordering defined by
|
|
6012
|
-
// `comparator`.
|
|
6013
|
-
while (beforeValue || afterValue) {
|
|
6014
|
-
let added = false;
|
|
6015
|
-
let removed = false;
|
|
6016
|
-
if (beforeValue && afterValue) {
|
|
6017
|
-
const cmp = comparator(beforeValue, afterValue);
|
|
6018
|
-
if (cmp < 0) {
|
|
6019
|
-
// The element was removed if the next element in our ordered
|
|
6020
|
-
// walkthrough is only in `before`.
|
|
6021
|
-
removed = true;
|
|
6022
|
-
}
|
|
6023
|
-
else if (cmp > 0) {
|
|
6024
|
-
// The element was added if the next element in our ordered walkthrough
|
|
6025
|
-
// is only in `after`.
|
|
6026
|
-
added = true;
|
|
6027
|
-
}
|
|
6028
|
-
}
|
|
6029
|
-
else if (beforeValue != null) {
|
|
6030
|
-
removed = true;
|
|
6031
|
-
}
|
|
6032
|
-
else {
|
|
6033
|
-
added = true;
|
|
6034
|
-
}
|
|
6035
|
-
if (added) {
|
|
6036
|
-
onAdd(afterValue);
|
|
6037
|
-
afterValue = advanceIterator(afterIt);
|
|
6038
|
-
}
|
|
6039
|
-
else if (removed) {
|
|
6040
|
-
onRemove(beforeValue);
|
|
6041
|
-
beforeValue = advanceIterator(beforeIt);
|
|
6042
|
-
}
|
|
6043
|
-
else {
|
|
6044
|
-
beforeValue = advanceIterator(beforeIt);
|
|
6045
|
-
afterValue = advanceIterator(afterIt);
|
|
6046
|
-
}
|
|
6108
|
+
size() {
|
|
6109
|
+
return this.innerSize;
|
|
6047
6110
|
}
|
|
6048
|
-
}
|
|
6049
|
-
/**
|
|
6050
|
-
* Returns the next element from the iterator or `undefined` if none available.
|
|
6051
|
-
*/
|
|
6052
|
-
function advanceIterator(it) {
|
|
6053
|
-
return it.hasNext() ? it.getNext() : undefined;
|
|
6054
6111
|
}
|
|
6055
6112
|
|
|
6056
6113
|
/**
|
|
@@ -6074,8 +6131,12 @@ function mutableDocumentMap() {
|
|
|
6074
6131
|
return EMPTY_MUTABLE_DOCUMENT_MAP;
|
|
6075
6132
|
}
|
|
6076
6133
|
const EMPTY_DOCUMENT_MAP = new SortedMap(DocumentKey.comparator);
|
|
6077
|
-
function documentMap() {
|
|
6078
|
-
|
|
6134
|
+
function documentMap(...docs) {
|
|
6135
|
+
let map = EMPTY_DOCUMENT_MAP;
|
|
6136
|
+
for (const doc of docs) {
|
|
6137
|
+
map = map.insert(doc.key, doc);
|
|
6138
|
+
}
|
|
6139
|
+
return map;
|
|
6079
6140
|
}
|
|
6080
6141
|
function newOverlayMap() {
|
|
6081
6142
|
return new ObjectMap(key => key.toString(), (l, r) => l.isEqual(r));
|
|
@@ -9968,9 +10029,9 @@ class MemoryIndexManager {
|
|
|
9968
10029
|
// Field indices are not supported with memory persistence.
|
|
9969
10030
|
return PersistencePromise.resolve(null);
|
|
9970
10031
|
}
|
|
9971
|
-
|
|
10032
|
+
getIndexType(transaction, target) {
|
|
9972
10033
|
// Field indices are not supported with memory persistence.
|
|
9973
|
-
return PersistencePromise.resolve(
|
|
10034
|
+
return PersistencePromise.resolve(0 /* NONE */);
|
|
9974
10035
|
}
|
|
9975
10036
|
getFieldIndexes(transaction, collectionGroup) {
|
|
9976
10037
|
// Field indices are not supported with memory persistence.
|
|
@@ -9980,6 +10041,9 @@ class MemoryIndexManager {
|
|
|
9980
10041
|
// Field indices are not supported with memory persistence.
|
|
9981
10042
|
return PersistencePromise.resolve(null);
|
|
9982
10043
|
}
|
|
10044
|
+
getMinOffset(transaction, target) {
|
|
10045
|
+
return PersistencePromise.resolve(IndexOffset.min());
|
|
10046
|
+
}
|
|
9983
10047
|
updateCollectionGroup(transaction, collectionGroup, offset) {
|
|
9984
10048
|
// Field indices are not supported with memory persistence.
|
|
9985
10049
|
return PersistencePromise.resolve();
|
|
@@ -10046,8 +10110,9 @@ const EMPTY_VALUE = new Uint8Array(0);
|
|
|
10046
10110
|
* data as it supports multi-tab access.
|
|
10047
10111
|
*/
|
|
10048
10112
|
class IndexedDbIndexManager {
|
|
10049
|
-
constructor(user) {
|
|
10113
|
+
constructor(user, databaseId) {
|
|
10050
10114
|
this.user = user;
|
|
10115
|
+
this.databaseId = databaseId;
|
|
10051
10116
|
/**
|
|
10052
10117
|
* An in-memory copy of the index entries we've already written since the SDK
|
|
10053
10118
|
* launched. Used to avoid re-writing the same entry repeatedly.
|
|
@@ -10143,7 +10208,8 @@ class IndexedDbIndexManager {
|
|
|
10143
10208
|
return PersistencePromise.resolve(null);
|
|
10144
10209
|
}
|
|
10145
10210
|
else {
|
|
10146
|
-
let
|
|
10211
|
+
let existingKeys = documentKeySet();
|
|
10212
|
+
const result = [];
|
|
10147
10213
|
return PersistencePromise.forEach(indexes, (index, subTarget) => {
|
|
10148
10214
|
logDebug(LOG_TAG$f, `Using index ${fieldIndexToString(index)} to execute ${canonifyTarget(target)}`);
|
|
10149
10215
|
const arrayValues = targetGetArrayValues(subTarget, index);
|
|
@@ -10159,7 +10225,11 @@ class IndexedDbIndexManager {
|
|
|
10159
10225
|
.loadFirst(indexRange, target.limit)
|
|
10160
10226
|
.next(entries => {
|
|
10161
10227
|
entries.forEach(entry => {
|
|
10162
|
-
|
|
10228
|
+
const documentKey = DocumentKey.fromSegments(entry.documentKey);
|
|
10229
|
+
if (!existingKeys.has(documentKey)) {
|
|
10230
|
+
existingKeys = existingKeys.add(documentKey);
|
|
10231
|
+
result.push(documentKey);
|
|
10232
|
+
}
|
|
10163
10233
|
});
|
|
10164
10234
|
});
|
|
10165
10235
|
});
|
|
@@ -10200,9 +10270,9 @@ class IndexedDbIndexManager {
|
|
|
10200
10270
|
const upperBound = upperBounds
|
|
10201
10271
|
? this.generateUpperBound(indexId, arrayValue, upperBounds[i % scansPerArrayElement], upperBoundInclusive)
|
|
10202
10272
|
: this.generateEmptyBound(indexId + 1);
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10273
|
+
const notInBound = notInValues.map(notIn => this.generateLowerBound(indexId, arrayValue, notIn,
|
|
10274
|
+
/* inclusive= */ true));
|
|
10275
|
+
indexRanges.push(...this.createRange(lowerBound, upperBound, notInBound));
|
|
10206
10276
|
}
|
|
10207
10277
|
return indexRanges;
|
|
10208
10278
|
}
|
|
@@ -10229,12 +10299,32 @@ class IndexedDbIndexManager {
|
|
|
10229
10299
|
? target.collectionGroup
|
|
10230
10300
|
: target.path.lastSegment();
|
|
10231
10301
|
return this.getFieldIndexes(transaction, collectionGroup).next(indexes => {
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10302
|
+
// Return the index with the most number of segments.
|
|
10303
|
+
let index = null;
|
|
10304
|
+
for (const candidate of indexes) {
|
|
10305
|
+
const matches = targetIndexMatcher.servedByIndex(candidate);
|
|
10306
|
+
if (matches &&
|
|
10307
|
+
(!index || candidate.fields.length > index.fields.length)) {
|
|
10308
|
+
index = candidate;
|
|
10309
|
+
}
|
|
10310
|
+
}
|
|
10311
|
+
return index;
|
|
10236
10312
|
});
|
|
10237
10313
|
}
|
|
10314
|
+
getIndexType(transaction, target) {
|
|
10315
|
+
let indexType = 2 /* FULL */;
|
|
10316
|
+
return PersistencePromise.forEach(this.getSubTargets(target), (target) => {
|
|
10317
|
+
return this.getFieldIndex(transaction, target).next(index => {
|
|
10318
|
+
if (!index) {
|
|
10319
|
+
indexType = 0 /* NONE */;
|
|
10320
|
+
}
|
|
10321
|
+
else if (indexType !== 0 /* NONE */ &&
|
|
10322
|
+
index.fields.length < targetGetSegmentCount(target)) {
|
|
10323
|
+
indexType = 1 /* PARTIAL */;
|
|
10324
|
+
}
|
|
10325
|
+
});
|
|
10326
|
+
}).next(() => indexType);
|
|
10327
|
+
}
|
|
10238
10328
|
/**
|
|
10239
10329
|
* Returns the byte encoded form of the directional values in the field index.
|
|
10240
10330
|
* Returns `null` if the document does not have all fields specified in the
|
|
@@ -10258,6 +10348,15 @@ class IndexedDbIndexManager {
|
|
|
10258
10348
|
FirestoreIndexValueWriter.INSTANCE.writeIndexValue(value, encoder.forKind(0 /* ASCENDING */));
|
|
10259
10349
|
return encoder.encodedBytes();
|
|
10260
10350
|
}
|
|
10351
|
+
/**
|
|
10352
|
+
* Returns an encoded form of the document key that sorts based on the key
|
|
10353
|
+
* ordering of the field index.
|
|
10354
|
+
*/
|
|
10355
|
+
encodeDirectionalKey(fieldIndex, documentKey) {
|
|
10356
|
+
const encoder = new IndexByteEncoder();
|
|
10357
|
+
FirestoreIndexValueWriter.INSTANCE.writeIndexValue(refValue(this.databaseId, documentKey), encoder.forKind(fieldIndexGetKeyOrder(fieldIndex)));
|
|
10358
|
+
return encoder.encodedBytes();
|
|
10359
|
+
}
|
|
10261
10360
|
/**
|
|
10262
10361
|
* Encodes the given field values according to the specification in `target`.
|
|
10263
10362
|
* For IN queries, a list of possible values is returned.
|
|
@@ -10381,7 +10480,7 @@ class IndexedDbIndexManager {
|
|
|
10381
10480
|
return this.getExistingIndexEntries(transaction, key, fieldIndex).next(existingEntries => {
|
|
10382
10481
|
const newEntries = this.computeIndexEntries(doc, fieldIndex);
|
|
10383
10482
|
if (!existingEntries.isEqual(newEntries)) {
|
|
10384
|
-
return this.updateEntries(transaction, doc, existingEntries, newEntries);
|
|
10483
|
+
return this.updateEntries(transaction, doc, fieldIndex, existingEntries, newEntries);
|
|
10385
10484
|
}
|
|
10386
10485
|
return PersistencePromise.resolve();
|
|
10387
10486
|
});
|
|
@@ -10389,24 +10488,26 @@ class IndexedDbIndexManager {
|
|
|
10389
10488
|
});
|
|
10390
10489
|
});
|
|
10391
10490
|
}
|
|
10392
|
-
addIndexEntry(transaction, document, indexEntry) {
|
|
10491
|
+
addIndexEntry(transaction, document, fieldIndex, indexEntry) {
|
|
10393
10492
|
const indexEntries = indexEntriesStore(transaction);
|
|
10394
10493
|
return indexEntries.put({
|
|
10395
10494
|
indexId: indexEntry.indexId,
|
|
10396
10495
|
uid: this.uid,
|
|
10397
10496
|
arrayValue: indexEntry.arrayValue,
|
|
10398
10497
|
directionalValue: indexEntry.directionalValue,
|
|
10399
|
-
|
|
10498
|
+
orderedDocumentKey: this.encodeDirectionalKey(fieldIndex, document.key),
|
|
10499
|
+
documentKey: document.key.path.toArray()
|
|
10400
10500
|
});
|
|
10401
10501
|
}
|
|
10402
|
-
deleteIndexEntry(transaction, document, indexEntry) {
|
|
10502
|
+
deleteIndexEntry(transaction, document, fieldIndex, indexEntry) {
|
|
10403
10503
|
const indexEntries = indexEntriesStore(transaction);
|
|
10404
10504
|
return indexEntries.delete([
|
|
10405
10505
|
indexEntry.indexId,
|
|
10406
10506
|
this.uid,
|
|
10407
10507
|
indexEntry.arrayValue,
|
|
10408
10508
|
indexEntry.directionalValue,
|
|
10409
|
-
|
|
10509
|
+
this.encodeDirectionalKey(fieldIndex, document.key),
|
|
10510
|
+
document.key.path.toArray()
|
|
10410
10511
|
]);
|
|
10411
10512
|
}
|
|
10412
10513
|
getExistingIndexEntries(transaction, documentKey, fieldIndex) {
|
|
@@ -10418,7 +10519,7 @@ class IndexedDbIndexManager {
|
|
|
10418
10519
|
range: IDBKeyRange.only([
|
|
10419
10520
|
fieldIndex.indexId,
|
|
10420
10521
|
this.uid,
|
|
10421
|
-
|
|
10522
|
+
this.encodeDirectionalKey(fieldIndex, documentKey)
|
|
10422
10523
|
])
|
|
10423
10524
|
}, (_, entry) => {
|
|
10424
10525
|
results = results.add(new IndexEntry(fieldIndex.indexId, documentKey, entry.arrayValue, entry.directionalValue));
|
|
@@ -10451,15 +10552,15 @@ class IndexedDbIndexManager {
|
|
|
10451
10552
|
* that are no longer referenced in `newEntries` and adding all newly added
|
|
10452
10553
|
* entries.
|
|
10453
10554
|
*/
|
|
10454
|
-
updateEntries(transaction, document, existingEntries, newEntries) {
|
|
10555
|
+
updateEntries(transaction, document, fieldIndex, existingEntries, newEntries) {
|
|
10455
10556
|
logDebug(LOG_TAG$f, "Updating index entries for document '%s'", document.key);
|
|
10456
10557
|
const promises = [];
|
|
10457
10558
|
diffSortedSets(existingEntries, newEntries, indexEntryComparator,
|
|
10458
10559
|
/* onAdd= */ entry => {
|
|
10459
|
-
promises.push(this.addIndexEntry(transaction, document, entry));
|
|
10560
|
+
promises.push(this.addIndexEntry(transaction, document, fieldIndex, entry));
|
|
10460
10561
|
},
|
|
10461
10562
|
/* onRemove= */ entry => {
|
|
10462
|
-
promises.push(this.deleteIndexEntry(transaction, document, entry));
|
|
10563
|
+
promises.push(this.deleteIndexEntry(transaction, document, fieldIndex, entry));
|
|
10463
10564
|
});
|
|
10464
10565
|
return PersistencePromise.waitFor(promises);
|
|
10465
10566
|
}
|
|
@@ -10483,7 +10584,7 @@ class IndexedDbIndexManager {
|
|
|
10483
10584
|
* '[foo > 2 && foo != 3]` becomes `[foo > 2 && < 3, foo > 3]`.
|
|
10484
10585
|
*/
|
|
10485
10586
|
createRange(lower, upper, notInValues) {
|
|
10486
|
-
// The
|
|
10587
|
+
// The notIn values need to be sorted and unique so that we can return a
|
|
10487
10588
|
// sorted set of non-overlapping ranges.
|
|
10488
10589
|
notInValues = notInValues
|
|
10489
10590
|
.sort((l, r) => indexEntryComparator(l, r))
|
|
@@ -10516,17 +10617,33 @@ class IndexedDbIndexManager {
|
|
|
10516
10617
|
this.uid,
|
|
10517
10618
|
bounds[i].arrayValue,
|
|
10518
10619
|
bounds[i].directionalValue,
|
|
10519
|
-
|
|
10620
|
+
EMPTY_VALUE,
|
|
10621
|
+
[]
|
|
10520
10622
|
], [
|
|
10521
10623
|
bounds[i + 1].indexId,
|
|
10522
10624
|
this.uid,
|
|
10523
10625
|
bounds[i + 1].arrayValue,
|
|
10524
10626
|
bounds[i + 1].directionalValue,
|
|
10525
|
-
|
|
10627
|
+
EMPTY_VALUE,
|
|
10628
|
+
[]
|
|
10526
10629
|
]));
|
|
10527
10630
|
}
|
|
10528
10631
|
return ranges;
|
|
10529
10632
|
}
|
|
10633
|
+
getMinOffset(transaction, target) {
|
|
10634
|
+
let offset;
|
|
10635
|
+
return PersistencePromise.forEach(this.getSubTargets(target), (target) => {
|
|
10636
|
+
return this.getFieldIndex(transaction, target).next(index => {
|
|
10637
|
+
if (!index) {
|
|
10638
|
+
offset = IndexOffset.min();
|
|
10639
|
+
}
|
|
10640
|
+
else if (!offset ||
|
|
10641
|
+
indexOffsetComparator(index.indexState.offset, offset) < 0) {
|
|
10642
|
+
offset = index.indexState.offset;
|
|
10643
|
+
}
|
|
10644
|
+
});
|
|
10645
|
+
}).next(() => offset);
|
|
10646
|
+
}
|
|
10530
10647
|
}
|
|
10531
10648
|
/**
|
|
10532
10649
|
* Helper to get a typed SimpleDbStore for the collectionParents
|
|
@@ -12276,12 +12393,31 @@ function dbCollectionGroupKey(collectionGroup, offset) {
|
|
|
12276
12393
|
}
|
|
12277
12394
|
/**
|
|
12278
12395
|
* Comparator that compares document keys according to the primary key sorting
|
|
12279
|
-
* used by the `DbRemoteDocumentDocument` store (by
|
|
12280
|
-
* document ID).
|
|
12396
|
+
* used by the `DbRemoteDocumentDocument` store (by prefix path, collection id
|
|
12397
|
+
* and then document ID).
|
|
12398
|
+
*
|
|
12399
|
+
* Visible for testing.
|
|
12281
12400
|
*/
|
|
12282
12401
|
function dbKeyComparator(l, r) {
|
|
12283
|
-
const
|
|
12284
|
-
|
|
12402
|
+
const left = l.path.toArray();
|
|
12403
|
+
const right = r.path.toArray();
|
|
12404
|
+
// The ordering is based on https://chromium.googlesource.com/chromium/blink/+/fe5c21fef94dae71c1c3344775b8d8a7f7e6d9ec/Source/modules/indexeddb/IDBKey.cpp#74
|
|
12405
|
+
let cmp = 0;
|
|
12406
|
+
for (let i = 0; i < left.length - 2 && i < right.length - 2; ++i) {
|
|
12407
|
+
cmp = primitiveComparator(left[i], right[i]);
|
|
12408
|
+
if (cmp) {
|
|
12409
|
+
return cmp;
|
|
12410
|
+
}
|
|
12411
|
+
}
|
|
12412
|
+
cmp = primitiveComparator(left.length, right.length);
|
|
12413
|
+
if (cmp) {
|
|
12414
|
+
return cmp;
|
|
12415
|
+
}
|
|
12416
|
+
cmp = primitiveComparator(left[left.length - 2], right[right.length - 2]);
|
|
12417
|
+
if (cmp) {
|
|
12418
|
+
return cmp;
|
|
12419
|
+
}
|
|
12420
|
+
return primitiveComparator(left[left.length - 1], right[right.length - 1]);
|
|
12285
12421
|
}
|
|
12286
12422
|
/**
|
|
12287
12423
|
* Schema Version for the Web client:
|
|
@@ -13182,7 +13318,7 @@ class IndexedDbPersistence {
|
|
|
13182
13318
|
return this.remoteDocumentCache;
|
|
13183
13319
|
}
|
|
13184
13320
|
getIndexManager(user) {
|
|
13185
|
-
return new IndexedDbIndexManager(user);
|
|
13321
|
+
return new IndexedDbIndexManager(user, this.serializer.remoteSerializer.databaseId);
|
|
13186
13322
|
}
|
|
13187
13323
|
getDocumentOverlayCache(user) {
|
|
13188
13324
|
return IndexedDbDocumentOverlayCache.forUser(this.serializer, user);
|
|
@@ -13678,7 +13814,7 @@ class LocalStoreImpl {
|
|
|
13678
13814
|
this.mutationQueue = this.persistence.getMutationQueue(user, this.indexManager);
|
|
13679
13815
|
this.localDocuments = new LocalDocumentsView(this.remoteDocuments, this.mutationQueue, this.indexManager);
|
|
13680
13816
|
this.remoteDocuments.setIndexManager(this.indexManager);
|
|
13681
|
-
this.queryEngine.
|
|
13817
|
+
this.queryEngine.initialize(this.localDocuments, this.indexManager);
|
|
13682
13818
|
}
|
|
13683
13819
|
collectGarbage(garbageCollector) {
|
|
13684
13820
|
return this.persistence.runTransaction('Collect garbage', 'readwrite-primary', txn => garbageCollector.collect(txn, this.targetDataByTarget));
|
|
@@ -15420,10 +15556,30 @@ class MemoryEagerDelegate {
|
|
|
15420
15556
|
* limitations under the License.
|
|
15421
15557
|
*/
|
|
15422
15558
|
/**
|
|
15423
|
-
*
|
|
15424
|
-
*
|
|
15425
|
-
*
|
|
15426
|
-
*
|
|
15559
|
+
* The Firestore query engine.
|
|
15560
|
+
*
|
|
15561
|
+
* Firestore queries can be executed in three modes. The Query Engine determines
|
|
15562
|
+
* what mode to use based on what data is persisted. The mode only determines
|
|
15563
|
+
* the runtime complexity of the query - the result set is equivalent across all
|
|
15564
|
+
* implementations.
|
|
15565
|
+
*
|
|
15566
|
+
* The Query engine will use indexed-based execution if a user has configured
|
|
15567
|
+
* any index that can be used to execute query (via `setIndexConfiguration()`).
|
|
15568
|
+
* Otherwise, the engine will try to optimize the query by re-using a previously
|
|
15569
|
+
* persisted query result. If that is not possible, the query will be executed
|
|
15570
|
+
* via a full collection scan.
|
|
15571
|
+
*
|
|
15572
|
+
* Index-based execution is the default when available. The query engine
|
|
15573
|
+
* supports partial indexed execution and merges the result from the index
|
|
15574
|
+
* lookup with documents that have not yet been indexed. The index evaluation
|
|
15575
|
+
* matches the backend's format and as such, the SDK can use indexing for all
|
|
15576
|
+
* queries that the backend supports.
|
|
15577
|
+
*
|
|
15578
|
+
* If no index exists, the query engine tries to take advantage of the target
|
|
15579
|
+
* document mapping in the TargetCache. These mappings exists for all queries
|
|
15580
|
+
* that have been synced with the backend at least once and allow the query
|
|
15581
|
+
* engine to only read documents that previously matched a query plus any
|
|
15582
|
+
* documents that were edited after the query was last listened to.
|
|
15427
15583
|
*
|
|
15428
15584
|
* There are some cases when this optimization is not guaranteed to produce
|
|
15429
15585
|
* the same results as full collection scans. In these cases, query
|
|
@@ -15438,16 +15594,41 @@ class MemoryEagerDelegate {
|
|
|
15438
15594
|
* - Queries that have never been CURRENT or free of limbo documents.
|
|
15439
15595
|
*/
|
|
15440
15596
|
class QueryEngine {
|
|
15597
|
+
constructor() {
|
|
15598
|
+
this.initialized = false;
|
|
15599
|
+
}
|
|
15441
15600
|
/** Sets the document view to query against. */
|
|
15442
|
-
|
|
15601
|
+
initialize(localDocuments, indexManager) {
|
|
15443
15602
|
this.localDocumentsView = localDocuments;
|
|
15603
|
+
this.indexManager = indexManager;
|
|
15604
|
+
this.initialized = true;
|
|
15444
15605
|
}
|
|
15445
15606
|
/** Returns all local documents matching the specified query. */
|
|
15446
15607
|
getDocumentsMatchingQuery(transaction, query, lastLimboFreeSnapshotVersion, remoteKeys) {
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15450
|
-
|
|
15608
|
+
return this.performQueryUsingIndex(transaction, query)
|
|
15609
|
+
.next(result => result
|
|
15610
|
+
? result
|
|
15611
|
+
: this.performQueryUsingRemoteKeys(transaction, query, remoteKeys, lastLimboFreeSnapshotVersion))
|
|
15612
|
+
.next(result => result ? result : this.executeFullCollectionScan(transaction, query));
|
|
15613
|
+
}
|
|
15614
|
+
/**
|
|
15615
|
+
* Performs an indexed query that evaluates the query based on a collection's
|
|
15616
|
+
* persisted index values. Returns `null` if an index is not available.
|
|
15617
|
+
*/
|
|
15618
|
+
performQueryUsingIndex(transaction, query) {
|
|
15619
|
+
{
|
|
15620
|
+
return PersistencePromise.resolve(null);
|
|
15621
|
+
}
|
|
15622
|
+
}
|
|
15623
|
+
/**
|
|
15624
|
+
* Performs a query based on the target's persisted query mapping. Returns
|
|
15625
|
+
* `null` if the mapping is not available or cannot be used.
|
|
15626
|
+
*/
|
|
15627
|
+
performQueryUsingRemoteKeys(transaction, query, remoteKeys, lastLimboFreeSnapshotVersion) {
|
|
15628
|
+
if (queryMatchesAllDocuments(query)) {
|
|
15629
|
+
// Queries that match all documents don't benefit from using
|
|
15630
|
+
// key-based lookups. It is more efficient to scan all documents in a
|
|
15631
|
+
// collection, rather than to perform individual lookups.
|
|
15451
15632
|
return this.executeFullCollectionScan(transaction, query);
|
|
15452
15633
|
}
|
|
15453
15634
|
// Queries that have never seen a snapshot without limbo free documents
|
|
@@ -15457,8 +15638,7 @@ class QueryEngine {
|
|
|
15457
15638
|
}
|
|
15458
15639
|
return this.localDocumentsView.getDocuments(transaction, remoteKeys).next(documents => {
|
|
15459
15640
|
const previousResults = this.applyQuery(query, documents);
|
|
15460
|
-
if ((
|
|
15461
|
-
this.needsRefill(query.limitType, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
|
|
15641
|
+
if (this.needsRefill(query, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
|
|
15462
15642
|
return this.executeFullCollectionScan(transaction, query);
|
|
15463
15643
|
}
|
|
15464
15644
|
if (getLogLevel() <= logger.LogLevel.DEBUG) {
|
|
@@ -15466,15 +15646,7 @@ class QueryEngine {
|
|
|
15466
15646
|
}
|
|
15467
15647
|
// Retrieve all results for documents that were updated since the last
|
|
15468
15648
|
// limbo-document free remote snapshot.
|
|
15469
|
-
return this.
|
|
15470
|
-
// We merge `previousResults` into `updateResults`, since
|
|
15471
|
-
// `updateResults` is already a DocumentMap. If a document is
|
|
15472
|
-
// contained in both lists, then its contents are the same.
|
|
15473
|
-
previousResults.forEach(doc => {
|
|
15474
|
-
updatedResults = updatedResults.insert(doc.key, doc);
|
|
15475
|
-
});
|
|
15476
|
-
return updatedResults;
|
|
15477
|
-
});
|
|
15649
|
+
return this.appendRemainingResults(transaction, previousResults, query, newIndexOffsetSuccessorFromReadTime(lastLimboFreeSnapshotVersion, INITIAL_LARGEST_BATCH_ID));
|
|
15478
15650
|
});
|
|
15479
15651
|
}
|
|
15480
15652
|
/** Applies the query filter and sorting to the provided documents. */
|
|
@@ -15493,6 +15665,7 @@ class QueryEngine {
|
|
|
15493
15665
|
* Determines if a limit query needs to be refilled from cache, making it
|
|
15494
15666
|
* ineligible for index-free execution.
|
|
15495
15667
|
*
|
|
15668
|
+
* @param query The query.
|
|
15496
15669
|
* @param sortedPreviousResults - The documents that matched the query when it
|
|
15497
15670
|
* was last synchronized, sorted by the query's comparator.
|
|
15498
15671
|
* @param remoteKeys - The document keys that matched the query at the last
|
|
@@ -15500,10 +15673,14 @@ class QueryEngine {
|
|
|
15500
15673
|
* @param limboFreeSnapshotVersion - The version of the snapshot when the
|
|
15501
15674
|
* query was last synchronized.
|
|
15502
15675
|
*/
|
|
15503
|
-
needsRefill(
|
|
15504
|
-
|
|
15505
|
-
|
|
15676
|
+
needsRefill(query, sortedPreviousResults, remoteKeys, limboFreeSnapshotVersion) {
|
|
15677
|
+
if (query.limit === null) {
|
|
15678
|
+
// Queries without limits do not need to be refilled.
|
|
15679
|
+
return false;
|
|
15680
|
+
}
|
|
15506
15681
|
if (remoteKeys.size !== sortedPreviousResults.size) {
|
|
15682
|
+
// The query needs to be refilled if a previously matching document no
|
|
15683
|
+
// longer matches.
|
|
15507
15684
|
return true;
|
|
15508
15685
|
}
|
|
15509
15686
|
// Limit queries are not eligible for index-free query execution if there is
|
|
@@ -15514,7 +15691,7 @@ class QueryEngine {
|
|
|
15514
15691
|
// the boundary of the limit itself did not change and documents from cache
|
|
15515
15692
|
// will continue to be "rejected" by this boundary. Therefore, we can ignore
|
|
15516
15693
|
// any modifications that don't affect the last document.
|
|
15517
|
-
const docAtLimitEdge = limitType === "F" /* First */
|
|
15694
|
+
const docAtLimitEdge = query.limitType === "F" /* First */
|
|
15518
15695
|
? sortedPreviousResults.last()
|
|
15519
15696
|
: sortedPreviousResults.first();
|
|
15520
15697
|
if (!docAtLimitEdge) {
|
|
@@ -15530,6 +15707,22 @@ class QueryEngine {
|
|
|
15530
15707
|
}
|
|
15531
15708
|
return this.localDocumentsView.getDocumentsMatchingQuery(transaction, query, IndexOffset.min());
|
|
15532
15709
|
}
|
|
15710
|
+
/**
|
|
15711
|
+
* Combines the results from an indexed execution with the remaining documents
|
|
15712
|
+
* that have not yet been indexed.
|
|
15713
|
+
*/
|
|
15714
|
+
appendRemainingResults(transaction, indexedResults, query, offset) {
|
|
15715
|
+
// Retrieve all results for documents that were updated since the offset.
|
|
15716
|
+
return this.localDocumentsView
|
|
15717
|
+
.getDocumentsMatchingQuery(transaction, query, offset)
|
|
15718
|
+
.next(remainingResults => {
|
|
15719
|
+
// Merge with existing results
|
|
15720
|
+
indexedResults.forEach(d => {
|
|
15721
|
+
remainingResults = remainingResults.insert(d.key, d);
|
|
15722
|
+
});
|
|
15723
|
+
return remainingResults;
|
|
15724
|
+
});
|
|
15725
|
+
}
|
|
15533
15726
|
}
|
|
15534
15727
|
|
|
15535
15728
|
/**
|
|
@@ -21789,10 +21982,12 @@ class View {
|
|
|
21789
21982
|
//
|
|
21790
21983
|
// Note that this should never get used in a refill (when previousChanges is
|
|
21791
21984
|
// set), because there will only be adds -- no deletes or updates.
|
|
21792
|
-
const lastDocInLimit =
|
|
21985
|
+
const lastDocInLimit = this.query.limitType === "F" /* First */ &&
|
|
21986
|
+
oldDocumentSet.size === this.query.limit
|
|
21793
21987
|
? oldDocumentSet.last()
|
|
21794
21988
|
: null;
|
|
21795
|
-
const firstDocInLimit =
|
|
21989
|
+
const firstDocInLimit = this.query.limitType === "L" /* Last */ &&
|
|
21990
|
+
oldDocumentSet.size === this.query.limit
|
|
21796
21991
|
? oldDocumentSet.first()
|
|
21797
21992
|
: null;
|
|
21798
21993
|
docChanges.inorderTraversal((key, entry) => {
|
|
@@ -21865,9 +22060,9 @@ class View {
|
|
|
21865
22060
|
}
|
|
21866
22061
|
});
|
|
21867
22062
|
// Drop documents out to meet limit/limitToLast requirement.
|
|
21868
|
-
if (
|
|
22063
|
+
if (this.query.limit !== null) {
|
|
21869
22064
|
while (newDocumentSet.size > this.query.limit) {
|
|
21870
|
-
const oldDoc =
|
|
22065
|
+
const oldDoc = this.query.limitType === "F" /* First */
|
|
21871
22066
|
? newDocumentSet.last()
|
|
21872
22067
|
: newDocumentSet.first();
|
|
21873
22068
|
newDocumentSet = newDocumentSet.delete(oldDoc.key);
|
|
@@ -26998,7 +27193,8 @@ function snapshotEqual(left, right) {
|
|
|
26998
27193
|
* limitations under the License.
|
|
26999
27194
|
*/
|
|
27000
27195
|
function validateHasExplicitOrderByForLimitToLast(query) {
|
|
27001
|
-
if (
|
|
27196
|
+
if (query.limitType === "L" /* Last */ &&
|
|
27197
|
+
query.explicitOrderBy.length === 0) {
|
|
27002
27198
|
throw new FirestoreError(Code.UNIMPLEMENTED, 'limitToLast() queries require specifying at least one orderBy() clause');
|
|
27003
27199
|
}
|
|
27004
27200
|
}
|