@firebase/firestore 3.4.8 → 3.4.9

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 (118) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/firestore/lite/index.d.ts +1 -0
  3. package/dist/firestore/src/api/transaction.d.ts +2 -1
  4. package/dist/firestore/src/api/transaction_options.d.ts +17 -0
  5. package/dist/firestore/src/api.d.ts +1 -0
  6. package/dist/firestore/src/core/firestore_client.d.ts +2 -1
  7. package/dist/firestore/src/core/query.d.ts +2 -4
  8. package/dist/firestore/src/core/target.d.ts +6 -4
  9. package/dist/firestore/src/core/transaction_options.d.ts +25 -0
  10. package/dist/firestore/src/core/transaction_runner.d.ts +3 -2
  11. package/dist/firestore/src/lite-api/transaction.d.ts +2 -1
  12. package/dist/firestore/src/lite-api/transaction_options.d.ts +23 -0
  13. package/dist/firestore/src/local/index_manager.d.ts +24 -3
  14. package/dist/firestore/src/local/indexeddb_index_manager.d.ts +4 -7
  15. package/dist/firestore/src/local/indexeddb_schema.d.ts +1 -0
  16. package/dist/firestore/src/local/local_documents_view.d.ts +1 -1
  17. package/dist/firestore/src/local/memory_index_manager.d.ts +3 -2
  18. package/dist/firestore/src/local/query_engine.d.ts +44 -5
  19. package/dist/firestore/src/model/collections.d.ts +1 -1
  20. package/dist/firestore/src/model/values.d.ts +14 -2
  21. package/dist/firestore/src/util/misc.d.ts +3 -0
  22. package/dist/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
  23. package/dist/firestore/test/unit/local/test_index_manager.d.ts +2 -2
  24. package/dist/index.d.ts +24 -1
  25. package/dist/index.esm2017.js +4108 -3992
  26. package/dist/index.esm2017.js.map +1 -1
  27. package/dist/index.esm5.js +4198 -4087
  28. package/dist/index.esm5.js.map +1 -1
  29. package/dist/index.node.cjs.js +1711 -1565
  30. package/dist/index.node.cjs.js.map +1 -1
  31. package/dist/index.node.mjs +1711 -1565
  32. package/dist/index.node.mjs.map +1 -1
  33. package/dist/index.rn.js +4100 -3984
  34. package/dist/index.rn.js.map +1 -1
  35. package/dist/internal.d.ts +50 -4
  36. package/dist/lite/firestore/lite/index.d.ts +1 -0
  37. package/dist/lite/firestore/src/api/transaction.d.ts +2 -1
  38. package/dist/lite/firestore/src/api/transaction_options.d.ts +17 -0
  39. package/dist/lite/firestore/src/api.d.ts +1 -0
  40. package/dist/lite/firestore/src/core/firestore_client.d.ts +2 -1
  41. package/dist/lite/firestore/src/core/query.d.ts +2 -4
  42. package/dist/lite/firestore/src/core/target.d.ts +6 -4
  43. package/dist/lite/firestore/src/core/transaction_options.d.ts +25 -0
  44. package/dist/lite/firestore/src/core/transaction_runner.d.ts +3 -2
  45. package/dist/lite/firestore/src/lite-api/transaction.d.ts +2 -1
  46. package/dist/lite/firestore/src/lite-api/transaction_options.d.ts +23 -0
  47. package/dist/lite/firestore/src/local/index_manager.d.ts +24 -3
  48. package/dist/lite/firestore/src/local/indexeddb_index_manager.d.ts +4 -7
  49. package/dist/lite/firestore/src/local/indexeddb_schema.d.ts +1 -0
  50. package/dist/lite/firestore/src/local/local_documents_view.d.ts +1 -1
  51. package/dist/lite/firestore/src/local/memory_index_manager.d.ts +3 -2
  52. package/dist/lite/firestore/src/local/query_engine.d.ts +44 -5
  53. package/dist/lite/firestore/src/model/collections.d.ts +1 -1
  54. package/dist/lite/firestore/src/model/values.d.ts +14 -2
  55. package/dist/lite/firestore/src/util/misc.d.ts +3 -0
  56. package/dist/lite/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
  57. package/dist/lite/firestore/test/unit/local/test_index_manager.d.ts +2 -2
  58. package/dist/lite/index.browser.esm2017.js +341 -312
  59. package/dist/lite/index.browser.esm2017.js.map +1 -1
  60. package/dist/lite/index.browser.esm5.js +299 -291
  61. package/dist/lite/index.browser.esm5.js.map +1 -1
  62. package/dist/lite/index.d.ts +24 -1
  63. package/dist/lite/index.node.cjs.js +56 -15
  64. package/dist/lite/index.node.cjs.js.map +1 -1
  65. package/dist/lite/index.node.mjs +56 -15
  66. package/dist/lite/index.node.mjs.map +1 -1
  67. package/dist/lite/index.rn.esm2017.js +1227 -1198
  68. package/dist/lite/index.rn.esm2017.js.map +1 -1
  69. package/dist/lite/internal.d.ts +25 -1
  70. package/dist/lite/packages/firestore/dist/lite/index.browser.esm2017.d.ts +28 -28
  71. package/dist/lite/packages/firestore/lite/index.d.ts +1 -0
  72. package/dist/lite/packages/firestore/src/api/transaction.d.ts +2 -1
  73. package/dist/lite/packages/firestore/src/api/transaction_options.d.ts +17 -0
  74. package/dist/lite/packages/firestore/src/api.d.ts +1 -0
  75. package/dist/lite/packages/firestore/src/core/firestore_client.d.ts +2 -1
  76. package/dist/lite/packages/firestore/src/core/query.d.ts +2 -4
  77. package/dist/lite/packages/firestore/src/core/target.d.ts +6 -4
  78. package/dist/lite/packages/firestore/src/core/transaction_options.d.ts +25 -0
  79. package/dist/lite/packages/firestore/src/core/transaction_runner.d.ts +3 -2
  80. package/dist/lite/packages/firestore/src/lite-api/transaction.d.ts +2 -1
  81. package/dist/lite/packages/firestore/src/lite-api/transaction_options.d.ts +23 -0
  82. package/dist/lite/packages/firestore/src/local/index_manager.d.ts +24 -3
  83. package/dist/lite/packages/firestore/src/local/indexeddb_index_manager.d.ts +4 -7
  84. package/dist/lite/packages/firestore/src/local/indexeddb_schema.d.ts +1 -0
  85. package/dist/lite/packages/firestore/src/local/local_documents_view.d.ts +1 -1
  86. package/dist/lite/packages/firestore/src/local/memory_index_manager.d.ts +3 -2
  87. package/dist/lite/packages/firestore/src/local/query_engine.d.ts +44 -5
  88. package/dist/lite/packages/firestore/src/model/collections.d.ts +1 -1
  89. package/dist/lite/packages/firestore/src/model/values.d.ts +14 -2
  90. package/dist/lite/packages/firestore/src/util/misc.d.ts +3 -0
  91. package/dist/lite/packages/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
  92. package/dist/lite/packages/firestore/test/unit/local/test_index_manager.d.ts +2 -2
  93. package/dist/lite/private.d.ts +25 -1
  94. package/dist/packages/firestore/dist/index.esm2017.d.ts +155 -154
  95. package/dist/packages/firestore/lite/index.d.ts +1 -0
  96. package/dist/packages/firestore/src/api/transaction.d.ts +2 -1
  97. package/dist/packages/firestore/src/api/transaction_options.d.ts +17 -0
  98. package/dist/packages/firestore/src/api.d.ts +1 -0
  99. package/dist/packages/firestore/src/core/firestore_client.d.ts +2 -1
  100. package/dist/packages/firestore/src/core/query.d.ts +2 -4
  101. package/dist/packages/firestore/src/core/target.d.ts +6 -4
  102. package/dist/packages/firestore/src/core/transaction_options.d.ts +25 -0
  103. package/dist/packages/firestore/src/core/transaction_runner.d.ts +3 -2
  104. package/dist/packages/firestore/src/lite-api/transaction.d.ts +2 -1
  105. package/dist/packages/firestore/src/lite-api/transaction_options.d.ts +23 -0
  106. package/dist/packages/firestore/src/local/index_manager.d.ts +24 -3
  107. package/dist/packages/firestore/src/local/indexeddb_index_manager.d.ts +4 -7
  108. package/dist/packages/firestore/src/local/indexeddb_schema.d.ts +1 -0
  109. package/dist/packages/firestore/src/local/local_documents_view.d.ts +1 -1
  110. package/dist/packages/firestore/src/local/memory_index_manager.d.ts +3 -2
  111. package/dist/packages/firestore/src/local/query_engine.d.ts +44 -5
  112. package/dist/packages/firestore/src/model/collections.d.ts +1 -1
  113. package/dist/packages/firestore/src/model/values.d.ts +14 -2
  114. package/dist/packages/firestore/src/util/misc.d.ts +3 -0
  115. package/dist/packages/firestore/test/unit/local/counting_query_engine.d.ts +2 -1
  116. package/dist/packages/firestore/test/unit/local/test_index_manager.d.ts +2 -2
  117. package/dist/private.d.ts +50 -4
  118. package/package.json +7 -7
@@ -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.8";
11
+ const version$1 = "3.4.9";
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.11";
64
+ const version = "9.8.0";
65
65
 
66
66
  /**
67
67
  * @license
@@ -3321,7 +3321,7 @@ function typeOrder(value) {
3321
3321
  return 4 /* ServerTimestampValue */;
3322
3322
  }
3323
3323
  else if (isMaxValue(value)) {
3324
- return 9 /* ArrayValue */;
3324
+ return 9007199254740991 /* MaxValue */;
3325
3325
  }
3326
3326
  return 10 /* ObjectValue */;
3327
3327
  }
@@ -3533,6 +3533,15 @@ function compareArrays(left, right) {
3533
3533
  return primitiveComparator(leftArray.length, rightArray.length);
3534
3534
  }
3535
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
+ }
3536
3545
  const leftMap = left.fields || {};
3537
3546
  const leftKeys = Object.keys(leftMap);
3538
3547
  const rightMap = right.fields || {};
@@ -3780,27 +3789,31 @@ function valuesGetUpperBound(value) {
3780
3789
  return fail();
3781
3790
  }
3782
3791
  }
3783
- function valuesMax(left, right) {
3784
- if (left === undefined) {
3785
- return right;
3792
+ function lowerBoundCompare(left, right) {
3793
+ const cmp = valueCompare(left.value, right.value);
3794
+ if (cmp !== 0) {
3795
+ return cmp;
3786
3796
  }
3787
- else if (right === undefined) {
3788
- return left;
3797
+ if (left.inclusive && !right.inclusive) {
3798
+ return -1;
3789
3799
  }
3790
- else {
3791
- return valueCompare(left, right) > 0 ? left : right;
3800
+ else if (!left.inclusive && right.inclusive) {
3801
+ return 1;
3792
3802
  }
3803
+ return 0;
3793
3804
  }
3794
- function valuesMin(left, right) {
3795
- if (left === undefined) {
3796
- return right;
3805
+ function upperBoundCompare(left, right) {
3806
+ const cmp = valueCompare(left.value, right.value);
3807
+ if (cmp !== 0) {
3808
+ return cmp;
3797
3809
  }
3798
- else if (right === undefined) {
3799
- return left;
3810
+ if (left.inclusive && !right.inclusive) {
3811
+ return 1;
3800
3812
  }
3801
- else {
3802
- return valueCompare(left, right) < 0 ? left : right;
3813
+ else if (!left.inclusive && right.inclusive) {
3814
+ return -1;
3803
3815
  }
3816
+ return 0;
3804
3817
  }
3805
3818
 
3806
3819
  /**
@@ -4310,7 +4323,7 @@ function indexOffsetComparator(left, right) {
4310
4323
 
4311
4324
  /**
4312
4325
  * @license
4313
- * Copyright 2019 Google LLC
4326
+ * Copyright 2017 Google LLC
4314
4327
  *
4315
4328
  * Licensed under the Apache License, Version 2.0 (the "License");
4316
4329
  * you may not use this file except in compliance with the License.
@@ -4324,609 +4337,445 @@ function indexOffsetComparator(left, right) {
4324
4337
  * See the License for the specific language governing permissions and
4325
4338
  * limitations under the License.
4326
4339
  */
4327
- // Visible for testing
4328
- class TargetImpl {
4329
- constructor(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
4330
- this.path = path;
4331
- this.collectionGroup = collectionGroup;
4332
- this.orderBy = orderBy;
4333
- this.filters = filters;
4334
- this.limit = limit;
4335
- this.startAt = startAt;
4336
- this.endAt = endAt;
4337
- 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;
4338
4346
  }
4339
- }
4340
- /**
4341
- * Initializes a Target with a path and optional additional query constraints.
4342
- * Path must currently be empty if this is a collection group query.
4343
- *
4344
- * NOTE: you should always construct `Target` from `Query.toTarget` instead of
4345
- * using this factory method, because `Query` provides an implicit `orderBy`
4346
- * property.
4347
- */
4348
- function newTarget(path, collectionGroup = null, orderBy = [], filters = [], limit = null, startAt = null, endAt = null) {
4349
- return new TargetImpl(path, collectionGroup, orderBy, filters, limit, startAt, endAt);
4350
- }
4351
- function canonifyTarget(target) {
4352
- const targetImpl = debugCast(target);
4353
- if (targetImpl.memoizedCanonicalId === null) {
4354
- let str = targetImpl.path.canonicalString();
4355
- if (targetImpl.collectionGroup !== null) {
4356
- str += '|cg:' + targetImpl.collectionGroup;
4357
- }
4358
- str += '|f:';
4359
- str += targetImpl.filters.map(f => canonifyFilter(f)).join(',');
4360
- str += '|ob:';
4361
- str += targetImpl.orderBy.map(o => canonifyOrderBy(o)).join(',');
4362
- if (!isNullOrUndefined(targetImpl.limit)) {
4363
- str += '|l:';
4364
- str += targetImpl.limit;
4365
- }
4366
- if (targetImpl.startAt) {
4367
- str += '|lb:';
4368
- str += targetImpl.startAt.inclusive ? 'b:' : 'a:';
4369
- str += targetImpl.startAt.position.map(p => canonicalId(p)).join(',');
4370
- }
4371
- if (targetImpl.endAt) {
4372
- str += '|ub:';
4373
- str += targetImpl.endAt.inclusive ? 'a:' : 'b:';
4374
- str += targetImpl.endAt.position.map(p => canonicalId(p)).join(',');
4375
- }
4376
- 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));
4377
4352
  }
4378
- return targetImpl.memoizedCanonicalId;
4379
- }
4380
- function stringifyTarget(target) {
4381
- let str = target.path.canonicalString();
4382
- if (target.collectionGroup !== null) {
4383
- 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));
4384
4358
  }
4385
- if (target.filters.length > 0) {
4386
- str += `, filters: [${target.filters
4387
- .map(f => stringifyFilter(f))
4388
- .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;
4389
4375
  }
4390
- if (!isNullOrUndefined(target.limit)) {
4391
- 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;
4392
4398
  }
4393
- if (target.orderBy.length > 0) {
4394
- str += `, orderBy: [${target.orderBy
4395
- .map(o => stringifyOrderBy(o))
4396
- .join(', ')}]`;
4399
+ isEmpty() {
4400
+ return this.root.isEmpty();
4397
4401
  }
4398
- if (target.startAt) {
4399
- str += ', startAt: ';
4400
- str += target.startAt.inclusive ? 'b:' : 'a:';
4401
- 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;
4402
4405
  }
4403
- if (target.endAt) {
4404
- str += ', endAt: ';
4405
- str += target.endAt.inclusive ? 'a:' : 'b:';
4406
- str += target.endAt.position.map(p => canonicalId(p)).join(',');
4406
+ // Returns the minimum key in the map.
4407
+ minKey() {
4408
+ return this.root.minKey();
4407
4409
  }
4408
- return `Target(${str})`;
4409
- }
4410
- function targetEquals(left, right) {
4411
- if (left.limit !== right.limit) {
4412
- return false;
4410
+ // Returns the maximum key in the map.
4411
+ maxKey() {
4412
+ return this.root.maxKey();
4413
4413
  }
4414
- if (left.orderBy.length !== right.orderBy.length) {
4415
- 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);
4416
4420
  }
4417
- for (let i = 0; i < left.orderBy.length; i++) {
4418
- if (!orderByEquals(left.orderBy[i], right.orderBy[i])) {
4421
+ forEach(fn) {
4422
+ this.inorderTraversal((k, v) => {
4423
+ fn(k, v);
4419
4424
  return false;
4420
- }
4421
- }
4422
- if (left.filters.length !== right.filters.length) {
4423
- return false;
4425
+ });
4424
4426
  }
4425
- for (let i = 0; i < left.filters.length; i++) {
4426
- if (!filterEquals(left.filters[i], right.filters[i])) {
4427
+ toString() {
4428
+ const descriptions = [];
4429
+ this.inorderTraversal((k, v) => {
4430
+ descriptions.push(`${k}:${v}`);
4427
4431
  return false;
4428
- }
4432
+ });
4433
+ return `{${descriptions.join(', ')}}`;
4429
4434
  }
4430
- if (left.collectionGroup !== right.collectionGroup) {
4431
- 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);
4432
4442
  }
4433
- if (!left.path.isEqual(right.path)) {
4434
- return false;
4443
+ // Returns an iterator over the SortedMap.
4444
+ getIterator() {
4445
+ return new SortedMapIterator(this.root, null, this.comparator, false);
4435
4446
  }
4436
- if (!boundEquals(left.startAt, right.startAt)) {
4437
- return false;
4447
+ getIteratorFrom(key) {
4448
+ return new SortedMapIterator(this.root, key, this.comparator, false);
4438
4449
  }
4439
- return boundEquals(left.endAt, right.endAt);
4440
- }
4441
- function targetIsDocumentTarget(target) {
4442
- return (DocumentKey.isDocumentKey(target.path) &&
4443
- target.collectionGroup === null &&
4444
- target.filters.length === 0);
4445
- }
4446
- /** Returns the field filters that target the given field path. */
4447
- function targetGetFieldFiltersForPath(target, path) {
4448
- return target.filters.filter(f => f instanceof FieldFilter && f.field.isEqual(path));
4449
- }
4450
- /**
4451
- * Returns the values that are used in ARRAY_CONTAINS or ARRAY_CONTAINS_ANY
4452
- * filters. Returns `null` if there are no such filters.
4453
- */
4454
- function targetGetArrayValues(target, fieldIndex) {
4455
- const segment = fieldIndexGetArraySegment(fieldIndex);
4456
- if (segment === undefined) {
4457
- return null;
4450
+ getReverseIterator() {
4451
+ return new SortedMapIterator(this.root, null, this.comparator, true);
4458
4452
  }
4459
- for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
4460
- switch (fieldFilter.op) {
4461
- case "array-contains-any" /* ARRAY_CONTAINS_ANY */:
4462
- return fieldFilter.value.arrayValue.values || [];
4463
- case "array-contains" /* ARRAY_CONTAINS */:
4464
- return [fieldFilter.value];
4465
- // Remaining filters are not array filters.
4466
- }
4453
+ getReverseIteratorFrom(key) {
4454
+ return new SortedMapIterator(this.root, key, this.comparator, true);
4467
4455
  }
4468
- return null;
4469
- }
4470
- /**
4471
- * Returns the list of values that are used in != or NOT_IN filters. Returns
4472
- * `null` if there are no such filters.
4473
- */
4474
- function targetGetNotInValues(target, fieldIndex) {
4475
- const values = new Map();
4476
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
4477
- for (const fieldFilter of targetGetFieldFiltersForPath(target, segment.fieldPath)) {
4478
- switch (fieldFilter.op) {
4479
- case "==" /* EQUAL */:
4480
- case "in" /* IN */:
4481
- // Encode equality prefix, which is encoded in the index value before
4482
- // the inequality (e.g. `a == 'a' && b != 'b'` is encoded to
4483
- // `value != 'ab'`).
4484
- values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
4485
- break;
4486
- case "not-in" /* NOT_IN */:
4487
- case "!=" /* NOT_EQUAL */:
4488
- // NotIn/NotEqual is always a suffix. There cannot be any remaining
4489
- // segments and hence we can return early here.
4490
- values.set(segment.fieldPath.canonicalString(), fieldFilter.value);
4491
- return Array.from(values.values());
4492
- // 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;
4468
+ }
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;
4493
+ }
4493
4494
  }
4494
4495
  }
4495
4496
  }
4496
- return null;
4497
- }
4498
- /**
4499
- * Returns a lower bound of field values that can be used as a starting point to
4500
- * scan the index defined by `fieldIndex`. Returns `null` if no lower bound
4501
- * exists.
4502
- */
4503
- function targetGetLowerBound(target, fieldIndex) {
4504
- const values = [];
4505
- let inclusive = true;
4506
- // For each segment, retrieve a lower bound if there is a suitable filter or
4507
- // startAt.
4508
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
4509
- const segmentBound = segment.kind === 0 /* ASCENDING */
4510
- ? targetGetLowerBoundForField(target, segment.fieldPath, target.startAt)
4511
- : targetGetUpperBoundForField(target, segment.fieldPath, target.startAt);
4512
- if (!segmentBound.value) {
4513
- // No lower bound exists
4514
- return null;
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;
4505
+ }
4515
4506
  }
4516
- values.push(segmentBound.value);
4517
- inclusive && (inclusive = segmentBound.inclusive);
4507
+ else {
4508
+ node = node.right;
4509
+ while (!node.isEmpty()) {
4510
+ this.nodeStack.push(node);
4511
+ node = node.left;
4512
+ }
4513
+ }
4514
+ return result;
4518
4515
  }
4519
- return new Bound(values, inclusive);
4520
- }
4521
- /**
4522
- * Returns an upper bound of field values that can be used as an ending point
4523
- * when scanning the index defined by `fieldIndex`. Returns `null` if no
4524
- * upper bound exists.
4525
- */
4526
- function targetGetUpperBound(target, fieldIndex) {
4527
- const values = [];
4528
- let inclusive = true;
4529
- // For each segment, retrieve an upper bound if there is a suitable filter or
4530
- // endAt.
4531
- for (const segment of fieldIndexGetDirectionalSegments(fieldIndex)) {
4532
- const segmentBound = segment.kind === 0 /* ASCENDING */
4533
- ? targetGetUpperBoundForField(target, segment.fieldPath, target.endAt)
4534
- : targetGetLowerBoundForField(target, segment.fieldPath, target.endAt);
4535
- if (!segmentBound.value) {
4536
- // No upper bound exists
4516
+ hasNext() {
4517
+ return this.nodeStack.length > 0;
4518
+ }
4519
+ peek() {
4520
+ if (this.nodeStack.length === 0) {
4537
4521
  return null;
4538
4522
  }
4539
- values.push(segmentBound.value);
4540
- inclusive && (inclusive = segmentBound.inclusive);
4523
+ const node = this.nodeStack[this.nodeStack.length - 1];
4524
+ return { key: node.key, value: node.value };
4541
4525
  }
4542
- return new Bound(values, inclusive);
4543
- }
4544
- /**
4545
- * Returns the value to use as the lower bound for ascending index segment at
4546
- * the provided `fieldPath` (or the upper bound for an descending segment).
4547
- */
4548
- function targetGetLowerBoundForField(target, fieldPath, bound) {
4549
- let value = undefined;
4550
- let inclusive = true;
4551
- // Process all filters to find a value for the current field segment
4552
- for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
4553
- let filterValue = undefined;
4554
- let filterInclusive = true;
4555
- switch (fieldFilter.op) {
4556
- case "<" /* LESS_THAN */:
4557
- case "<=" /* LESS_THAN_OR_EQUAL */:
4558
- filterValue = valuesGetLowerBound(fieldFilter.value);
4559
- break;
4560
- case "==" /* EQUAL */:
4561
- case "in" /* IN */:
4562
- case ">=" /* GREATER_THAN_OR_EQUAL */:
4563
- filterValue = fieldFilter.value;
4564
- break;
4565
- case ">" /* GREATER_THAN */:
4566
- filterValue = fieldFilter.value;
4567
- filterInclusive = false;
4568
- break;
4569
- case "!=" /* NOT_EQUAL */:
4570
- case "not-in" /* NOT_IN */:
4571
- filterValue = MIN_VALUE;
4572
- break;
4573
- // Remaining filters cannot be used as lower bounds.
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;
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;
4536
+ }
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;
4574
4566
  }
4575
- if (valuesMax(value, filterValue) === filterValue) {
4576
- value = filterValue;
4577
- inclusive = filterInclusive;
4567
+ else {
4568
+ return this.left.min();
4578
4569
  }
4579
4570
  }
4580
- // If there is an additional bound, compare the values against the existing
4581
- // range to see if we can narrow the scope.
4582
- if (bound !== null) {
4583
- for (let i = 0; i < target.orderBy.length; ++i) {
4584
- const orderBy = target.orderBy[i];
4585
- if (orderBy.field.isEqual(fieldPath)) {
4586
- const cursorValue = bound.position[i];
4587
- if (valuesMax(value, cursorValue) === cursorValue) {
4588
- value = cursorValue;
4589
- inclusive = bound.inclusive;
4590
- }
4591
- break;
4592
- }
4593
- }
4571
+ // Returns the maximum key in the tree.
4572
+ minKey() {
4573
+ return this.min().key;
4594
4574
  }
4595
- return { value, inclusive };
4596
- }
4597
- /**
4598
- * Returns the value to use as the upper bound for ascending index segment at
4599
- * the provided `fieldPath` (or the lower bound for an descending segment).
4600
- */
4601
- function targetGetUpperBoundForField(target, fieldPath, bound) {
4602
- let value = undefined;
4603
- let inclusive = true;
4604
- // Process all filters to find a value for the current field segment
4605
- for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) {
4606
- let filterValue = undefined;
4607
- let filterInclusive = true;
4608
- switch (fieldFilter.op) {
4609
- case ">=" /* GREATER_THAN_OR_EQUAL */:
4610
- case ">" /* GREATER_THAN */:
4611
- filterValue = valuesGetUpperBound(fieldFilter.value);
4612
- filterInclusive = false;
4613
- break;
4614
- case "==" /* EQUAL */:
4615
- case "in" /* IN */:
4616
- case "<=" /* LESS_THAN_OR_EQUAL */:
4617
- filterValue = fieldFilter.value;
4618
- break;
4619
- case "<" /* LESS_THAN */:
4620
- filterValue = fieldFilter.value;
4621
- filterInclusive = false;
4622
- break;
4623
- case "!=" /* NOT_EQUAL */:
4624
- case "not-in" /* NOT_IN */:
4625
- filterValue = MAX_VALUE;
4626
- break;
4627
- // Remaining filters cannot be used as upper bounds.
4575
+ // Returns the maximum key in the tree.
4576
+ maxKey() {
4577
+ if (this.right.isEmpty()) {
4578
+ return this.key;
4628
4579
  }
4629
- if (valuesMin(value, filterValue) === filterValue) {
4630
- value = filterValue;
4631
- inclusive = filterInclusive;
4580
+ else {
4581
+ return this.right.maxKey();
4632
4582
  }
4633
4583
  }
4634
- // If there is an additional bound, compare the values against the existing
4635
- // range to see if we can narrow the scope.
4636
- if (bound !== null) {
4637
- for (let i = 0; i < target.orderBy.length; ++i) {
4638
- const orderBy = target.orderBy[i];
4639
- if (orderBy.field.isEqual(fieldPath)) {
4640
- const cursorValue = bound.position[i];
4641
- if (valuesMin(value, cursorValue) === cursorValue) {
4642
- value = cursorValue;
4643
- inclusive = bound.inclusive;
4644
- }
4645
- break;
4646
- }
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);
4647
4593
  }
4594
+ else {
4595
+ n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
4596
+ }
4597
+ return n.fixUp();
4648
4598
  }
4649
- return { value, inclusive };
4650
- }
4651
- class Filter {
4652
- }
4653
- class FieldFilter extends Filter {
4654
- constructor(field, op, value) {
4655
- super();
4656
- this.field = field;
4657
- this.op = op;
4658
- this.value = value;
4599
+ removeMin() {
4600
+ if (this.left.isEmpty()) {
4601
+ return LLRBNode.EMPTY;
4602
+ }
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();
4659
4609
  }
4660
- /**
4661
- * Creates a filter based on the provided arguments.
4662
- */
4663
- static create(field, op, value) {
4664
- if (field.isKeyField()) {
4665
- if (op === "in" /* IN */ || op === "not-in" /* NOT_IN */) {
4666
- return this.createKeyFieldInFilter(field, op, value);
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();
4667
4617
  }
4668
- else {
4669
- return new KeyFieldFilter(field, op, value);
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
+ }
4670
4635
  }
4636
+ n = n.copy(null, null, null, null, n.right.remove(key, comparator));
4671
4637
  }
4672
- else if (op === "array-contains" /* ARRAY_CONTAINS */) {
4673
- return new ArrayContainsFilter(field, value);
4638
+ return n.fixUp();
4639
+ }
4640
+ isRed() {
4641
+ return this.color;
4642
+ }
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();
4674
4648
  }
4675
- else if (op === "in" /* IN */) {
4676
- return new InFilter(field, value);
4649
+ if (n.left.isRed() && n.left.left.isRed()) {
4650
+ n = n.rotateRight();
4677
4651
  }
4678
- else if (op === "not-in" /* NOT_IN */) {
4679
- return new NotInFilter(field, value);
4652
+ if (n.left.isRed() && n.right.isRed()) {
4653
+ n = n.colorFlip();
4680
4654
  }
4681
- else if (op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
4682
- return new ArrayContainsAnyFilter(field, value);
4655
+ return n;
4656
+ }
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();
4683
4663
  }
4684
- else {
4685
- return new FieldFilter(field, op, value);
4664
+ return n;
4665
+ }
4666
+ moveRedRight() {
4667
+ let n = this.colorFlip();
4668
+ if (n.left.left.isRed()) {
4669
+ n = n.rotateRight();
4670
+ n = n.colorFlip();
4686
4671
  }
4672
+ return n;
4687
4673
  }
4688
- static createKeyFieldInFilter(field, op, value) {
4689
- return op === "in" /* IN */
4690
- ? new KeyFieldInFilter(field, value)
4691
- : new KeyFieldNotInFilter(field, value);
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);
4692
4677
  }
4693
- matches(doc) {
4694
- const other = doc.data.field(this.field);
4695
- // Types do not have to match in NOT_EQUAL filters.
4696
- if (this.op === "!=" /* NOT_EQUAL */) {
4697
- return (other !== null &&
4698
- this.matchesComparison(valueCompare(other, this.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);
4681
+ }
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);
4686
+ }
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;
4699
4695
  }
4700
- // Only compare types with matching backend order (such as double and int).
4701
- return (other !== null &&
4702
- typeOrder(this.value) === typeOrder(other) &&
4703
- this.matchesComparison(valueCompare(other, this.value)));
4704
4696
  }
4705
- matchesComparison(comparison) {
4706
- switch (this.op) {
4707
- case "<" /* LESS_THAN */:
4708
- return comparison < 0;
4709
- case "<=" /* LESS_THAN_OR_EQUAL */:
4710
- return comparison <= 0;
4711
- case "==" /* EQUAL */:
4712
- return comparison === 0;
4713
- case "!=" /* NOT_EQUAL */:
4714
- return comparison !== 0;
4715
- case ">" /* GREATER_THAN */:
4716
- return comparison > 0;
4717
- case ">=" /* GREATER_THAN_OR_EQUAL */:
4718
- return comparison >= 0;
4719
- default:
4720
- return fail();
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);
4721
4712
  }
4722
4713
  }
4723
- isInequality() {
4724
- return ([
4725
- "<" /* LESS_THAN */,
4726
- "<=" /* LESS_THAN_OR_EQUAL */,
4727
- ">" /* GREATER_THAN */,
4728
- ">=" /* GREATER_THAN_OR_EQUAL */,
4729
- "!=" /* NOT_EQUAL */,
4730
- "not-in" /* NOT_IN */
4731
- ].indexOf(this.op) >= 0);
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;
4732
4724
  }
4733
- }
4734
- function canonifyFilter(filter) {
4735
- // TODO(b/29183165): Technically, this won't be unique if two values have
4736
- // the same description, such as the int 3 and the string "3". So we should
4737
- // add the types in here somehow, too.
4738
- return (filter.field.canonicalString() +
4739
- filter.op.toString() +
4740
- canonicalId(filter.value));
4741
- }
4742
- function filterEquals(f1, f2) {
4743
- return (f1.op === f2.op &&
4744
- f1.field.isEqual(f2.field) &&
4745
- valueEquals(f1.value, f2.value));
4746
- }
4747
- /** Returns a debug description for `filter`. */
4748
- function stringifyFilter(filter) {
4749
- return `${filter.field.canonicalString()} ${filter.op} ${canonicalId(filter.value)}`;
4750
- }
4751
- /** Filter that matches on key fields (i.e. '__name__'). */
4752
- class KeyFieldFilter extends FieldFilter {
4753
- constructor(field, op, value) {
4754
- super(field, op, value);
4755
- this.key = DocumentKey.fromName(value.referenceValue);
4725
+ get key() {
4726
+ throw fail();
4756
4727
  }
4757
- matches(doc) {
4758
- const comparison = DocumentKey.comparator(doc.key, this.key);
4759
- return this.matchesComparison(comparison);
4728
+ get value() {
4729
+ throw fail();
4760
4730
  }
4761
- }
4762
- /** Filter that matches on key fields within an array. */
4763
- class KeyFieldInFilter extends FieldFilter {
4764
- constructor(field, value) {
4765
- super(field, "in" /* IN */, value);
4766
- this.keys = extractDocumentKeysFromArrayValue("in" /* IN */, value);
4731
+ get color() {
4732
+ throw fail();
4767
4733
  }
4768
- matches(doc) {
4769
- return this.keys.some(key => key.isEqual(doc.key));
4734
+ get left() {
4735
+ throw fail();
4770
4736
  }
4771
- }
4772
- /** Filter that matches on key fields not present within an array. */
4773
- class KeyFieldNotInFilter extends FieldFilter {
4774
- constructor(field, value) {
4775
- super(field, "not-in" /* NOT_IN */, value);
4776
- this.keys = extractDocumentKeysFromArrayValue("not-in" /* NOT_IN */, value);
4737
+ get right() {
4738
+ throw fail();
4777
4739
  }
4778
- matches(doc) {
4779
- return !this.keys.some(key => key.isEqual(doc.key));
4740
+ // Returns a copy of the current node.
4741
+ copy(key, value, color, left, right) {
4742
+ return this;
4780
4743
  }
4781
- }
4782
- function extractDocumentKeysFromArrayValue(op, value) {
4783
- var _a;
4784
- return (((_a = value.arrayValue) === null || _a === void 0 ? void 0 : _a.values) || []).map(v => {
4785
- return DocumentKey.fromName(v.referenceValue);
4786
- });
4787
- }
4788
- /** A Filter that implements the array-contains operator. */
4789
- class ArrayContainsFilter extends FieldFilter {
4790
- constructor(field, value) {
4791
- super(field, "array-contains" /* ARRAY_CONTAINS */, value);
4744
+ // Returns a copy of the tree, with the specified key/value added.
4745
+ insert(key, value, comparator) {
4746
+ return new LLRBNode(key, value);
4792
4747
  }
4793
- matches(doc) {
4794
- const other = doc.data.field(this.field);
4795
- return isArray(other) && arrayValueContains(other.arrayValue, this.value);
4748
+ // Returns a copy of the tree, with the specified key removed.
4749
+ remove(key, comparator) {
4750
+ return this;
4796
4751
  }
4797
- }
4798
- /** A Filter that implements the IN operator. */
4799
- class InFilter extends FieldFilter {
4800
- constructor(field, value) {
4801
- super(field, "in" /* IN */, value);
4752
+ isEmpty() {
4753
+ return true;
4802
4754
  }
4803
- matches(doc) {
4804
- const other = doc.data.field(this.field);
4805
- return other !== null && arrayValueContains(this.value.arrayValue, other);
4806
- }
4807
- }
4808
- /** A Filter that implements the not-in operator. */
4809
- class NotInFilter extends FieldFilter {
4810
- constructor(field, value) {
4811
- super(field, "not-in" /* NOT_IN */, value);
4812
- }
4813
- matches(doc) {
4814
- if (arrayValueContains(this.value.arrayValue, { nullValue: 'NULL_VALUE' })) {
4815
- return false;
4816
- }
4817
- const other = doc.data.field(this.field);
4818
- return other !== null && !arrayValueContains(this.value.arrayValue, other);
4819
- }
4820
- }
4821
- /** A Filter that implements the array-contains-any operator. */
4822
- class ArrayContainsAnyFilter extends FieldFilter {
4823
- constructor(field, value) {
4824
- super(field, "array-contains-any" /* ARRAY_CONTAINS_ANY */, value);
4825
- }
4826
- matches(doc) {
4827
- const other = doc.data.field(this.field);
4828
- if (!isArray(other) || !other.arrayValue.values) {
4829
- return false;
4830
- }
4831
- return other.arrayValue.values.some(val => arrayValueContains(this.value.arrayValue, val));
4832
- }
4833
- }
4834
- /**
4835
- * Represents a bound of a query.
4836
- *
4837
- * The bound is specified with the given components representing a position and
4838
- * whether it's just before or just after the position (relative to whatever the
4839
- * query order is).
4840
- *
4841
- * The position represents a logical index position for a query. It's a prefix
4842
- * of values for the (potentially implicit) order by clauses of a query.
4843
- *
4844
- * Bound provides a function to determine whether a document comes before or
4845
- * after a bound. This is influenced by whether the position is just before or
4846
- * just after the provided values.
4847
- */
4848
- class Bound {
4849
- constructor(position, inclusive) {
4850
- this.position = position;
4851
- this.inclusive = inclusive;
4755
+ inorderTraversal(action) {
4756
+ return false;
4852
4757
  }
4853
- }
4854
- /**
4855
- * An ordering on a field, in some Direction. Direction defaults to ASCENDING.
4856
- */
4857
- class OrderBy {
4858
- constructor(field, dir = "asc" /* ASCENDING */) {
4859
- this.field = field;
4860
- this.dir = dir;
4758
+ reverseTraversal(action) {
4759
+ return false;
4861
4760
  }
4862
- }
4863
- function canonifyOrderBy(orderBy) {
4864
- // TODO(b/29183165): Make this collision robust.
4865
- return orderBy.field.canonicalString() + orderBy.dir;
4866
- }
4867
- function stringifyOrderBy(orderBy) {
4868
- return `${orderBy.field.canonicalString()} (${orderBy.dir})`;
4869
- }
4870
- function orderByEquals(left, right) {
4871
- return left.dir === right.dir && left.field.isEqual(right.field);
4872
- }
4873
- function boundCompareToDocument(bound, orderBy, doc) {
4874
- let comparison = 0;
4875
- for (let i = 0; i < bound.position.length; i++) {
4876
- const orderByComponent = orderBy[i];
4877
- const component = bound.position[i];
4878
- if (orderByComponent.field.isKeyField()) {
4879
- comparison = DocumentKey.comparator(DocumentKey.fromName(component.referenceValue), doc.key);
4880
- }
4881
- else {
4882
- const docValue = doc.data.field(orderByComponent.field);
4883
- comparison = valueCompare(component, docValue);
4884
- }
4885
- if (orderByComponent.dir === "desc" /* DESCENDING */) {
4886
- comparison = comparison * -1;
4887
- }
4888
- if (comparison !== 0) {
4889
- break;
4890
- }
4761
+ minKey() {
4762
+ return null;
4891
4763
  }
4892
- return comparison;
4893
- }
4894
- /**
4895
- * Returns true if a document sorts after a bound using the provided sort
4896
- * order.
4897
- */
4898
- function boundSortsAfterDocument(bound, orderBy, doc) {
4899
- const comparison = boundCompareToDocument(bound, orderBy, doc);
4900
- return bound.inclusive ? comparison >= 0 : comparison > 0;
4901
- }
4902
- /**
4903
- * Returns true if a document sorts before a bound using the provided sort
4904
- * order.
4905
- */
4906
- function boundSortsBeforeDocument(bound, orderBy, doc) {
4907
- const comparison = boundCompareToDocument(bound, orderBy, doc);
4908
- return bound.inclusive ? comparison <= 0 : comparison < 0;
4909
- }
4910
- function boundEquals(left, right) {
4911
- if (left === null) {
4912
- return right === null;
4764
+ maxKey() {
4765
+ return null;
4913
4766
  }
4914
- else if (right === null) {
4767
+ isRed() {
4915
4768
  return false;
4916
4769
  }
4917
- if (left.inclusive !== right.inclusive ||
4918
- left.position.length !== right.position.length) {
4919
- return false;
4770
+ // For testing.
4771
+ checkMaxDepth() {
4772
+ return true;
4920
4773
  }
4921
- for (let i = 0; i < left.position.length; i++) {
4922
- const leftPosition = left.position[i];
4923
- const rightPosition = right.position[i];
4924
- if (!valueEquals(leftPosition, rightPosition)) {
4925
- return false;
4926
- }
4774
+ check() {
4775
+ return 0;
4927
4776
  }
4928
- return true;
4929
- }
4777
+ } // end LLRBEmptyNode
4778
+ LLRBNode.EMPTY = new LLRBEmptyNode();
4930
4779
 
4931
4780
  /**
4932
4781
  * @license
@@ -4945,325 +4794,223 @@ function boundEquals(left, right) {
4945
4794
  * limitations under the License.
4946
4795
  */
4947
4796
  /**
4948
- * Query encapsulates all the query attributes we support in the SDK. It can
4949
- * be run against the LocalStore, as well as be converted to a `Target` to
4950
- * query the RemoteStore results.
4797
+ * SortedSet is an immutable (copy-on-write) collection that holds elements
4798
+ * in order specified by the provided comparator.
4951
4799
  *
4952
- * Visible for testing.
4800
+ * NOTE: if provided comparator returns 0 for two elements, we consider them to
4801
+ * be equal!
4953
4802
  */
4954
- class QueryImpl {
4955
- /**
4956
- * Initializes a Query with a path and optional additional query constraints.
4957
- * Path must currently be empty if this is a collection group query.
4958
- */
4959
- constructor(path, collectionGroup = null, explicitOrderBy = [], filters = [], limit = null, limitType = "F" /* First */, startAt = null, endAt = null) {
4960
- this.path = path;
4961
- this.collectionGroup = collectionGroup;
4962
- this.explicitOrderBy = explicitOrderBy;
4963
- this.filters = filters;
4964
- this.limit = limit;
4965
- this.limitType = limitType;
4966
- this.startAt = startAt;
4967
- this.endAt = endAt;
4968
- this.memoizedOrderBy = null;
4969
- // The corresponding `Target` of this `Query` instance.
4970
- this.memoizedTarget = null;
4971
- if (this.startAt) ;
4972
- if (this.endAt) ;
4803
+ class SortedSet {
4804
+ constructor(comparator) {
4805
+ this.comparator = comparator;
4806
+ this.data = new SortedMap(this.comparator);
4973
4807
  }
4974
- }
4975
- /** Creates a new Query instance with the options provided. */
4976
- function newQuery(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt) {
4977
- return new QueryImpl(path, collectionGroup, explicitOrderBy, filters, limit, limitType, startAt, endAt);
4978
- }
4979
- /** Creates a new Query for a query that matches all documents at `path` */
4980
- function newQueryForPath(path) {
4981
- return new QueryImpl(path);
4982
- }
4983
- /**
4984
- * Helper to convert a collection group query into a collection query at a
4985
- * specific path. This is used when executing collection group queries, since
4986
- * we have to split the query into a set of collection queries at multiple
4987
- * paths.
4988
- */
4989
- function asCollectionQueryAtPath(query, path) {
4990
- return new QueryImpl(path,
4991
- /*collectionGroup=*/ null, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
4992
- }
4993
- /**
4994
- * Returns true if this query does not specify any query constraints that
4995
- * could remove results.
4996
- */
4997
- function matchesAllDocuments(query) {
4998
- return (query.filters.length === 0 &&
4999
- query.limit === null &&
5000
- query.startAt == null &&
5001
- query.endAt == null &&
5002
- (query.explicitOrderBy.length === 0 ||
5003
- (query.explicitOrderBy.length === 1 &&
5004
- query.explicitOrderBy[0].field.isKeyField())));
5005
- }
5006
- function hasLimitToFirst(query) {
5007
- return !isNullOrUndefined(query.limit) && query.limitType === "F" /* First */;
5008
- }
5009
- function hasLimitToLast(query) {
5010
- return !isNullOrUndefined(query.limit) && query.limitType === "L" /* Last */;
5011
- }
5012
- function getFirstOrderByField(query) {
5013
- return query.explicitOrderBy.length > 0
5014
- ? query.explicitOrderBy[0].field
5015
- : null;
5016
- }
5017
- function getInequalityFilterField(query) {
5018
- for (const filter of query.filters) {
5019
- if (filter.isInequality()) {
5020
- return filter.field;
5021
- }
4808
+ has(elem) {
4809
+ return this.data.get(elem) !== null;
5022
4810
  }
5023
- return null;
5024
- }
5025
- /**
5026
- * Checks if any of the provided Operators are included in the query and
5027
- * returns the first one that is, or null if none are.
5028
- */
5029
- function findFilterOperator(query, operators) {
5030
- for (const filter of query.filters) {
5031
- if (operators.indexOf(filter.op) >= 0) {
5032
- return filter.op;
4811
+ first() {
4812
+ return this.data.minKey();
4813
+ }
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;
4837
+ }
4838
+ cb(elem.key);
5033
4839
  }
5034
4840
  }
5035
- return null;
5036
- }
5037
- /**
5038
- * Creates a new Query for a collection group query that matches all documents
5039
- * within the provided collection group.
5040
- */
5041
- function newQueryForCollectionGroup(collectionId) {
5042
- return new QueryImpl(ResourcePath.emptyPath(), collectionId);
5043
- }
5044
- /**
5045
- * Returns whether the query matches a single document by path (rather than a
5046
- * collection).
5047
- */
5048
- function isDocumentQuery$1(query) {
5049
- return (DocumentKey.isDocumentKey(query.path) &&
5050
- query.collectionGroup === null &&
5051
- query.filters.length === 0);
5052
- }
5053
- /**
5054
- * Returns whether the query matches a collection group rather than a specific
5055
- * collection.
5056
- */
5057
- function isCollectionGroupQuery(query) {
5058
- return query.collectionGroup !== null;
5059
- }
5060
- /**
5061
- * Returns the implicit order by constraint that is used to execute the Query,
5062
- * which can be different from the order by constraints the user provided (e.g.
5063
- * the SDK and backend always orders by `__name__`).
5064
- */
5065
- function queryOrderBy(query) {
5066
- const queryImpl = debugCast(query);
5067
- if (queryImpl.memoizedOrderBy === null) {
5068
- queryImpl.memoizedOrderBy = [];
5069
- const inequalityField = getInequalityFilterField(queryImpl);
5070
- const firstOrderByField = getFirstOrderByField(queryImpl);
5071
- if (inequalityField !== null && firstOrderByField === null) {
5072
- // In order to implicitly add key ordering, we must also add the
5073
- // inequality filter field for it to be a valid query.
5074
- // Note that the default inequality field and key ordering is ascending.
5075
- if (!inequalityField.isKeyField()) {
5076
- queryImpl.memoizedOrderBy.push(new OrderBy(inequalityField));
5077
- }
5078
- queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), "asc" /* ASCENDING */));
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);
5079
4848
  }
5080
4849
  else {
5081
- let foundKeyOrdering = false;
5082
- for (const orderBy of queryImpl.explicitOrderBy) {
5083
- queryImpl.memoizedOrderBy.push(orderBy);
5084
- if (orderBy.field.isKeyField()) {
5085
- foundKeyOrdering = true;
5086
- }
5087
- }
5088
- if (!foundKeyOrdering) {
5089
- // The order of the implicit key ordering always matches the last
5090
- // explicit order by
5091
- const lastDirection = queryImpl.explicitOrderBy.length > 0
5092
- ? queryImpl.explicitOrderBy[queryImpl.explicitOrderBy.length - 1]
5093
- .dir
5094
- : "asc" /* ASCENDING */;
5095
- queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), lastDirection));
5096
- }
5097
- }
5098
- }
5099
- return queryImpl.memoizedOrderBy;
5100
- }
5101
- /**
5102
- * Converts this `Query` instance to it's corresponding `Target` representation.
5103
- */
5104
- function queryToTarget(query) {
5105
- const queryImpl = debugCast(query);
5106
- if (!queryImpl.memoizedTarget) {
5107
- if (queryImpl.limitType === "F" /* First */) {
5108
- queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, queryOrderBy(queryImpl), queryImpl.filters, queryImpl.limit, queryImpl.startAt, queryImpl.endAt);
4850
+ iter = this.data.getIterator();
5109
4851
  }
5110
- else {
5111
- // Flip the orderBy directions since we want the last results
5112
- const orderBys = [];
5113
- for (const orderBy of queryOrderBy(queryImpl)) {
5114
- const dir = orderBy.dir === "desc" /* DESCENDING */
5115
- ? "asc" /* ASCENDING */
5116
- : "desc" /* DESCENDING */;
5117
- 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;
5118
4857
  }
5119
- // We need to swap the cursors to match the now-flipped query ordering.
5120
- const startAt = queryImpl.endAt
5121
- ? new Bound(queryImpl.endAt.position, !queryImpl.endAt.inclusive)
5122
- : null;
5123
- const endAt = queryImpl.startAt
5124
- ? new Bound(queryImpl.startAt.position, !queryImpl.startAt.inclusive)
5125
- : null;
5126
- // Now return as a LimitType.First query.
5127
- queryImpl.memoizedTarget = newTarget(queryImpl.path, queryImpl.collectionGroup, orderBys, queryImpl.filters, queryImpl.limit, startAt, endAt);
5128
4858
  }
5129
4859
  }
5130
- return queryImpl.memoizedTarget;
5131
- }
5132
- function queryWithAddedFilter(query, filter) {
5133
- const newFilters = query.filters.concat([filter]);
5134
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), newFilters, query.limit, query.limitType, query.startAt, query.endAt);
5135
- }
5136
- function queryWithAddedOrderBy(query, orderBy) {
5137
- // TODO(dimond): validate that orderBy does not list the same key twice.
5138
- const newOrderBy = query.explicitOrderBy.concat([orderBy]);
5139
- return new QueryImpl(query.path, query.collectionGroup, newOrderBy, query.filters.slice(), query.limit, query.limitType, query.startAt, query.endAt);
5140
- }
5141
- function queryWithLimit(query, limit, limitType) {
5142
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), limit, limitType, query.startAt, query.endAt);
5143
- }
5144
- function queryWithStartAt(query, bound) {
5145
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, bound, query.endAt);
5146
- }
5147
- function queryWithEndAt(query, bound) {
5148
- return new QueryImpl(query.path, query.collectionGroup, query.explicitOrderBy.slice(), query.filters.slice(), query.limit, query.limitType, query.startAt, bound);
5149
- }
5150
- function queryEquals(left, right) {
5151
- return (targetEquals(queryToTarget(left), queryToTarget(right)) &&
5152
- left.limitType === right.limitType);
5153
- }
5154
- // TODO(b/29183165): This is used to get a unique string from a query to, for
5155
- // example, use as a dictionary key, but the implementation is subject to
5156
- // collisions. Make it collision-free.
5157
- function canonifyQuery(query) {
5158
- return `${canonifyTarget(queryToTarget(query))}|lt:${query.limitType}`;
5159
- }
5160
- function stringifyQuery(query) {
5161
- return `Query(target=${stringifyTarget(queryToTarget(query))}; limitType=${query.limitType})`;
5162
- }
5163
- /** Returns whether `doc` matches the constraints of `query`. */
5164
- function queryMatches(query, doc) {
5165
- return (doc.isFoundDocument() &&
5166
- queryMatchesPathAndCollectionGroup(query, doc) &&
5167
- queryMatchesOrderBy(query, doc) &&
5168
- queryMatchesFilters(query, doc) &&
5169
- queryMatchesBounds(query, doc));
5170
- }
5171
- function queryMatchesPathAndCollectionGroup(query, doc) {
5172
- const docPath = doc.key.path;
5173
- if (query.collectionGroup !== null) {
5174
- // NOTE: this.path is currently always empty since we don't expose Collection
5175
- // Group queries rooted at a document path yet.
5176
- return (doc.key.hasCollectionId(query.collectionGroup) &&
5177
- 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;
5178
4864
  }
5179
- else if (DocumentKey.isDocumentKey(query.path)) {
5180
- // exact match for document queries
5181
- return query.path.isEqual(docPath);
4865
+ getIterator() {
4866
+ return new SortedSetIterator(this.data.getIterator());
5182
4867
  }
5183
- else {
5184
- // shallow ancestor queries by default
5185
- return query.path.isImmediateParentOf(docPath);
4868
+ getIteratorFrom(key) {
4869
+ return new SortedSetIterator(this.data.getIteratorFrom(key));
5186
4870
  }
5187
- }
5188
- /**
5189
- * A document must have a value for every ordering clause in order to show up
5190
- * in the results.
5191
- */
5192
- function queryMatchesOrderBy(query, doc) {
5193
- for (const orderBy of query.explicitOrderBy) {
5194
- // order by key always matches
5195
- if (!orderBy.field.isKeyField() && doc.data.field(orderBy.field) === null) {
5196
- 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;
5197
4879
  }
4880
+ return this.copy(this.data.remove(elem));
5198
4881
  }
5199
- return true;
5200
- }
5201
- function queryMatchesFilters(query, doc) {
5202
- for (const filter of query.filters) {
5203
- 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)) {
5204
4899
  return false;
5205
4900
  }
4901
+ if (this.size !== other.size) {
4902
+ return false;
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;
5206
4931
  }
5207
- return true;
5208
4932
  }
5209
- /** Makes sure a document is within the bounds, if provided. */
5210
- function queryMatchesBounds(query, doc) {
5211
- if (query.startAt &&
5212
- !boundSortsBeforeDocument(query.startAt, queryOrderBy(query), doc)) {
5213
- return false;
4933
+ class SortedSetIterator {
4934
+ constructor(iter) {
4935
+ this.iter = iter;
5214
4936
  }
5215
- if (query.endAt &&
5216
- !boundSortsAfterDocument(query.endAt, queryOrderBy(query), doc)) {
5217
- return false;
4937
+ getNext() {
4938
+ return this.iter.getNext().key;
4939
+ }
4940
+ hasNext() {
4941
+ return this.iter.hasNext();
5218
4942
  }
5219
- return true;
5220
4943
  }
5221
4944
  /**
5222
- * 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`.
5223
4949
  *
5224
- * PORTING NOTE: This is only used in the Web SDK to facilitate multi-tab
5225
- * synchronization for query results.
5226
- */
5227
- function queryCollectionGroup(query) {
5228
- return (query.collectionGroup ||
5229
- (query.path.length % 2 === 1
5230
- ? query.path.lastSegment()
5231
- : query.path.get(query.path.length - 2)));
5232
- }
5233
- /**
5234
- * Returns a new comparator function that can be used to compare two documents
5235
- * based on the Query's ordering constraint.
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`.
5236
4960
  */
5237
- function newQueryComparator(query) {
5238
- return (d1, d2) => {
5239
- let comparedOnKeyField = false;
5240
- for (const orderBy of queryOrderBy(query)) {
5241
- const comp = compareDocs(orderBy, d1, d2);
5242
- if (comp !== 0) {
5243
- return comp;
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;
5244
4982
  }
5245
- comparedOnKeyField = comparedOnKeyField || orderBy.field.isKeyField();
5246
4983
  }
5247
- return 0;
5248
- };
5249
- }
5250
- function compareDocs(orderBy, d1, d2) {
5251
- const comparison = orderBy.field.isKeyField()
5252
- ? DocumentKey.comparator(d1.key, d2.key)
5253
- : compareDocumentsByField(orderBy.field, d1, d2);
5254
- switch (orderBy.dir) {
5255
- case "asc" /* ASCENDING */:
5256
- return comparison;
5257
- case "desc" /* DESCENDING */:
5258
- return -1 * comparison;
5259
- default:
5260
- return fail();
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
+ }
5261
5002
  }
5003
+ }
5004
+ /**
5005
+ * Returns the next element from the iterator or `undefined` if none available.
5006
+ */
5007
+ function advanceIterator(it) {
5008
+ return it.hasNext() ? it.getNext() : undefined;
5262
5009
  }
5263
5010
 
5264
5011
  /**
5265
5012
  * @license
5266
- * Copyright 2017 Google LLC
5013
+ * Copyright 2019 Google LLC
5267
5014
  *
5268
5015
  * Licensed under the Apache License, Version 2.0 (the "License");
5269
5016
  * you may not use this file except in compliance with the License.
@@ -5277,555 +5024,961 @@ function compareDocs(orderBy, d1, d2) {
5277
5024
  * See the License for the specific language governing permissions and
5278
5025
  * limitations under the License.
5279
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
+ }
5280
5040
  /**
5281
- * A map implementation that uses objects as keys. Objects must have an
5282
- * associated equals function and must be immutable. Entries in the map are
5283
- * stored together with the key being produced from the mapKeyFn. This map
5284
- * 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.
5285
5047
  */
5286
- class ObjectMap {
5287
- constructor(mapKeyFn, equalsFn) {
5288
- this.mapKeyFn = mapKeyFn;
5289
- this.equalsFn = equalsFn;
5290
- /**
5291
- * The inner map for a key/value pair. Due to the possibility of collisions we
5292
- * keep a list of entries that we do a linear search through to find an actual
5293
- * match. Note that collisions should be rare, so we still expect near
5294
- * constant time lookups in practice.
5295
- */
5296
- this.inner = {};
5297
- /** The number of entries stored in the map */
5298
- this.innerSize = 0;
5299
- }
5300
- /** Get a value for this key, or undefined if it does not exist. */
5301
- get(key) {
5302
- const id = this.mapKeyFn(key);
5303
- const matches = this.inner[id];
5304
- if (matches === undefined) {
5305
- 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;
5306
5057
  }
5307
- for (const [otherKey, value] of matches) {
5308
- if (this.equalsFn(otherKey, key)) {
5309
- return value;
5310
- }
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;
5311
5065
  }
5312
- return undefined;
5313
- }
5314
- has(key) {
5315
- return this.get(key) !== undefined;
5316
- }
5317
- /** Put this key and value in the map. */
5318
- set(key, value) {
5319
- const id = this.mapKeyFn(key);
5320
- const matches = this.inner[id];
5321
- if (matches === undefined) {
5322
- this.inner[id] = [[key, value]];
5323
- this.innerSize++;
5324
- 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(',');
5325
5070
  }
5326
- for (let i = 0; i < matches.length; i++) {
5327
- if (this.equalsFn(matches[i][0], key)) {
5328
- // This is updating an existing entry and does not increase `innerSize`.
5329
- matches[i] = [key, value];
5330
- return;
5331
- }
5071
+ if (targetImpl.endAt) {
5072
+ str += '|ub:';
5073
+ str += targetImpl.endAt.inclusive ? 'a:' : 'b:';
5074
+ str += targetImpl.endAt.position.map(p => canonicalId(p)).join(',');
5332
5075
  }
5333
- matches.push([key, value]);
5334
- this.innerSize++;
5076
+ targetImpl.memoizedCanonicalId = str;
5335
5077
  }
5336
- /**
5337
- * Remove this key from the map. Returns a boolean if anything was deleted.
5338
- */
5339
- delete(key) {
5340
- const id = this.mapKeyFn(key);
5341
- const matches = this.inner[id];
5342
- 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])) {
5343
5119
  return false;
5344
5120
  }
5345
- for (let i = 0; i < matches.length; i++) {
5346
- if (this.equalsFn(matches[i][0], key)) {
5347
- if (matches.length === 1) {
5348
- delete this.inner[id];
5349
- }
5350
- else {
5351
- matches.splice(i, 1);
5352
- }
5353
- this.innerSize--;
5354
- return true;
5355
- }
5356
- }
5357
- return false;
5358
5121
  }
5359
- forEach(fn) {
5360
- forEach(this.inner, (_, entries) => {
5361
- for (const [k, v] of entries) {
5362
- fn(k, v);
5363
- }
5364
- });
5122
+ if (left.filters.length !== right.filters.length) {
5123
+ return false;
5365
5124
  }
5366
- isEmpty() {
5367
- return isEmpty(this.inner);
5125
+ for (let i = 0; i < left.filters.length; i++) {
5126
+ if (!filterEquals(left.filters[i], right.filters[i])) {
5127
+ return false;
5128
+ }
5368
5129
  }
5369
- size() {
5370
- return this.innerSize;
5130
+ if (left.collectionGroup !== right.collectionGroup) {
5131
+ return false;
5371
5132
  }
5372
- }
5373
-
5133
+ if (!left.path.isEqual(right.path)) {
5134
+ return false;
5135
+ }
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
+ }
5374
5150
  /**
5375
- * @license
5376
- * Copyright 2017 Google LLC
5377
- *
5378
- * Licensed under the Apache License, Version 2.0 (the "License");
5379
- * you may not use this file except in compliance with the License.
5380
- * You may obtain a copy of the License at
5381
- *
5382
- * http://www.apache.org/licenses/LICENSE-2.0
5383
- *
5384
- * Unless required by applicable law or agreed to in writing, software
5385
- * distributed under the License is distributed on an "AS IS" BASIS,
5386
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5387
- * See the License for the specific language governing permissions and
5388
- * 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.
5389
5153
  */
5390
- // An immutable sorted map implementation, based on a Left-leaning Red-Black
5391
- // tree.
5392
- class SortedMap {
5393
- constructor(comparator, root) {
5394
- this.comparator = comparator;
5395
- this.root = root ? root : LLRBNode.EMPTY;
5396
- }
5397
- // Returns a copy of the map, with the specified key/value added or replaced.
5398
- insert(key, value) {
5399
- return new SortedMap(this.comparator, this.root
5400
- .insert(key, value, this.comparator)
5401
- .copy(null, null, LLRBNode.BLACK, null, null));
5154
+ function targetGetArrayValues(target, fieldIndex) {
5155
+ const segment = fieldIndexGetArraySegment(fieldIndex);
5156
+ if (segment === undefined) {
5157
+ return null;
5402
5158
  }
5403
- // Returns a copy of the map, with the specified key removed.
5404
- remove(key) {
5405
- return new SortedMap(this.comparator, this.root
5406
- .remove(key, this.comparator)
5407
- .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
+ }
5408
5167
  }
5409
- // Returns the value of the node with the given key, or null.
5410
- get(key) {
5411
- let node = this.root;
5412
- while (!node.isEmpty()) {
5413
- const cmp = this.comparator(key, node.key);
5414
- if (cmp === 0) {
5415
- return node.value;
5416
- }
5417
- else if (cmp < 0) {
5418
- node = node.left;
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.
5419
5193
  }
5420
- else if (cmp > 0) {
5421
- node = node.right;
5194
+ }
5195
+ }
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);
5214
+ }
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);
5233
+ }
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;
5282
+ }
5283
+ break;
5422
5284
  }
5423
5285
  }
5424
- return null;
5425
5286
  }
5426
- // Returns the index of the element in this sorted map, or -1 if it doesn't
5427
- // exist.
5428
- indexOf(key) {
5429
- // Number of nodes that were pruned when descending right
5430
- let prunedNodes = 0;
5431
- let node = this.root;
5432
- while (!node.isEmpty()) {
5433
- const cmp = this.comparator(key, node.key);
5434
- if (cmp === 0) {
5435
- return prunedNodes + node.left.size;
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;
5336
+ }
5337
+ break;
5436
5338
  }
5437
- else if (cmp < 0) {
5438
- node = node.left;
5339
+ }
5340
+ }
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;
5362
+ }
5363
+ else {
5364
+ fields = fields.add(fieldFilter.field);
5365
+ }
5366
+ }
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);
5372
+ }
5373
+ }
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;
5383
+ this.value = value;
5384
+ }
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);
5439
5392
  }
5440
5393
  else {
5441
- // Count all nodes left of the node plus the node itself
5442
- prunedNodes += node.left.size + 1;
5443
- node = node.right;
5394
+ return new KeyFieldFilter(field, op, value);
5444
5395
  }
5445
5396
  }
5446
- // Node not found
5447
- return -1;
5397
+ else if (op === "array-contains" /* ARRAY_CONTAINS */) {
5398
+ return new ArrayContainsFilter(field, value);
5399
+ }
5400
+ else if (op === "in" /* IN */) {
5401
+ return new InFilter(field, value);
5402
+ }
5403
+ else if (op === "not-in" /* NOT_IN */) {
5404
+ return new NotInFilter(field, value);
5405
+ }
5406
+ else if (op === "array-contains-any" /* ARRAY_CONTAINS_ANY */) {
5407
+ return new ArrayContainsAnyFilter(field, value);
5408
+ }
5409
+ else {
5410
+ return new FieldFilter(field, op, value);
5411
+ }
5448
5412
  }
5449
- isEmpty() {
5450
- return this.root.isEmpty();
5413
+ static createKeyFieldInFilter(field, op, value) {
5414
+ return op === "in" /* IN */
5415
+ ? new KeyFieldInFilter(field, value)
5416
+ : new KeyFieldNotInFilter(field, value);
5451
5417
  }
5452
- // Returns the total number of nodes in the map.
5453
- get size() {
5454
- return this.root.size;
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)));
5424
+ }
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)));
5455
5429
  }
5456
- // Returns the minimum key in the map.
5457
- minKey() {
5458
- return this.root.minKey();
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();
5446
+ }
5459
5447
  }
5460
- // Returns the maximum key in the map.
5461
- maxKey() {
5462
- return this.root.maxKey();
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);
5463
5457
  }
5464
- // Traverses the map in key order and calls the specified action function
5465
- // for each key/value pair. If action returns true, traversal is aborted.
5466
- // Returns the first truthy value returned by action, or the last falsey
5467
- // value returned by action.
5468
- inorderTraversal(action) {
5469
- return this.root.inorderTraversal(action);
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);
5470
5481
  }
5471
- forEach(fn) {
5472
- this.inorderTraversal((k, v) => {
5473
- fn(k, v);
5474
- return false;
5475
- });
5482
+ matches(doc) {
5483
+ const comparison = DocumentKey.comparator(doc.key, this.key);
5484
+ return this.matchesComparison(comparison);
5476
5485
  }
5477
- toString() {
5478
- const descriptions = [];
5479
- this.inorderTraversal((k, v) => {
5480
- descriptions.push(`${k}:${v}`);
5481
- return false;
5482
- });
5483
- return `{${descriptions.join(', ')}}`;
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);
5484
5492
  }
5485
- // Traverses the map in reverse key order and calls the specified action
5486
- // function for each key/value pair. If action returns true, traversal is
5487
- // aborted.
5488
- // Returns the first truthy value returned by action, or the last falsey
5489
- // value returned by action.
5490
- reverseTraversal(action) {
5491
- return this.root.reverseTraversal(action);
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);
5492
5527
  }
5493
- // Returns an iterator over the SortedMap.
5494
- getIterator() {
5495
- return new SortedMapIterator(this.root, null, this.comparator, false);
5528
+ matches(doc) {
5529
+ const other = doc.data.field(this.field);
5530
+ return other !== null && arrayValueContains(this.value.arrayValue, other);
5496
5531
  }
5497
- getIteratorFrom(key) {
5498
- return new SortedMapIterator(this.root, key, this.comparator, false);
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);
5499
5537
  }
5500
- getReverseIterator() {
5501
- return new SortedMapIterator(this.root, null, this.comparator, true);
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);
5502
5544
  }
5503
- getReverseIteratorFrom(key) {
5504
- return new SortedMapIterator(this.root, key, this.comparator, true);
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);
5505
5550
  }
5506
- } // end SortedMap
5507
- // An iterator over an LLRBNode.
5508
- class SortedMapIterator {
5509
- constructor(node, startKey, comparator, isReverse) {
5510
- this.isReverse = isReverse;
5511
- this.nodeStack = [];
5512
- let cmp = 1;
5513
- while (!node.isEmpty()) {
5514
- cmp = startKey ? comparator(node.key, startKey) : 1;
5515
- // flip the comparison if we're going in reverse
5516
- if (startKey && isReverse) {
5517
- cmp *= -1;
5518
- }
5519
- if (cmp < 0) {
5520
- // This node is less than our start key. ignore it
5521
- if (this.isReverse) {
5522
- node = node.left;
5523
- }
5524
- else {
5525
- node = node.right;
5526
- }
5527
- }
5528
- else if (cmp === 0) {
5529
- // This node is exactly equal to our start key. Push it on the stack,
5530
- // but stop iterating;
5531
- this.nodeStack.push(node);
5532
- break;
5533
- }
5534
- else {
5535
- // This node is greater than our start key, add it to the stack and move
5536
- // to the next one
5537
- this.nodeStack.push(node);
5538
- if (this.isReverse) {
5539
- node = node.right;
5540
- }
5541
- else {
5542
- node = node.left;
5543
- }
5544
- }
5551
+ matches(doc) {
5552
+ const other = doc.data.field(this.field);
5553
+ if (!isArray(other) || !other.arrayValue.values) {
5554
+ return false;
5545
5555
  }
5556
+ return other.arrayValue.values.some(val => arrayValueContains(this.value.arrayValue, val));
5546
5557
  }
5547
- getNext() {
5548
- let node = this.nodeStack.pop();
5549
- const result = { key: node.key, value: node.value };
5550
- if (this.isReverse) {
5551
- node = node.left;
5552
- while (!node.isEmpty()) {
5553
- this.nodeStack.push(node);
5554
- node = node.right;
5555
- }
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);
5556
5605
  }
5557
5606
  else {
5558
- node = node.right;
5559
- while (!node.isEmpty()) {
5560
- this.nodeStack.push(node);
5561
- node = node.left;
5562
- }
5607
+ const docValue = doc.data.field(orderByComponent.field);
5608
+ comparison = valueCompare(component, docValue);
5563
5609
  }
5564
- return result;
5565
- }
5566
- hasNext() {
5567
- return this.nodeStack.length > 0;
5568
- }
5569
- peek() {
5570
- if (this.nodeStack.length === 0) {
5571
- return null;
5610
+ if (orderByComponent.dir === "desc" /* DESCENDING */) {
5611
+ comparison = comparison * -1;
5612
+ }
5613
+ if (comparison !== 0) {
5614
+ break;
5572
5615
  }
5573
- const node = this.nodeStack[this.nodeStack.length - 1];
5574
- return { key: node.key, value: node.value };
5575
- }
5576
- } // end SortedMapIterator
5577
- // Represents a node in a Left-leaning Red-Black tree.
5578
- class LLRBNode {
5579
- constructor(key, value, color, left, right) {
5580
- this.key = key;
5581
- this.value = value;
5582
- this.color = color != null ? color : LLRBNode.RED;
5583
- this.left = left != null ? left : LLRBNode.EMPTY;
5584
- this.right = right != null ? right : LLRBNode.EMPTY;
5585
- this.size = this.left.size + 1 + this.right.size;
5586
5616
  }
5587
- // Returns a copy of the current node, optionally replacing pieces of it.
5588
- copy(key, value, color, left, right) {
5589
- 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);
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;
5590
5638
  }
5591
- isEmpty() {
5639
+ else if (right === null) {
5592
5640
  return false;
5593
5641
  }
5594
- // Traverses the tree in key order and calls the specified action function
5595
- // for each node. If action returns true, traversal is aborted.
5596
- // Returns the first truthy value returned by action, or the last falsey
5597
- // value returned by action.
5598
- inorderTraversal(action) {
5599
- return (this.left.inorderTraversal(action) ||
5600
- action(this.key, this.value) ||
5601
- this.right.inorderTraversal(action));
5602
- }
5603
- // Traverses the tree in reverse key order and calls the specified action
5604
- // function for each node. If action returns true, traversal is aborted.
5605
- // Returns the first truthy value returned by action, or the last falsey
5606
- // value returned by action.
5607
- reverseTraversal(action) {
5608
- return (this.right.reverseTraversal(action) ||
5609
- action(this.key, this.value) ||
5610
- this.left.reverseTraversal(action));
5642
+ if (left.inclusive !== right.inclusive ||
5643
+ left.position.length !== right.position.length) {
5644
+ return false;
5611
5645
  }
5612
- // Returns the minimum node in the tree.
5613
- min() {
5614
- if (this.left.isEmpty()) {
5615
- return this;
5616
- }
5617
- else {
5618
- return this.left.min();
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;
5619
5651
  }
5620
5652
  }
5621
- // Returns the maximum key in the tree.
5622
- minKey() {
5623
- return this.min().key;
5624
- }
5625
- // Returns the maximum key in the tree.
5626
- maxKey() {
5627
- if (this.right.isEmpty()) {
5628
- return this.key;
5629
- }
5630
- else {
5631
- return this.right.maxKey();
5632
- }
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) ;
5633
5698
  }
5634
- // Returns new tree, with the key/value added.
5635
- insert(key, value, comparator) {
5636
- let n = this;
5637
- const cmp = comparator(key, n.key);
5638
- if (cmp < 0) {
5639
- n = n.copy(null, null, null, n.left.insert(key, value, comparator), null);
5640
- }
5641
- else if (cmp === 0) {
5642
- n = n.copy(null, value, null, null, null);
5643
- }
5644
- else {
5645
- n = n.copy(null, null, null, null, n.right.insert(key, value, comparator));
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;
5646
5740
  }
5647
- return n.fixUp();
5648
5741
  }
5649
- removeMin() {
5650
- if (this.left.isEmpty()) {
5651
- return LLRBNode.EMPTY;
5652
- }
5653
- let n = this;
5654
- if (!n.left.isRed() && !n.left.left.isRed()) {
5655
- n = n.moveRedLeft();
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;
5656
5752
  }
5657
- n = n.copy(null, null, null, n.left.removeMin(), null);
5658
- return n.fixUp();
5659
5753
  }
5660
- // Returns new tree, with the specified item removed.
5661
- remove(key, comparator) {
5662
- let smallest;
5663
- let n = this;
5664
- if (comparator(key, n.key) < 0) {
5665
- if (!n.left.isEmpty() && !n.left.isRed() && !n.left.left.isRed()) {
5666
- n = n.moveRedLeft();
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));
5667
5796
  }
5668
- n = n.copy(null, null, null, n.left.remove(key, comparator), null);
5797
+ queryImpl.memoizedOrderBy.push(new OrderBy(FieldPath$1.keyField(), "asc" /* ASCENDING */));
5669
5798
  }
5670
5799
  else {
5671
- if (n.left.isRed()) {
5672
- n = n.rotateRight();
5673
- }
5674
- if (!n.right.isEmpty() && !n.right.isRed() && !n.right.left.isRed()) {
5675
- n = n.moveRedRight();
5676
- }
5677
- if (comparator(key, n.key) === 0) {
5678
- if (n.right.isEmpty()) {
5679
- return LLRBNode.EMPTY;
5680
- }
5681
- else {
5682
- smallest = n.right.min();
5683
- n = n.copy(smallest.key, smallest.value, null, null, n.right.removeMin());
5800
+ let foundKeyOrdering = false;
5801
+ for (const orderBy of queryImpl.explicitOrderBy) {
5802
+ queryImpl.memoizedOrderBy.push(orderBy);
5803
+ if (orderBy.field.isKeyField()) {
5804
+ foundKeyOrdering = true;
5684
5805
  }
5685
5806
  }
5686
- n = n.copy(null, null, null, null, n.right.remove(key, comparator));
5687
- }
5688
- return n.fixUp();
5689
- }
5690
- isRed() {
5691
- return this.color;
5692
- }
5693
- // Returns new tree after performing any needed rotations.
5694
- fixUp() {
5695
- let n = this;
5696
- if (n.right.isRed() && !n.left.isRed()) {
5697
- n = n.rotateLeft();
5698
- }
5699
- if (n.left.isRed() && n.left.left.isRed()) {
5700
- n = n.rotateRight();
5701
- }
5702
- if (n.left.isRed() && n.right.isRed()) {
5703
- n = n.colorFlip();
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
+ }
5704
5816
  }
5705
- return n;
5706
5817
  }
5707
- moveRedLeft() {
5708
- let n = this.colorFlip();
5709
- if (n.right.left.isRed()) {
5710
- n = n.copy(null, null, null, null, n.right.rotateRight());
5711
- n = n.rotateLeft();
5712
- n = n.colorFlip();
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);
5713
5828
  }
5714
- return n;
5715
- }
5716
- moveRedRight() {
5717
- let n = this.colorFlip();
5718
- if (n.left.left.isRed()) {
5719
- n = n.rotateRight();
5720
- n = n.colorFlip();
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);
5721
5847
  }
5722
- return n;
5723
5848
  }
5724
- rotateLeft() {
5725
- const nl = this.copy(null, null, LLRBNode.RED, null, this.right.left);
5726
- return this.right.copy(null, null, this.color, nl, null);
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));
5727
5897
  }
5728
- rotateRight() {
5729
- const nr = this.copy(null, null, LLRBNode.RED, this.left.right, null);
5730
- 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);
5731
5901
  }
5732
- colorFlip() {
5733
- const left = this.left.copy(null, null, !this.left.color, null, null);
5734
- const right = this.right.copy(null, null, !this.right.color, null, null);
5735
- return this.copy(null, null, !this.color, left, right);
5902
+ else {
5903
+ // shallow ancestor queries by default
5904
+ return query.path.isImmediateParentOf(docPath);
5736
5905
  }
5737
- // For testing.
5738
- checkMaxDepth() {
5739
- const blackDepth = this.check();
5740
- if (Math.pow(2.0, blackDepth) <= this.size + 1) {
5741
- return true;
5742
- }
5743
- 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) {
5744
5915
  return false;
5745
5916
  }
5746
5917
  }
5747
- // In a balanced RB tree, the black-depth (number of black nodes) from root to
5748
- // leaves is equal on both sides. This function verifies that or asserts.
5749
- check() {
5750
- if (this.isRed() && this.left.isRed()) {
5751
- throw fail();
5752
- }
5753
- if (this.right.isRed()) {
5754
- throw fail();
5755
- }
5756
- const blackDepth = this.left.check();
5757
- if (blackDepth !== this.right.check()) {
5758
- throw fail();
5759
- }
5760
- else {
5761
- 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;
5762
5924
  }
5763
5925
  }
5764
- } // end LLRBNode
5765
- // Empty node is shared between all LLRB trees.
5766
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5767
- LLRBNode.EMPTY = null;
5768
- LLRBNode.RED = true;
5769
- LLRBNode.BLACK = false;
5770
- // Represents an empty node (a leaf node in the Red-Black Tree).
5771
- class LLRBEmptyNode {
5772
- constructor() {
5773
- this.size = 0;
5774
- }
5775
- get key() {
5776
- throw fail();
5777
- }
5778
- get value() {
5779
- throw fail();
5780
- }
5781
- get color() {
5782
- throw fail();
5783
- }
5784
- get left() {
5785
- throw fail();
5786
- }
5787
- get right() {
5788
- throw fail();
5789
- }
5790
- // Returns a copy of the current node.
5791
- copy(key, value, color, left, right) {
5792
- return this;
5793
- }
5794
- // Returns a copy of the tree, with the specified key/value added.
5795
- insert(key, value, comparator) {
5796
- return new LLRBNode(key, value);
5797
- }
5798
- // Returns a copy of the tree, with the specified key removed.
5799
- remove(key, comparator) {
5800
- return this;
5801
- }
5802
- isEmpty() {
5803
- return true;
5804
- }
5805
- inorderTraversal(action) {
5806
- return false;
5807
- }
5808
- 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)) {
5809
5932
  return false;
5810
5933
  }
5811
- minKey() {
5812
- return null;
5813
- }
5814
- maxKey() {
5815
- return null;
5816
- }
5817
- isRed() {
5934
+ if (query.endAt &&
5935
+ !boundSortsAfterDocument(query.endAt, queryOrderBy(query), doc)) {
5818
5936
  return false;
5819
5937
  }
5820
- // For testing.
5821
- checkMaxDepth() {
5822
- return true;
5823
- }
5824
- 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
+ }
5825
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();
5826
5980
  }
5827
- } // end LLRBEmptyNode
5828
- LLRBNode.EMPTY = new LLRBEmptyNode();
5981
+ }
5829
5982
 
5830
5983
  /**
5831
5984
  * @license
@@ -5844,218 +5997,97 @@ LLRBNode.EMPTY = new LLRBEmptyNode();
5844
5997
  * limitations under the License.
5845
5998
  */
5846
5999
  /**
5847
- * SortedSet is an immutable (copy-on-write) collection that holds elements
5848
- * in order specified by the provided comparator.
5849
- *
5850
- * NOTE: if provided comparator returns 0 for two elements, we consider them to
5851
- * 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.
5852
6004
  */
5853
- class SortedSet {
5854
- constructor(comparator) {
5855
- this.comparator = comparator;
5856
- this.data = new SortedMap(this.comparator);
5857
- }
5858
- has(elem) {
5859
- return this.data.get(elem) !== null;
5860
- }
5861
- first() {
5862
- return this.data.minKey();
5863
- }
5864
- last() {
5865
- return this.data.maxKey();
5866
- }
5867
- get size() {
5868
- return this.data.size;
5869
- }
5870
- indexOf(elem) {
5871
- return this.data.indexOf(elem);
5872
- }
5873
- /** Iterates elements in order defined by "comparator" */
5874
- forEach(cb) {
5875
- this.data.inorderTraversal((k, v) => {
5876
- cb(k);
5877
- return false;
5878
- });
5879
- }
5880
- /** Iterates over `elem`s such that: range[0] &lt;= elem &lt; range[1]. */
5881
- forEachInRange(range, cb) {
5882
- const iter = this.data.getIteratorFrom(range[0]);
5883
- while (iter.hasNext()) {
5884
- const elem = iter.getNext();
5885
- if (this.comparator(elem.key, range[1]) >= 0) {
5886
- return;
5887
- }
5888
- cb(elem.key);
5889
- }
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;
5890
6018
  }
5891
- /**
5892
- * Iterates over `elem`s such that: start &lt;= elem until false is returned.
5893
- */
5894
- forEachWhile(cb, start) {
5895
- let iter;
5896
- if (start !== undefined) {
5897
- iter = this.data.getIteratorFrom(start);
5898
- }
5899
- else {
5900
- iter = this.data.getIterator();
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;
5901
6025
  }
5902
- while (iter.hasNext()) {
5903
- const elem = iter.getNext();
5904
- const result = cb(elem.key);
5905
- if (!result) {
5906
- return;
6026
+ for (const [otherKey, value] of matches) {
6027
+ if (this.equalsFn(otherKey, key)) {
6028
+ return value;
5907
6029
  }
5908
6030
  }
6031
+ return undefined;
5909
6032
  }
5910
- /** Finds the least element greater than or equal to `elem`. */
5911
- firstAfterOrEqual(elem) {
5912
- const iter = this.data.getIteratorFrom(elem);
5913
- return iter.hasNext() ? iter.getNext().key : null;
5914
- }
5915
- getIterator() {
5916
- return new SortedSetIterator(this.data.getIterator());
5917
- }
5918
- getIteratorFrom(key) {
5919
- return new SortedSetIterator(this.data.getIteratorFrom(key));
5920
- }
5921
- /** Inserts or updates an element */
5922
- add(elem) {
5923
- return this.copy(this.data.remove(elem).insert(elem, true));
6033
+ has(key) {
6034
+ return this.get(key) !== undefined;
5924
6035
  }
5925
- /** Deletes an element */
5926
- delete(elem) {
5927
- if (!this.has(elem)) {
5928
- 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;
5929
6044
  }
5930
- return this.copy(this.data.remove(elem));
5931
- }
5932
- isEmpty() {
5933
- return this.data.isEmpty();
5934
- }
5935
- unionWith(other) {
5936
- let result = this;
5937
- // Make sure `result` always refers to the larger one of the two sets.
5938
- if (result.size < other.size) {
5939
- result = other;
5940
- 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
+ }
5941
6051
  }
5942
- other.forEach(elem => {
5943
- result = result.add(elem);
5944
- });
5945
- return result;
6052
+ matches.push([key, value]);
6053
+ this.innerSize++;
5946
6054
  }
5947
- isEqual(other) {
5948
- if (!(other instanceof SortedSet)) {
5949
- return false;
5950
- }
5951
- 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) {
5952
6062
  return false;
5953
6063
  }
5954
- const thisIt = this.data.getIterator();
5955
- const otherIt = other.data.getIterator();
5956
- while (thisIt.hasNext()) {
5957
- const thisElem = thisIt.getNext().key;
5958
- const otherElem = otherIt.getNext().key;
5959
- if (this.comparator(thisElem, otherElem) !== 0) {
5960
- 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;
5961
6074
  }
5962
6075
  }
5963
- return true;
6076
+ return false;
5964
6077
  }
5965
- toArray() {
5966
- const res = [];
5967
- this.forEach(targetId => {
5968
- res.push(targetId);
6078
+ forEach(fn) {
6079
+ forEach(this.inner, (_, entries) => {
6080
+ for (const [k, v] of entries) {
6081
+ fn(k, v);
6082
+ }
5969
6083
  });
5970
- return res;
5971
- }
5972
- toString() {
5973
- const result = [];
5974
- this.forEach(elem => result.push(elem));
5975
- return 'SortedSet(' + result.toString() + ')';
5976
- }
5977
- copy(data) {
5978
- const result = new SortedSet(this.comparator);
5979
- result.data = data;
5980
- return result;
5981
- }
5982
- }
5983
- class SortedSetIterator {
5984
- constructor(iter) {
5985
- this.iter = iter;
5986
6084
  }
5987
- getNext() {
5988
- return this.iter.getNext().key;
5989
- }
5990
- hasNext() {
5991
- return this.iter.hasNext();
6085
+ isEmpty() {
6086
+ return isEmpty(this.inner);
5992
6087
  }
5993
- }
5994
- /**
5995
- * Compares two sorted sets for equality using their natural ordering. The
5996
- * method computes the intersection and invokes `onAdd` for every element that
5997
- * is in `after` but not `before`. `onRemove` is invoked for every element in
5998
- * `before` but missing from `after`.
5999
- *
6000
- * The method creates a copy of both `before` and `after` and runs in O(n log
6001
- * n), where n is the size of the two lists.
6002
- *
6003
- * @param before - The elements that exist in the original set.
6004
- * @param after - The elements to diff against the original set.
6005
- * @param comparator - The comparator for the elements in before and after.
6006
- * @param onAdd - A function to invoke for every element that is part of `
6007
- * after` but not `before`.
6008
- * @param onRemove - A function to invoke for every element that is part of
6009
- * `before` but not `after`.
6010
- */
6011
- function diffSortedSets(before, after, comparator, onAdd, onRemove) {
6012
- const beforeIt = before.getIterator();
6013
- const afterIt = after.getIterator();
6014
- let beforeValue = advanceIterator(beforeIt);
6015
- let afterValue = advanceIterator(afterIt);
6016
- // Walk through the two sets at the same time, using the ordering defined by
6017
- // `comparator`.
6018
- while (beforeValue || afterValue) {
6019
- let added = false;
6020
- let removed = false;
6021
- if (beforeValue && afterValue) {
6022
- const cmp = comparator(beforeValue, afterValue);
6023
- if (cmp < 0) {
6024
- // The element was removed if the next element in our ordered
6025
- // walkthrough is only in `before`.
6026
- removed = true;
6027
- }
6028
- else if (cmp > 0) {
6029
- // The element was added if the next element in our ordered walkthrough
6030
- // is only in `after`.
6031
- added = true;
6032
- }
6033
- }
6034
- else if (beforeValue != null) {
6035
- removed = true;
6036
- }
6037
- else {
6038
- added = true;
6039
- }
6040
- if (added) {
6041
- onAdd(afterValue);
6042
- afterValue = advanceIterator(afterIt);
6043
- }
6044
- else if (removed) {
6045
- onRemove(beforeValue);
6046
- beforeValue = advanceIterator(beforeIt);
6047
- }
6048
- else {
6049
- beforeValue = advanceIterator(beforeIt);
6050
- afterValue = advanceIterator(afterIt);
6051
- }
6088
+ size() {
6089
+ return this.innerSize;
6052
6090
  }
6053
- }
6054
- /**
6055
- * Returns the next element from the iterator or `undefined` if none available.
6056
- */
6057
- function advanceIterator(it) {
6058
- return it.hasNext() ? it.getNext() : undefined;
6059
6091
  }
6060
6092
 
6061
6093
  /**
@@ -6079,8 +6111,12 @@ function mutableDocumentMap() {
6079
6111
  return EMPTY_MUTABLE_DOCUMENT_MAP;
6080
6112
  }
6081
6113
  const EMPTY_DOCUMENT_MAP = new SortedMap(DocumentKey.comparator);
6082
- function documentMap() {
6083
- 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;
6084
6120
  }
6085
6121
  function newOverlayMap() {
6086
6122
  return new ObjectMap(key => key.toString(), (l, r) => l.isEqual(r));
@@ -9973,9 +10009,9 @@ class MemoryIndexManager {
9973
10009
  // Field indices are not supported with memory persistence.
9974
10010
  return PersistencePromise.resolve(null);
9975
10011
  }
9976
- getFieldIndex(transaction, target) {
10012
+ getIndexType(transaction, target) {
9977
10013
  // Field indices are not supported with memory persistence.
9978
- return PersistencePromise.resolve(null);
10014
+ return PersistencePromise.resolve(0 /* NONE */);
9979
10015
  }
9980
10016
  getFieldIndexes(transaction, collectionGroup) {
9981
10017
  // Field indices are not supported with memory persistence.
@@ -9985,6 +10021,9 @@ class MemoryIndexManager {
9985
10021
  // Field indices are not supported with memory persistence.
9986
10022
  return PersistencePromise.resolve(null);
9987
10023
  }
10024
+ getMinOffset(transaction, target) {
10025
+ return PersistencePromise.resolve(IndexOffset.min());
10026
+ }
9988
10027
  updateCollectionGroup(transaction, collectionGroup, offset) {
9989
10028
  // Field indices are not supported with memory persistence.
9990
10029
  return PersistencePromise.resolve();
@@ -10160,7 +10199,7 @@ class IndexedDbIndexManager {
10160
10199
  const lowerBoundEncoded = this.encodeBound(index, subTarget, lowerBound);
10161
10200
  const upperBoundEncoded = this.encodeBound(index, subTarget, upperBound);
10162
10201
  const notInEncoded = this.encodeValues(index, subTarget, notInValues);
10163
- 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);
10164
10203
  return PersistencePromise.forEach(indexRanges, (indexRange) => {
10165
10204
  return indexEntries
10166
10205
  .loadFirst(indexRange, target.limit)
@@ -10198,22 +10237,18 @@ class IndexedDbIndexManager {
10198
10237
  // index range per value in an ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filter
10199
10238
  // combined with the values from the query bounds.
10200
10239
  const totalScans = (arrayValues != null ? arrayValues.length : 1) *
10201
- Math.max(lowerBounds != null ? lowerBounds.length : 1, upperBounds != null ? upperBounds.length : 1);
10240
+ Math.max(lowerBounds.length, upperBounds.length);
10202
10241
  const scansPerArrayElement = totalScans / (arrayValues != null ? arrayValues.length : 1);
10203
10242
  const indexRanges = [];
10204
10243
  for (let i = 0; i < totalScans; ++i) {
10205
10244
  const arrayValue = arrayValues
10206
10245
  ? this.encodeSingleElement(arrayValues[i / scansPerArrayElement])
10207
10246
  : EMPTY_VALUE;
10208
- const lowerBound = lowerBounds
10209
- ? this.generateLowerBound(indexId, arrayValue, lowerBounds[i % scansPerArrayElement], lowerBoundInclusive)
10210
- : this.generateEmptyBound(indexId);
10211
- const upperBound = upperBounds
10212
- ? this.generateUpperBound(indexId, arrayValue, upperBounds[i % scansPerArrayElement], upperBoundInclusive)
10213
- : this.generateEmptyBound(indexId + 1);
10214
- indexRanges.push(...this.createRange(lowerBound, upperBound, notInValues.map((notIn // make non-nullable
10215
- ) => this.generateLowerBound(indexId, arrayValue, notIn,
10216
- /* 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));
10217
10252
  }
10218
10253
  return indexRanges;
10219
10254
  }
@@ -10227,25 +10262,38 @@ class IndexedDbIndexManager {
10227
10262
  const entry = new IndexEntry(indexId, DocumentKey.empty(), arrayValue, directionalValue);
10228
10263
  return inclusive ? entry.successor() : entry;
10229
10264
  }
10230
- /**
10231
- * Generates an empty bound that scopes the index scan to the current index
10232
- * and user.
10233
- */
10234
- generateEmptyBound(indexId) {
10235
- return new IndexEntry(indexId, DocumentKey.empty(), EMPTY_VALUE, EMPTY_VALUE);
10236
- }
10237
10265
  getFieldIndex(transaction, target) {
10238
10266
  const targetIndexMatcher = new TargetIndexMatcher(target);
10239
10267
  const collectionGroup = target.collectionGroup != null
10240
10268
  ? target.collectionGroup
10241
10269
  : target.path.lastSegment();
10242
10270
  return this.getFieldIndexes(transaction, collectionGroup).next(indexes => {
10243
- const matchingIndexes = indexes.filter(i => targetIndexMatcher.servedByIndex(i));
10244
- // Return the index that matches the most number of segments.
10245
- matchingIndexes.sort((l, r) => r.fields.length - l.fields.length);
10246
- 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;
10247
10281
  });
10248
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
+ }
10249
10297
  /**
10250
10298
  * Returns the byte encoded form of the directional values in the field index.
10251
10299
  * Returns `null` if the document does not have all fields specified in the
@@ -10308,9 +10356,6 @@ class IndexedDbIndexManager {
10308
10356
  * queries, a list of possible values is returned.
10309
10357
  */
10310
10358
  encodeBound(fieldIndex, target, bound) {
10311
- if (bound == null) {
10312
- return null;
10313
- }
10314
10359
  return this.encodeValues(fieldIndex, target, bound.position);
10315
10360
  }
10316
10361
  /** Returns the byte representation for the provided encoders. */
@@ -10505,7 +10550,7 @@ class IndexedDbIndexManager {
10505
10550
  * '[foo > 2 && foo != 3]` becomes `[foo > 2 && < 3, foo > 3]`.
10506
10551
  */
10507
10552
  createRange(lower, upper, notInValues) {
10508
- // 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
10509
10554
  // sorted set of non-overlapping ranges.
10510
10555
  notInValues = notInValues
10511
10556
  .sort((l, r) => indexEntryComparator(l, r))
@@ -10551,6 +10596,20 @@ class IndexedDbIndexManager {
10551
10596
  }
10552
10597
  return ranges;
10553
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
+ }
10554
10613
  }
10555
10614
  /**
10556
10615
  * Helper to get a typed SimpleDbStore for the collectionParents
@@ -13721,7 +13780,7 @@ class LocalStoreImpl {
13721
13780
  this.mutationQueue = this.persistence.getMutationQueue(user, this.indexManager);
13722
13781
  this.localDocuments = new LocalDocumentsView(this.remoteDocuments, this.mutationQueue, this.indexManager);
13723
13782
  this.remoteDocuments.setIndexManager(this.indexManager);
13724
- this.queryEngine.setLocalDocumentsView(this.localDocuments);
13783
+ this.queryEngine.initialize(this.localDocuments, this.indexManager);
13725
13784
  }
13726
13785
  collectGarbage(garbageCollector) {
13727
13786
  return this.persistence.runTransaction('Collect garbage', 'readwrite-primary', txn => garbageCollector.collect(txn, this.targetDataByTarget));
@@ -15463,10 +15522,30 @@ class MemoryEagerDelegate {
15463
15522
  * limitations under the License.
15464
15523
  */
15465
15524
  /**
15466
- * A query engine that takes advantage of the target document mapping in the
15467
- * QueryCache. Query execution is optimized by only reading the documents that
15468
- * previously matched a query plus any documents that were edited after the
15469
- * 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.
15470
15549
  *
15471
15550
  * There are some cases when this optimization is not guaranteed to produce
15472
15551
  * the same results as full collection scans. In these cases, query
@@ -15481,16 +15560,41 @@ class MemoryEagerDelegate {
15481
15560
  * - Queries that have never been CURRENT or free of limbo documents.
15482
15561
  */
15483
15562
  class QueryEngine {
15563
+ constructor() {
15564
+ this.initialized = false;
15565
+ }
15484
15566
  /** Sets the document view to query against. */
15485
- setLocalDocumentsView(localDocuments) {
15567
+ initialize(localDocuments, indexManager) {
15486
15568
  this.localDocumentsView = localDocuments;
15569
+ this.indexManager = indexManager;
15570
+ this.initialized = true;
15487
15571
  }
15488
15572
  /** Returns all local documents matching the specified query. */
15489
15573
  getDocumentsMatchingQuery(transaction, query, lastLimboFreeSnapshotVersion, remoteKeys) {
15490
- // Queries that match all documents don't benefit from using
15491
- // key-based lookups. It is more efficient to scan all documents in a
15492
- // collection, rather than to perform individual lookups.
15493
- 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.
15494
15598
  return this.executeFullCollectionScan(transaction, query);
15495
15599
  }
15496
15600
  // Queries that have never seen a snapshot without limbo free documents
@@ -15500,8 +15604,7 @@ class QueryEngine {
15500
15604
  }
15501
15605
  return this.localDocumentsView.getDocuments(transaction, remoteKeys).next(documents => {
15502
15606
  const previousResults = this.applyQuery(query, documents);
15503
- if ((hasLimitToFirst(query) || hasLimitToLast(query)) &&
15504
- this.needsRefill(query.limitType, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
15607
+ if (this.needsRefill(query, previousResults, remoteKeys, lastLimboFreeSnapshotVersion)) {
15505
15608
  return this.executeFullCollectionScan(transaction, query);
15506
15609
  }
15507
15610
  if (getLogLevel() <= LogLevel.DEBUG) {
@@ -15509,15 +15612,7 @@ class QueryEngine {
15509
15612
  }
15510
15613
  // Retrieve all results for documents that were updated since the last
15511
15614
  // limbo-document free remote snapshot.
15512
- return this.localDocumentsView.getDocumentsMatchingQuery(transaction, query, newIndexOffsetSuccessorFromReadTime(lastLimboFreeSnapshotVersion, INITIAL_LARGEST_BATCH_ID)).next(updatedResults => {
15513
- // We merge `previousResults` into `updateResults`, since
15514
- // `updateResults` is already a DocumentMap. If a document is
15515
- // contained in both lists, then its contents are the same.
15516
- previousResults.forEach(doc => {
15517
- updatedResults = updatedResults.insert(doc.key, doc);
15518
- });
15519
- return updatedResults;
15520
- });
15615
+ return this.appendRemainingResults(transaction, previousResults, query, newIndexOffsetSuccessorFromReadTime(lastLimboFreeSnapshotVersion, INITIAL_LARGEST_BATCH_ID));
15521
15616
  });
15522
15617
  }
15523
15618
  /** Applies the query filter and sorting to the provided documents. */
@@ -15536,6 +15631,7 @@ class QueryEngine {
15536
15631
  * Determines if a limit query needs to be refilled from cache, making it
15537
15632
  * ineligible for index-free execution.
15538
15633
  *
15634
+ * @param query The query.
15539
15635
  * @param sortedPreviousResults - The documents that matched the query when it
15540
15636
  * was last synchronized, sorted by the query's comparator.
15541
15637
  * @param remoteKeys - The document keys that matched the query at the last
@@ -15543,10 +15639,14 @@ class QueryEngine {
15543
15639
  * @param limboFreeSnapshotVersion - The version of the snapshot when the
15544
15640
  * query was last synchronized.
15545
15641
  */
15546
- needsRefill(limitType, sortedPreviousResults, remoteKeys, limboFreeSnapshotVersion) {
15547
- // The query needs to be refilled if a previously matching document no
15548
- // 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
+ }
15549
15647
  if (remoteKeys.size !== sortedPreviousResults.size) {
15648
+ // The query needs to be refilled if a previously matching document no
15649
+ // longer matches.
15550
15650
  return true;
15551
15651
  }
15552
15652
  // Limit queries are not eligible for index-free query execution if there is
@@ -15557,7 +15657,7 @@ class QueryEngine {
15557
15657
  // the boundary of the limit itself did not change and documents from cache
15558
15658
  // will continue to be "rejected" by this boundary. Therefore, we can ignore
15559
15659
  // any modifications that don't affect the last document.
15560
- const docAtLimitEdge = limitType === "F" /* First */
15660
+ const docAtLimitEdge = query.limitType === "F" /* First */
15561
15661
  ? sortedPreviousResults.last()
15562
15662
  : sortedPreviousResults.first();
15563
15663
  if (!docAtLimitEdge) {
@@ -15573,6 +15673,22 @@ class QueryEngine {
15573
15673
  }
15574
15674
  return this.localDocumentsView.getDocumentsMatchingQuery(transaction, query, IndexOffset.min());
15575
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
+ }
15576
15692
  }
15577
15693
 
15578
15694
  /**
@@ -21832,10 +21948,12 @@ class View {
21832
21948
  //
21833
21949
  // Note that this should never get used in a refill (when previousChanges is
21834
21950
  // set), because there will only be adds -- no deletes or updates.
21835
- const lastDocInLimit = hasLimitToFirst(this.query) && oldDocumentSet.size === this.query.limit
21951
+ const lastDocInLimit = this.query.limitType === "F" /* First */ &&
21952
+ oldDocumentSet.size === this.query.limit
21836
21953
  ? oldDocumentSet.last()
21837
21954
  : null;
21838
- const firstDocInLimit = hasLimitToLast(this.query) && oldDocumentSet.size === this.query.limit
21955
+ const firstDocInLimit = this.query.limitType === "L" /* Last */ &&
21956
+ oldDocumentSet.size === this.query.limit
21839
21957
  ? oldDocumentSet.first()
21840
21958
  : null;
21841
21959
  docChanges.inorderTraversal((key, entry) => {
@@ -21908,9 +22026,9 @@ class View {
21908
22026
  }
21909
22027
  });
21910
22028
  // Drop documents out to meet limit/limitToLast requirement.
21911
- if (hasLimitToFirst(this.query) || hasLimitToLast(this.query)) {
22029
+ if (this.query.limit !== null) {
21912
22030
  while (newDocumentSet.size > this.query.limit) {
21913
- const oldDoc = hasLimitToFirst(this.query)
22031
+ const oldDoc = this.query.limitType === "F" /* First */
21914
22032
  ? newDocumentSet.last()
21915
22033
  : newDocumentSet.first();
21916
22034
  newDocumentSet = newDocumentSet.delete(oldDoc.key);
@@ -23911,18 +24029,18 @@ class Transaction$2 {
23911
24029
  * See the License for the specific language governing permissions and
23912
24030
  * limitations under the License.
23913
24031
  */
23914
- const DEFAULT_MAX_ATTEMPTS_COUNT = 5;
23915
24032
  /**
23916
24033
  * TransactionRunner encapsulates the logic needed to run and retry transactions
23917
24034
  * with backoff.
23918
24035
  */
23919
24036
  class TransactionRunner {
23920
- constructor(asyncQueue, datastore, updateFunction, deferred) {
24037
+ constructor(asyncQueue, datastore, options, updateFunction, deferred) {
23921
24038
  this.asyncQueue = asyncQueue;
23922
24039
  this.datastore = datastore;
24040
+ this.options = options;
23923
24041
  this.updateFunction = updateFunction;
23924
24042
  this.deferred = deferred;
23925
- this.attemptsRemaining = DEFAULT_MAX_ATTEMPTS_COUNT;
24043
+ this.attemptsRemaining = options.maxAttempts;
23926
24044
  this.backoff = new ExponentialBackoff(this.asyncQueue, "transaction_retry" /* TransactionRetry */);
23927
24045
  }
23928
24046
  /** Runs the transaction and sets the result on deferred. */
@@ -24280,11 +24398,11 @@ function firestoreClientAddSnapshotsInSyncListener(client, observer) {
24280
24398
  * been committed. For this reason, it is required that all reads are
24281
24399
  * performed before any writes. Transactions must be performed while online.
24282
24400
  */
24283
- function firestoreClientTransaction(client, updateFunction) {
24401
+ function firestoreClientTransaction(client, updateFunction, options) {
24284
24402
  const deferred = new Deferred();
24285
24403
  client.asyncQueue.enqueueAndForget(async () => {
24286
24404
  const datastore = await getDatastore(client);
24287
- new TransactionRunner(client.asyncQueue, datastore, updateFunction, deferred).run();
24405
+ new TransactionRunner(client.asyncQueue, datastore, options, updateFunction, deferred).run();
24288
24406
  });
24289
24407
  return deferred.promise;
24290
24408
  }
@@ -27041,7 +27159,8 @@ function snapshotEqual(left, right) {
27041
27159
  * limitations under the License.
27042
27160
  */
27043
27161
  function validateHasExplicitOrderByForLimitToLast(query) {
27044
- if (hasLimitToLast(query) && query.explicitOrderBy.length === 0) {
27162
+ if (query.limitType === "L" /* Last */ &&
27163
+ query.explicitOrderBy.length === 0) {
27045
27164
  throw new FirestoreError(Code.UNIMPLEMENTED, 'limitToLast() queries require specifying at least one orderBy() clause');
27046
27165
  }
27047
27166
  }
@@ -27483,6 +27602,31 @@ function validateOrderByAndInequalityMatch(baseQuery, inequality, orderBy) {
27483
27602
  }
27484
27603
  }
27485
27604
 
27605
+ /**
27606
+ * @license
27607
+ * Copyright 2022 Google LLC
27608
+ *
27609
+ * Licensed under the Apache License, Version 2.0 (the "License");
27610
+ * you may not use this file except in compliance with the License.
27611
+ * You may obtain a copy of the License at
27612
+ *
27613
+ * http://www.apache.org/licenses/LICENSE-2.0
27614
+ *
27615
+ * Unless required by applicable law or agreed to in writing, software
27616
+ * distributed under the License is distributed on an "AS IS" BASIS,
27617
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27618
+ * See the License for the specific language governing permissions and
27619
+ * limitations under the License.
27620
+ */
27621
+ const DEFAULT_TRANSACTION_OPTIONS = {
27622
+ maxAttempts: 5
27623
+ };
27624
+ function validateTransactionOptions(options) {
27625
+ if (options.maxAttempts < 1) {
27626
+ throw new FirestoreError(Code.INVALID_ARGUMENT, 'Max attempts must be at least 1');
27627
+ }
27628
+ }
27629
+
27486
27630
  /**
27487
27631
  * @license
27488
27632
  * Copyright 2020 Google LLC
@@ -28204,10 +28348,12 @@ class Transaction extends Transaction$1 {
28204
28348
  * `updateFunction `is returned here. Otherwise, if the transaction failed, a
28205
28349
  * rejected promise with the corresponding failure error is returned.
28206
28350
  */
28207
- function runTransaction(firestore, updateFunction) {
28351
+ function runTransaction(firestore, updateFunction, options) {
28208
28352
  firestore = cast(firestore, Firestore);
28353
+ const optionsWithDefaults = Object.assign(Object.assign({}, DEFAULT_TRANSACTION_OPTIONS), options);
28354
+ validateTransactionOptions(optionsWithDefaults);
28209
28355
  const client = ensureFirestoreConfigured(firestore);
28210
- return firestoreClientTransaction(client, internalTransaction => updateFunction(new Transaction(firestore, internalTransaction)));
28356
+ return firestoreClientTransaction(client, internalTransaction => updateFunction(new Transaction(firestore, internalTransaction)), optionsWithDefaults);
28211
28357
  }
28212
28358
 
28213
28359
  /**