Skip to content

Commit 15409a7

Browse files
Support plumbing for partial index execution (#6143)
1 parent 1cf124e commit 15409a7

File tree

6 files changed

+238
-21
lines changed

6 files changed

+238
-21
lines changed

packages/firestore/src/core/target.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
} from '../model/values';
4242
import { Value as ProtoValue } from '../protos/firestore_proto_api';
4343
import { debugAssert, debugCast, fail } from '../util/assert';
44+
import { SortedSet } from '../util/sorted_set';
4445
import { isNullOrUndefined } from '../util/types';
4546

4647
/**
@@ -484,6 +485,46 @@ function targetGetUpperBoundForField(
484485
return { value, inclusive };
485486
}
486487

488+
/** Returns the number of segments of a perfect index for this target. */
489+
export function targetGetSegmentCount(target: Target): number {
490+
let fields = new SortedSet<FieldPath>(FieldPath.comparator);
491+
let hasArraySegment = false;
492+
493+
for (const filter of target.filters) {
494+
// TODO(orquery): Use the flattened filters here
495+
const fieldFilter = filter as FieldFilter;
496+
497+
// __name__ is not an explicit segment of any index, so we don't need to
498+
// count it.
499+
if (fieldFilter.field.isKeyField()) {
500+
continue;
501+
}
502+
503+
// ARRAY_CONTAINS or ARRAY_CONTAINS_ANY filters must be counted separately.
504+
// For instance, it is possible to have an index for "a ARRAY a ASC". Even
505+
// though these are on the same field, they should be counted as two
506+
// separate segments in an index.
507+
if (
508+
fieldFilter.op === Operator.ARRAY_CONTAINS ||
509+
fieldFilter.op === Operator.ARRAY_CONTAINS_ANY
510+
) {
511+
hasArraySegment = true;
512+
} else {
513+
fields = fields.add(fieldFilter.field);
514+
}
515+
}
516+
517+
for (const orderBy of target.orderBy) {
518+
// __name__ is not an explicit segment of any index, so we don't need to
519+
// count it.
520+
if (!orderBy.field.isKeyField()) {
521+
fields = fields.add(orderBy.field);
522+
}
523+
}
524+
525+
return fields.size + (hasArraySegment ? 1 : 0);
526+
}
527+
487528
export abstract class Filter {
488529
abstract matches(doc: Document): boolean;
489530
}

packages/firestore/src/local/index_manager.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ import { ResourcePath } from '../model/path';
2424
import { PersistencePromise } from './persistence_promise';
2525
import { PersistenceTransaction } from './persistence_transaction';
2626

27+
/** Represents the index state as it relates to a particular target. */
28+
export const enum IndexType {
29+
/** Indicates that no index could be found for serving the target. */
30+
NONE,
31+
/**
32+
* Indicates that only a "partial index" could be found for serving the
33+
* target. A partial index is one which does not have a segment for every
34+
* filter/orderBy in the target.
35+
*/
36+
PARTIAL,
37+
/**
38+
* Indicates that a "full index" could be found for serving the target. A full
39+
* index is one which has a segment for every filter/orderBy in the target.
40+
*/
41+
FULL
42+
}
43+
2744
/**
2845
* Represents a set of indexes that are used to execute queries efficiently.
2946
*
@@ -94,13 +111,13 @@ export interface IndexManager {
94111
): PersistencePromise<FieldIndex[]>;
95112

96113
/**
97-
* Returns an index that can be used to serve the provided target. Returns
98-
* `null` if no index is configured.
114+
* Returns the type of index (if any) that can be used to serve the given
115+
* target.
99116
*/
100-
getFieldIndex(
117+
getIndexType(
101118
transaction: PersistenceTransaction,
102119
target: Target
103-
): PersistencePromise<FieldIndex | null>;
120+
): PersistencePromise<IndexType>;
104121

105122
/**
106123
* Returns the documents that match the given target based on the provided

packages/firestore/src/local/indexeddb_index_manager.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
targetGetArrayValues,
2828
targetGetLowerBound,
2929
targetGetNotInValues,
30+
targetGetSegmentCount,
3031
targetGetUpperBound
3132
} from '../core/target';
3233
import { FirestoreIndexValueWriter } from '../index/firestore_index_value_writer';
@@ -59,7 +60,7 @@ import {
5960
decodeResourcePath,
6061
encodeResourcePath
6162
} from './encoded_resource_path';
62-
import { IndexManager } from './index_manager';
63+
import { IndexManager, IndexType } from './index_manager';
6364
import {
6465
DbCollectionParent,
6566
DbIndexConfiguration,
@@ -438,7 +439,7 @@ export class IndexedDbIndexManager implements IndexManager {
438439
);
439440
}
440441

441-
getFieldIndex(
442+
private getFieldIndex(
442443
transaction: PersistenceTransaction,
443444
target: Target
444445
): PersistencePromise<FieldIndex | null> {
@@ -449,13 +450,33 @@ export class IndexedDbIndexManager implements IndexManager {
449450
: target.path.lastSegment();
450451

451452
return this.getFieldIndexes(transaction, collectionGroup).next(indexes => {
452-
const matchingIndexes = indexes.filter(i =>
453-
targetIndexMatcher.servedByIndex(i)
454-
);
453+
// Return the index with the most number of segments.
454+
let index: FieldIndex | null = null;
455+
for (const candidate of indexes) {
456+
const matches = targetIndexMatcher.servedByIndex(candidate);
457+
if (
458+
matches &&
459+
(!index || candidate.fields.length > index.fields.length)
460+
) {
461+
index = candidate;
462+
}
463+
}
464+
return index;
465+
});
466+
}
455467

456-
// Return the index that matches the most number of segments.
457-
matchingIndexes.sort((l, r) => r.fields.length - l.fields.length);
458-
return matchingIndexes.length > 0 ? matchingIndexes[0] : null;
468+
getIndexType(
469+
transaction: PersistenceTransaction,
470+
target: Target
471+
): PersistencePromise<IndexType> {
472+
// TODO(orqueries): We should look at the subtargets here
473+
return this.getFieldIndex(transaction, target).next(index => {
474+
if (!index) {
475+
return IndexType.NONE as IndexType;
476+
}
477+
return index.fields.length < targetGetSegmentCount(target)
478+
? IndexType.PARTIAL
479+
: IndexType.FULL;
459480
});
460481
}
461482

packages/firestore/src/local/memory_index_manager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { ResourcePath } from '../model/path';
2323
import { debugAssert } from '../util/assert';
2424
import { SortedSet } from '../util/sorted_set';
2525

26-
import { IndexManager } from './index_manager';
26+
import { IndexManager, IndexType } from './index_manager';
2727
import { PersistencePromise } from './persistence_promise';
2828
import { PersistenceTransaction } from './persistence_transaction';
2929

@@ -74,12 +74,12 @@ export class MemoryIndexManager implements IndexManager {
7474
return PersistencePromise.resolve<DocumentKey[] | null>(null);
7575
}
7676

77-
getFieldIndex(
77+
getIndexType(
7878
transaction: PersistenceTransaction,
7979
target: Target
80-
): PersistencePromise<FieldIndex | null> {
80+
): PersistencePromise<IndexType> {
8181
// Field indices are not supported with memory persistence.
82-
return PersistencePromise.resolve<FieldIndex | null>(null);
82+
return PersistencePromise.resolve<IndexType>(IndexType.NONE);
8383
}
8484

8585
getFieldIndexes(

packages/firestore/test/unit/local/index_manager.test.ts

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
queryWithStartAt
3131
} from '../../../src/core/query';
3232
import { FieldFilter } from '../../../src/core/target';
33+
import { IndexType } from '../../../src/local/index_manager';
3334
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
3435
import { INDEXING_SCHEMA_VERSION } from '../../../src/local/indexeddb_schema';
3536
import { Persistence } from '../../../src/local/persistence';
@@ -631,7 +632,9 @@ describe('IndexedDbIndexManager', async () => {
631632
query('coll'),
632633
filter('unknown', '==', true)
633634
);
634-
expect(await indexManager.getFieldIndex(queryToTarget(q))).to.be.null;
635+
expect(await indexManager.getIndexType(queryToTarget(q))).to.equal(
636+
IndexType.NONE
637+
);
635638
expect(await indexManager.getDocumentsMatchingTarget(queryToTarget(q))).to
636639
.be.null;
637640
});
@@ -1416,6 +1419,141 @@ describe('IndexedDbIndexManager', async () => {
14161419
);
14171420
});
14181421

1422+
it('serves partial and full index', async () => {
1423+
await indexManager.addFieldIndex(
1424+
fieldIndex('coll', { fields: [['a', IndexKind.ASCENDING]] })
1425+
);
1426+
await indexManager.addFieldIndex(
1427+
fieldIndex('coll', { fields: [['b', IndexKind.ASCENDING]] })
1428+
);
1429+
await indexManager.addFieldIndex(
1430+
fieldIndex('coll', {
1431+
fields: [
1432+
['c', IndexKind.ASCENDING],
1433+
['d', IndexKind.ASCENDING]
1434+
]
1435+
})
1436+
);
1437+
1438+
const query1 = queryWithAddedFilter(query('coll'), filter('a', '==', 1));
1439+
await validateIsFullIndex(query1);
1440+
1441+
const query2 = queryWithAddedFilter(query('coll'), filter('b', '==', 1));
1442+
await validateIsFullIndex(query2);
1443+
1444+
const query3 = queryWithAddedOrderBy(
1445+
queryWithAddedFilter(query('coll'), filter('a', '==', 1)),
1446+
orderBy('a')
1447+
);
1448+
await validateIsFullIndex(query3);
1449+
1450+
const query4 = queryWithAddedOrderBy(
1451+
queryWithAddedFilter(query('coll'), filter('b', '==', 1)),
1452+
orderBy('b')
1453+
);
1454+
await validateIsFullIndex(query4);
1455+
1456+
const query5 = queryWithAddedFilter(
1457+
queryWithAddedFilter(query('coll'), filter('a', '==', 1)),
1458+
filter('b', '==', 1)
1459+
);
1460+
await validateIsPartialIndex(query5);
1461+
1462+
const query6 = queryWithAddedOrderBy(
1463+
queryWithAddedFilter(query('coll'), filter('a', '==', 1)),
1464+
orderBy('b')
1465+
);
1466+
await validateIsPartialIndex(query6);
1467+
1468+
const query7 = queryWithAddedOrderBy(
1469+
queryWithAddedFilter(query('coll'), filter('b', '==', 1)),
1470+
orderBy('a')
1471+
);
1472+
await validateIsPartialIndex(query7);
1473+
1474+
const query8 = queryWithAddedFilter(
1475+
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
1476+
filter('d', '==', 1)
1477+
);
1478+
await validateIsFullIndex(query8);
1479+
1480+
const query9 = queryWithAddedOrderBy(
1481+
queryWithAddedFilter(
1482+
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
1483+
filter('d', '==', 1)
1484+
),
1485+
orderBy('c')
1486+
);
1487+
await validateIsFullIndex(query9);
1488+
1489+
const query10 = queryWithAddedOrderBy(
1490+
queryWithAddedFilter(
1491+
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
1492+
filter('d', '==', 1)
1493+
),
1494+
orderBy('d')
1495+
);
1496+
await validateIsFullIndex(query10);
1497+
1498+
const query11 = queryWithAddedOrderBy(
1499+
queryWithAddedOrderBy(
1500+
queryWithAddedFilter(
1501+
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
1502+
filter('d', '==', 1)
1503+
),
1504+
orderBy('c')
1505+
),
1506+
orderBy('d')
1507+
);
1508+
await validateIsFullIndex(query11);
1509+
1510+
const query12 = queryWithAddedOrderBy(
1511+
queryWithAddedOrderBy(
1512+
queryWithAddedFilter(
1513+
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
1514+
filter('d', '==', 1)
1515+
),
1516+
orderBy('d')
1517+
),
1518+
orderBy('c')
1519+
);
1520+
await validateIsFullIndex(query12);
1521+
1522+
const query13 = queryWithAddedOrderBy(
1523+
queryWithAddedFilter(
1524+
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
1525+
filter('d', '==', 1)
1526+
),
1527+
orderBy('e')
1528+
);
1529+
await validateIsPartialIndex(query13);
1530+
1531+
const query14 = queryWithAddedFilter(
1532+
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
1533+
filter('d', '<=', 1)
1534+
);
1535+
await validateIsFullIndex(query14);
1536+
1537+
const query15 = queryWithAddedOrderBy(
1538+
queryWithAddedFilter(
1539+
queryWithAddedFilter(query('coll'), filter('c', '==', 1)),
1540+
filter('d', '>', 1)
1541+
),
1542+
orderBy('d')
1543+
);
1544+
await validateIsFullIndex(query15);
1545+
});
1546+
1547+
async function validateIsPartialIndex(query: Query): Promise<void> {
1548+
const indexType = await indexManager.getIndexType(queryToTarget(query));
1549+
expect(indexType).to.equal(IndexType.PARTIAL);
1550+
}
1551+
1552+
async function validateIsFullIndex(query: Query): Promise<void> {
1553+
const indexType = await indexManager.getIndexType(queryToTarget(query));
1554+
expect(indexType).to.equal(IndexType.FULL);
1555+
}
1556+
14191557
async function setUpSingleValueFilter(): Promise<void> {
14201558
await indexManager.addFieldIndex(
14211559
fieldIndex('coll', { fields: [['count', IndexKind.ASCENDING]] })

packages/firestore/test/unit/local/test_index_manager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import { Target } from '../../../src/core/target';
19-
import { IndexManager } from '../../../src/local/index_manager';
19+
import { IndexManager, IndexType } from '../../../src/local/index_manager';
2020
import { Persistence } from '../../../src/local/persistence';
2121
import { DocumentMap } from '../../../src/model/collections';
2222
import { DocumentKey } from '../../../src/model/document_key';
@@ -71,9 +71,9 @@ export class TestIndexManager {
7171
);
7272
}
7373

74-
getFieldIndex(target: Target): Promise<FieldIndex | null> {
75-
return this.persistence.runTransaction('getFieldIndex', 'readonly', txn =>
76-
this.indexManager.getFieldIndex(txn, target)
74+
getIndexType(target: Target): Promise<IndexType> {
75+
return this.persistence.runTransaction('getIndexType', 'readonly', txn =>
76+
this.indexManager.getIndexType(txn, target)
7777
);
7878
}
7979

0 commit comments

Comments
 (0)