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