@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.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import * as grpc from '@grpc/grpc-js';
|
|
|
8
8
|
import * as protoLoader from '@grpc/proto-loader';
|
|
9
9
|
|
|
10
10
|
const name = "@firebase/firestore";
|
|
11
|
-
const version$1 = "3.4.
|
|
11
|
+
const version$1 = "3.4.8-canary.44948ac91";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @license
|
|
@@ -61,7 +61,7 @@ User.GOOGLE_CREDENTIALS = new User('google-credentials-uid');
|
|
|
61
61
|
User.FIRST_PARTY = new User('first-party-uid');
|
|
62
62
|
User.MOCK_USER = new User('mock-user');
|
|
63
63
|
|
|
64
|
-
const version = "9.6.
|
|
64
|
+
const version = "9.6.11-canary.44948ac91";
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* @license
|
|
@@ -1393,13 +1393,14 @@ const DbIndexEntryKeyPath = [
|
|
|
1393
1393
|
'uid',
|
|
1394
1394
|
'arrayValue',
|
|
1395
1395
|
'directionalValue',
|
|
1396
|
+
'orderedDocumentKey',
|
|
1396
1397
|
'documentKey'
|
|
1397
1398
|
];
|
|
1398
1399
|
const DbIndexEntryDocumentKeyIndex = 'documentKeyIndex';
|
|
1399
1400
|
const DbIndexEntryDocumentKeyIndexPath = [
|
|
1400
1401
|
'indexId',
|
|
1401
1402
|
'uid',
|
|
1402
|
-
'
|
|
1403
|
+
'orderedDocumentKey'
|
|
1403
1404
|
];
|
|
1404
1405
|
/** Name of the IndexedDb object store. */
|
|
1405
1406
|
const DbDocumentOverlayStore = 'documentOverlays';
|
|
@@ -2285,9 +2286,7 @@ class SimpleDbStore {
|
|
|
2285
2286
|
cursor.continue(controller.skipToKey);
|
|
2286
2287
|
}
|
|
2287
2288
|
};
|
|
2288
|
-
}).next(() =>
|
|
2289
|
-
return PersistencePromise.waitFor(results);
|
|
2290
|
-
});
|
|
2289
|
+
}).next(() => PersistencePromise.waitFor(results));
|
|
2291
2290
|
}
|
|
2292
2291
|
options(indexOrRange, range) {
|
|
2293
2292
|
let indexName = undefined;
|
|
@@ -4196,6 +4195,17 @@ function fieldIndexGetArraySegment(fieldIndex) {
|
|
|
4196
4195
|
function fieldIndexGetDirectionalSegments(fieldIndex) {
|
|
4197
4196
|
return fieldIndex.fields.filter(s => s.kind !== 2 /* CONTAINS */);
|
|
4198
4197
|
}
|
|
4198
|
+
/**
|
|
4199
|
+
* Returns the order of the document key component for the given index.
|
|
4200
|
+
*
|
|
4201
|
+
* PORTING NOTE: This is only used in the Web IndexedDb implementation.
|
|
4202
|
+
*/
|
|
4203
|
+
function fieldIndexGetKeyOrder(fieldIndex) {
|
|
4204
|
+
const directionalSegments = fieldIndexGetDirectionalSegments(fieldIndex);
|
|
4205
|
+
return directionalSegments.length === 0
|
|
4206
|
+
? 0 /* ASCENDING */
|
|
4207
|
+
: directionalSegments[directionalSegments.length - 1].kind;
|
|
4208
|
+
}
|
|
4199
4209
|
/** Returns a debug representation of the field index */
|
|
4200
4210
|
function fieldIndexToString(fieldIndex) {
|
|
4201
4211
|
return `id=${fieldIndex.indexId}|cg=${fieldIndex.collectionGroup}|f=${fieldIndex.fields.map(f => `${f.fieldPath}:${f.kind}`).join(',')}`;
|
|
@@ -4300,7 +4310,7 @@ function indexOffsetComparator(left, right) {
|
|
|
4300
4310
|
|
|
4301
4311
|
/**
|
|
4302
4312
|
* @license
|
|
4303
|
-
* Copyright
|
|
4313
|
+
* Copyright 2017 Google LLC
|
|
4304
4314
|
*
|
|
4305
4315
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4306
4316
|
* you may not use this file except in compliance with the License.
|
|
@@ -4314,1488 +4324,1656 @@ function indexOffsetComparator(left, right) {
|
|
|
4314
4324
|
* See the License for the specific language governing permissions and
|
|
4315
4325
|
* limitations under the License.
|
|
4316
4326
|
*/
|
|
4317
|
-
//
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
this.
|
|
4322
|
-
this.
|
|
4323
|
-
this.filters = filters;
|
|
4324
|
-
this.limit = limit;
|
|
4325
|
-
this.startAt = startAt;
|
|
4326
|
-
this.endAt = endAt;
|
|
4327
|
-
this.memoizedCanonicalId = null;
|
|
4327
|
+
// An immutable sorted map implementation, based on a Left-leaning Red-Black
|
|
4328
|
+
// tree.
|
|
4329
|
+
class SortedMap {
|
|
4330
|
+
constructor(comparator, root) {
|
|
4331
|
+
this.comparator = comparator;
|
|
4332
|
+
this.root = root ? root : LLRBNode.EMPTY;
|
|
4328
4333
|
}
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
* NOTE: you should always construct `Target` from `Query.toTarget` instead of
|
|
4335
|
-
* using this factory method, because `Query` provides an implicit `orderBy`
|
|
4336
|
-
* property.
|
|
4337
|
-
*/
|
|
4338
|
-
function newTarget(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
|
|
4339
|
-
return new TargetImpl(path, collectionGroup, orderBy, filters, limit, startAt, endAt);
|
|
4340
|
-
}
|
|
4341
|
-
function canonifyTarget(target) {
|
|
4342
|
-
const targetImpl = debugCast(target);
|
|
4343
|
-
if (targetImpl.memoizedCanonicalId === null) {
|
|
4344
|
-
let str = targetImpl.path.canonicalString();
|
|
4345
|
-
if (targetImpl.collectionGroup !== null) {
|
|
4346
|
-
str += '|cg:' + targetImpl.collectionGroup;
|
|
4347
|
-
}
|
|
4348
|
-
str += '|f:';
|
|
4349
|
-
str += targetImpl.filters.map(f => canonifyFilter(f)).join(',');
|
|
4350
|
-
str += '|ob:';
|
|
4351
|
-
str += targetImpl.orderBy.map(o => canonifyOrderBy(o)).join(',');
|
|
4352
|
-
if (!isNullOrUndefined(targetImpl.limit)) {
|
|
4353
|
-
str += '|l:';
|
|
4354
|
-
str += targetImpl.limit;
|
|
4355
|
-
}
|
|
4356
|
-
if (targetImpl.startAt) {
|
|
4357
|
-
str += '|lb:';
|
|
4358
|
-
str += targetImpl.startAt.inclusive ? 'b:' : 'a:';
|
|
4359
|
-
str += targetImpl.startAt.position.map(p => canonicalId(p)).join(',');
|
|
4360
|
-
}
|
|
4361
|
-
if (targetImpl.endAt) {
|
|
4362
|
-
str += '|ub:';
|
|
4363
|
-
str += targetImpl.endAt.inclusive ? 'a:' : 'b:';
|
|
4364
|
-
str += targetImpl.endAt.position.map(p => canonicalId(p)).join(',');
|
|
4365
|
-
}
|
|
4366
|
-
targetImpl.memoizedCanonicalId = str;
|
|
4334
|
+
// Returns a copy of the map, with the specified key/value added or replaced.
|
|
4335
|
+
insert(key, value) {
|
|
4336
|
+
return new SortedMap(this.comparator, this.root
|
|
4337
|
+
.insert(key, value, this.comparator)
|
|
4338
|
+
.copy(null, null, LLRBNode.BLACK, null, null));
|
|
4367
4339
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
str += ' collectionGroup=' + target.collectionGroup;
|
|
4340
|
+
// Returns a copy of the map, with the specified key removed.
|
|
4341
|
+
remove(key) {
|
|
4342
|
+
return new SortedMap(this.comparator, this.root
|
|
4343
|
+
.remove(key, this.comparator)
|
|
4344
|
+
.copy(null, null, LLRBNode.BLACK, null, null));
|
|
4374
4345
|
}
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4346
|
+
// Returns the value of the node with the given key, or null.
|
|
4347
|
+
get(key) {
|
|
4348
|
+
let node = this.root;
|
|
4349
|
+
while (!node.isEmpty()) {
|
|
4350
|
+
const cmp = this.comparator(key, node.key);
|
|
4351
|
+
if (cmp === 0) {
|
|
4352
|
+
return node.value;
|
|
4353
|
+
}
|
|
4354
|
+
else if (cmp < 0) {
|
|
4355
|
+
node = node.left;
|
|
4356
|
+
}
|
|
4357
|
+
else if (cmp > 0) {
|
|
4358
|
+
node = node.right;
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
return null;
|
|
4379
4362
|
}
|
|
4380
|
-
if
|
|
4381
|
-
|
|
4363
|
+
// Returns the index of the element in this sorted map, or -1 if it doesn't
|
|
4364
|
+
// exist.
|
|
4365
|
+
indexOf(key) {
|
|
4366
|
+
// Number of nodes that were pruned when descending right
|
|
4367
|
+
let prunedNodes = 0;
|
|
4368
|
+
let node = this.root;
|
|
4369
|
+
while (!node.isEmpty()) {
|
|
4370
|
+
const cmp = this.comparator(key, node.key);
|
|
4371
|
+
if (cmp === 0) {
|
|
4372
|
+
return prunedNodes + node.left.size;
|
|
4373
|
+
}
|
|
4374
|
+
else if (cmp < 0) {
|
|
4375
|
+
node = node.left;
|
|
4376
|
+
}
|
|
4377
|
+
else {
|
|
4378
|
+
// Count all nodes left of the node plus the node itself
|
|
4379
|
+
prunedNodes += node.left.size + 1;
|
|
4380
|
+
node = node.right;
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
// Node not found
|
|
4384
|
+
return -1;
|
|
4382
4385
|
}
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
.map(o => stringifyOrderBy(o))
|
|
4386
|
-
.join(', ')}]`;
|
|
4386
|
+
isEmpty() {
|
|
4387
|
+
return this.root.isEmpty();
|
|
4387
4388
|
}
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
str += target.startAt.position.map(p => canonicalId(p)).join(',');
|
|
4389
|
+
// Returns the total number of nodes in the map.
|
|
4390
|
+
get size() {
|
|
4391
|
+
return this.root.size;
|
|
4392
4392
|
}
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
str += target.endAt.position.map(p => canonicalId(p)).join(',');
|
|
4393
|
+
// Returns the minimum key in the map.
|
|
4394
|
+
minKey() {
|
|
4395
|
+
return this.root.minKey();
|
|
4397
4396
|
}
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
if (left.limit !== right.limit) {
|
|
4402
|
-
return false;
|
|
4397
|
+
// Returns the maximum key in the map.
|
|
4398
|
+
maxKey() {
|
|
4399
|
+
return this.root.maxKey();
|
|
4403
4400
|
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4401
|
+
// Traverses the map in key order and calls the specified action function
|
|
4402
|
+
// for each key/value pair. If action returns true, traversal is aborted.
|
|
4403
|
+
// Returns the first truthy value returned by action, or the last falsey
|
|
4404
|
+
// value returned by action.
|
|
4405
|
+
inorderTraversal(action) {
|
|
4406
|
+
return this.root.inorderTraversal(action);
|
|
4406
4407
|
}
|
|
4407
|
-
|
|
4408
|
-
|
|
4408
|
+
forEach(fn) {
|
|
4409
|
+
this.inorderTraversal((k, v) => {
|
|
4410
|
+
fn(k, v);
|
|
4409
4411
|
return false;
|
|
4410
|
-
}
|
|
4411
|
-
}
|
|
4412
|
-
if (left.filters.length !== right.filters.length) {
|
|
4413
|
-
return false;
|
|
4412
|
+
});
|
|
4414
4413
|
}
|
|
4415
|
-
|
|
4416
|
-
|
|
4414
|
+
toString() {
|
|
4415
|
+
const descriptions = [];
|
|
4416
|
+
this.inorderTraversal((k, v) => {
|
|
4417
|
+
descriptions.push(`${k}:${v}`);
|
|
4417
4418
|
return false;
|
|
4418
|
-
}
|
|
4419
|
-
|
|
4420
|
-
if (left.collectionGroup !== right.collectionGroup) {
|
|
4421
|
-
return false;
|
|
4419
|
+
});
|
|
4420
|
+
return `{${descriptions.join(', ')}}`;
|
|
4422
4421
|
}
|
|
4423
|
-
|
|
4424
|
-
|
|
4422
|
+
// Traverses the map in reverse key order and calls the specified action
|
|
4423
|
+
// function for each key/value pair. If action returns true, traversal is
|
|
4424
|
+
// aborted.
|
|
4425
|
+
// Returns the first truthy value returned by action, or the last falsey
|
|
4426
|
+
// value returned by action.
|
|
4427
|
+
reverseTraversal(action) {
|
|
4428
|
+
return this.root.reverseTraversal(action);
|
|
4425
4429
|
}
|
|
4426
|
-
|
|
4427
|
-
|
|
4430
|
+
// Returns an iterator over the SortedMap.
|
|
4431
|
+
getIterator() {
|
|
4432
|
+
return new SortedMapIterator(this.root, null, this.comparator, false);
|
|
4428
4433
|
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
function targetIsDocumentTarget(target) {
|
|
4432
|
-
return (DocumentKey.isDocumentKey(target.path) &&
|
|
4433
|
-
target.collectionGroup === null &&
|
|
4434
|
-
target.filters.length === 0);
|
|
4435
|
-
}
|
|
4436
|
-
/** Returns the field filters that target the given field path. */
|
|
4437
|
-
function targetGetFieldFiltersForPath(target, path) {
|
|
4438
|
-
return target.filters.filter(f => f instanceof FieldFilter && f.field.isEqual(path));
|
|
4439
|
-
}
|
|
4440
|
-
/**
|
|
4441
|
-
* Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY
|
|
4442
|
-
* filters. Returns `null` if there are no such filters.
|
|
4443
|
-
*/
|
|
4444
|
-
function targetGetArrayValues(target, fieldIndex) {
|
|
4445
|
-
const segment = fieldIndexGetArraySegment(fieldIndex);
|
|
4446
|
-
if (segment === undefined) {
|
|
4447
|
-
return null;
|
|
4434
|
+
getIteratorFrom(key) {
|
|
4435
|
+
return new SortedMapIterator(this.root, key, this.comparator, false);
|
|
4448
4436
|
}
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
case "array-contains-any" /* ARRAY_CONTAINS_ANY */:
|
|
4452
|
-
return fieldFilter.value.arrayValue.values || [];
|
|
4453
|
-
case "array-contains" /* ARRAY_CONTAINS */:
|
|
4454
|
-
return [fieldFilter.value];
|
|
4455
|
-
// Remaining filters are not array filters.
|
|
4456
|
-
}
|
|
4437
|
+
getReverseIterator() {
|
|
4438
|
+
return new SortedMapIterator(this.root, null, this.comparator, true);
|
|
4457
4439
|
}
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
/**
|
|
4461
|
-
* Returns the list of values that are used in != or NOT_IN filters. Returns
|
|
4462
|
-
* `null` if there are no such filters.
|
|
4463
|
-
*/
|
|
4464
|
-
function targetGetNotInValues(target, fieldIndex) {
|
|
4465
|
-
const values = new Map();
|
|
4466
|
-
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
4467
|
-
for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
|
|
4468
|
-
switch (fieldFilter.op) {
|
|
4469
|
-
case "==" /* EQUAL */:
|
|
4470
|
-
case "in" /* IN */:
|
|
4471
|
-
// Encode equality prefix, which is encoded in the index value before
|
|
4472
|
-
// the inequality (e.g. `a == 'a' && b != 'b'` is encoded to
|
|
4473
|
-
// `value != 'ab'`).
|
|
4474
|
-
values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
|
|
4475
|
-
break;
|
|
4476
|
-
case "not-in" /* NOT_IN */:
|
|
4477
|
-
case "!=" /* NOT_EQUAL */:
|
|
4478
|
-
// NotIn/NotEqual is always a suffix. There cannot be any remaining
|
|
4479
|
-
// segments and hence we can return early here.
|
|
4480
|
-
values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
|
|
4481
|
-
return Array.from(values.values());
|
|
4482
|
-
// Remaining filters cannot be used as notIn bounds.
|
|
4483
|
-
}
|
|
4484
|
-
}
|
|
4440
|
+
getReverseIteratorFrom(key) {
|
|
4441
|
+
return new SortedMapIterator(this.root, key, this.comparator, true);
|
|
4485
4442
|
}
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
// Remaining filters cannot be used as lower bounds.
|
|
4524
|
-
}
|
|
4525
|
-
if (valuesMax(segmentValue, filterValue) === filterValue) {
|
|
4526
|
-
segmentValue = filterValue;
|
|
4527
|
-
segmentInclusive = filterInclusive;
|
|
4528
|
-
}
|
|
4529
|
-
}
|
|
4530
|
-
// If there is a startAt bound, compare the values against the existing
|
|
4531
|
-
// boundary to see if we can narrow the scope.
|
|
4532
|
-
if (target.startAt !== null) {
|
|
4533
|
-
for (let i = 0; i < target.orderBy.length; ++i) {
|
|
4534
|
-
const orderBy = target.orderBy[i];
|
|
4535
|
-
if (orderBy.field.isEqual(segment.fieldPath)) {
|
|
4536
|
-
const cursorValue = target.startAt.position[i];
|
|
4537
|
-
if (valuesMax(segmentValue, cursorValue) === cursorValue) {
|
|
4538
|
-
segmentValue = cursorValue;
|
|
4539
|
-
segmentInclusive = target.startAt.inclusive;
|
|
4540
|
-
}
|
|
4541
|
-
break;
|
|
4443
|
+
} // end SortedMap
|
|
4444
|
+
// An iterator over an LLRBNode.
|
|
4445
|
+
class SortedMapIterator {
|
|
4446
|
+
constructor(node, startKey, comparator, isReverse) {
|
|
4447
|
+
this.isReverse = isReverse;
|
|
4448
|
+
this.nodeStack = [];
|
|
4449
|
+
let cmp = 1;
|
|
4450
|
+
while (!node.isEmpty()) {
|
|
4451
|
+
cmp = startKey ? comparator(node.key, startKey) : 1;
|
|
4452
|
+
// flip the comparison if we're going in reverse
|
|
4453
|
+
if (startKey && isReverse) {
|
|
4454
|
+
cmp *= -1;
|
|
4455
|
+
}
|
|
4456
|
+
if (cmp < 0) {
|
|
4457
|
+
// This node is less than our start key. ignore it
|
|
4458
|
+
if (this.isReverse) {
|
|
4459
|
+
node = node.left;
|
|
4460
|
+
}
|
|
4461
|
+
else {
|
|
4462
|
+
node = node.right;
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
else if (cmp === 0) {
|
|
4466
|
+
// This node is exactly equal to our start key. Push it on the stack,
|
|
4467
|
+
// but stop iterating;
|
|
4468
|
+
this.nodeStack.push(node);
|
|
4469
|
+
break;
|
|
4470
|
+
}
|
|
4471
|
+
else {
|
|
4472
|
+
// This node is greater than our start key, add it to the stack and move
|
|
4473
|
+
// to the next one
|
|
4474
|
+
this.nodeStack.push(node);
|
|
4475
|
+
if (this.isReverse) {
|
|
4476
|
+
node = node.right;
|
|
4477
|
+
}
|
|
4478
|
+
else {
|
|
4479
|
+
node = node.left;
|
|
4542
4480
|
}
|
|
4543
4481
|
}
|
|
4544
4482
|
}
|
|
4545
|
-
if (segmentValue === undefined) {
|
|
4546
|
-
// No lower bound exists
|
|
4547
|
-
return null;
|
|
4548
|
-
}
|
|
4549
|
-
values.push(segmentValue);
|
|
4550
|
-
inclusive && (inclusive = segmentInclusive);
|
|
4551
4483
|
}
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
const values = [];
|
|
4561
|
-
let inclusive = true;
|
|
4562
|
-
// For each segment, retrieve an upper bound if there is a suitable filter or
|
|
4563
|
-
// endAt.
|
|
4564
|
-
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
4565
|
-
let segmentValue = undefined;
|
|
4566
|
-
let segmentInclusive = true;
|
|
4567
|
-
// Process all filters to find a value for the current field segment
|
|
4568
|
-
for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
|
|
4569
|
-
let filterValue = undefined;
|
|
4570
|
-
let filterInclusive = true;
|
|
4571
|
-
switch (fieldFilter.op) {
|
|
4572
|
-
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
4573
|
-
case ">" /* GREATER_THAN */:
|
|
4574
|
-
filterValue = valuesGetUpperBound(fieldFilter.value);
|
|
4575
|
-
filterInclusive = false;
|
|
4576
|
-
break;
|
|
4577
|
-
case "==" /* EQUAL */:
|
|
4578
|
-
case "in" /* IN */:
|
|
4579
|
-
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
4580
|
-
filterValue = fieldFilter.value;
|
|
4581
|
-
break;
|
|
4582
|
-
case "<" /* LESS_THAN */:
|
|
4583
|
-
filterValue = fieldFilter.value;
|
|
4584
|
-
filterInclusive = false;
|
|
4585
|
-
break;
|
|
4586
|
-
case "!=" /* NOT_EQUAL */:
|
|
4587
|
-
case "not-in" /* NOT_IN */:
|
|
4588
|
-
filterValue = MAX_VALUE;
|
|
4589
|
-
break;
|
|
4590
|
-
// Remaining filters cannot be used as upper bounds.
|
|
4591
|
-
}
|
|
4592
|
-
if (valuesMin(segmentValue, filterValue) === filterValue) {
|
|
4593
|
-
segmentValue = filterValue;
|
|
4594
|
-
segmentInclusive = filterInclusive;
|
|
4595
|
-
}
|
|
4596
|
-
}
|
|
4597
|
-
// If there is a endAt bound, compare the values against the existing
|
|
4598
|
-
// boundary to see if we can narrow the scope.
|
|
4599
|
-
if (target.endAt !== null) {
|
|
4600
|
-
for (let i = 0; i < target.orderBy.length; ++i) {
|
|
4601
|
-
const orderBy = target.orderBy[i];
|
|
4602
|
-
if (orderBy.field.isEqual(segment.fieldPath)) {
|
|
4603
|
-
const cursorValue = target.endAt.position[i];
|
|
4604
|
-
if (valuesMin(segmentValue, cursorValue) === cursorValue) {
|
|
4605
|
-
segmentValue = cursorValue;
|
|
4606
|
-
segmentInclusive = target.endAt.inclusive;
|
|
4607
|
-
}
|
|
4608
|
-
break;
|
|
4609
|
-
}
|
|
4484
|
+
getNext() {
|
|
4485
|
+
let node = this.nodeStack.pop();
|
|
4486
|
+
const result = { key: node.key, value: node.value };
|
|
4487
|
+
if (this.isReverse) {
|
|
4488
|
+
node = node.left;
|
|
4489
|
+
while (!node.isEmpty()) {
|
|
4490
|
+
this.nodeStack.push(node);
|
|
4491
|
+
node = node.right;
|
|
4610
4492
|
}
|
|
4611
4493
|
}
|
|
4612
|
-
|
|
4613
|
-
|
|
4494
|
+
else {
|
|
4495
|
+
node = node.right;
|
|
4496
|
+
while (!node.isEmpty()) {
|
|
4497
|
+
this.nodeStack.push(node);
|
|
4498
|
+
node = node.left;
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
return result;
|
|
4502
|
+
}
|
|
4503
|
+
hasNext() {
|
|
4504
|
+
return this.nodeStack.length > 0;
|
|
4505
|
+
}
|
|
4506
|
+
peek() {
|
|
4507
|
+
if (this.nodeStack.length === 0) {
|
|
4614
4508
|
return null;
|
|
4615
4509
|
}
|
|
4616
|
-
|
|
4617
|
-
|
|
4510
|
+
const node = this.nodeStack[this.nodeStack.length - 1];
|
|
4511
|
+
return { key: node.key, value: node.value };
|
|
4618
4512
|
}
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
class
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
constructor(field, op, value) {
|
|
4625
|
-
super();
|
|
4626
|
-
this.field = field;
|
|
4627
|
-
this.op = op;
|
|
4513
|
+
} // end SortedMapIterator
|
|
4514
|
+
// Represents a node in a Left-leaning Red-Black tree.
|
|
4515
|
+
class LLRBNode {
|
|
4516
|
+
constructor(key, value, color, left, right) {
|
|
4517
|
+
this.key = key;
|
|
4628
4518
|
this.value = value;
|
|
4519
|
+
this.color = color != null ? color : LLRBNode.RED;
|
|
4520
|
+
this.left = left != null ? left : LLRBNode.EMPTY;
|
|
4521
|
+
this.right = right != null ? right : LLRBNode.EMPTY;
|
|
4522
|
+
this.size = this.left.size + 1 + this.right.size;
|
|
4629
4523
|
}
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4524
|
+
// Returns a copy of the current node, optionally replacing pieces of it.
|
|
4525
|
+
copy(key, value, color, left, right) {
|
|
4526
|
+
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);
|
|
4527
|
+
}
|
|
4528
|
+
isEmpty() {
|
|
4529
|
+
return false;
|
|
4530
|
+
}
|
|
4531
|
+
// Traverses the tree in key order and calls the specified action function
|
|
4532
|
+
// for each node. If action returns true, traversal is aborted.
|
|
4533
|
+
// Returns the first truthy value returned by action, or the last falsey
|
|
4534
|
+
// value returned by action.
|
|
4535
|
+
inorderTraversal(action) {
|
|
4536
|
+
return (this.left.inorderTraversal(action) ||
|
|
4537
|
+
action(this.key, this.value) ||
|
|
4538
|
+
this.right.inorderTraversal(action));
|
|
4539
|
+
}
|
|
4540
|
+
// Traverses the tree in reverse key order and calls the specified action
|
|
4541
|
+
// function for each node. If action returns true, traversal is aborted.
|
|
4542
|
+
// Returns the first truthy value returned by action, or the last falsey
|
|
4543
|
+
// value returned by action.
|
|
4544
|
+
reverseTraversal(action) {
|
|
4545
|
+
return (this.right.reverseTraversal(action) ||
|
|
4546
|
+
action(this.key, this.value) ||
|
|
4547
|
+
this.left.reverseTraversal(action));
|
|
4548
|
+
}
|
|
4549
|
+
// Returns the minimum node in the tree.
|
|
4550
|
+
min() {
|
|
4551
|
+
if (this.left.isEmpty()) {
|
|
4552
|
+
return this;
|
|
4641
4553
|
}
|
|
4642
|
-
else
|
|
4643
|
-
return
|
|
4554
|
+
else {
|
|
4555
|
+
return this.left.min();
|
|
4644
4556
|
}
|
|
4645
|
-
|
|
4646
|
-
|
|
4557
|
+
}
|
|
4558
|
+
// Returns the maximum key in the tree.
|
|
4559
|
+
minKey() {
|
|
4560
|
+
return this.min().key;
|
|
4561
|
+
}
|
|
4562
|
+
// Returns the maximum key in the tree.
|
|
4563
|
+
maxKey() {
|
|
4564
|
+
if (this.right.isEmpty()) {
|
|
4565
|
+
return this.key;
|
|
4647
4566
|
}
|
|
4648
|
-
else
|
|
4649
|
-
return
|
|
4567
|
+
else {
|
|
4568
|
+
return this.right.maxKey();
|
|
4650
4569
|
}
|
|
4651
|
-
|
|
4652
|
-
|
|
4570
|
+
}
|
|
4571
|
+
// Returns new tree, with the key/value added.
|
|
4572
|
+
insert(key, value, comparator) {
|
|
4573
|
+
let n = this;
|
|
4574
|
+
const cmp = comparator(key, n.key);
|
|
4575
|
+
if (cmp < 0) {
|
|
4576
|
+
n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
|
|
4577
|
+
}
|
|
4578
|
+
else if (cmp === 0) {
|
|
4579
|
+
n = n.copy(null, value, null, null, null);
|
|
4653
4580
|
}
|
|
4654
4581
|
else {
|
|
4655
|
-
|
|
4582
|
+
n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
|
|
4656
4583
|
}
|
|
4584
|
+
return n.fixUp();
|
|
4657
4585
|
}
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
: new KeyFieldNotInFilter(field, value);
|
|
4662
|
-
}
|
|
4663
|
-
matches(doc) {
|
|
4664
|
-
const other = doc.data.field(this.field);
|
|
4665
|
-
// Types do not have to match in NOT_EQUAL filters.
|
|
4666
|
-
if (this.op === "!=" /* NOT_EQUAL */) {
|
|
4667
|
-
return (other !== null &&
|
|
4668
|
-
this.matchesComparison(valueCompare(other, this.value)));
|
|
4586
|
+
removeMin() {
|
|
4587
|
+
if (this.left.isEmpty()) {
|
|
4588
|
+
return LLRBNode.EMPTY;
|
|
4669
4589
|
}
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
this.matchesComparison(valueCompare(other, this.value)));
|
|
4674
|
-
}
|
|
4675
|
-
matchesComparison(comparison) {
|
|
4676
|
-
switch (this.op) {
|
|
4677
|
-
case "<" /* LESS_THAN */:
|
|
4678
|
-
return comparison < 0;
|
|
4679
|
-
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
4680
|
-
return comparison <= 0;
|
|
4681
|
-
case "==" /* EQUAL */:
|
|
4682
|
-
return comparison === 0;
|
|
4683
|
-
case "!=" /* NOT_EQUAL */:
|
|
4684
|
-
return comparison !== 0;
|
|
4685
|
-
case ">" /* GREATER_THAN */:
|
|
4686
|
-
return comparison > 0;
|
|
4687
|
-
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
4688
|
-
return comparison >= 0;
|
|
4689
|
-
default:
|
|
4690
|
-
return fail();
|
|
4590
|
+
let n = this;
|
|
4591
|
+
if (!n.left.isRed() && !n.left.left.isRed()) {
|
|
4592
|
+
n = n.moveRedLeft();
|
|
4691
4593
|
}
|
|
4594
|
+
n = n.copy(null, null, null, n.left.removeMin(), null);
|
|
4595
|
+
return n.fixUp();
|
|
4692
4596
|
}
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4597
|
+
// Returns new tree, with the specified item removed.
|
|
4598
|
+
remove(key, comparator) {
|
|
4599
|
+
let smallest;
|
|
4600
|
+
let n = this;
|
|
4601
|
+
if (comparator(key, n.key) < 0) {
|
|
4602
|
+
if (!n.left.isEmpty() && !n.left.isRed() && !n.left.left.isRed()) {
|
|
4603
|
+
n = n.moveRedLeft();
|
|
4604
|
+
}
|
|
4605
|
+
n = n.copy(null, null, null, n.left.remove(key, comparator), null);
|
|
4606
|
+
}
|
|
4607
|
+
else {
|
|
4608
|
+
if (n.left.isRed()) {
|
|
4609
|
+
n = n.rotateRight();
|
|
4610
|
+
}
|
|
4611
|
+
if (!n.right.isEmpty() && !n.right.isRed() && !n.right.left.isRed()) {
|
|
4612
|
+
n = n.moveRedRight();
|
|
4613
|
+
}
|
|
4614
|
+
if (comparator(key, n.key) === 0) {
|
|
4615
|
+
if (n.right.isEmpty()) {
|
|
4616
|
+
return LLRBNode.EMPTY;
|
|
4617
|
+
}
|
|
4618
|
+
else {
|
|
4619
|
+
smallest = n.right.min();
|
|
4620
|
+
n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin());
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
n = n.copy(null, null, null, null, n.right.remove(key, comparator));
|
|
4624
|
+
}
|
|
4625
|
+
return n.fixUp();
|
|
4702
4626
|
}
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
// TODO(b/29183165): Technically, this won't be unique if two values have
|
|
4706
|
-
// the same description, such as the int 3 and the string "3". So we should
|
|
4707
|
-
// add the types in here somehow, too.
|
|
4708
|
-
return (filter.field.canonicalString() +
|
|
4709
|
-
filter.op.toString() +
|
|
4710
|
-
canonicalId(filter.value));
|
|
4711
|
-
}
|
|
4712
|
-
function filterEquals(f1, f2) {
|
|
4713
|
-
return (f1.op === f2.op &&
|
|
4714
|
-
f1.field.isEqual(f2.field) &&
|
|
4715
|
-
valueEquals(f1.value, f2.value));
|
|
4716
|
-
}
|
|
4717
|
-
/** Returns a debug description for `filter`. */
|
|
4718
|
-
function stringifyFilter(filter) {
|
|
4719
|
-
return `${filter.field.canonicalString()} ${filter.op} ${canonicalId(filter.value)}`;
|
|
4720
|
-
}
|
|
4721
|
-
/** Filter that matches on key fields (i.e. '__name__'). */
|
|
4722
|
-
class KeyFieldFilter extends FieldFilter {
|
|
4723
|
-
constructor(field, op, value) {
|
|
4724
|
-
super(field, op, value);
|
|
4725
|
-
this.key = DocumentKey.fromName(value.referenceValue);
|
|
4627
|
+
isRed() {
|
|
4628
|
+
return this.color;
|
|
4726
4629
|
}
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4630
|
+
// Returns new tree after performing any needed rotations.
|
|
4631
|
+
fixUp() {
|
|
4632
|
+
let n = this;
|
|
4633
|
+
if (n.right.isRed() && !n.left.isRed()) {
|
|
4634
|
+
n = n.rotateLeft();
|
|
4635
|
+
}
|
|
4636
|
+
if (n.left.isRed() && n.left.left.isRed()) {
|
|
4637
|
+
n = n.rotateRight();
|
|
4638
|
+
}
|
|
4639
|
+
if (n.left.isRed() && n.right.isRed()) {
|
|
4640
|
+
n = n.colorFlip();
|
|
4641
|
+
}
|
|
4642
|
+
return n;
|
|
4730
4643
|
}
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4644
|
+
moveRedLeft() {
|
|
4645
|
+
let n = this.colorFlip();
|
|
4646
|
+
if (n.right.left.isRed()) {
|
|
4647
|
+
n = n.copy(null, null, null, null, n.right.rotateRight());
|
|
4648
|
+
n = n.rotateLeft();
|
|
4649
|
+
n = n.colorFlip();
|
|
4650
|
+
}
|
|
4651
|
+
return n;
|
|
4737
4652
|
}
|
|
4738
|
-
|
|
4739
|
-
|
|
4653
|
+
moveRedRight() {
|
|
4654
|
+
let n = this.colorFlip();
|
|
4655
|
+
if (n.left.left.isRed()) {
|
|
4656
|
+
n = n.rotateRight();
|
|
4657
|
+
n = n.colorFlip();
|
|
4658
|
+
}
|
|
4659
|
+
return n;
|
|
4740
4660
|
}
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
constructor(field, value) {
|
|
4745
|
-
super(field, "not-in" /* NOT_IN */, value);
|
|
4746
|
-
this.keys = extractDocumentKeysFromArrayValue("not-in" /* NOT_IN */, value);
|
|
4661
|
+
rotateLeft() {
|
|
4662
|
+
const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
|
|
4663
|
+
return this.right.copy(null, null, this.color, nl, null);
|
|
4747
4664
|
}
|
|
4748
|
-
|
|
4749
|
-
|
|
4665
|
+
rotateRight() {
|
|
4666
|
+
const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
|
|
4667
|
+
return this.left.copy(null, null, this.color, null, nr);
|
|
4750
4668
|
}
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
return DocumentKey.fromName(v.referenceValue);
|
|
4756
|
-
});
|
|
4757
|
-
}
|
|
4758
|
-
/** A Filter that implements the array-contains operator. */
|
|
4759
|
-
class ArrayContainsFilter extends FieldFilter {
|
|
4760
|
-
constructor(field, value) {
|
|
4761
|
-
super(field, "array-contains" /* ARRAY_CONTAINS */, value);
|
|
4669
|
+
colorFlip() {
|
|
4670
|
+
const left = this.left.copy(null, null, !this.left.color, null, null);
|
|
4671
|
+
const right = this.right.copy(null, null, !this.right.color, null, null);
|
|
4672
|
+
return this.copy(null, null, !this.color, left, right);
|
|
4762
4673
|
}
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4674
|
+
// For testing.
|
|
4675
|
+
checkMaxDepth() {
|
|
4676
|
+
const blackDepth = this.check();
|
|
4677
|
+
if (Math.pow(2.0, blackDepth) <= this.size + 1) {
|
|
4678
|
+
return true;
|
|
4679
|
+
}
|
|
4680
|
+
else {
|
|
4681
|
+
return false;
|
|
4682
|
+
}
|
|
4766
4683
|
}
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4684
|
+
// In a balanced RB tree, the black-depth (number of black nodes) from root to
|
|
4685
|
+
// leaves is equal on both sides. This function verifies that or asserts.
|
|
4686
|
+
check() {
|
|
4687
|
+
if (this.isRed() && this.left.isRed()) {
|
|
4688
|
+
throw fail();
|
|
4689
|
+
}
|
|
4690
|
+
if (this.right.isRed()) {
|
|
4691
|
+
throw fail();
|
|
4692
|
+
}
|
|
4693
|
+
const blackDepth = this.left.check();
|
|
4694
|
+
if (blackDepth !== this.right.check()) {
|
|
4695
|
+
throw fail();
|
|
4696
|
+
}
|
|
4697
|
+
else {
|
|
4698
|
+
return blackDepth + (this.isRed() ? 0 : 1);
|
|
4699
|
+
}
|
|
4772
4700
|
}
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4701
|
+
} // end LLRBNode
|
|
4702
|
+
// Empty node is shared between all LLRB trees.
|
|
4703
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4704
|
+
LLRBNode.EMPTY = null;
|
|
4705
|
+
LLRBNode.RED = true;
|
|
4706
|
+
LLRBNode.BLACK = false;
|
|
4707
|
+
// Represents an empty node (a leaf node in the Red-Black Tree).
|
|
4708
|
+
class LLRBEmptyNode {
|
|
4709
|
+
constructor() {
|
|
4710
|
+
this.size = 0;
|
|
4776
4711
|
}
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
class NotInFilter extends FieldFilter {
|
|
4780
|
-
constructor(field, value) {
|
|
4781
|
-
super(field, "not-in" /* NOT_IN */, value);
|
|
4712
|
+
get key() {
|
|
4713
|
+
throw fail();
|
|
4782
4714
|
}
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
return false;
|
|
4786
|
-
}
|
|
4787
|
-
const other = doc.data.field(this.field);
|
|
4788
|
-
return other !== null && !arrayValueContains(this.value.arrayValue, other);
|
|
4715
|
+
get value() {
|
|
4716
|
+
throw fail();
|
|
4789
4717
|
}
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
class ArrayContainsAnyFilter extends FieldFilter {
|
|
4793
|
-
constructor(field, value) {
|
|
4794
|
-
super(field, "array-contains-any" /* ARRAY_CONTAINS_ANY */, value);
|
|
4718
|
+
get color() {
|
|
4719
|
+
throw fail();
|
|
4795
4720
|
}
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
if (!isArray(other) || !other.arrayValue.values) {
|
|
4799
|
-
return false;
|
|
4800
|
-
}
|
|
4801
|
-
return other.arrayValue.values.some(val => arrayValueContains(this.value.arrayValue, val));
|
|
4721
|
+
get left() {
|
|
4722
|
+
throw fail();
|
|
4802
4723
|
}
|
|
4803
|
-
|
|
4724
|
+
get right() {
|
|
4725
|
+
throw fail();
|
|
4726
|
+
}
|
|
4727
|
+
// Returns a copy of the current node.
|
|
4728
|
+
copy(key, value, color, left, right) {
|
|
4729
|
+
return this;
|
|
4730
|
+
}
|
|
4731
|
+
// Returns a copy of the tree, with the specified key/value added.
|
|
4732
|
+
insert(key, value, comparator) {
|
|
4733
|
+
return new LLRBNode(key, value);
|
|
4734
|
+
}
|
|
4735
|
+
// Returns a copy of the tree, with the specified key removed.
|
|
4736
|
+
remove(key, comparator) {
|
|
4737
|
+
return this;
|
|
4738
|
+
}
|
|
4739
|
+
isEmpty() {
|
|
4740
|
+
return true;
|
|
4741
|
+
}
|
|
4742
|
+
inorderTraversal(action) {
|
|
4743
|
+
return false;
|
|
4744
|
+
}
|
|
4745
|
+
reverseTraversal(action) {
|
|
4746
|
+
return false;
|
|
4747
|
+
}
|
|
4748
|
+
minKey() {
|
|
4749
|
+
return null;
|
|
4750
|
+
}
|
|
4751
|
+
maxKey() {
|
|
4752
|
+
return null;
|
|
4753
|
+
}
|
|
4754
|
+
isRed() {
|
|
4755
|
+
return false;
|
|
4756
|
+
}
|
|
4757
|
+
// For testing.
|
|
4758
|
+
checkMaxDepth() {
|
|
4759
|
+
return true;
|
|
4760
|
+
}
|
|
4761
|
+
check() {
|
|
4762
|
+
return 0;
|
|
4763
|
+
}
|
|
4764
|
+
} // end LLRBEmptyNode
|
|
4765
|
+
LLRBNode.EMPTY = new LLRBEmptyNode();
|
|
4766
|
+
|
|
4804
4767
|
/**
|
|
4805
|
-
*
|
|
4768
|
+
* @license
|
|
4769
|
+
* Copyright 2017 Google LLC
|
|
4806
4770
|
*
|
|
4807
|
-
*
|
|
4808
|
-
*
|
|
4809
|
-
*
|
|
4771
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4772
|
+
* you may not use this file except in compliance with the License.
|
|
4773
|
+
* You may obtain a copy of the License at
|
|
4810
4774
|
*
|
|
4811
|
-
*
|
|
4812
|
-
* of values for the (potentially implicit) order by clauses of a query.
|
|
4775
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
4813
4776
|
*
|
|
4814
|
-
*
|
|
4815
|
-
*
|
|
4816
|
-
*
|
|
4777
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
4778
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
4779
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
4780
|
+
* See the License for the specific language governing permissions and
|
|
4781
|
+
* limitations under the License.
|
|
4817
4782
|
*/
|
|
4818
|
-
class Bound {
|
|
4819
|
-
constructor(position, inclusive) {
|
|
4820
|
-
this.position = position;
|
|
4821
|
-
this.inclusive = inclusive;
|
|
4822
|
-
}
|
|
4823
|
-
}
|
|
4824
4783
|
/**
|
|
4825
|
-
*
|
|
4784
|
+
* SortedSet is an immutable (copy-on-write) collection that holds elements
|
|
4785
|
+
* in order specified by the provided comparator.
|
|
4786
|
+
*
|
|
4787
|
+
* NOTE: if provided comparator returns 0 for two elements, we consider them to
|
|
4788
|
+
* be equal!
|
|
4826
4789
|
*/
|
|
4827
|
-
class
|
|
4828
|
-
constructor(
|
|
4829
|
-
this.
|
|
4830
|
-
this.
|
|
4790
|
+
class SortedSet {
|
|
4791
|
+
constructor(comparator) {
|
|
4792
|
+
this.comparator = comparator;
|
|
4793
|
+
this.data = new SortedMap(this.comparator);
|
|
4831
4794
|
}
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4795
|
+
has(elem) {
|
|
4796
|
+
return this.data.get(elem) !== null;
|
|
4797
|
+
}
|
|
4798
|
+
first() {
|
|
4799
|
+
return this.data.minKey();
|
|
4800
|
+
}
|
|
4801
|
+
last() {
|
|
4802
|
+
return this.data.maxKey();
|
|
4803
|
+
}
|
|
4804
|
+
get size() {
|
|
4805
|
+
return this.data.size;
|
|
4806
|
+
}
|
|
4807
|
+
indexOf(elem) {
|
|
4808
|
+
return this.data.indexOf(elem);
|
|
4809
|
+
}
|
|
4810
|
+
/** Iterates elements in order defined by "comparator" */
|
|
4811
|
+
forEach(cb) {
|
|
4812
|
+
this.data.inorderTraversal((k, v) => {
|
|
4813
|
+
cb(k);
|
|
4814
|
+
return false;
|
|
4815
|
+
});
|
|
4816
|
+
}
|
|
4817
|
+
/** Iterates over `elem`s such that: range[0] <= elem < range[1]. */
|
|
4818
|
+
forEachInRange(range, cb) {
|
|
4819
|
+
const iter = this.data.getIteratorFrom(range[0]);
|
|
4820
|
+
while (iter.hasNext()) {
|
|
4821
|
+
const elem = iter.getNext();
|
|
4822
|
+
if (this.comparator(elem.key, range[1]) >= 0) {
|
|
4823
|
+
return;
|
|
4824
|
+
}
|
|
4825
|
+
cb(elem.key);
|
|
4850
4826
|
}
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4827
|
+
}
|
|
4828
|
+
/**
|
|
4829
|
+
* Iterates over `elem`s such that: start <= elem until false is returned.
|
|
4830
|
+
*/
|
|
4831
|
+
forEachWhile(cb, start) {
|
|
4832
|
+
let iter;
|
|
4833
|
+
if (start !== undefined) {
|
|
4834
|
+
iter = this.data.getIteratorFrom(start);
|
|
4854
4835
|
}
|
|
4855
|
-
|
|
4856
|
-
|
|
4836
|
+
else {
|
|
4837
|
+
iter = this.data.getIterator();
|
|
4857
4838
|
}
|
|
4858
|
-
|
|
4859
|
-
|
|
4839
|
+
while (iter.hasNext()) {
|
|
4840
|
+
const elem = iter.getNext();
|
|
4841
|
+
const result = cb(elem.key);
|
|
4842
|
+
if (!result) {
|
|
4843
|
+
return;
|
|
4844
|
+
}
|
|
4860
4845
|
}
|
|
4861
4846
|
}
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
* order.
|
|
4867
|
-
*/
|
|
4868
|
-
function boundSortsAfterDocument(bound, orderBy, doc) {
|
|
4869
|
-
const comparison = boundCompareToDocument(bound, orderBy, doc);
|
|
4870
|
-
return bound.inclusive ? comparison >= 0 : comparison > 0;
|
|
4871
|
-
}
|
|
4872
|
-
/**
|
|
4873
|
-
* Returns true if a document sorts before a bound using the provided sort
|
|
4874
|
-
* order.
|
|
4875
|
-
*/
|
|
4876
|
-
function boundSortsBeforeDocument(bound, orderBy, doc) {
|
|
4877
|
-
const comparison = boundCompareToDocument(bound, orderBy, doc);
|
|
4878
|
-
return bound.inclusive ? comparison <= 0 : comparison < 0;
|
|
4879
|
-
}
|
|
4880
|
-
function boundEquals(left, right) {
|
|
4881
|
-
if (left === null) {
|
|
4882
|
-
return right === null;
|
|
4847
|
+
/** Finds the least element greater than or equal to `elem`. */
|
|
4848
|
+
firstAfterOrEqual(elem) {
|
|
4849
|
+
const iter = this.data.getIteratorFrom(elem);
|
|
4850
|
+
return iter.hasNext() ? iter.getNext().key : null;
|
|
4883
4851
|
}
|
|
4884
|
-
|
|
4885
|
-
return
|
|
4852
|
+
getIterator() {
|
|
4853
|
+
return new SortedSetIterator(this.data.getIterator());
|
|
4886
4854
|
}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
return false;
|
|
4855
|
+
getIteratorFrom(key) {
|
|
4856
|
+
return new SortedSetIterator(this.data.getIteratorFrom(key));
|
|
4890
4857
|
}
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4858
|
+
/** Inserts or updates an element */
|
|
4859
|
+
add(elem) {
|
|
4860
|
+
return this.copy(this.data.remove(elem).insert(elem, true));
|
|
4861
|
+
}
|
|
4862
|
+
/** Deletes an element */
|
|
4863
|
+
delete(elem) {
|
|
4864
|
+
if (!this.has(elem)) {
|
|
4865
|
+
return this;
|
|
4866
|
+
}
|
|
4867
|
+
return this.copy(this.data.remove(elem));
|
|
4868
|
+
}
|
|
4869
|
+
isEmpty() {
|
|
4870
|
+
return this.data.isEmpty();
|
|
4871
|
+
}
|
|
4872
|
+
unionWith(other) {
|
|
4873
|
+
let result = this;
|
|
4874
|
+
// Make sure `result` always refers to the larger one of the two sets.
|
|
4875
|
+
if (result.size < other.size) {
|
|
4876
|
+
result = other;
|
|
4877
|
+
other = this;
|
|
4878
|
+
}
|
|
4879
|
+
other.forEach(elem => {
|
|
4880
|
+
result = result.add(elem);
|
|
4881
|
+
});
|
|
4882
|
+
return result;
|
|
4883
|
+
}
|
|
4884
|
+
isEqual(other) {
|
|
4885
|
+
if (!(other instanceof SortedSet)) {
|
|
4886
|
+
return false;
|
|
4887
|
+
}
|
|
4888
|
+
if (this.size !== other.size) {
|
|
4895
4889
|
return false;
|
|
4896
4890
|
}
|
|
4891
|
+
const thisIt = this.data.getIterator();
|
|
4892
|
+
const otherIt = other.data.getIterator();
|
|
4893
|
+
while (thisIt.hasNext()) {
|
|
4894
|
+
const thisElem = thisIt.getNext().key;
|
|
4895
|
+
const otherElem = otherIt.getNext().key;
|
|
4896
|
+
if (this.comparator(thisElem, otherElem) !== 0) {
|
|
4897
|
+
return false;
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
return true;
|
|
4897
4901
|
}
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
* See the License for the specific language governing permissions and
|
|
4915
|
-
* limitations under the License.
|
|
4916
|
-
*/
|
|
4917
|
-
/**
|
|
4918
|
-
* Query encapsulates all the query attributes we support in the SDK. It can
|
|
4919
|
-
* be run against the LocalStore, as well as be converted to a `Target` to
|
|
4920
|
-
* query the RemoteStore results.
|
|
4921
|
-
*
|
|
4922
|
-
* Visible for testing.
|
|
4923
|
-
*/
|
|
4924
|
-
class QueryImpl {
|
|
4925
|
-
/**
|
|
4926
|
-
* Initializes a Query with a path and optional additional query constraints.
|
|
4927
|
-
* Path must currently be empty if this is a collection group query.
|
|
4928
|
-
*/
|
|
4929
|
-
constructor(path, collectionGroup = null, explicitOrderBy = [], filters = [], limit = null, limitType = "F" /* First */, startAt = null, endAt = null) {
|
|
4930
|
-
this.path = path;
|
|
4931
|
-
this.collectionGroup = collectionGroup;
|
|
4932
|
-
this.explicitOrderBy = explicitOrderBy;
|
|
4933
|
-
this.filters = filters;
|
|
4934
|
-
this.limit = limit;
|
|
4935
|
-
this.limitType = limitType;
|
|
4936
|
-
this.startAt = startAt;
|
|
4937
|
-
this.endAt = endAt;
|
|
4938
|
-
this.memoizedOrderBy = null;
|
|
4939
|
-
// The corresponding `Target` of this `Query` instance.
|
|
4940
|
-
this.memoizedTarget = null;
|
|
4941
|
-
if (this.startAt) ;
|
|
4942
|
-
if (this.endAt) ;
|
|
4902
|
+
toArray() {
|
|
4903
|
+
const res = [];
|
|
4904
|
+
this.forEach(targetId => {
|
|
4905
|
+
res.push(targetId);
|
|
4906
|
+
});
|
|
4907
|
+
return res;
|
|
4908
|
+
}
|
|
4909
|
+
toString() {
|
|
4910
|
+
const result = [];
|
|
4911
|
+
this.forEach(elem => result.push(elem));
|
|
4912
|
+
return 'SortedSet(' + result.toString() + ')';
|
|
4913
|
+
}
|
|
4914
|
+
copy(data) {
|
|
4915
|
+
const result = new SortedSet(this.comparator);
|
|
4916
|
+
result.data = data;
|
|
4917
|
+
return result;
|
|
4943
4918
|
}
|
|
4944
4919
|
}
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
}
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4920
|
+
class SortedSetIterator {
|
|
4921
|
+
constructor(iter) {
|
|
4922
|
+
this.iter = iter;
|
|
4923
|
+
}
|
|
4924
|
+
getNext() {
|
|
4925
|
+
return this.iter.getNext().key;
|
|
4926
|
+
}
|
|
4927
|
+
hasNext() {
|
|
4928
|
+
return this.iter.hasNext();
|
|
4929
|
+
}
|
|
4952
4930
|
}
|
|
4953
4931
|
/**
|
|
4954
|
-
*
|
|
4955
|
-
*
|
|
4956
|
-
*
|
|
4957
|
-
*
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
*
|
|
4965
|
-
*
|
|
4932
|
+
* Compares two sorted sets for equality using their natural ordering. The
|
|
4933
|
+
* method computes the intersection and invokes `onAdd` for every element that
|
|
4934
|
+
* is in `after` but not `before`. `onRemove` is invoked for every element in
|
|
4935
|
+
* `before` but missing from `after`.
|
|
4936
|
+
*
|
|
4937
|
+
* The method creates a copy of both `before` and `after` and runs in O(n log
|
|
4938
|
+
* n), where n is the size of the two lists.
|
|
4939
|
+
*
|
|
4940
|
+
* @param before - The elements that exist in the original set.
|
|
4941
|
+
* @param after - The elements to diff against the original set.
|
|
4942
|
+
* @param comparator - The comparator for the elements in before and after.
|
|
4943
|
+
* @param onAdd - A function to invoke for every element that is part of `
|
|
4944
|
+
* after` but not `before`.
|
|
4945
|
+
* @param onRemove - A function to invoke for every element that is part of
|
|
4946
|
+
* `before` but not `after`.
|
|
4966
4947
|
*/
|
|
4967
|
-
function
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
if (filter.isInequality()) {
|
|
4990
|
-
return filter.field;
|
|
4948
|
+
function diffSortedSets(before, after, comparator, onAdd, onRemove) {
|
|
4949
|
+
const beforeIt = before.getIterator();
|
|
4950
|
+
const afterIt = after.getIterator();
|
|
4951
|
+
let beforeValue = advanceIterator(beforeIt);
|
|
4952
|
+
let afterValue = advanceIterator(afterIt);
|
|
4953
|
+
// Walk through the two sets at the same time, using the ordering defined by
|
|
4954
|
+
// `comparator`.
|
|
4955
|
+
while (beforeValue || afterValue) {
|
|
4956
|
+
let added = false;
|
|
4957
|
+
let removed = false;
|
|
4958
|
+
if (beforeValue && afterValue) {
|
|
4959
|
+
const cmp = comparator(beforeValue, afterValue);
|
|
4960
|
+
if (cmp < 0) {
|
|
4961
|
+
// The element was removed if the next element in our ordered
|
|
4962
|
+
// walkthrough is only in `before`.
|
|
4963
|
+
removed = true;
|
|
4964
|
+
}
|
|
4965
|
+
else if (cmp > 0) {
|
|
4966
|
+
// The element was added if the next element in our ordered walkthrough
|
|
4967
|
+
// is only in `after`.
|
|
4968
|
+
added = true;
|
|
4969
|
+
}
|
|
4991
4970
|
}
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
}
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
4971
|
+
else if (beforeValue != null) {
|
|
4972
|
+
removed = true;
|
|
4973
|
+
}
|
|
4974
|
+
else {
|
|
4975
|
+
added = true;
|
|
4976
|
+
}
|
|
4977
|
+
if (added) {
|
|
4978
|
+
onAdd(afterValue);
|
|
4979
|
+
afterValue = advanceIterator(afterIt);
|
|
4980
|
+
}
|
|
4981
|
+
else if (removed) {
|
|
4982
|
+
onRemove(beforeValue);
|
|
4983
|
+
beforeValue = advanceIterator(beforeIt);
|
|
4984
|
+
}
|
|
4985
|
+
else {
|
|
4986
|
+
beforeValue = advanceIterator(beforeIt);
|
|
4987
|
+
afterValue = advanceIterator(afterIt);
|
|
5003
4988
|
}
|
|
5004
4989
|
}
|
|
5005
|
-
return null;
|
|
5006
4990
|
}
|
|
5007
4991
|
/**
|
|
5008
|
-
*
|
|
5009
|
-
* within the provided collection group.
|
|
4992
|
+
* Returns the next element from the iterator or `undefined` if none available.
|
|
5010
4993
|
*/
|
|
5011
|
-
function
|
|
5012
|
-
return
|
|
5013
|
-
}
|
|
4994
|
+
function advanceIterator(it) {
|
|
4995
|
+
return it.hasNext() ? it.getNext() : undefined;
|
|
4996
|
+
}
|
|
4997
|
+
|
|
5014
4998
|
/**
|
|
5015
|
-
*
|
|
5016
|
-
*
|
|
4999
|
+
* @license
|
|
5000
|
+
* Copyright 2019 Google LLC
|
|
5001
|
+
*
|
|
5002
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5003
|
+
* you may not use this file except in compliance with the License.
|
|
5004
|
+
* You may obtain a copy of the License at
|
|
5005
|
+
*
|
|
5006
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
5007
|
+
*
|
|
5008
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
5009
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
5010
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
5011
|
+
* See the License for the specific language governing permissions and
|
|
5012
|
+
* limitations under the License.
|
|
5017
5013
|
*/
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5014
|
+
// Visible for testing
|
|
5015
|
+
class TargetImpl {
|
|
5016
|
+
constructor(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
|
|
5017
|
+
this.path = path;
|
|
5018
|
+
this.collectionGroup = collectionGroup;
|
|
5019
|
+
this.orderBy = orderBy;
|
|
5020
|
+
this.filters = filters;
|
|
5021
|
+
this.limit = limit;
|
|
5022
|
+
this.startAt = startAt;
|
|
5023
|
+
this.endAt = endAt;
|
|
5024
|
+
this.memoizedCanonicalId = null;
|
|
5025
|
+
}
|
|
5022
5026
|
}
|
|
5023
5027
|
/**
|
|
5024
|
-
*
|
|
5025
|
-
* collection.
|
|
5028
|
+
* Initializes a Target with a path and optional additional query constraints.
|
|
5029
|
+
* Path must currently be empty if this is a collection group query.
|
|
5030
|
+
*
|
|
5031
|
+
* NOTE: you should always construct `Target` from `Query.toTarget` instead of
|
|
5032
|
+
* using this factory method, because `Query` provides an implicit `orderBy`
|
|
5033
|
+
* property.
|
|
5026
5034
|
*/
|
|
5027
|
-
function
|
|
5028
|
-
return
|
|
5035
|
+
function newTarget(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
|
|
5036
|
+
return new TargetImpl(path, collectionGroup, orderBy, filters, limit, startAt, endAt);
|
|
5029
5037
|
}
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
const queryImpl = debugCast(query);
|
|
5037
|
-
if (queryImpl.memoizedOrderBy === null) {
|
|
5038
|
-
queryImpl.memoizedOrderBy = [];
|
|
5039
|
-
const inequalityField = getInequalityFilterField(queryImpl);
|
|
5040
|
-
const firstOrderByField = getFirstOrderByField(queryImpl);
|
|
5041
|
-
if (inequalityField !== null && firstOrderByField === null) {
|
|
5042
|
-
// In order to implicitly add key ordering, we must also add the
|
|
5043
|
-
// inequality filter field for it to be a valid query.
|
|
5044
|
-
// Note that the default inequality field and key ordering is ascending.
|
|
5045
|
-
if (!inequalityField.isKeyField()) {
|
|
5046
|
-
queryImpl.memoizedOrderBy.push(new OrderBy(inequalityField));
|
|
5047
|
-
}
|
|
5048
|
-
queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), "asc" /* ASCENDING */));
|
|
5038
|
+
function canonifyTarget(target) {
|
|
5039
|
+
const targetImpl = debugCast(target);
|
|
5040
|
+
if (targetImpl.memoizedCanonicalId === null) {
|
|
5041
|
+
let str = targetImpl.path.canonicalString();
|
|
5042
|
+
if (targetImpl.collectionGroup !== null) {
|
|
5043
|
+
str += '|cg:' + targetImpl.collectionGroup;
|
|
5049
5044
|
}
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
}
|
|
5058
|
-
if (!foundKeyOrdering) {
|
|
5059
|
-
// The order of the implicit key ordering always matches the last
|
|
5060
|
-
// explicit order by
|
|
5061
|
-
const lastDirection = queryImpl.explicitOrderBy.length > 0
|
|
5062
|
-
? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
|
|
5063
|
-
.dir
|
|
5064
|
-
: "asc" /* ASCENDING */;
|
|
5065
|
-
queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), lastDirection));
|
|
5066
|
-
}
|
|
5045
|
+
str += '|f:';
|
|
5046
|
+
str += targetImpl.filters.map(f => canonifyFilter(f)).join(',');
|
|
5047
|
+
str += '|ob:';
|
|
5048
|
+
str += targetImpl.orderBy.map(o => canonifyOrderBy(o)).join(',');
|
|
5049
|
+
if (!isNullOrUndefined(targetImpl.limit)) {
|
|
5050
|
+
str += '|l:';
|
|
5051
|
+
str += targetImpl.limit;
|
|
5067
5052
|
}
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
* Converts this `Query` instance to it's corresponding `Target` representation.
|
|
5073
|
-
*/
|
|
5074
|
-
function queryToTarget(query) {
|
|
5075
|
-
const queryImpl = debugCast(query);
|
|
5076
|
-
if (!queryImpl.memoizedTarget) {
|
|
5077
|
-
if (queryImpl.limitType === "F" /* First */) {
|
|
5078
|
-
queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, queryOrderBy(queryImpl), queryImpl.filters, queryImpl.limit, queryImpl.startAt, queryImpl.endAt);
|
|
5053
|
+
if (targetImpl.startAt) {
|
|
5054
|
+
str += '|lb:';
|
|
5055
|
+
str += targetImpl.startAt.inclusive ? 'b:' : 'a:';
|
|
5056
|
+
str += targetImpl.startAt.position.map(p => canonicalId(p)).join(',');
|
|
5079
5057
|
}
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
const dir = orderBy.dir === "desc" /* DESCENDING */
|
|
5085
|
-
? "asc" /* ASCENDING */
|
|
5086
|
-
: "desc" /* DESCENDING */;
|
|
5087
|
-
orderBys.push(new OrderBy(orderBy.field, dir));
|
|
5088
|
-
}
|
|
5089
|
-
// We need to swap the cursors to match the now-flipped query ordering.
|
|
5090
|
-
const startAt = queryImpl.endAt
|
|
5091
|
-
? new Bound(queryImpl.endAt.position, !queryImpl.endAt.inclusive)
|
|
5092
|
-
: null;
|
|
5093
|
-
const endAt = queryImpl.startAt
|
|
5094
|
-
? new Bound(queryImpl.startAt.position, !queryImpl.startAt.inclusive)
|
|
5095
|
-
: null;
|
|
5096
|
-
// Now return as a LimitType.First query.
|
|
5097
|
-
queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, orderBys, queryImpl.filters, queryImpl.limit, startAt, endAt);
|
|
5058
|
+
if (targetImpl.endAt) {
|
|
5059
|
+
str += '|ub:';
|
|
5060
|
+
str += targetImpl.endAt.inclusive ? 'a:' : 'b:';
|
|
5061
|
+
str += targetImpl.endAt.position.map(p => canonicalId(p)).join(',');
|
|
5098
5062
|
}
|
|
5063
|
+
targetImpl.memoizedCanonicalId = str;
|
|
5099
5064
|
}
|
|
5100
|
-
return
|
|
5101
|
-
}
|
|
5102
|
-
function queryWithAddedFilter(query, filter) {
|
|
5103
|
-
const newFilters = query.filters.concat([filter]);
|
|
5104
|
-
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), newFilters, query.limit, query.limitType, query.startAt, query.endAt);
|
|
5105
|
-
}
|
|
5106
|
-
function queryWithAddedOrderBy(query, orderBy) {
|
|
5107
|
-
// TODO(dimond): validate that orderBy does not list the same key twice.
|
|
5108
|
-
const newOrderBy = query.explicitOrderBy.concat([orderBy]);
|
|
5109
|
-
return new QueryImpl(query.path, query.collectionGroup, newOrderBy, query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
|
|
5110
|
-
}
|
|
5111
|
-
function queryWithLimit(query, limit, limitType) {
|
|
5112
|
-
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), limit, limitType, query.startAt, query.endAt);
|
|
5113
|
-
}
|
|
5114
|
-
function queryWithStartAt(query, bound) {
|
|
5115
|
-
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, bound, query.endAt);
|
|
5116
|
-
}
|
|
5117
|
-
function queryWithEndAt(query, bound) {
|
|
5118
|
-
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, bound);
|
|
5065
|
+
return targetImpl.memoizedCanonicalId;
|
|
5119
5066
|
}
|
|
5120
|
-
function
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
// TODO(b/29183165): This is used to get a unique string from a query to, for
|
|
5125
|
-
// example, use as a dictionary key, but the implementation is subject to
|
|
5126
|
-
// collisions. Make it collision-free.
|
|
5127
|
-
function canonifyQuery(query) {
|
|
5128
|
-
return `${canonifyTarget(queryToTarget(query))}|lt:${query.limitType}`;
|
|
5129
|
-
}
|
|
5130
|
-
function stringifyQuery(query) {
|
|
5131
|
-
return `Query(target=${stringifyTarget(queryToTarget(query))}; limitType=${query.limitType})`;
|
|
5132
|
-
}
|
|
5133
|
-
/** Returns whether `doc` matches the constraints of `query`. */
|
|
5134
|
-
function queryMatches(query, doc) {
|
|
5135
|
-
return (doc.isFoundDocument() &&
|
|
5136
|
-
queryMatchesPathAndCollectionGroup(query, doc) &&
|
|
5137
|
-
queryMatchesOrderBy(query, doc) &&
|
|
5138
|
-
queryMatchesFilters(query, doc) &&
|
|
5139
|
-
queryMatchesBounds(query, doc));
|
|
5140
|
-
}
|
|
5141
|
-
function queryMatchesPathAndCollectionGroup(query, doc) {
|
|
5142
|
-
const docPath = doc.key.path;
|
|
5143
|
-
if (query.collectionGroup !== null) {
|
|
5144
|
-
// NOTE: this.path is currently always empty since we don't expose Collection
|
|
5145
|
-
// Group queries rooted at a document path yet.
|
|
5146
|
-
return (doc.key.hasCollectionId(query.collectionGroup) &&
|
|
5147
|
-
query.path.isPrefixOf(docPath));
|
|
5067
|
+
function stringifyTarget(target) {
|
|
5068
|
+
let str = target.path.canonicalString();
|
|
5069
|
+
if (target.collectionGroup !== null) {
|
|
5070
|
+
str += ' collectionGroup=' + target.collectionGroup;
|
|
5148
5071
|
}
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5072
|
+
if (target.filters.length > 0) {
|
|
5073
|
+
str += `, filters: [${target.filters
|
|
5074
|
+
.map(f => stringifyFilter(f))
|
|
5075
|
+
.join(', ')}]`;
|
|
5152
5076
|
}
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5077
|
+
if (!isNullOrUndefined(target.limit)) {
|
|
5078
|
+
str += ', limit: ' + target.limit;
|
|
5079
|
+
}
|
|
5080
|
+
if (target.orderBy.length > 0) {
|
|
5081
|
+
str += `, orderBy: [${target.orderBy
|
|
5082
|
+
.map(o => stringifyOrderBy(o))
|
|
5083
|
+
.join(', ')}]`;
|
|
5084
|
+
}
|
|
5085
|
+
if (target.startAt) {
|
|
5086
|
+
str += ', startAt: ';
|
|
5087
|
+
str += target.startAt.inclusive ? 'b:' : 'a:';
|
|
5088
|
+
str += target.startAt.position.map(p => canonicalId(p)).join(',');
|
|
5089
|
+
}
|
|
5090
|
+
if (target.endAt) {
|
|
5091
|
+
str += ', endAt: ';
|
|
5092
|
+
str += target.endAt.inclusive ? 'a:' : 'b:';
|
|
5093
|
+
str += target.endAt.position.map(p => canonicalId(p)).join(',');
|
|
5156
5094
|
}
|
|
5095
|
+
return `Target(${str})`;
|
|
5157
5096
|
}
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5097
|
+
function targetEquals(left, right) {
|
|
5098
|
+
if (left.limit !== right.limit) {
|
|
5099
|
+
return false;
|
|
5100
|
+
}
|
|
5101
|
+
if (left.orderBy.length !== right.orderBy.length) {
|
|
5102
|
+
return false;
|
|
5103
|
+
}
|
|
5104
|
+
for (let i = 0; i < left.orderBy.length; i++) {
|
|
5105
|
+
if (!orderByEquals(left.orderBy[i], right.orderBy[i])) {
|
|
5166
5106
|
return false;
|
|
5167
5107
|
}
|
|
5168
5108
|
}
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
for (
|
|
5173
|
-
if (!
|
|
5109
|
+
if (left.filters.length !== right.filters.length) {
|
|
5110
|
+
return false;
|
|
5111
|
+
}
|
|
5112
|
+
for (let i = 0; i < left.filters.length; i++) {
|
|
5113
|
+
if (!filterEquals(left.filters[i], right.filters[i])) {
|
|
5174
5114
|
return false;
|
|
5175
5115
|
}
|
|
5176
5116
|
}
|
|
5177
|
-
|
|
5178
|
-
}
|
|
5179
|
-
/** Makes sure a document is within the bounds, if provided. */
|
|
5180
|
-
function queryMatchesBounds(query, doc) {
|
|
5181
|
-
if (query.startAt &&
|
|
5182
|
-
!boundSortsBeforeDocument(query.startAt, queryOrderBy(query), doc)) {
|
|
5117
|
+
if (left.collectionGroup !== right.collectionGroup) {
|
|
5183
5118
|
return false;
|
|
5184
5119
|
}
|
|
5185
|
-
if (
|
|
5186
|
-
!boundSortsAfterDocument(query.endAt, queryOrderBy(query), doc)) {
|
|
5120
|
+
if (!left.path.isEqual(right.path)) {
|
|
5187
5121
|
return false;
|
|
5188
5122
|
}
|
|
5189
|
-
|
|
5123
|
+
if (!boundEquals(left.startAt, right.startAt)) {
|
|
5124
|
+
return false;
|
|
5125
|
+
}
|
|
5126
|
+
return boundEquals(left.endAt, right.endAt);
|
|
5127
|
+
}
|
|
5128
|
+
function targetIsDocumentTarget(target) {
|
|
5129
|
+
return (DocumentKey.isDocumentKey(target.path) &&
|
|
5130
|
+
target.collectionGroup === null &&
|
|
5131
|
+
target.filters.length === 0);
|
|
5132
|
+
}
|
|
5133
|
+
/** Returns the field filters that target the given field path. */
|
|
5134
|
+
function targetGetFieldFiltersForPath(target, path) {
|
|
5135
|
+
return target.filters.filter(f => f instanceof FieldFilter && f.field.isEqual(path));
|
|
5190
5136
|
}
|
|
5191
5137
|
/**
|
|
5192
|
-
* Returns the
|
|
5193
|
-
*
|
|
5194
|
-
* PORTING NOTE: This is only used in the Web SDK to facilitate multi-tab
|
|
5195
|
-
* synchronization for query results.
|
|
5138
|
+
* Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY
|
|
5139
|
+
* filters. Returns `null` if there are no such filters.
|
|
5196
5140
|
*/
|
|
5197
|
-
function
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5141
|
+
function targetGetArrayValues(target, fieldIndex) {
|
|
5142
|
+
const segment = fieldIndexGetArraySegment(fieldIndex);
|
|
5143
|
+
if (segment === undefined) {
|
|
5144
|
+
return null;
|
|
5145
|
+
}
|
|
5146
|
+
for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
|
|
5147
|
+
switch (fieldFilter.op) {
|
|
5148
|
+
case "array-contains-any" /* ARRAY_CONTAINS_ANY */:
|
|
5149
|
+
return fieldFilter.value.arrayValue.values || [];
|
|
5150
|
+
case "array-contains" /* ARRAY_CONTAINS */:
|
|
5151
|
+
return [fieldFilter.value];
|
|
5152
|
+
// Remaining filters are not array filters.
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
return null;
|
|
5202
5156
|
}
|
|
5203
5157
|
/**
|
|
5204
|
-
* Returns
|
|
5205
|
-
*
|
|
5158
|
+
* Returns the list of values that are used in != or NOT_IN filters. Returns
|
|
5159
|
+
* `null` if there are no such filters.
|
|
5206
5160
|
*/
|
|
5207
|
-
function
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
for (const
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5161
|
+
function targetGetNotInValues(target, fieldIndex) {
|
|
5162
|
+
const values = new Map();
|
|
5163
|
+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
5164
|
+
for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
|
|
5165
|
+
switch (fieldFilter.op) {
|
|
5166
|
+
case "==" /* EQUAL */:
|
|
5167
|
+
case "in" /* IN */:
|
|
5168
|
+
// Encode equality prefix, which is encoded in the index value before
|
|
5169
|
+
// the inequality (e.g. `a == 'a' && b != 'b'` is encoded to
|
|
5170
|
+
// `value != 'ab'`).
|
|
5171
|
+
values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
|
|
5172
|
+
break;
|
|
5173
|
+
case "not-in" /* NOT_IN */:
|
|
5174
|
+
case "!=" /* NOT_EQUAL */:
|
|
5175
|
+
// NotIn/NotEqual is always a suffix. There cannot be any remaining
|
|
5176
|
+
// segments and hence we can return early here.
|
|
5177
|
+
values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
|
|
5178
|
+
return Array.from(values.values());
|
|
5179
|
+
// Remaining filters cannot be used as notIn bounds.
|
|
5214
5180
|
}
|
|
5215
|
-
comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
|
|
5216
5181
|
}
|
|
5217
|
-
return 0;
|
|
5218
|
-
};
|
|
5219
|
-
}
|
|
5220
|
-
function compareDocs(orderBy, d1, d2) {
|
|
5221
|
-
const comparison = orderBy.field.isKeyField()
|
|
5222
|
-
? DocumentKey.comparator(d1.key, d2.key)
|
|
5223
|
-
: compareDocumentsByField(orderBy.field, d1, d2);
|
|
5224
|
-
switch (orderBy.dir) {
|
|
5225
|
-
case "asc" /* ASCENDING */:
|
|
5226
|
-
return comparison;
|
|
5227
|
-
case "desc" /* DESCENDING */:
|
|
5228
|
-
return -1 * comparison;
|
|
5229
|
-
default:
|
|
5230
|
-
return fail();
|
|
5231
5182
|
}
|
|
5232
|
-
|
|
5233
|
-
|
|
5183
|
+
return null;
|
|
5184
|
+
}
|
|
5234
5185
|
/**
|
|
5235
|
-
*
|
|
5236
|
-
*
|
|
5237
|
-
*
|
|
5238
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5239
|
-
* you may not use this file except in compliance with the License.
|
|
5240
|
-
* You may obtain a copy of the License at
|
|
5241
|
-
*
|
|
5242
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
5243
|
-
*
|
|
5244
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
5245
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
5246
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
5247
|
-
* See the License for the specific language governing permissions and
|
|
5248
|
-
* limitations under the License.
|
|
5186
|
+
* Returns a lower bound of field values that can be used as a starting point to
|
|
5187
|
+
* scan the index defined by `fieldIndex`. Returns `null` if no lower bound
|
|
5188
|
+
* exists.
|
|
5249
5189
|
*/
|
|
5190
|
+
function targetGetLowerBound(target, fieldIndex) {
|
|
5191
|
+
const values = [];
|
|
5192
|
+
let inclusive = true;
|
|
5193
|
+
// For each segment, retrieve a lower bound if there is a suitable filter or
|
|
5194
|
+
// startAt.
|
|
5195
|
+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
5196
|
+
const segmentBound = segment.kind === 0 /* ASCENDING */
|
|
5197
|
+
? targetGetAscendingBound(target, segment.fieldPath, target.startAt)
|
|
5198
|
+
: targetGetDescendingBound(target, segment.fieldPath, target.startAt);
|
|
5199
|
+
if (!segmentBound.value) {
|
|
5200
|
+
// No lower bound exists
|
|
5201
|
+
return null;
|
|
5202
|
+
}
|
|
5203
|
+
values.push(segmentBound.value);
|
|
5204
|
+
inclusive && (inclusive = segmentBound.inclusive);
|
|
5205
|
+
}
|
|
5206
|
+
return new Bound(values, inclusive);
|
|
5207
|
+
}
|
|
5250
5208
|
/**
|
|
5251
|
-
*
|
|
5252
|
-
*
|
|
5253
|
-
*
|
|
5254
|
-
* automatically handles collisions of keys.
|
|
5209
|
+
* Returns an upper bound of field values that can be used as an ending point
|
|
5210
|
+
* when scanning the index defined by `fieldIndex`. Returns `null` if no
|
|
5211
|
+
* upper bound exists.
|
|
5255
5212
|
*/
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
this.innerSize = 0;
|
|
5269
|
-
}
|
|
5270
|
-
/** Get a value for this key, or undefined if it does not exist. */
|
|
5271
|
-
get(key) {
|
|
5272
|
-
const id = this.mapKeyFn(key);
|
|
5273
|
-
const matches = this.inner[id];
|
|
5274
|
-
if (matches === undefined) {
|
|
5275
|
-
return undefined;
|
|
5276
|
-
}
|
|
5277
|
-
for (const [otherKey, value] of matches) {
|
|
5278
|
-
if (this.equalsFn(otherKey, key)) {
|
|
5279
|
-
return value;
|
|
5280
|
-
}
|
|
5213
|
+
function targetGetUpperBound(target, fieldIndex) {
|
|
5214
|
+
const values = [];
|
|
5215
|
+
let inclusive = true;
|
|
5216
|
+
// For each segment, retrieve an upper bound if there is a suitable filter or
|
|
5217
|
+
// endAt.
|
|
5218
|
+
for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
|
|
5219
|
+
const segmentBound = segment.kind === 0 /* ASCENDING */
|
|
5220
|
+
? targetGetDescendingBound(target, segment.fieldPath, target.endAt)
|
|
5221
|
+
: targetGetAscendingBound(target, segment.fieldPath, target.endAt);
|
|
5222
|
+
if (!segmentBound.value) {
|
|
5223
|
+
// No upper bound exists
|
|
5224
|
+
return null;
|
|
5281
5225
|
}
|
|
5282
|
-
|
|
5226
|
+
values.push(segmentBound.value);
|
|
5227
|
+
inclusive && (inclusive = segmentBound.inclusive);
|
|
5283
5228
|
}
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5229
|
+
return new Bound(values, inclusive);
|
|
5230
|
+
}
|
|
5231
|
+
/**
|
|
5232
|
+
* Returns the value to use as the lower bound for ascending index segment at
|
|
5233
|
+
* the provided `fieldPath` (or the upper bound for an descending segment).
|
|
5234
|
+
*/
|
|
5235
|
+
function targetGetAscendingBound(target, fieldPath, bound) {
|
|
5236
|
+
let value = undefined;
|
|
5237
|
+
let inclusive = true;
|
|
5238
|
+
// Process all filters to find a value for the current field segment
|
|
5239
|
+
for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
|
|
5240
|
+
let filterValue = undefined;
|
|
5241
|
+
let filterInclusive = true;
|
|
5242
|
+
switch (fieldFilter.op) {
|
|
5243
|
+
case "<" /* LESS_THAN */:
|
|
5244
|
+
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
5245
|
+
filterValue = valuesGetLowerBound(fieldFilter.value);
|
|
5246
|
+
break;
|
|
5247
|
+
case "==" /* EQUAL */:
|
|
5248
|
+
case "in" /* IN */:
|
|
5249
|
+
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
5250
|
+
filterValue = fieldFilter.value;
|
|
5251
|
+
break;
|
|
5252
|
+
case ">" /* GREATER_THAN */:
|
|
5253
|
+
filterValue = fieldFilter.value;
|
|
5254
|
+
filterInclusive = false;
|
|
5255
|
+
break;
|
|
5256
|
+
case "!=" /* NOT_EQUAL */:
|
|
5257
|
+
case "not-in" /* NOT_IN */:
|
|
5258
|
+
filterValue = MIN_VALUE;
|
|
5259
|
+
break;
|
|
5260
|
+
// Remaining filters cannot be used as lower bounds.
|
|
5261
|
+
}
|
|
5262
|
+
if (valuesMax(value, filterValue) === filterValue) {
|
|
5263
|
+
value = filterValue;
|
|
5264
|
+
inclusive = filterInclusive;
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
// If there is an additional bound, compare the values against the existing
|
|
5268
|
+
// range to see if we can narrow the scope.
|
|
5269
|
+
if (bound !== null) {
|
|
5270
|
+
for (let i = 0; i < target.orderBy.length; ++i) {
|
|
5271
|
+
const orderBy = target.orderBy[i];
|
|
5272
|
+
if (orderBy.field.isEqual(fieldPath)) {
|
|
5273
|
+
const cursorValue = bound.position[i];
|
|
5274
|
+
if (valuesMax(value, cursorValue) === cursorValue) {
|
|
5275
|
+
value = cursorValue;
|
|
5276
|
+
inclusive = bound.inclusive;
|
|
5322
5277
|
}
|
|
5323
|
-
|
|
5324
|
-
return true;
|
|
5278
|
+
break;
|
|
5325
5279
|
}
|
|
5326
5280
|
}
|
|
5327
|
-
return false;
|
|
5328
|
-
}
|
|
5329
|
-
forEach(fn) {
|
|
5330
|
-
forEach(this.inner, (_, entries) => {
|
|
5331
|
-
for (const [k, v] of entries) {
|
|
5332
|
-
fn(k, v);
|
|
5333
|
-
}
|
|
5334
|
-
});
|
|
5335
|
-
}
|
|
5336
|
-
isEmpty() {
|
|
5337
|
-
return isEmpty(this.inner);
|
|
5338
5281
|
}
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
}
|
|
5342
|
-
}
|
|
5343
|
-
|
|
5282
|
+
return { value, inclusive };
|
|
5283
|
+
}
|
|
5344
5284
|
/**
|
|
5345
|
-
*
|
|
5346
|
-
*
|
|
5347
|
-
*
|
|
5348
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5349
|
-
* you may not use this file except in compliance with the License.
|
|
5350
|
-
* You may obtain a copy of the License at
|
|
5351
|
-
*
|
|
5352
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
5353
|
-
*
|
|
5354
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
5355
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
5356
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
5357
|
-
* See the License for the specific language governing permissions and
|
|
5358
|
-
* limitations under the License.
|
|
5285
|
+
* Returns the value to use as the upper bound for ascending index segment at
|
|
5286
|
+
* the provided `fieldPath` (or the lower bound for a descending segment).
|
|
5359
5287
|
*/
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5288
|
+
function targetGetDescendingBound(target, fieldPath, bound) {
|
|
5289
|
+
let value = undefined;
|
|
5290
|
+
let inclusive = true;
|
|
5291
|
+
// Process all filters to find a value for the current field segment
|
|
5292
|
+
for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
|
|
5293
|
+
let filterValue = undefined;
|
|
5294
|
+
let filterInclusive = true;
|
|
5295
|
+
switch (fieldFilter.op) {
|
|
5296
|
+
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
5297
|
+
case ">" /* GREATER_THAN */:
|
|
5298
|
+
filterValue = valuesGetUpperBound(fieldFilter.value);
|
|
5299
|
+
filterInclusive = false;
|
|
5300
|
+
break;
|
|
5301
|
+
case "==" /* EQUAL */:
|
|
5302
|
+
case "in" /* IN */:
|
|
5303
|
+
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
5304
|
+
filterValue = fieldFilter.value;
|
|
5305
|
+
break;
|
|
5306
|
+
case "<" /* LESS_THAN */:
|
|
5307
|
+
filterValue = fieldFilter.value;
|
|
5308
|
+
filterInclusive = false;
|
|
5309
|
+
break;
|
|
5310
|
+
case "!=" /* NOT_EQUAL */:
|
|
5311
|
+
case "not-in" /* NOT_IN */:
|
|
5312
|
+
filterValue = MAX_VALUE;
|
|
5313
|
+
break;
|
|
5314
|
+
// Remaining filters cannot be used as upper bounds.
|
|
5315
|
+
}
|
|
5316
|
+
if (valuesMin(value, filterValue) === filterValue) {
|
|
5317
|
+
value = filterValue;
|
|
5318
|
+
inclusive = filterInclusive;
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
// If there is an additional bound, compare the values against the existing
|
|
5322
|
+
// range to see if we can narrow the scope.
|
|
5323
|
+
if (bound !== null) {
|
|
5324
|
+
for (let i = 0; i < target.orderBy.length; ++i) {
|
|
5325
|
+
const orderBy = target.orderBy[i];
|
|
5326
|
+
if (orderBy.field.isEqual(fieldPath)) {
|
|
5327
|
+
const cursorValue = bound.position[i];
|
|
5328
|
+
if (valuesMin(value, cursorValue) === cursorValue) {
|
|
5329
|
+
value = cursorValue;
|
|
5330
|
+
inclusive = bound.inclusive;
|
|
5331
|
+
}
|
|
5332
|
+
break;
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5372
5335
|
}
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5336
|
+
return { value, inclusive };
|
|
5337
|
+
}
|
|
5338
|
+
/** Returns the number of segments of a perfect index for this target. */
|
|
5339
|
+
function targetGetSegmentCount(target) {
|
|
5340
|
+
let fields = new SortedSet(FieldPath$1.comparator);
|
|
5341
|
+
let hasArraySegment = false;
|
|
5342
|
+
for (const filter of target.filters) {
|
|
5343
|
+
// TODO(orquery): Use the flattened filters here
|
|
5344
|
+
const fieldFilter = filter;
|
|
5345
|
+
// __name__ is not an explicit segment of any index, so we don't need to
|
|
5346
|
+
// count it.
|
|
5347
|
+
if (fieldFilter.field.isKeyField()) {
|
|
5348
|
+
continue;
|
|
5349
|
+
}
|
|
5350
|
+
// ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filters must be counted separately.
|
|
5351
|
+
// For instance, it is possible to have an index for "a ARRAY a ASC". Even
|
|
5352
|
+
// though these are on the same field, they should be counted as two
|
|
5353
|
+
// separate segments in an index.
|
|
5354
|
+
if (fieldFilter.op === "array-contains" /* ARRAY_CONTAINS */ ||
|
|
5355
|
+
fieldFilter.op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
|
|
5356
|
+
hasArraySegment = true;
|
|
5357
|
+
}
|
|
5358
|
+
else {
|
|
5359
|
+
fields = fields.add(fieldFilter.field);
|
|
5360
|
+
}
|
|
5378
5361
|
}
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
if (cmp === 0) {
|
|
5385
|
-
return node.value;
|
|
5386
|
-
}
|
|
5387
|
-
else if (cmp < 0) {
|
|
5388
|
-
node = node.left;
|
|
5389
|
-
}
|
|
5390
|
-
else if (cmp > 0) {
|
|
5391
|
-
node = node.right;
|
|
5392
|
-
}
|
|
5362
|
+
for (const orderBy of target.orderBy) {
|
|
5363
|
+
// __name__ is not an explicit segment of any index, so we don't need to
|
|
5364
|
+
// count it.
|
|
5365
|
+
if (!orderBy.field.isKeyField()) {
|
|
5366
|
+
fields = fields.add(orderBy.field);
|
|
5393
5367
|
}
|
|
5394
|
-
return null;
|
|
5395
5368
|
}
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5369
|
+
return fields.size + (hasArraySegment ? 1 : 0);
|
|
5370
|
+
}
|
|
5371
|
+
class Filter {
|
|
5372
|
+
}
|
|
5373
|
+
class FieldFilter extends Filter {
|
|
5374
|
+
constructor(field, op, value) {
|
|
5375
|
+
super();
|
|
5376
|
+
this.field = field;
|
|
5377
|
+
this.op = op;
|
|
5378
|
+
this.value = value;
|
|
5379
|
+
}
|
|
5380
|
+
/**
|
|
5381
|
+
* Creates a filter based on the provided arguments.
|
|
5382
|
+
*/
|
|
5383
|
+
static create(field, op, value) {
|
|
5384
|
+
if (field.isKeyField()) {
|
|
5385
|
+
if (op === "in" /* IN */ || op === "not-in" /* NOT_IN */) {
|
|
5386
|
+
return this.createKeyFieldInFilter(field, op, value);
|
|
5409
5387
|
}
|
|
5410
5388
|
else {
|
|
5411
|
-
|
|
5412
|
-
prunedNodes += node.left.size + 1;
|
|
5413
|
-
node = node.right;
|
|
5389
|
+
return new KeyFieldFilter(field, op, value);
|
|
5414
5390
|
}
|
|
5415
5391
|
}
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5392
|
+
else if (op === "array-contains" /* ARRAY_CONTAINS */) {
|
|
5393
|
+
return new ArrayContainsFilter(field, value);
|
|
5394
|
+
}
|
|
5395
|
+
else if (op === "in" /* IN */) {
|
|
5396
|
+
return new InFilter(field, value);
|
|
5397
|
+
}
|
|
5398
|
+
else if (op === "not-in" /* NOT_IN */) {
|
|
5399
|
+
return new NotInFilter(field, value);
|
|
5400
|
+
}
|
|
5401
|
+
else if (op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
|
|
5402
|
+
return new ArrayContainsAnyFilter(field, value);
|
|
5403
|
+
}
|
|
5404
|
+
else {
|
|
5405
|
+
return new FieldFilter(field, op, value);
|
|
5406
|
+
}
|
|
5421
5407
|
}
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5408
|
+
static createKeyFieldInFilter(field, op, value) {
|
|
5409
|
+
return op === "in" /* IN */
|
|
5410
|
+
? new KeyFieldInFilter(field, value)
|
|
5411
|
+
: new KeyFieldNotInFilter(field, value);
|
|
5425
5412
|
}
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5413
|
+
matches(doc) {
|
|
5414
|
+
const other = doc.data.field(this.field);
|
|
5415
|
+
// Types do not have to match in NOT_EQUAL filters.
|
|
5416
|
+
if (this.op === "!=" /* NOT_EQUAL */) {
|
|
5417
|
+
return (other !== null &&
|
|
5418
|
+
this.matchesComparison(valueCompare(other, this.value)));
|
|
5419
|
+
}
|
|
5420
|
+
// Only compare types with matching backend order (such as double and int).
|
|
5421
|
+
return (other !== null &&
|
|
5422
|
+
typeOrder(this.value) === typeOrder(other) &&
|
|
5423
|
+
this.matchesComparison(valueCompare(other, this.value)));
|
|
5429
5424
|
}
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5425
|
+
matchesComparison(comparison) {
|
|
5426
|
+
switch (this.op) {
|
|
5427
|
+
case "<" /* LESS_THAN */:
|
|
5428
|
+
return comparison < 0;
|
|
5429
|
+
case "<=" /* LESS_THAN_OR_EQUAL */:
|
|
5430
|
+
return comparison <= 0;
|
|
5431
|
+
case "==" /* EQUAL */:
|
|
5432
|
+
return comparison === 0;
|
|
5433
|
+
case "!=" /* NOT_EQUAL */:
|
|
5434
|
+
return comparison !== 0;
|
|
5435
|
+
case ">" /* GREATER_THAN */:
|
|
5436
|
+
return comparison > 0;
|
|
5437
|
+
case ">=" /* GREATER_THAN_OR_EQUAL */:
|
|
5438
|
+
return comparison >= 0;
|
|
5439
|
+
default:
|
|
5440
|
+
return fail();
|
|
5441
|
+
}
|
|
5433
5442
|
}
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5443
|
+
isInequality() {
|
|
5444
|
+
return ([
|
|
5445
|
+
"<" /* LESS_THAN */,
|
|
5446
|
+
"<=" /* LESS_THAN_OR_EQUAL */,
|
|
5447
|
+
">" /* GREATER_THAN */,
|
|
5448
|
+
">=" /* GREATER_THAN_OR_EQUAL */,
|
|
5449
|
+
"!=" /* NOT_EQUAL */,
|
|
5450
|
+
"not-in" /* NOT_IN */
|
|
5451
|
+
].indexOf(this.op) >= 0);
|
|
5440
5452
|
}
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5453
|
+
}
|
|
5454
|
+
function canonifyFilter(filter) {
|
|
5455
|
+
// TODO(b/29183165): Technically, this won't be unique if two values have
|
|
5456
|
+
// the same description, such as the int 3 and the string "3". So we should
|
|
5457
|
+
// add the types in here somehow, too.
|
|
5458
|
+
return (filter.field.canonicalString() +
|
|
5459
|
+
filter.op.toString() +
|
|
5460
|
+
canonicalId(filter.value));
|
|
5461
|
+
}
|
|
5462
|
+
function filterEquals(f1, f2) {
|
|
5463
|
+
return (f1.op === f2.op &&
|
|
5464
|
+
f1.field.isEqual(f2.field) &&
|
|
5465
|
+
valueEquals(f1.value, f2.value));
|
|
5466
|
+
}
|
|
5467
|
+
/** Returns a debug description for `filter`. */
|
|
5468
|
+
function stringifyFilter(filter) {
|
|
5469
|
+
return `${filter.field.canonicalString()} ${filter.op} ${canonicalId(filter.value)}`;
|
|
5470
|
+
}
|
|
5471
|
+
/** Filter that matches on key fields (i.e. '__name__'). */
|
|
5472
|
+
class KeyFieldFilter extends FieldFilter {
|
|
5473
|
+
constructor(field, op, value) {
|
|
5474
|
+
super(field, op, value);
|
|
5475
|
+
this.key = DocumentKey.fromName(value.referenceValue);
|
|
5446
5476
|
}
|
|
5447
|
-
|
|
5448
|
-
const
|
|
5449
|
-
this.
|
|
5450
|
-
descriptions.push(`${k}:${v}`);
|
|
5451
|
-
return false;
|
|
5452
|
-
});
|
|
5453
|
-
return `{${descriptions.join(', ')}}`;
|
|
5477
|
+
matches(doc) {
|
|
5478
|
+
const comparison = DocumentKey.comparator(doc.key, this.key);
|
|
5479
|
+
return this.matchesComparison(comparison);
|
|
5454
5480
|
}
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
return this.root.reverseTraversal(action);
|
|
5481
|
+
}
|
|
5482
|
+
/** Filter that matches on key fields within an array. */
|
|
5483
|
+
class KeyFieldInFilter extends FieldFilter {
|
|
5484
|
+
constructor(field, value) {
|
|
5485
|
+
super(field, "in" /* IN */, value);
|
|
5486
|
+
this.keys = extractDocumentKeysFromArrayValue("in" /* IN */, value);
|
|
5462
5487
|
}
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
return new SortedMapIterator(this.root, null, this.comparator, false);
|
|
5488
|
+
matches(doc) {
|
|
5489
|
+
return this.keys.some(key => key.isEqual(doc.key));
|
|
5466
5490
|
}
|
|
5467
|
-
|
|
5468
|
-
|
|
5491
|
+
}
|
|
5492
|
+
/** Filter that matches on key fields not present within an array. */
|
|
5493
|
+
class KeyFieldNotInFilter extends FieldFilter {
|
|
5494
|
+
constructor(field, value) {
|
|
5495
|
+
super(field, "not-in" /* NOT_IN */, value);
|
|
5496
|
+
this.keys = extractDocumentKeysFromArrayValue("not-in" /* NOT_IN */, value);
|
|
5469
5497
|
}
|
|
5470
|
-
|
|
5471
|
-
return
|
|
5498
|
+
matches(doc) {
|
|
5499
|
+
return !this.keys.some(key => key.isEqual(doc.key));
|
|
5472
5500
|
}
|
|
5473
|
-
|
|
5474
|
-
|
|
5501
|
+
}
|
|
5502
|
+
function extractDocumentKeysFromArrayValue(op, value) {
|
|
5503
|
+
var _a;
|
|
5504
|
+
return (((_a = value.arrayValue) === null || _a === void 0 ? void 0 : _a.values) || []).map(v => {
|
|
5505
|
+
return DocumentKey.fromName(v.referenceValue);
|
|
5506
|
+
});
|
|
5507
|
+
}
|
|
5508
|
+
/** A Filter that implements the array-contains operator. */
|
|
5509
|
+
class ArrayContainsFilter extends FieldFilter {
|
|
5510
|
+
constructor(field, value) {
|
|
5511
|
+
super(field, "array-contains" /* ARRAY_CONTAINS */, value);
|
|
5475
5512
|
}
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
constructor(node, startKey, comparator, isReverse) {
|
|
5480
|
-
this.isReverse = isReverse;
|
|
5481
|
-
this.nodeStack = [];
|
|
5482
|
-
let cmp = 1;
|
|
5483
|
-
while (!node.isEmpty()) {
|
|
5484
|
-
cmp = startKey ? comparator(node.key, startKey) : 1;
|
|
5485
|
-
// flip the comparison if we're going in reverse
|
|
5486
|
-
if (startKey && isReverse) {
|
|
5487
|
-
cmp *= -1;
|
|
5488
|
-
}
|
|
5489
|
-
if (cmp < 0) {
|
|
5490
|
-
// This node is less than our start key. ignore it
|
|
5491
|
-
if (this.isReverse) {
|
|
5492
|
-
node = node.left;
|
|
5493
|
-
}
|
|
5494
|
-
else {
|
|
5495
|
-
node = node.right;
|
|
5496
|
-
}
|
|
5497
|
-
}
|
|
5498
|
-
else if (cmp === 0) {
|
|
5499
|
-
// This node is exactly equal to our start key. Push it on the stack,
|
|
5500
|
-
// but stop iterating;
|
|
5501
|
-
this.nodeStack.push(node);
|
|
5502
|
-
break;
|
|
5503
|
-
}
|
|
5504
|
-
else {
|
|
5505
|
-
// This node is greater than our start key, add it to the stack and move
|
|
5506
|
-
// to the next one
|
|
5507
|
-
this.nodeStack.push(node);
|
|
5508
|
-
if (this.isReverse) {
|
|
5509
|
-
node = node.right;
|
|
5510
|
-
}
|
|
5511
|
-
else {
|
|
5512
|
-
node = node.left;
|
|
5513
|
-
}
|
|
5514
|
-
}
|
|
5515
|
-
}
|
|
5513
|
+
matches(doc) {
|
|
5514
|
+
const other = doc.data.field(this.field);
|
|
5515
|
+
return isArray(other) && arrayValueContains(other.arrayValue, this.value);
|
|
5516
5516
|
}
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
while (!node.isEmpty()) {
|
|
5523
|
-
this.nodeStack.push(node);
|
|
5524
|
-
node = node.right;
|
|
5525
|
-
}
|
|
5526
|
-
}
|
|
5527
|
-
else {
|
|
5528
|
-
node = node.right;
|
|
5529
|
-
while (!node.isEmpty()) {
|
|
5530
|
-
this.nodeStack.push(node);
|
|
5531
|
-
node = node.left;
|
|
5532
|
-
}
|
|
5533
|
-
}
|
|
5534
|
-
return result;
|
|
5517
|
+
}
|
|
5518
|
+
/** A Filter that implements the IN operator. */
|
|
5519
|
+
class InFilter extends FieldFilter {
|
|
5520
|
+
constructor(field, value) {
|
|
5521
|
+
super(field, "in" /* IN */, value);
|
|
5535
5522
|
}
|
|
5536
|
-
|
|
5537
|
-
|
|
5523
|
+
matches(doc) {
|
|
5524
|
+
const other = doc.data.field(this.field);
|
|
5525
|
+
return other !== null && arrayValueContains(this.value.arrayValue, other);
|
|
5538
5526
|
}
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
return { key: node.key, value: node.value };
|
|
5527
|
+
}
|
|
5528
|
+
/** A Filter that implements the not-in operator. */
|
|
5529
|
+
class NotInFilter extends FieldFilter {
|
|
5530
|
+
constructor(field, value) {
|
|
5531
|
+
super(field, "not-in" /* NOT_IN */, value);
|
|
5545
5532
|
}
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
this.value
|
|
5552
|
-
this.color = color != null ? color : LLRBNode.RED;
|
|
5553
|
-
this.left = left != null ? left : LLRBNode.EMPTY;
|
|
5554
|
-
this.right = right != null ? right : LLRBNode.EMPTY;
|
|
5555
|
-
this.size = this.left.size + 1 + this.right.size;
|
|
5533
|
+
matches(doc) {
|
|
5534
|
+
if (arrayValueContains(this.value.arrayValue, { nullValue: 'NULL_VALUE' })) {
|
|
5535
|
+
return false;
|
|
5536
|
+
}
|
|
5537
|
+
const other = doc.data.field(this.field);
|
|
5538
|
+
return other !== null && !arrayValueContains(this.value.arrayValue, other);
|
|
5556
5539
|
}
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5540
|
+
}
|
|
5541
|
+
/** A Filter that implements the array-contains-any operator. */
|
|
5542
|
+
class ArrayContainsAnyFilter extends FieldFilter {
|
|
5543
|
+
constructor(field, value) {
|
|
5544
|
+
super(field, "array-contains-any" /* ARRAY_CONTAINS_ANY */, value);
|
|
5560
5545
|
}
|
|
5561
|
-
|
|
5562
|
-
|
|
5546
|
+
matches(doc) {
|
|
5547
|
+
const other = doc.data.field(this.field);
|
|
5548
|
+
if (!isArray(other) || !other.arrayValue.values) {
|
|
5549
|
+
return false;
|
|
5550
|
+
}
|
|
5551
|
+
return other.arrayValue.values.some(val => arrayValueContains(this.value.arrayValue, val));
|
|
5563
5552
|
}
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5553
|
+
}
|
|
5554
|
+
/**
|
|
5555
|
+
* Represents a bound of a query.
|
|
5556
|
+
*
|
|
5557
|
+
* The bound is specified with the given components representing a position and
|
|
5558
|
+
* whether it's just before or just after the position (relative to whatever the
|
|
5559
|
+
* query order is).
|
|
5560
|
+
*
|
|
5561
|
+
* The position represents a logical index position for a query. It's a prefix
|
|
5562
|
+
* of values for the (potentially implicit) order by clauses of a query.
|
|
5563
|
+
*
|
|
5564
|
+
* Bound provides a function to determine whether a document comes before or
|
|
5565
|
+
* after a bound. This is influenced by whether the position is just before or
|
|
5566
|
+
* just after the provided values.
|
|
5567
|
+
*/
|
|
5568
|
+
class Bound {
|
|
5569
|
+
constructor(position, inclusive) {
|
|
5570
|
+
this.position = position;
|
|
5571
|
+
this.inclusive = inclusive;
|
|
5572
5572
|
}
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5573
|
+
}
|
|
5574
|
+
/**
|
|
5575
|
+
* An ordering on a field, in some Direction. Direction defaults to ASCENDING.
|
|
5576
|
+
*/
|
|
5577
|
+
class OrderBy {
|
|
5578
|
+
constructor(field, dir = "asc" /* ASCENDING */) {
|
|
5579
|
+
this.field = field;
|
|
5580
|
+
this.dir = dir;
|
|
5581
5581
|
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5582
|
+
}
|
|
5583
|
+
function canonifyOrderBy(orderBy) {
|
|
5584
|
+
// TODO(b/29183165): Make this collision robust.
|
|
5585
|
+
return orderBy.field.canonicalString() + orderBy.dir;
|
|
5586
|
+
}
|
|
5587
|
+
function stringifyOrderBy(orderBy) {
|
|
5588
|
+
return `${orderBy.field.canonicalString()} (${orderBy.dir})`;
|
|
5589
|
+
}
|
|
5590
|
+
function orderByEquals(left, right) {
|
|
5591
|
+
return left.dir === right.dir && left.field.isEqual(right.field);
|
|
5592
|
+
}
|
|
5593
|
+
function boundCompareToDocument(bound, orderBy, doc) {
|
|
5594
|
+
let comparison = 0;
|
|
5595
|
+
for (let i = 0; i < bound.position.length; i++) {
|
|
5596
|
+
const orderByComponent = orderBy[i];
|
|
5597
|
+
const component = bound.position[i];
|
|
5598
|
+
if (orderByComponent.field.isKeyField()) {
|
|
5599
|
+
comparison = DocumentKey.comparator(DocumentKey.fromName(component.referenceValue), doc.key);
|
|
5586
5600
|
}
|
|
5587
5601
|
else {
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
}
|
|
5591
|
-
// Returns the maximum key in the tree.
|
|
5592
|
-
minKey() {
|
|
5593
|
-
return this.min().key;
|
|
5594
|
-
}
|
|
5595
|
-
// Returns the maximum key in the tree.
|
|
5596
|
-
maxKey() {
|
|
5597
|
-
if (this.right.isEmpty()) {
|
|
5598
|
-
return this.key;
|
|
5599
|
-
}
|
|
5600
|
-
else {
|
|
5601
|
-
return this.right.maxKey();
|
|
5602
|
-
}
|
|
5603
|
-
}
|
|
5604
|
-
// Returns new tree, with the key/value added.
|
|
5605
|
-
insert(key, value, comparator) {
|
|
5606
|
-
let n = this;
|
|
5607
|
-
const cmp = comparator(key, n.key);
|
|
5608
|
-
if (cmp < 0) {
|
|
5609
|
-
n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
|
|
5602
|
+
const docValue = doc.data.field(orderByComponent.field);
|
|
5603
|
+
comparison = valueCompare(component, docValue);
|
|
5610
5604
|
}
|
|
5611
|
-
|
|
5612
|
-
|
|
5605
|
+
if (orderByComponent.dir === "desc" /* DESCENDING */) {
|
|
5606
|
+
comparison = comparison * -1;
|
|
5613
5607
|
}
|
|
5614
|
-
|
|
5615
|
-
|
|
5608
|
+
if (comparison !== 0) {
|
|
5609
|
+
break;
|
|
5616
5610
|
}
|
|
5617
|
-
return n.fixUp();
|
|
5618
5611
|
}
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5612
|
+
return comparison;
|
|
5613
|
+
}
|
|
5614
|
+
/**
|
|
5615
|
+
* Returns true if a document sorts after a bound using the provided sort
|
|
5616
|
+
* order.
|
|
5617
|
+
*/
|
|
5618
|
+
function boundSortsAfterDocument(bound, orderBy, doc) {
|
|
5619
|
+
const comparison = boundCompareToDocument(bound, orderBy, doc);
|
|
5620
|
+
return bound.inclusive ? comparison >= 0 : comparison > 0;
|
|
5621
|
+
}
|
|
5622
|
+
/**
|
|
5623
|
+
* Returns true if a document sorts before a bound using the provided sort
|
|
5624
|
+
* order.
|
|
5625
|
+
*/
|
|
5626
|
+
function boundSortsBeforeDocument(bound, orderBy, doc) {
|
|
5627
|
+
const comparison = boundCompareToDocument(bound, orderBy, doc);
|
|
5628
|
+
return bound.inclusive ? comparison <= 0 : comparison < 0;
|
|
5629
|
+
}
|
|
5630
|
+
function boundEquals(left, right) {
|
|
5631
|
+
if (left === null) {
|
|
5632
|
+
return right === null;
|
|
5629
5633
|
}
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
let smallest;
|
|
5633
|
-
let n = this;
|
|
5634
|
-
if (comparator(key, n.key) < 0) {
|
|
5635
|
-
if (!n.left.isEmpty() && !n.left.isRed() && !n.left.left.isRed()) {
|
|
5636
|
-
n = n.moveRedLeft();
|
|
5637
|
-
}
|
|
5638
|
-
n = n.copy(null, null, null, n.left.remove(key, comparator), null);
|
|
5639
|
-
}
|
|
5640
|
-
else {
|
|
5641
|
-
if (n.left.isRed()) {
|
|
5642
|
-
n = n.rotateRight();
|
|
5643
|
-
}
|
|
5644
|
-
if (!n.right.isEmpty() && !n.right.isRed() && !n.right.left.isRed()) {
|
|
5645
|
-
n = n.moveRedRight();
|
|
5646
|
-
}
|
|
5647
|
-
if (comparator(key, n.key) === 0) {
|
|
5648
|
-
if (n.right.isEmpty()) {
|
|
5649
|
-
return LLRBNode.EMPTY;
|
|
5650
|
-
}
|
|
5651
|
-
else {
|
|
5652
|
-
smallest = n.right.min();
|
|
5653
|
-
n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin());
|
|
5654
|
-
}
|
|
5655
|
-
}
|
|
5656
|
-
n = n.copy(null, null, null, null, n.right.remove(key, comparator));
|
|
5657
|
-
}
|
|
5658
|
-
return n.fixUp();
|
|
5634
|
+
else if (right === null) {
|
|
5635
|
+
return false;
|
|
5659
5636
|
}
|
|
5660
|
-
|
|
5661
|
-
|
|
5637
|
+
if (left.inclusive !== right.inclusive ||
|
|
5638
|
+
left.position.length !== right.position.length) {
|
|
5639
|
+
return false;
|
|
5662
5640
|
}
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
if (
|
|
5667
|
-
|
|
5668
|
-
}
|
|
5669
|
-
if (n.left.isRed() && n.left.left.isRed()) {
|
|
5670
|
-
n = n.rotateRight();
|
|
5671
|
-
}
|
|
5672
|
-
if (n.left.isRed() && n.right.isRed()) {
|
|
5673
|
-
n = n.colorFlip();
|
|
5641
|
+
for (let i = 0; i < left.position.length; i++) {
|
|
5642
|
+
const leftPosition = left.position[i];
|
|
5643
|
+
const rightPosition = right.position[i];
|
|
5644
|
+
if (!valueEquals(leftPosition, rightPosition)) {
|
|
5645
|
+
return false;
|
|
5674
5646
|
}
|
|
5675
|
-
return n;
|
|
5676
5647
|
}
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5648
|
+
return true;
|
|
5649
|
+
}
|
|
5650
|
+
|
|
5651
|
+
/**
|
|
5652
|
+
* @license
|
|
5653
|
+
* Copyright 2017 Google LLC
|
|
5654
|
+
*
|
|
5655
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5656
|
+
* you may not use this file except in compliance with the License.
|
|
5657
|
+
* You may obtain a copy of the License at
|
|
5658
|
+
*
|
|
5659
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
5660
|
+
*
|
|
5661
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
5662
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
5663
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
5664
|
+
* See the License for the specific language governing permissions and
|
|
5665
|
+
* limitations under the License.
|
|
5666
|
+
*/
|
|
5667
|
+
/**
|
|
5668
|
+
* Query encapsulates all the query attributes we support in the SDK. It can
|
|
5669
|
+
* be run against the LocalStore, as well as be converted to a `Target` to
|
|
5670
|
+
* query the RemoteStore results.
|
|
5671
|
+
*
|
|
5672
|
+
* Visible for testing.
|
|
5673
|
+
*/
|
|
5674
|
+
class QueryImpl {
|
|
5675
|
+
/**
|
|
5676
|
+
* Initializes a Query with a path and optional additional query constraints.
|
|
5677
|
+
* Path must currently be empty if this is a collection group query.
|
|
5678
|
+
*/
|
|
5679
|
+
constructor(path, collectionGroup = null, explicitOrderBy = [], filters = [], limit = null, limitType = "F" /* First */, startAt = null, endAt = null) {
|
|
5680
|
+
this.path = path;
|
|
5681
|
+
this.collectionGroup = collectionGroup;
|
|
5682
|
+
this.explicitOrderBy = explicitOrderBy;
|
|
5683
|
+
this.filters = filters;
|
|
5684
|
+
this.limit = limit;
|
|
5685
|
+
this.limitType = limitType;
|
|
5686
|
+
this.startAt = startAt;
|
|
5687
|
+
this.endAt = endAt;
|
|
5688
|
+
this.memoizedOrderBy = null;
|
|
5689
|
+
// The corresponding `Target` of this `Query` instance.
|
|
5690
|
+
this.memoizedTarget = null;
|
|
5691
|
+
if (this.startAt) ;
|
|
5692
|
+
if (this.endAt) ;
|
|
5685
5693
|
}
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5694
|
+
}
|
|
5695
|
+
/** Creates a new Query instance with the options provided. */
|
|
5696
|
+
function newQuery(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt) {
|
|
5697
|
+
return new QueryImpl(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt);
|
|
5698
|
+
}
|
|
5699
|
+
/** Creates a new Query for a query that matches all documents at `path` */
|
|
5700
|
+
function newQueryForPath(path) {
|
|
5701
|
+
return new QueryImpl(path);
|
|
5702
|
+
}
|
|
5703
|
+
/**
|
|
5704
|
+
* Helper to convert a collection group query into a collection query at a
|
|
5705
|
+
* specific path. This is used when executing collection group queries, since
|
|
5706
|
+
* we have to split the query into a set of collection queries at multiple
|
|
5707
|
+
* paths.
|
|
5708
|
+
*/
|
|
5709
|
+
function asCollectionQueryAtPath(query, path) {
|
|
5710
|
+
return new QueryImpl(path,
|
|
5711
|
+
/*collectionGroup=*/ null, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
|
|
5712
|
+
}
|
|
5713
|
+
/**
|
|
5714
|
+
* Returns true if this query does not specify any query constraints that
|
|
5715
|
+
* could remove results.
|
|
5716
|
+
*/
|
|
5717
|
+
function queryMatchesAllDocuments(query) {
|
|
5718
|
+
return (query.filters.length === 0 &&
|
|
5719
|
+
query.limit === null &&
|
|
5720
|
+
query.startAt == null &&
|
|
5721
|
+
query.endAt == null &&
|
|
5722
|
+
(query.explicitOrderBy.length === 0 ||
|
|
5723
|
+
(query.explicitOrderBy.length === 1 &&
|
|
5724
|
+
query.explicitOrderBy[0].field.isKeyField())));
|
|
5725
|
+
}
|
|
5726
|
+
function getFirstOrderByField(query) {
|
|
5727
|
+
return query.explicitOrderBy.length > 0
|
|
5728
|
+
? query.explicitOrderBy[0].field
|
|
5729
|
+
: null;
|
|
5730
|
+
}
|
|
5731
|
+
function getInequalityFilterField(query) {
|
|
5732
|
+
for (const filter of query.filters) {
|
|
5733
|
+
if (filter.isInequality()) {
|
|
5734
|
+
return filter.field;
|
|
5691
5735
|
}
|
|
5692
|
-
return n;
|
|
5693
|
-
}
|
|
5694
|
-
rotateLeft() {
|
|
5695
|
-
const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
|
|
5696
|
-
return this.right.copy(null, null, this.color, nl, null);
|
|
5697
|
-
}
|
|
5698
|
-
rotateRight() {
|
|
5699
|
-
const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
|
|
5700
|
-
return this.left.copy(null, null, this.color, null, nr);
|
|
5701
5736
|
}
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5737
|
+
return null;
|
|
5738
|
+
}
|
|
5739
|
+
/**
|
|
5740
|
+
* Checks if any of the provided Operators are included in the query and
|
|
5741
|
+
* returns the first one that is, or null if none are.
|
|
5742
|
+
*/
|
|
5743
|
+
function findFilterOperator(query, operators) {
|
|
5744
|
+
for (const filter of query.filters) {
|
|
5745
|
+
if (operators.indexOf(filter.op) >= 0) {
|
|
5746
|
+
return filter.op;
|
|
5747
|
+
}
|
|
5706
5748
|
}
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5749
|
+
return null;
|
|
5750
|
+
}
|
|
5751
|
+
/**
|
|
5752
|
+
* Creates a new Query for a collection group query that matches all documents
|
|
5753
|
+
* within the provided collection group.
|
|
5754
|
+
*/
|
|
5755
|
+
function newQueryForCollectionGroup(collectionId) {
|
|
5756
|
+
return new QueryImpl(ResourcePath.emptyPath(), collectionId);
|
|
5757
|
+
}
|
|
5758
|
+
/**
|
|
5759
|
+
* Returns whether the query matches a single document by path (rather than a
|
|
5760
|
+
* collection).
|
|
5761
|
+
*/
|
|
5762
|
+
function isDocumentQuery$1(query) {
|
|
5763
|
+
return (DocumentKey.isDocumentKey(query.path) &&
|
|
5764
|
+
query.collectionGroup === null &&
|
|
5765
|
+
query.filters.length === 0);
|
|
5766
|
+
}
|
|
5767
|
+
/**
|
|
5768
|
+
* Returns whether the query matches a collection group rather than a specific
|
|
5769
|
+
* collection.
|
|
5770
|
+
*/
|
|
5771
|
+
function isCollectionGroupQuery(query) {
|
|
5772
|
+
return query.collectionGroup !== null;
|
|
5773
|
+
}
|
|
5774
|
+
/**
|
|
5775
|
+
* Returns the implicit order by constraint that is used to execute the Query,
|
|
5776
|
+
* which can be different from the order by constraints the user provided (e.g.
|
|
5777
|
+
* the SDK and backend always orders by `__name__`).
|
|
5778
|
+
*/
|
|
5779
|
+
function queryOrderBy(query) {
|
|
5780
|
+
const queryImpl = debugCast(query);
|
|
5781
|
+
if (queryImpl.memoizedOrderBy === null) {
|
|
5782
|
+
queryImpl.memoizedOrderBy = [];
|
|
5783
|
+
const inequalityField = getInequalityFilterField(queryImpl);
|
|
5784
|
+
const firstOrderByField = getFirstOrderByField(queryImpl);
|
|
5785
|
+
if (inequalityField !== null && firstOrderByField === null) {
|
|
5786
|
+
// In order to implicitly add key ordering, we must also add the
|
|
5787
|
+
// inequality filter field for it to be a valid query.
|
|
5788
|
+
// Note that the default inequality field and key ordering is ascending.
|
|
5789
|
+
if (!inequalityField.isKeyField()) {
|
|
5790
|
+
queryImpl.memoizedOrderBy.push(new OrderBy(inequalityField));
|
|
5791
|
+
}
|
|
5792
|
+
queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), "asc" /* ASCENDING */));
|
|
5712
5793
|
}
|
|
5713
5794
|
else {
|
|
5714
|
-
|
|
5795
|
+
let foundKeyOrdering = false;
|
|
5796
|
+
for (const orderBy of queryImpl.explicitOrderBy) {
|
|
5797
|
+
queryImpl.memoizedOrderBy.push(orderBy);
|
|
5798
|
+
if (orderBy.field.isKeyField()) {
|
|
5799
|
+
foundKeyOrdering = true;
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
if (!foundKeyOrdering) {
|
|
5803
|
+
// The order of the implicit key ordering always matches the last
|
|
5804
|
+
// explicit order by
|
|
5805
|
+
const lastDirection = queryImpl.explicitOrderBy.length > 0
|
|
5806
|
+
? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
|
|
5807
|
+
.dir
|
|
5808
|
+
: "asc" /* ASCENDING */;
|
|
5809
|
+
queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), lastDirection));
|
|
5810
|
+
}
|
|
5715
5811
|
}
|
|
5716
5812
|
}
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
if (blackDepth !== this.right.check()) {
|
|
5728
|
-
throw fail();
|
|
5813
|
+
return queryImpl.memoizedOrderBy;
|
|
5814
|
+
}
|
|
5815
|
+
/**
|
|
5816
|
+
* Converts this `Query` instance to it's corresponding `Target` representation.
|
|
5817
|
+
*/
|
|
5818
|
+
function queryToTarget(query) {
|
|
5819
|
+
const queryImpl = debugCast(query);
|
|
5820
|
+
if (!queryImpl.memoizedTarget) {
|
|
5821
|
+
if (queryImpl.limitType === "F" /* First */) {
|
|
5822
|
+
queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, queryOrderBy(queryImpl), queryImpl.filters, queryImpl.limit, queryImpl.startAt, queryImpl.endAt);
|
|
5729
5823
|
}
|
|
5730
5824
|
else {
|
|
5731
|
-
|
|
5825
|
+
// Flip the orderBy directions since we want the last results
|
|
5826
|
+
const orderBys = [];
|
|
5827
|
+
for (const orderBy of queryOrderBy(queryImpl)) {
|
|
5828
|
+
const dir = orderBy.dir === "desc" /* DESCENDING */
|
|
5829
|
+
? "asc" /* ASCENDING */
|
|
5830
|
+
: "desc" /* DESCENDING */;
|
|
5831
|
+
orderBys.push(new OrderBy(orderBy.field, dir));
|
|
5832
|
+
}
|
|
5833
|
+
// We need to swap the cursors to match the now-flipped query ordering.
|
|
5834
|
+
const startAt = queryImpl.endAt
|
|
5835
|
+
? new Bound(queryImpl.endAt.position, queryImpl.endAt.inclusive)
|
|
5836
|
+
: null;
|
|
5837
|
+
const endAt = queryImpl.startAt
|
|
5838
|
+
? new Bound(queryImpl.startAt.position, queryImpl.startAt.inclusive)
|
|
5839
|
+
: null;
|
|
5840
|
+
// Now return as a LimitType.First query.
|
|
5841
|
+
queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, orderBys, queryImpl.filters, queryImpl.limit, startAt, endAt);
|
|
5732
5842
|
}
|
|
5733
5843
|
}
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5844
|
+
return queryImpl.memoizedTarget;
|
|
5845
|
+
}
|
|
5846
|
+
function queryWithAddedFilter(query, filter) {
|
|
5847
|
+
const newFilters = query.filters.concat([filter]);
|
|
5848
|
+
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), newFilters, query.limit, query.limitType, query.startAt, query.endAt);
|
|
5849
|
+
}
|
|
5850
|
+
function queryWithAddedOrderBy(query, orderBy) {
|
|
5851
|
+
// TODO(dimond): validate that orderBy does not list the same key twice.
|
|
5852
|
+
const newOrderBy = query.explicitOrderBy.concat([orderBy]);
|
|
5853
|
+
return new QueryImpl(query.path, query.collectionGroup, newOrderBy, query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
|
|
5854
|
+
}
|
|
5855
|
+
function queryWithLimit(query, limit, limitType) {
|
|
5856
|
+
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), limit, limitType, query.startAt, query.endAt);
|
|
5857
|
+
}
|
|
5858
|
+
function queryWithStartAt(query, bound) {
|
|
5859
|
+
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, bound, query.endAt);
|
|
5860
|
+
}
|
|
5861
|
+
function queryWithEndAt(query, bound) {
|
|
5862
|
+
return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, bound);
|
|
5863
|
+
}
|
|
5864
|
+
function queryEquals(left, right) {
|
|
5865
|
+
return (targetEquals(queryToTarget(left), queryToTarget(right)) &&
|
|
5866
|
+
left.limitType === right.limitType);
|
|
5867
|
+
}
|
|
5868
|
+
// TODO(b/29183165): This is used to get a unique string from a query to, for
|
|
5869
|
+
// example, use as a dictionary key, but the implementation is subject to
|
|
5870
|
+
// collisions. Make it collision-free.
|
|
5871
|
+
function canonifyQuery(query) {
|
|
5872
|
+
return `${canonifyTarget(queryToTarget(query))}|lt:${query.limitType}`;
|
|
5873
|
+
}
|
|
5874
|
+
function stringifyQuery(query) {
|
|
5875
|
+
return `Query(target=${stringifyTarget(queryToTarget(query))}; limitType=${query.limitType})`;
|
|
5876
|
+
}
|
|
5877
|
+
/** Returns whether `doc` matches the constraints of `query`. */
|
|
5878
|
+
function queryMatches(query, doc) {
|
|
5879
|
+
return (doc.isFoundDocument() &&
|
|
5880
|
+
queryMatchesPathAndCollectionGroup(query, doc) &&
|
|
5881
|
+
queryMatchesOrderBy(query, doc) &&
|
|
5882
|
+
queryMatchesFilters(query, doc) &&
|
|
5883
|
+
queryMatchesBounds(query, doc));
|
|
5884
|
+
}
|
|
5885
|
+
function queryMatchesPathAndCollectionGroup(query, doc) {
|
|
5886
|
+
const docPath = doc.key.path;
|
|
5887
|
+
if (query.collectionGroup !== null) {
|
|
5888
|
+
// NOTE: this.path is currently always empty since we don't expose Collection
|
|
5889
|
+
// Group queries rooted at a document path yet.
|
|
5890
|
+
return (doc.key.hasCollectionId(query.collectionGroup) &&
|
|
5891
|
+
query.path.isPrefixOf(docPath));
|
|
5763
5892
|
}
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
return
|
|
5893
|
+
else if (DocumentKey.isDocumentKey(query.path)) {
|
|
5894
|
+
// exact match for document queries
|
|
5895
|
+
return query.path.isEqual(docPath);
|
|
5767
5896
|
}
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
return
|
|
5897
|
+
else {
|
|
5898
|
+
// shallow ancestor queries by default
|
|
5899
|
+
return query.path.isImmediateParentOf(docPath);
|
|
5771
5900
|
}
|
|
5772
|
-
|
|
5773
|
-
|
|
5901
|
+
}
|
|
5902
|
+
/**
|
|
5903
|
+
* A document must have a value for every ordering clause in order to show up
|
|
5904
|
+
* in the results.
|
|
5905
|
+
*/
|
|
5906
|
+
function queryMatchesOrderBy(query, doc) {
|
|
5907
|
+
for (const orderBy of query.explicitOrderBy) {
|
|
5908
|
+
// order by key always matches
|
|
5909
|
+
if (!orderBy.field.isKeyField() && doc.data.field(orderBy.field) === null) {
|
|
5910
|
+
return false;
|
|
5911
|
+
}
|
|
5774
5912
|
}
|
|
5775
|
-
|
|
5776
|
-
|
|
5913
|
+
return true;
|
|
5914
|
+
}
|
|
5915
|
+
function queryMatchesFilters(query, doc) {
|
|
5916
|
+
for (const filter of query.filters) {
|
|
5917
|
+
if (!filter.matches(doc)) {
|
|
5918
|
+
return false;
|
|
5919
|
+
}
|
|
5777
5920
|
}
|
|
5778
|
-
|
|
5921
|
+
return true;
|
|
5922
|
+
}
|
|
5923
|
+
/** Makes sure a document is within the bounds, if provided. */
|
|
5924
|
+
function queryMatchesBounds(query, doc) {
|
|
5925
|
+
if (query.startAt &&
|
|
5926
|
+
!boundSortsBeforeDocument(query.startAt, queryOrderBy(query), doc)) {
|
|
5779
5927
|
return false;
|
|
5780
5928
|
}
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
}
|
|
5784
|
-
maxKey() {
|
|
5785
|
-
return null;
|
|
5786
|
-
}
|
|
5787
|
-
isRed() {
|
|
5929
|
+
if (query.endAt &&
|
|
5930
|
+
!boundSortsAfterDocument(query.endAt, queryOrderBy(query), doc)) {
|
|
5788
5931
|
return false;
|
|
5789
5932
|
}
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5933
|
+
return true;
|
|
5934
|
+
}
|
|
5935
|
+
/**
|
|
5936
|
+
* Returns the collection group that this query targets.
|
|
5937
|
+
*
|
|
5938
|
+
* PORTING NOTE: This is only used in the Web SDK to facilitate multi-tab
|
|
5939
|
+
* synchronization for query results.
|
|
5940
|
+
*/
|
|
5941
|
+
function queryCollectionGroup(query) {
|
|
5942
|
+
return (query.collectionGroup ||
|
|
5943
|
+
(query.path.length % 2 === 1
|
|
5944
|
+
? query.path.lastSegment()
|
|
5945
|
+
: query.path.get(query.path.length - 2)));
|
|
5946
|
+
}
|
|
5947
|
+
/**
|
|
5948
|
+
* Returns a new comparator function that can be used to compare two documents
|
|
5949
|
+
* based on the Query's ordering constraint.
|
|
5950
|
+
*/
|
|
5951
|
+
function newQueryComparator(query) {
|
|
5952
|
+
return (d1, d2) => {
|
|
5953
|
+
let comparedOnKeyField = false;
|
|
5954
|
+
for (const orderBy of queryOrderBy(query)) {
|
|
5955
|
+
const comp = compareDocs(orderBy, d1, d2);
|
|
5956
|
+
if (comp !== 0) {
|
|
5957
|
+
return comp;
|
|
5958
|
+
}
|
|
5959
|
+
comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
|
|
5960
|
+
}
|
|
5795
5961
|
return 0;
|
|
5962
|
+
};
|
|
5963
|
+
}
|
|
5964
|
+
function compareDocs(orderBy, d1, d2) {
|
|
5965
|
+
const comparison = orderBy.field.isKeyField()
|
|
5966
|
+
? DocumentKey.comparator(d1.key, d2.key)
|
|
5967
|
+
: compareDocumentsByField(orderBy.field, d1, d2);
|
|
5968
|
+
switch (orderBy.dir) {
|
|
5969
|
+
case "asc" /* ASCENDING */:
|
|
5970
|
+
return comparison;
|
|
5971
|
+
case "desc" /* DESCENDING */:
|
|
5972
|
+
return -1 * comparison;
|
|
5973
|
+
default:
|
|
5974
|
+
return fail();
|
|
5796
5975
|
}
|
|
5797
|
-
}
|
|
5798
|
-
LLRBNode.EMPTY = new LLRBEmptyNode();
|
|
5976
|
+
}
|
|
5799
5977
|
|
|
5800
5978
|
/**
|
|
5801
5979
|
* @license
|
|
@@ -5814,218 +5992,97 @@ LLRBNode.EMPTY = new LLRBEmptyNode();
|
|
|
5814
5992
|
* limitations under the License.
|
|
5815
5993
|
*/
|
|
5816
5994
|
/**
|
|
5817
|
-
*
|
|
5818
|
-
*
|
|
5819
|
-
*
|
|
5820
|
-
*
|
|
5821
|
-
* be equal!
|
|
5995
|
+
* A map implementation that uses objects as keys. Objects must have an
|
|
5996
|
+
* associated equals function and must be immutable. Entries in the map are
|
|
5997
|
+
* stored together with the key being produced from the mapKeyFn. This map
|
|
5998
|
+
* automatically handles collisions of keys.
|
|
5822
5999
|
*/
|
|
5823
|
-
class
|
|
5824
|
-
constructor(
|
|
5825
|
-
this.
|
|
5826
|
-
this.
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
}
|
|
5837
|
-
get size() {
|
|
5838
|
-
return this.data.size;
|
|
5839
|
-
}
|
|
5840
|
-
indexOf(elem) {
|
|
5841
|
-
return this.data.indexOf(elem);
|
|
5842
|
-
}
|
|
5843
|
-
/** Iterates elements in order defined by "comparator" */
|
|
5844
|
-
forEach(cb) {
|
|
5845
|
-
this.data.inorderTraversal((k, v) => {
|
|
5846
|
-
cb(k);
|
|
5847
|
-
return false;
|
|
5848
|
-
});
|
|
5849
|
-
}
|
|
5850
|
-
/** Iterates over `elem`s such that: range[0] <= elem < range[1]. */
|
|
5851
|
-
forEachInRange(range, cb) {
|
|
5852
|
-
const iter = this.data.getIteratorFrom(range[0]);
|
|
5853
|
-
while (iter.hasNext()) {
|
|
5854
|
-
const elem = iter.getNext();
|
|
5855
|
-
if (this.comparator(elem.key, range[1]) >= 0) {
|
|
5856
|
-
return;
|
|
5857
|
-
}
|
|
5858
|
-
cb(elem.key);
|
|
5859
|
-
}
|
|
6000
|
+
class ObjectMap {
|
|
6001
|
+
constructor(mapKeyFn, equalsFn) {
|
|
6002
|
+
this.mapKeyFn = mapKeyFn;
|
|
6003
|
+
this.equalsFn = equalsFn;
|
|
6004
|
+
/**
|
|
6005
|
+
* The inner map for a key/value pair. Due to the possibility of collisions we
|
|
6006
|
+
* keep a list of entries that we do a linear search through to find an actual
|
|
6007
|
+
* match. Note that collisions should be rare, so we still expect near
|
|
6008
|
+
* constant time lookups in practice.
|
|
6009
|
+
*/
|
|
6010
|
+
this.inner = {};
|
|
6011
|
+
/** The number of entries stored in the map */
|
|
6012
|
+
this.innerSize = 0;
|
|
5860
6013
|
}
|
|
5861
|
-
/**
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
iter = this.data.getIteratorFrom(start);
|
|
5868
|
-
}
|
|
5869
|
-
else {
|
|
5870
|
-
iter = this.data.getIterator();
|
|
6014
|
+
/** Get a value for this key, or undefined if it does not exist. */
|
|
6015
|
+
get(key) {
|
|
6016
|
+
const id = this.mapKeyFn(key);
|
|
6017
|
+
const matches = this.inner[id];
|
|
6018
|
+
if (matches === undefined) {
|
|
6019
|
+
return undefined;
|
|
5871
6020
|
}
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
if (!result) {
|
|
5876
|
-
return;
|
|
6021
|
+
for (const [otherKey, value] of matches) {
|
|
6022
|
+
if (this.equalsFn(otherKey, key)) {
|
|
6023
|
+
return value;
|
|
5877
6024
|
}
|
|
5878
6025
|
}
|
|
6026
|
+
return undefined;
|
|
5879
6027
|
}
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
const iter = this.data.getIteratorFrom(elem);
|
|
5883
|
-
return iter.hasNext() ? iter.getNext().key : null;
|
|
5884
|
-
}
|
|
5885
|
-
getIterator() {
|
|
5886
|
-
return new SortedSetIterator(this.data.getIterator());
|
|
5887
|
-
}
|
|
5888
|
-
getIteratorFrom(key) {
|
|
5889
|
-
return new SortedSetIterator(this.data.getIteratorFrom(key));
|
|
5890
|
-
}
|
|
5891
|
-
/** Inserts or updates an element */
|
|
5892
|
-
add(elem) {
|
|
5893
|
-
return this.copy(this.data.remove(elem).insert(elem, true));
|
|
6028
|
+
has(key) {
|
|
6029
|
+
return this.get(key) !== undefined;
|
|
5894
6030
|
}
|
|
5895
|
-
/**
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
6031
|
+
/** Put this key and value in the map. */
|
|
6032
|
+
set(key, value) {
|
|
6033
|
+
const id = this.mapKeyFn(key);
|
|
6034
|
+
const matches = this.inner[id];
|
|
6035
|
+
if (matches === undefined) {
|
|
6036
|
+
this.inner[id] = [[key, value]];
|
|
6037
|
+
this.innerSize++;
|
|
6038
|
+
return;
|
|
5899
6039
|
}
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
let result = this;
|
|
5907
|
-
// Make sure `result` always refers to the larger one of the two sets.
|
|
5908
|
-
if (result.size < other.size) {
|
|
5909
|
-
result = other;
|
|
5910
|
-
other = this;
|
|
6040
|
+
for (let i = 0; i < matches.length; i++) {
|
|
6041
|
+
if (this.equalsFn(matches[i][0], key)) {
|
|
6042
|
+
// This is updating an existing entry and does not increase `innerSize`.
|
|
6043
|
+
matches[i] = [key, value];
|
|
6044
|
+
return;
|
|
6045
|
+
}
|
|
5911
6046
|
}
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
});
|
|
5915
|
-
return result;
|
|
6047
|
+
matches.push([key, value]);
|
|
6048
|
+
this.innerSize++;
|
|
5916
6049
|
}
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
6050
|
+
/**
|
|
6051
|
+
* Remove this key from the map. Returns a boolean if anything was deleted.
|
|
6052
|
+
*/
|
|
6053
|
+
delete(key) {
|
|
6054
|
+
const id = this.mapKeyFn(key);
|
|
6055
|
+
const matches = this.inner[id];
|
|
6056
|
+
if (matches === undefined) {
|
|
5922
6057
|
return false;
|
|
5923
6058
|
}
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
6059
|
+
for (let i = 0; i < matches.length; i++) {
|
|
6060
|
+
if (this.equalsFn(matches[i][0], key)) {
|
|
6061
|
+
if (matches.length === 1) {
|
|
6062
|
+
delete this.inner[id];
|
|
6063
|
+
}
|
|
6064
|
+
else {
|
|
6065
|
+
matches.splice(i, 1);
|
|
6066
|
+
}
|
|
6067
|
+
this.innerSize--;
|
|
6068
|
+
return true;
|
|
5931
6069
|
}
|
|
5932
6070
|
}
|
|
5933
|
-
return
|
|
6071
|
+
return false;
|
|
5934
6072
|
}
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
6073
|
+
forEach(fn) {
|
|
6074
|
+
forEach(this.inner, (_, entries) => {
|
|
6075
|
+
for (const [k, v] of entries) {
|
|
6076
|
+
fn(k, v);
|
|
6077
|
+
}
|
|
5939
6078
|
});
|
|
5940
|
-
return res;
|
|
5941
|
-
}
|
|
5942
|
-
toString() {
|
|
5943
|
-
const result = [];
|
|
5944
|
-
this.forEach(elem => result.push(elem));
|
|
5945
|
-
return 'SortedSet(' + result.toString() + ')';
|
|
5946
|
-
}
|
|
5947
|
-
copy(data) {
|
|
5948
|
-
const result = new SortedSet(this.comparator);
|
|
5949
|
-
result.data = data;
|
|
5950
|
-
return result;
|
|
5951
6079
|
}
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
constructor(iter) {
|
|
5955
|
-
this.iter = iter;
|
|
5956
|
-
}
|
|
5957
|
-
getNext() {
|
|
5958
|
-
return this.iter.getNext().key;
|
|
5959
|
-
}
|
|
5960
|
-
hasNext() {
|
|
5961
|
-
return this.iter.hasNext();
|
|
6080
|
+
isEmpty() {
|
|
6081
|
+
return isEmpty(this.inner);
|
|
5962
6082
|
}
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
* Compares two sorted sets for equality using their natural ordering. The
|
|
5966
|
-
* method computes the intersection and invokes `onAdd` for every element that
|
|
5967
|
-
* is in `after` but not `before`. `onRemove` is invoked for every element in
|
|
5968
|
-
* `before` but missing from `after`.
|
|
5969
|
-
*
|
|
5970
|
-
* The method creates a copy of both `before` and `after` and runs in O(n log
|
|
5971
|
-
* n), where n is the size of the two lists.
|
|
5972
|
-
*
|
|
5973
|
-
* @param before - The elements that exist in the original set.
|
|
5974
|
-
* @param after - The elements to diff against the original set.
|
|
5975
|
-
* @param comparator - The comparator for the elements in before and after.
|
|
5976
|
-
* @param onAdd - A function to invoke for every element that is part of `
|
|
5977
|
-
* after` but not `before`.
|
|
5978
|
-
* @param onRemove - A function to invoke for every element that is part of
|
|
5979
|
-
* `before` but not `after`.
|
|
5980
|
-
*/
|
|
5981
|
-
function diffSortedSets(before, after, comparator, onAdd, onRemove) {
|
|
5982
|
-
const beforeIt = before.getIterator();
|
|
5983
|
-
const afterIt = after.getIterator();
|
|
5984
|
-
let beforeValue = advanceIterator(beforeIt);
|
|
5985
|
-
let afterValue = advanceIterator(afterIt);
|
|
5986
|
-
// Walk through the two sets at the same time, using the ordering defined by
|
|
5987
|
-
// `comparator`.
|
|
5988
|
-
while (beforeValue || afterValue) {
|
|
5989
|
-
let added = false;
|
|
5990
|
-
let removed = false;
|
|
5991
|
-
if (beforeValue && afterValue) {
|
|
5992
|
-
const cmp = comparator(beforeValue, afterValue);
|
|
5993
|
-
if (cmp < 0) {
|
|
5994
|
-
// The element was removed if the next element in our ordered
|
|
5995
|
-
// walkthrough is only in `before`.
|
|
5996
|
-
removed = true;
|
|
5997
|
-
}
|
|
5998
|
-
else if (cmp > 0) {
|
|
5999
|
-
// The element was added if the next element in our ordered walkthrough
|
|
6000
|
-
// is only in `after`.
|
|
6001
|
-
added = true;
|
|
6002
|
-
}
|
|
6003
|
-
}
|
|
6004
|
-
else if (beforeValue != null) {
|
|
6005
|
-
removed = true;
|
|
6006
|
-
}
|
|
6007
|
-
else {
|
|
6008
|
-
added = true;
|
|
6009
|
-
}
|
|
6010
|
-
if (added) {
|
|
6011
|
-
onAdd(afterValue);
|
|
6012
|
-
afterValue = advanceIterator(afterIt);
|
|
6013
|
-
}
|
|
6014
|
-
else if (removed) {
|
|
6015
|
-
onRemove(beforeValue);
|
|
6016
|
-
beforeValue = advanceIterator(beforeIt);
|
|
6017
|
-
}
|
|
6018
|
-
else {
|
|
6019
|
-
beforeValue = advanceIterator(beforeIt);
|
|
6020
|
-
afterValue = advanceIterator(afterIt);
|
|
6021
|
-
}
|
|
6083
|
+
size() {
|
|
6084
|
+
return this.innerSize;
|
|
6022
6085
|
}
|
|
6023
|
-
}
|
|
6024
|
-
/**
|
|
6025
|
-
* Returns the next element from the iterator or `undefined` if none available.
|
|
6026
|
-
*/
|
|
6027
|
-
function advanceIterator(it) {
|
|
6028
|
-
return it.hasNext() ? it.getNext() : undefined;
|
|
6029
6086
|
}
|
|
6030
6087
|
|
|
6031
6088
|
/**
|
|
@@ -6049,8 +6106,12 @@ function mutableDocumentMap() {
|
|
|
6049
6106
|
return EMPTY_MUTABLE_DOCUMENT_MAP;
|
|
6050
6107
|
}
|
|
6051
6108
|
const EMPTY_DOCUMENT_MAP = new SortedMap(DocumentKey.comparator);
|
|
6052
|
-
function documentMap() {
|
|
6053
|
-
|
|
6109
|
+
function documentMap(...docs) {
|
|
6110
|
+
let map = EMPTY_DOCUMENT_MAP;
|
|
6111
|
+
for (const doc of docs) {
|
|
6112
|
+
map = map.insert(doc.key, doc);
|
|
6113
|
+
}
|
|
6114
|
+
return map;
|
|
6054
6115
|
}
|
|
6055
6116
|
function newOverlayMap() {
|
|
6056
6117
|
return new ObjectMap(key => key.toString(), (l, r) => l.isEqual(r));
|
|
@@ -9943,9 +10004,9 @@ class MemoryIndexManager {
|
|
|
9943
10004
|
// Field indices are not supported with memory persistence.
|
|
9944
10005
|
return PersistencePromise.resolve(null);
|
|
9945
10006
|
}
|
|
9946
|
-
|
|
10007
|
+
getIndexType(transaction, target) {
|
|
9947
10008
|
// Field indices are not supported with memory persistence.
|
|
9948
|
-
return PersistencePromise.resolve(
|
|
10009
|
+
return PersistencePromise.resolve(0 /* NONE */);
|
|
9949
10010
|
}
|
|
9950
10011
|
getFieldIndexes(transaction, collectionGroup) {
|
|
9951
10012
|
// Field indices are not supported with memory persistence.
|
|
@@ -9955,6 +10016,9 @@ class MemoryIndexManager {
|
|
|
9955
10016
|
// Field indices are not supported with memory persistence.
|
|
9956
10017
|
return PersistencePromise.resolve(null);
|
|
9957
10018
|
}
|
|
10019
|
+
getMinOffset(transaction, target) {
|
|
10020
|
+
return PersistencePromise.resolve(IndexOffset.min());
|
|
10021
|
+
}
|
|
9958
10022
|
updateCollectionGroup(transaction, collectionGroup, offset) {
|
|
9959
10023
|
// Field indices are not supported with memory persistence.
|
|
9960
10024
|
return PersistencePromise.resolve();
|
|
@@ -10021,8 +10085,9 @@ const EMPTY_VALUE = new Uint8Array(0);
|
|
|
10021
10085
|
* data as it supports multi-tab access.
|
|
10022
10086
|
*/
|
|
10023
10087
|
class IndexedDbIndexManager {
|
|
10024
|
-
constructor(user) {
|
|
10088
|
+
constructor(user, databaseId) {
|
|
10025
10089
|
this.user = user;
|
|
10090
|
+
this.databaseId = databaseId;
|
|
10026
10091
|
/**
|
|
10027
10092
|
* An in-memory copy of the index entries we've already written since the SDK
|
|
10028
10093
|
* launched. Used to avoid re-writing the same entry repeatedly.
|
|
@@ -10118,7 +10183,8 @@ class IndexedDbIndexManager {
|
|
|
10118
10183
|
return PersistencePromise.resolve(null);
|
|
10119
10184
|
}
|
|
10120
10185
|
else {
|
|
10121
|
-
let
|
|
10186
|
+
let existingKeys = documentKeySet();
|
|
10187
|
+
const result = [];
|
|
10122
10188
|
return PersistencePromise.forEach(indexes, (index, subTarget) => {
|
|
10123
10189
|
logDebug(LOG_TAG$f, `Using index ${fieldIndexToString(index)} to execute ${canonifyTarget(target)}`);
|
|
10124
10190
|
const arrayValues = targetGetArrayValues(subTarget, index);
|
|
@@ -10134,7 +10200,11 @@ class IndexedDbIndexManager {
|
|
|
10134
10200
|
.loadFirst(indexRange, target.limit)
|
|
10135
10201
|
.next(entries => {
|
|
10136
10202
|
entries.forEach(entry => {
|
|
10137
|
-
|
|
10203
|
+
const documentKey = DocumentKey.fromSegments(entry.documentKey);
|
|
10204
|
+
if (!existingKeys.has(documentKey)) {
|
|
10205
|
+
existingKeys = existingKeys.add(documentKey);
|
|
10206
|
+
result.push(documentKey);
|
|
10207
|
+
}
|
|
10138
10208
|
});
|
|
10139
10209
|
});
|
|
10140
10210
|
});
|
|
@@ -10175,9 +10245,9 @@ class IndexedDbIndexManager {
|
|
|
10175
10245
|
const upperBound = upperBounds
|
|
10176
10246
|
? this.generateUpperBound(indexId, arrayValue, upperBounds[i % scansPerArrayElement], upperBoundInclusive)
|
|
10177
10247
|
: this.generateEmptyBound(indexId + 1);
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10248
|
+
const notInBound = notInValues.map(notIn => this.generateLowerBound(indexId, arrayValue, notIn,
|
|
10249
|
+
/* inclusive= */ true));
|
|
10250
|
+
indexRanges.push(...this.createRange(lowerBound, upperBound, notInBound));
|
|
10181
10251
|
}
|
|
10182
10252
|
return indexRanges;
|
|
10183
10253
|
}
|
|
@@ -10204,12 +10274,32 @@ class IndexedDbIndexManager {
|
|
|
10204
10274
|
? target.collectionGroup
|
|
10205
10275
|
: target.path.lastSegment();
|
|
10206
10276
|
return this.getFieldIndexes(transaction, collectionGroup).next(indexes => {
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10277
|
+
// Return the index with the most number of segments.
|
|
10278
|
+
let index = null;
|
|
10279
|
+
for (const candidate of indexes) {
|
|
10280
|
+
const matches = targetIndexMatcher.servedByIndex(candidate);
|
|
10281
|
+
if (matches &&
|
|
10282
|
+
(!index || candidate.fields.length > index.fields.length)) {
|
|
10283
|
+
index = candidate;
|
|
10284
|
+
}
|
|
10285
|
+
}
|
|
10286
|
+
return index;
|
|
10211
10287
|
});
|
|
10212
10288
|
}
|
|
10289
|
+
getIndexType(transaction, target) {
|
|
10290
|
+
let indexType = 2 /* FULL */;
|
|
10291
|
+
return PersistencePromise.forEach(this.getSubTargets(target), (target) => {
|
|
10292
|
+
return this.getFieldIndex(transaction, target).next(index => {
|
|
10293
|
+
if (!index) {
|
|
10294
|
+
indexType = 0 /* NONE */;
|
|
10295
|
+
}
|
|
10296
|
+
else if (indexType !== 0 /* NONE */ &&
|
|
10297
|
+
index.fields.length < targetGetSegmentCount(target)) {
|
|
10298
|
+
indexType = 1 /* PARTIAL */;
|
|
10299
|
+
}
|
|
10300
|
+
});
|
|
10301
|
+
}).next(() => indexType);
|
|
10302
|
+
}
|
|
10213
10303
|
/**
|
|
10214
10304
|
* Returns the byte encoded form of the directional values in the field index.
|
|
10215
10305
|
* Returns `null` if the document does not have all fields specified in the
|
|
@@ -10233,6 +10323,15 @@ class IndexedDbIndexManager {
|
|
|
10233
10323
|
FirestoreIndexValueWriter.INSTANCE.writeIndexValue(value, encoder.forKind(0 /* ASCENDING */));
|
|
10234
10324
|
return encoder.encodedBytes();
|
|
10235
10325
|
}
|
|
10326
|
+
/**
|
|
10327
|
+
* Returns an encoded form of the document key that sorts based on the key
|
|
10328
|
+
* ordering of the field index.
|
|
10329
|
+
*/
|
|
10330
|
+
encodeDirectionalKey(fieldIndex, documentKey) {
|
|
10331
|
+
const encoder = new IndexByteEncoder();
|
|
10332
|
+
FirestoreIndexValueWriter.INSTANCE.writeIndexValue(refValue(this.databaseId, documentKey), encoder.forKind(fieldIndexGetKeyOrder(fieldIndex)));
|
|
10333
|
+
return encoder.encodedBytes();
|
|
10334
|
+
}
|
|
10236
10335
|
/**
|
|
10237
10336
|
* Encodes the given field values according to the specification in `target`.
|
|
10238
10337
|
* For IN queries, a list of possible values is returned.
|
|
@@ -10356,7 +10455,7 @@ class IndexedDbIndexManager {
|
|
|
10356
10455
|
return this.getExistingIndexEntries(transaction, key, fieldIndex).next(existingEntries => {
|
|
10357
10456
|
const newEntries = this.computeIndexEntries(doc, fieldIndex);
|
|
10358
10457
|
if (!existingEntries.isEqual(newEntries)) {
|
|
10359
|
-
return this.updateEntries(transaction, doc, existingEntries, newEntries);
|
|
10458
|
+
return this.updateEntries(transaction, doc, fieldIndex, existingEntries, newEntries);
|
|
10360
10459
|
}
|
|
10361
10460
|
return PersistencePromise.resolve();
|
|
10362
10461
|
});
|
|
@@ -10364,24 +10463,26 @@ class IndexedDbIndexManager {
|
|
|
10364
10463
|
});
|
|
10365
10464
|
});
|
|
10366
10465
|
}
|
|
10367
|
-
addIndexEntry(transaction, document, indexEntry) {
|
|
10466
|
+
addIndexEntry(transaction, document, fieldIndex, indexEntry) {
|
|
10368
10467
|
const indexEntries = indexEntriesStore(transaction);
|
|
10369
10468
|
return indexEntries.put({
|
|
10370
10469
|
indexId: indexEntry.indexId,
|
|
10371
10470
|
uid: this.uid,
|
|
10372
10471
|
arrayValue: indexEntry.arrayValue,
|
|
10373
10472
|
directionalValue: indexEntry.directionalValue,
|
|
10374
|
-
|
|
10473
|
+
orderedDocumentKey: this.encodeDirectionalKey(fieldIndex, document.key),
|
|
10474
|
+
documentKey: document.key.path.toArray()
|
|
10375
10475
|
});
|
|
10376
10476
|
}
|
|
10377
|
-
deleteIndexEntry(transaction, document, indexEntry) {
|
|
10477
|
+
deleteIndexEntry(transaction, document, fieldIndex, indexEntry) {
|
|
10378
10478
|
const indexEntries = indexEntriesStore(transaction);
|
|
10379
10479
|
return indexEntries.delete([
|
|
10380
10480
|
indexEntry.indexId,
|
|
10381
10481
|
this.uid,
|
|
10382
10482
|
indexEntry.arrayValue,
|
|
10383
10483
|
indexEntry.directionalValue,
|
|
10384
|
-
|
|
10484
|
+
this.encodeDirectionalKey(fieldIndex, document.key),
|
|
10485
|
+
document.key.path.toArray()
|
|
10385
10486
|
]);
|
|
10386
10487
|
}
|
|
10387
10488
|
getExistingIndexEntries(transaction, documentKey, fieldIndex) {
|
|
@@ -10393,7 +10494,7 @@ class IndexedDbIndexManager {
|
|
|
10393
10494
|
range: IDBKeyRange.only([
|
|
10394
10495
|
fieldIndex.indexId,
|
|
10395
10496
|
this.uid,
|
|
10396
|
-
|
|
10497
|
+
this.encodeDirectionalKey(fieldIndex, documentKey)
|
|
10397
10498
|
])
|
|
10398
10499
|
}, (_, entry) => {
|
|
10399
10500
|
results = results.add(new IndexEntry(fieldIndex.indexId, documentKey, entry.arrayValue, entry.directionalValue));
|
|
@@ -10426,15 +10527,15 @@ class IndexedDbIndexManager {
|
|
|
10426
10527
|
* that are no longer referenced in `newEntries` and adding all newly added
|
|
10427
10528
|
* entries.
|
|
10428
10529
|
*/
|
|
10429
|
-
updateEntries(transaction, document, existingEntries, newEntries) {
|
|
10530
|
+
updateEntries(transaction, document, fieldIndex, existingEntries, newEntries) {
|
|
10430
10531
|
logDebug(LOG_TAG$f, "Updating index entries for document '%s'", document.key);
|
|
10431
10532
|
const promises = [];
|
|
10432
10533
|
diffSortedSets(existingEntries, newEntries, indexEntryComparator,
|
|
10433
10534
|
/* onAdd= */ entry => {
|
|
10434
|
-
promises.push(this.addIndexEntry(transaction, document, entry));
|
|
10535
|
+
promises.push(this.addIndexEntry(transaction, document, fieldIndex, entry));
|
|
10435
10536
|
},
|
|
10436
10537
|
/* onRemove= */ entry => {
|
|
10437
|
-
promises.push(this.deleteIndexEntry(transaction, document, entry));
|
|
10538
|
+
promises.push(this.deleteIndexEntry(transaction, document, fieldIndex, entry));
|
|
10438
10539
|
});
|
|
10439
10540
|
return PersistencePromise.waitFor(promises);
|
|
10440
10541
|
}
|
|
@@ -10458,7 +10559,7 @@ class IndexedDbIndexManager {
|
|
|
10458
10559
|
* '[foo > 2 && foo != 3]` becomes `[foo > 2 && < 3, foo > 3]`.
|
|
10459
10560
|
*/
|
|
10460
10561
|
createRange(lower, upper, notInValues) {
|
|
10461
|
-
// The
|
|
10562
|
+
// The notIn values need to be sorted and unique so that we can return a
|
|
10462
10563
|
// sorted set of non-overlapping ranges.
|
|
10463
10564
|
notInValues = notInValues
|
|
10464
10565
|
.sort((l, r) => indexEntryComparator(l, r))
|
|
@@ -10491,17 +10592,33 @@ class IndexedDbIndexManager {
|
|
|
10491
10592
|
this.uid,
|
|
10492
10593
|
bounds[i].arrayValue,
|
|
10493
10594
|
bounds[i].directionalValue,
|
|
10494
|
-
|
|
10595
|
+
EMPTY_VALUE,
|
|
10596
|
+
[]
|
|
10495
10597
|
], [
|
|
10496
10598
|
bounds[i + 1].indexId,
|
|
10497
10599
|
this.uid,
|
|
10498
10600
|
bounds[i + 1].arrayValue,
|
|
10499
10601
|
bounds[i + 1].directionalValue,
|
|
10500
|
-
|
|
10602
|
+
EMPTY_VALUE,
|
|
10603
|
+
[]
|
|
10501
10604
|
]));
|
|
10502
10605
|
}
|
|
10503
10606
|
return ranges;
|
|
10504
10607
|
}
|
|
10608
|
+
getMinOffset(transaction, target) {
|
|
10609
|
+
let offset;
|
|
10610
|
+
return PersistencePromise.forEach(this.getSubTargets(target), (target) => {
|
|
10611
|
+
return this.getFieldIndex(transaction, target).next(index => {
|
|
10612
|
+
if (!index) {
|
|
10613
|
+
offset = IndexOffset.min();
|
|
10614
|
+
}
|
|
10615
|
+
else if (!offset ||
|
|
10616
|
+
indexOffsetComparator(index.indexState.offset, offset) < 0) {
|
|
10617
|
+
offset = index.indexState.offset;
|
|
10618
|
+
}
|
|
10619
|
+
});
|
|
10620
|
+
}).next(() => offset);
|
|
10621
|
+
}
|
|
10505
10622
|
}
|
|
10506
10623
|
/**
|
|
10507
10624
|
* Helper to get a typed SimpleDbStore for the collectionParents
|
|
@@ -12251,12 +12368,31 @@ function dbCollectionGroupKey(collectionGroup, offset) {
|
|
|
12251
12368
|
}
|
|
12252
12369
|
/**
|
|
12253
12370
|
* Comparator that compares document keys according to the primary key sorting
|
|
12254
|
-
* used by the `DbRemoteDocumentDocument` store (by
|
|
12255
|
-
* document ID).
|
|
12371
|
+
* used by the `DbRemoteDocumentDocument` store (by prefix path, collection id
|
|
12372
|
+
* and then document ID).
|
|
12373
|
+
*
|
|
12374
|
+
* Visible for testing.
|
|
12256
12375
|
*/
|
|
12257
12376
|
function dbKeyComparator(l, r) {
|
|
12258
|
-
const
|
|
12259
|
-
|
|
12377
|
+
const left = l.path.toArray();
|
|
12378
|
+
const right = r.path.toArray();
|
|
12379
|
+
// The ordering is based on https://chromium.googlesource.com/chromium/blink/+/fe5c21fef94dae71c1c3344775b8d8a7f7e6d9ec/Source/modules/indexeddb/IDBKey.cpp#74
|
|
12380
|
+
let cmp = 0;
|
|
12381
|
+
for (let i = 0; i < left.length - 2 && i < right.length - 2; ++i) {
|
|
12382
|
+
cmp = primitiveComparator(left[i], right[i]);
|
|
12383
|
+
if (cmp) {
|
|
12384
|
+
return cmp;
|
|
12385
|
+
}
|
|
12386
|
+
}
|
|
12387
|
+
cmp = primitiveComparator(left.length, right.length);
|
|
12388
|
+
if (cmp) {
|
|
12389
|
+
return cmp;
|
|
12390
|
+
}
|
|
12391
|
+
cmp = primitiveComparator(left[left.length - 2], right[right.length - 2]);
|
|
12392
|
+
if (cmp) {
|
|
12393
|
+
return cmp;
|
|
12394
|
+
}
|
|
12395
|
+
return primitiveComparator(left[left.length - 1], right[right.length - 1]);
|
|
12260
12396
|
}
|
|
12261
12397
|
/**
|
|
12262
12398
|
* Schema Version for the Web client:
|
|
@@ -13157,7 +13293,7 @@ class IndexedDbPersistence {
|
|
|
13157
13293
|
return this.remoteDocumentCache;
|
|
13158
13294
|
}
|
|
13159
13295
|
getIndexManager(user) {
|
|
13160
|
-
return new IndexedDbIndexManager(user);
|
|
13296
|
+
return new IndexedDbIndexManager(user, this.serializer.remoteSerializer.databaseId);
|
|
13161
13297
|
}
|
|
13162
13298
|
getDocumentOverlayCache(user) {
|
|
13163
13299
|
return IndexedDbDocumentOverlayCache.forUser(this.serializer, user);
|
|
@@ -13653,7 +13789,7 @@ class LocalStoreImpl {
|
|
|
13653
13789
|
this.mutationQueue = this.persistence.getMutationQueue(user, this.indexManager);
|
|
13654
13790
|
this.localDocuments = new LocalDocumentsView(this.remoteDocuments, this.mutationQueue, this.indexManager);
|
|
13655
13791
|
this.remoteDocuments.setIndexManager(this.indexManager);
|
|
13656
|
-
this.queryEngine.
|
|
13792
|
+
this.queryEngine.initialize(this.localDocuments, this.indexManager);
|
|
13657
13793
|
}
|
|
13658
13794
|
collectGarbage(garbageCollector) {
|
|
13659
13795
|
return this.persistence.runTransaction('Collect garbage', 'readwrite-primary', txn => garbageCollector.collect(txn, this.targetDataByTarget));
|
|
@@ -15395,10 +15531,30 @@ class MemoryEagerDelegate {
|
|
|
15395
15531
|
* limitations under the License.
|
|
15396
15532
|
*/
|
|
15397
15533
|
/**
|
|
15398
|
-
*
|
|
15399
|
-
*
|
|
15400
|
-
*
|
|
15401
|
-
*
|
|
15534
|
+
* The Firestore query engine.
|
|
15535
|
+
*
|
|
15536
|
+
* Firestore queries can be executed in three modes. The Query Engine determines
|
|
15537
|
+
* what mode to use based on what data is persisted. The mode only determines
|
|
15538
|
+
* the runtime complexity of the query - the result set is equivalent across all
|
|
15539
|
+
* implementations.
|
|
15540
|
+
*
|
|
15541
|
+
* The Query engine will use indexed-based execution if a user has configured
|
|
15542
|
+
* any index that can be used to execute query (via `setIndexConfiguration()`).
|
|
15543
|
+
* Otherwise, the engine will try to optimize the query by re-using a previously
|
|
15544
|
+
* persisted query result. If that is not possible, the query will be executed
|
|
15545
|
+
* via a full collection scan.
|
|
15546
|
+
*
|
|
15547
|
+
* Index-based execution is the default when available. The query engine
|
|
15548
|
+
* supports partial indexed execution and merges the result from the index
|
|
15549
|
+
* lookup with documents that have not yet been indexed. The index evaluation
|
|
15550
|
+
* matches the backend's format and as such, the SDK can use indexing for all
|
|
15551
|
+
* queries that the backend supports.
|
|
15552
|
+
*
|
|
15553
|
+
* If no index exists, the query engine tries to take advantage of the target
|
|
15554
|
+
* document mapping in the TargetCache. These mappings exists for all queries
|
|
15555
|
+
* that have been synced with the backend at least once and allow the query
|
|
15556
|
+
* engine to only read documents that previously matched a query plus any
|
|
15557
|
+
* documents that were edited after the query was last listened to.
|
|
15402
15558
|
*
|
|
15403
15559
|
* There are some cases when this optimization is not guaranteed to produce
|
|
15404
15560
|
* the same results as full collection scans. In these cases, query
|
|
@@ -15413,16 +15569,41 @@ class MemoryEagerDelegate {
|
|
|
15413
15569
|
* - Queries that have never been CURRENT or free of limbo documents.
|
|
15414
15570
|
*/
|
|
15415
15571
|
class QueryEngine {
|
|
15572
|
+
constructor() {
|
|
15573
|
+
this.initialized = false;
|
|
15574
|
+
}
|
|
15416
15575
|
/** Sets the document view to query against. */
|
|
15417
|
-
|
|
15576
|
+
initialize(localDocuments, indexManager) {
|
|
15418
15577
|
this.localDocumentsView = localDocuments;
|
|
15578
|
+
this.indexManager = indexManager;
|
|
15579
|
+
this.initialized = true;
|
|
15419
15580
|
}
|
|
15420
15581
|
/** Returns all local documents matching the specified query. */
|
|
15421
15582
|
getDocumentsMatchingQuery(transaction, query, lastLimboFreeSnapshotVersion, remoteKeys) {
|
|
15422
|
-
|
|
15423
|
-
|
|
15424
|
-
|
|
15425
|
-
|
|
15583
|
+
return this.performQueryUsingIndex(transaction, query)
|
|
15584
|
+
.next(result => result
|
|
15585
|
+
? result
|
|
15586
|
+
: this.performQueryUsingRemoteKeys(transaction, query, remoteKeys, lastLimboFreeSnapshotVersion))
|
|
15587
|
+
.next(result => result ? result : this.executeFullCollectionScan(transaction, query));
|
|
15588
|
+
}
|
|
15589
|
+
/**
|
|
15590
|
+
* Performs an indexed query that evaluates the query based on a collection's
|
|
15591
|
+
* persisted index values. Returns `null` if an index is not available.
|
|
15592
|
+
*/
|
|
15593
|
+
performQueryUsingIndex(transaction, query) {
|
|
15594
|
+
{
|
|
15595
|
+
return PersistencePromise.resolve(null);
|
|
15596
|
+
}
|
|
15597
|
+
}
|
|
15598
|
+
/**
|
|
15599
|
+
* Performs a query based on the target's persisted query mapping. Returns
|
|
15600
|
+
* `null` if the mapping is not available or cannot be used.
|
|
15601
|
+
*/
|
|
15602
|
+
performQueryUsingRemoteKeys(transaction, query, remoteKeys, lastLimboFreeSnapshotVersion) {
|
|
15603
|
+
if (queryMatchesAllDocuments(query)) {
|
|
15604
|
+
// Queries that match all documents don't benefit from using
|
|
15605
|
+
// key-based lookups. It is more efficient to scan all documents in a
|
|
15606
|
+
// collection, rather than to perform individual lookups.
|
|
15426
15607
|
return this.executeFullCollectionScan(transaction, query);
|
|
15427
15608
|
}
|
|
15428
15609
|
// Queries that have never seen a snapshot without limbo free documents
|
|
@@ -15432,8 +15613,7 @@ class QueryEngine {
|
|
|
15432
15613
|
}
|
|
15433
15614
|
return this.localDocumentsView.getDocuments(transaction, remoteKeys).next(documents => {
|
|
15434
15615
|
const previousResults = this.applyQuery(query, documents);
|
|
15435
|
-
if ((
|
|
15436
|
-
this.needsRefill(query.limitType, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
|
|
15616
|
+
if (this.needsRefill(query, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
|
|
15437
15617
|
return this.executeFullCollectionScan(transaction, query);
|
|
15438
15618
|
}
|
|
15439
15619
|
if (getLogLevel() <= LogLevel.DEBUG) {
|
|
@@ -15441,15 +15621,7 @@ class QueryEngine {
|
|
|
15441
15621
|
}
|
|
15442
15622
|
// Retrieve all results for documents that were updated since the last
|
|
15443
15623
|
// limbo-document free remote snapshot.
|
|
15444
|
-
return this.
|
|
15445
|
-
// We merge `previousResults` into `updateResults`, since
|
|
15446
|
-
// `updateResults` is already a DocumentMap. If a document is
|
|
15447
|
-
// contained in both lists, then its contents are the same.
|
|
15448
|
-
previousResults.forEach(doc => {
|
|
15449
|
-
updatedResults = updatedResults.insert(doc.key, doc);
|
|
15450
|
-
});
|
|
15451
|
-
return updatedResults;
|
|
15452
|
-
});
|
|
15624
|
+
return this.appendRemainingResults(transaction, previousResults, query, newIndexOffsetSuccessorFromReadTime(lastLimboFreeSnapshotVersion, INITIAL_LARGEST_BATCH_ID));
|
|
15453
15625
|
});
|
|
15454
15626
|
}
|
|
15455
15627
|
/** Applies the query filter and sorting to the provided documents. */
|
|
@@ -15468,6 +15640,7 @@ class QueryEngine {
|
|
|
15468
15640
|
* Determines if a limit query needs to be refilled from cache, making it
|
|
15469
15641
|
* ineligible for index-free execution.
|
|
15470
15642
|
*
|
|
15643
|
+
* @param query The query.
|
|
15471
15644
|
* @param sortedPreviousResults - The documents that matched the query when it
|
|
15472
15645
|
* was last synchronized, sorted by the query's comparator.
|
|
15473
15646
|
* @param remoteKeys - The document keys that matched the query at the last
|
|
@@ -15475,10 +15648,14 @@ class QueryEngine {
|
|
|
15475
15648
|
* @param limboFreeSnapshotVersion - The version of the snapshot when the
|
|
15476
15649
|
* query was last synchronized.
|
|
15477
15650
|
*/
|
|
15478
|
-
needsRefill(
|
|
15479
|
-
|
|
15480
|
-
|
|
15651
|
+
needsRefill(query, sortedPreviousResults, remoteKeys, limboFreeSnapshotVersion) {
|
|
15652
|
+
if (query.limit === null) {
|
|
15653
|
+
// Queries without limits do not need to be refilled.
|
|
15654
|
+
return false;
|
|
15655
|
+
}
|
|
15481
15656
|
if (remoteKeys.size !== sortedPreviousResults.size) {
|
|
15657
|
+
// The query needs to be refilled if a previously matching document no
|
|
15658
|
+
// longer matches.
|
|
15482
15659
|
return true;
|
|
15483
15660
|
}
|
|
15484
15661
|
// Limit queries are not eligible for index-free query execution if there is
|
|
@@ -15489,7 +15666,7 @@ class QueryEngine {
|
|
|
15489
15666
|
// the boundary of the limit itself did not change and documents from cache
|
|
15490
15667
|
// will continue to be "rejected" by this boundary. Therefore, we can ignore
|
|
15491
15668
|
// any modifications that don't affect the last document.
|
|
15492
|
-
const docAtLimitEdge = limitType === "F" /* First */
|
|
15669
|
+
const docAtLimitEdge = query.limitType === "F" /* First */
|
|
15493
15670
|
? sortedPreviousResults.last()
|
|
15494
15671
|
: sortedPreviousResults.first();
|
|
15495
15672
|
if (!docAtLimitEdge) {
|
|
@@ -15505,6 +15682,22 @@ class QueryEngine {
|
|
|
15505
15682
|
}
|
|
15506
15683
|
return this.localDocumentsView.getDocumentsMatchingQuery(transaction, query, IndexOffset.min());
|
|
15507
15684
|
}
|
|
15685
|
+
/**
|
|
15686
|
+
* Combines the results from an indexed execution with the remaining documents
|
|
15687
|
+
* that have not yet been indexed.
|
|
15688
|
+
*/
|
|
15689
|
+
appendRemainingResults(transaction, indexedResults, query, offset) {
|
|
15690
|
+
// Retrieve all results for documents that were updated since the offset.
|
|
15691
|
+
return this.localDocumentsView
|
|
15692
|
+
.getDocumentsMatchingQuery(transaction, query, offset)
|
|
15693
|
+
.next(remainingResults => {
|
|
15694
|
+
// Merge with existing results
|
|
15695
|
+
indexedResults.forEach(d => {
|
|
15696
|
+
remainingResults = remainingResults.insert(d.key, d);
|
|
15697
|
+
});
|
|
15698
|
+
return remainingResults;
|
|
15699
|
+
});
|
|
15700
|
+
}
|
|
15508
15701
|
}
|
|
15509
15702
|
|
|
15510
15703
|
/**
|
|
@@ -21764,10 +21957,12 @@ class View {
|
|
|
21764
21957
|
//
|
|
21765
21958
|
// Note that this should never get used in a refill (when previousChanges is
|
|
21766
21959
|
// set), because there will only be adds -- no deletes or updates.
|
|
21767
|
-
const lastDocInLimit =
|
|
21960
|
+
const lastDocInLimit = this.query.limitType === "F" /* First */ &&
|
|
21961
|
+
oldDocumentSet.size === this.query.limit
|
|
21768
21962
|
? oldDocumentSet.last()
|
|
21769
21963
|
: null;
|
|
21770
|
-
const firstDocInLimit =
|
|
21964
|
+
const firstDocInLimit = this.query.limitType === "L" /* Last */ &&
|
|
21965
|
+
oldDocumentSet.size === this.query.limit
|
|
21771
21966
|
? oldDocumentSet.first()
|
|
21772
21967
|
: null;
|
|
21773
21968
|
docChanges.inorderTraversal((key, entry) => {
|
|
@@ -21840,9 +22035,9 @@ class View {
|
|
|
21840
22035
|
}
|
|
21841
22036
|
});
|
|
21842
22037
|
// Drop documents out to meet limit/limitToLast requirement.
|
|
21843
|
-
if (
|
|
22038
|
+
if (this.query.limit !== null) {
|
|
21844
22039
|
while (newDocumentSet.size > this.query.limit) {
|
|
21845
|
-
const oldDoc =
|
|
22040
|
+
const oldDoc = this.query.limitType === "F" /* First */
|
|
21846
22041
|
? newDocumentSet.last()
|
|
21847
22042
|
: newDocumentSet.first();
|
|
21848
22043
|
newDocumentSet = newDocumentSet.delete(oldDoc.key);
|
|
@@ -26973,7 +27168,8 @@ function snapshotEqual(left, right) {
|
|
|
26973
27168
|
* limitations under the License.
|
|
26974
27169
|
*/
|
|
26975
27170
|
function validateHasExplicitOrderByForLimitToLast(query) {
|
|
26976
|
-
if (
|
|
27171
|
+
if (query.limitType === "L" /* Last */ &&
|
|
27172
|
+
query.explicitOrderBy.length === 0) {
|
|
26977
27173
|
throw new FirestoreError(Code.UNIMPLEMENTED, 'limitToLast() queries require specifying at least one orderBy() clause');
|
|
26978
27174
|
}
|
|
26979
27175
|
}
|