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