@firebase/firestore 3.4.7 → 3.4.8-canary.19e31a231

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