@firebase/firestore 3.4.7 → 3.4.8-canary.44948ac91

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