Skip to content

Commit 09d1c36

Browse files
authored
feat(indexes): show shard key in indexes table COMPASS-6868 (#7539)
1 parent 0c34290 commit 09d1c36

File tree

9 files changed

+317
-49
lines changed

9 files changed

+317
-49
lines changed

packages/compass-crud/src/utils/cancellable-queries.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ describe('cancellable-queries', function () {
1818
const cluster = mochaTestServer();
1919
let dataService: DataService;
2020
let preferences: PreferencesAccess;
21-
let abortController;
22-
let signal;
21+
let abortController: AbortController;
22+
let signal: AbortSignal;
2323

2424
before(async function () {
2525
preferences = await createSandboxFromDefaultPreferences();
@@ -99,7 +99,7 @@ describe('cancellable-queries', function () {
9999
dataService,
100100
preferences,
101101
'cancel.numbers',
102-
null,
102+
{},
103103
{
104104
signal,
105105
}
@@ -138,7 +138,7 @@ describe('cancellable-queries', function () {
138138
dataService,
139139
preferences,
140140
'cancel.numbers',
141-
'this is not a filter',
141+
{},
142142
{
143143
signal,
144144
hint: { _id_: 1 }, // this collection doesn't have this index so this query should fail

packages/compass-crud/src/utils/cancellable-queries.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,13 @@ export async function fetchShardingKeys(
7979
}
8080
): Promise<BSONObject> {
8181
try {
82-
const docs = await dataService.find(
83-
'config.collections',
84-
{
85-
_id: ns as any,
86-
// unsplittable introduced in SPM-3364 to mark unsharded collections
87-
// that are still being tracked in the catalog
88-
unsplittable: { $ne: true },
89-
},
90-
{ maxTimeMS, projection: { key: 1, _id: 0 } },
82+
const shardKey = await dataService.fetchShardKey(
83+
ns,
84+
{ maxTimeMS },
9185
{ abortSignal: signal }
9286
);
93-
return docs.length ? docs[0].key : {};
94-
} catch (err: any) {
87+
return shardKey ?? {};
88+
} catch (err) {
9589
// rethrow if we aborted along the way
9690
if (dataService.isCancelError(err)) {
9791
throw err;

packages/compass-crud/src/utils/data-service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export type RequiredDataServiceProps =
2121
| 'collectionStats'
2222
| 'collectionInfo'
2323
| 'listCollections'
24-
| 'isListSearchIndexesSupported';
24+
| 'isListSearchIndexesSupported'
25+
| 'fetchShardKey';
2526
// TODO: It might make sense to refactor the DataService interface to be closer to
2627
// { ..., getCSFLEMode(): 'unavailable' } | { ..., getCSFLEMode(): 'unavailable' | 'enabled' | 'disabled', isUpdateAllowed(): ..., knownSchemaForCollection(): ... }
2728
// so that either these methods are always present together or always absent

packages/compass-indexes/src/components/regular-indexes-table/property-field.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ export const getPropertyTooltip = (
3535
return null;
3636
};
3737

38+
const HIDDEN_INDEX_TEXT = 'HIDDEN';
39+
const SHARD_KEY_INDEX_TEXT = 'SHARD KEY';
40+
41+
export const getPropertyText = (
42+
property: RegularIndex['properties'][number]
43+
): string => {
44+
if (property === 'shardKey') {
45+
return SHARD_KEY_INDEX_TEXT;
46+
}
47+
48+
return property;
49+
};
50+
3851
const PropertyBadgeWithTooltip: React.FunctionComponent<{
3952
text: string;
4053
link: string;
@@ -65,8 +78,6 @@ type PropertyFieldProps = {
6578
properties: RegularIndex['properties'];
6679
};
6780

68-
const HIDDEN_INDEX_TEXT = 'HIDDEN';
69-
7081
const PropertyField: React.FunctionComponent<PropertyFieldProps> = ({
7182
extra,
7283
properties,
@@ -79,7 +90,7 @@ const PropertyField: React.FunctionComponent<PropertyFieldProps> = ({
7990
return (
8091
<PropertyBadgeWithTooltip
8192
key={property}
82-
text={property}
93+
text={getPropertyText(property)}
8394
link={getIndexHelpLink(property) ?? '#'}
8495
tooltip={getPropertyTooltip(property, extra)}
8596
/>

packages/compass-indexes/src/utils/index-link-helper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ const HELP_URLS = {
2222
'https://docs.mongodb.com/master/reference/bson-type-comparison-order/#collation',
2323
COLLATION_REF: 'https://docs.mongodb.com/master/reference/collation',
2424
HIDDEN: 'https://www.mongodb.com/docs/manual/core/index-hidden/',
25+
SHARDKEY: 'https://www.mongodb.com/docs/manual/core/sharding-shard-key/',
2526
UNKNOWN: null,
2627
};
2728

2829
export type HELP_URL_KEY =
30+
| 'shardKey' // The only camelCase key at the moment.
2931
| Uppercase<keyof typeof HELP_URLS>
3032
| Lowercase<keyof typeof HELP_URLS>;
3133

packages/data-service/src/data-service.spec.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe('DataService', function () {
115115
fatal: () => {},
116116
};
117117

118-
let dataServiceLogTest;
118+
let dataServiceLogTest: DataService | undefined;
119119

120120
beforeEach(function () {
121121
logs.length = 0;
@@ -1557,6 +1557,24 @@ describe('DataService', function () {
15571557
});
15581558
});
15591559

1560+
describe('#fetchShardKey', function () {
1561+
beforeEach(async function () {
1562+
await mongoClient
1563+
.db(testDatabaseName)
1564+
.collection(testCollectionName)
1565+
.createIndex(
1566+
{
1567+
a: 1,
1568+
},
1569+
{}
1570+
);
1571+
});
1572+
1573+
it('fetches the shard key (there is none in this test)', async function () {
1574+
expect(await dataService.fetchShardKey(testNamespace)).to.equal(null);
1575+
});
1576+
});
1577+
15601578
describe('CSFLE logging', function () {
15611579
it('picks a selected set of CSFLE options for logging', function () {
15621580
const fleOptions: ConnectionFleOptions = {
@@ -2010,6 +2028,122 @@ describe('DataService', function () {
20102028
});
20112029
});
20122030

2031+
context('with real sharded cluster', function () {
2032+
this.slow(10_000);
2033+
this.timeout(20_000);
2034+
2035+
const cluster = mochaTestServer({
2036+
topology: 'sharded',
2037+
secondaries: 0,
2038+
});
2039+
2040+
let dataService: DataServiceImpl;
2041+
let mongoClient: MongoClient;
2042+
let connectionOptions: ConnectionOptions;
2043+
let testCollectionName: string;
2044+
let testDatabaseName: string;
2045+
let testNamespace: string;
2046+
2047+
before(async function () {
2048+
testDatabaseName = `compass-data-service-sharded-tests`;
2049+
const connectionString = cluster().connectionString;
2050+
connectionOptions = {
2051+
connectionString,
2052+
};
2053+
2054+
mongoClient = new MongoClient(connectionOptions.connectionString);
2055+
await mongoClient.connect();
2056+
2057+
dataService = new DataServiceImpl(connectionOptions);
2058+
await dataService.connect();
2059+
});
2060+
2061+
after(async function () {
2062+
// eslint-disable-next-line no-console
2063+
await dataService?.disconnect().catch(console.log);
2064+
await mongoClient?.close();
2065+
});
2066+
2067+
beforeEach(async function () {
2068+
testCollectionName = `coll-${new UUID().toString()}`;
2069+
testNamespace = `${testDatabaseName}.${testCollectionName}`;
2070+
2071+
await mongoClient
2072+
.db(testDatabaseName)
2073+
.collection(testCollectionName)
2074+
.insertMany(TEST_DOCS);
2075+
2076+
await mongoClient
2077+
.db(testDatabaseName)
2078+
.collection(testCollectionName)
2079+
.createIndex(
2080+
{
2081+
a: 1,
2082+
},
2083+
{}
2084+
);
2085+
});
2086+
2087+
afterEach(async function () {
2088+
sinon.restore();
2089+
2090+
await mongoClient
2091+
.db(testDatabaseName)
2092+
.collection(testCollectionName)
2093+
.drop();
2094+
});
2095+
2096+
describe('with a sharded collection', function () {
2097+
beforeEach(async function () {
2098+
await runCommand(dataService['_database']('admin', 'META'), {
2099+
shardCollection: testNamespace,
2100+
key: {
2101+
a: 1,
2102+
},
2103+
// We don't run the shardCollection command outside of tests
2104+
// so it isn't part of the runCommand type.
2105+
} as unknown as Parameters<typeof runCommand>[1]);
2106+
});
2107+
2108+
describe('#fetchShardKey', function () {
2109+
it('fetches the shard key', async function () {
2110+
expect(await dataService.fetchShardKey(testNamespace)).to.deep.equal({
2111+
a: 1,
2112+
});
2113+
2114+
// Can be cancelled.
2115+
const abortController = new AbortController();
2116+
const abortSignal = abortController.signal;
2117+
const promise = dataService
2118+
.fetchShardKey(
2119+
testNamespace,
2120+
{},
2121+
{ abortSignal: abortSignal as unknown as AbortSignal }
2122+
)
2123+
.catch((err) => err);
2124+
abortController.abort();
2125+
const error = await promise;
2126+
2127+
expect(dataService.isCancelError(error)).to.be.true;
2128+
});
2129+
});
2130+
2131+
describe('#indexes', function () {
2132+
it('includes the shard key', async function () {
2133+
const indexes = await dataService.indexes(testNamespace);
2134+
2135+
expect(indexes.length).to.equal(2);
2136+
expect(
2137+
indexes.find((index) => index.key._id === 1)?.properties
2138+
).to.not.include('shardKey');
2139+
expect(
2140+
indexes.find((index) => index.key.a === 1)?.properties
2141+
).to.include('shardKey');
2142+
});
2143+
});
2144+
});
2145+
});
2146+
20132147
context('with mocked client', function () {
20142148
function createDataServiceWithMockedClient(
20152149
clientConfig: Partial<ClientMockOptions>

packages/data-service/src/data-service.ts

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,19 @@ export interface DataService {
733733
}
734734
): Promise<Document[]>;
735735

736+
/**
737+
* Fetch shard keys for the collection from the collections config.
738+
*
739+
* @param ns - The namespace to try to find shard key for.
740+
* @param options - The query options.
741+
* @param executionOptions - The execution options.
742+
*/
743+
fetchShardKey(
744+
ns: string,
745+
options?: Omit<FindOptions, 'projection'>,
746+
executionOptions?: ExecutionOptions
747+
): Promise<Record<string, unknown> | null>;
748+
736749
/*** Insert ***/
737750

738751
/**
@@ -2234,6 +2247,44 @@ class DataServiceImpl extends WithLogContext implements DataService {
22342247
return indexToProgress;
22352248
}
22362249

2250+
@op(mongoLogId(1_001_000_380))
2251+
async fetchShardKey(
2252+
ns: string,
2253+
options: Omit<FindOptions, 'projection'> = {},
2254+
executionOptions?: ExecutionOptions
2255+
): Promise<Record<string, unknown> | null> {
2256+
const docs = await this.find(
2257+
'config.collections',
2258+
{
2259+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2260+
_id: ns as any,
2261+
// unsplittable introduced in SPM-3364 to mark unsharded collections
2262+
// that are still being tracked in the catalog
2263+
unsplittable: { $ne: true },
2264+
},
2265+
{ ...options, projection: { key: 1, _id: 0 } },
2266+
{ abortSignal: executionOptions?.abortSignal }
2267+
);
2268+
return docs.length ? docs[0].key : null;
2269+
}
2270+
2271+
private async _fetchShardKeyWithSilentFail(
2272+
...args: Parameters<DataService['fetchShardKey']>
2273+
): ReturnType<DataService['fetchShardKey']> {
2274+
try {
2275+
return await this.fetchShardKey(...args);
2276+
} catch (err) {
2277+
// Rethrow if we aborted along the way.
2278+
if (this.isCancelError(err)) {
2279+
throw err;
2280+
}
2281+
2282+
// Return null on error.
2283+
// This is oftentimes a lack of permissions to run a find on config.collections.
2284+
return null;
2285+
}
2286+
}
2287+
22372288
@op(mongoLogId(1_001_000_047))
22382289
async indexes(
22392290
ns: string,
@@ -2245,19 +2296,21 @@ class DataServiceImpl extends WithLogContext implements DataService {
22452296
);
22462297
return indexes.map((compactIndexEntry) => {
22472298
const [name, keys] = compactIndexEntry;
2248-
return createIndexDefinition(ns, {
2299+
return createIndexDefinition(ns, null, {
22492300
name,
22502301
key: Object.fromEntries(keys),
22512302
});
22522303
});
22532304
}
22542305

2255-
const [indexes, indexStats, indexSizes, indexProgress] = await Promise.all([
2256-
this._collection(ns, 'CRUD').indexes({ ...options, full: true }),
2257-
this._indexStats(ns),
2258-
this._indexSizes(ns),
2259-
this._indexProgress(ns),
2260-
]);
2306+
const [indexes, indexStats, indexSizes, indexProgress, shardKey] =
2307+
await Promise.all([
2308+
this._collection(ns, 'CRUD').indexes({ ...options, full: true }),
2309+
this._indexStats(ns),
2310+
this._indexSizes(ns),
2311+
this._indexProgress(ns),
2312+
this._fetchShardKeyWithSilentFail(ns),
2313+
]);
22612314

22622315
const maxSize = Math.max(...Object.values(indexSizes));
22632316

@@ -2269,6 +2322,7 @@ class DataServiceImpl extends WithLogContext implements DataService {
22692322
const name = index.name;
22702323
return createIndexDefinition(
22712324
ns,
2325+
shardKey ?? null,
22722326
index,
22732327
indexStats[name],
22742328
indexSizes[name],

0 commit comments

Comments
 (0)