Skip to content

Commit 20396e1

Browse files
feat(NODE-6342): support maxTimeMS for explain commands (#4207)
1 parent 7b71e1f commit 20396e1

File tree

10 files changed

+410
-20
lines changed

10 files changed

+410
-20
lines changed

src/cursor/aggregation_cursor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Document } from '../bson';
2-
import type { ExplainVerbosityLike } from '../explain';
2+
import type { ExplainCommandOptions, ExplainVerbosityLike } from '../explain';
33
import type { MongoClient } from '../mongo_client';
44
import { AggregateOperation, type AggregateOptions } from '../operations/aggregate';
55
import { executeOperation } from '../operations/execute_operation';
@@ -66,7 +66,7 @@ export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
6666
}
6767

6868
/** Execute the explain for the cursor */
69-
async explain(verbosity?: ExplainVerbosityLike): Promise<Document> {
69+
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
7070
return (
7171
await executeOperation(
7272
this.client,

src/cursor/find_cursor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Document } from '../bson';
22
import { CursorResponse } from '../cmap/wire_protocol/responses';
33
import { MongoInvalidArgumentError, MongoTailableCursorError } from '../error';
4-
import { type ExplainVerbosityLike } from '../explain';
4+
import { type ExplainCommandOptions, type ExplainVerbosityLike } from '../explain';
55
import type { MongoClient } from '../mongo_client';
66
import type { CollationOptions } from '../operations/command';
77
import { CountOperation, type CountOptions } from '../operations/count';
@@ -133,7 +133,7 @@ export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
133133
}
134134

135135
/** Execute the explain for the cursor */
136-
async explain(verbosity?: ExplainVerbosityLike): Promise<Document> {
136+
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
137137
return (
138138
await executeOperation(
139139
this.client,

src/explain.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { MongoInvalidArgumentError } from './error';
2-
31
/** @public */
42
export const ExplainVerbosity = Object.freeze({
53
queryPlanner: 'queryPlanner',
@@ -19,33 +17,72 @@ export type ExplainVerbosity = string;
1917
export type ExplainVerbosityLike = ExplainVerbosity | boolean;
2018

2119
/** @public */
20+
export interface ExplainCommandOptions {
21+
/** The explain verbosity for the command. */
22+
verbosity: ExplainVerbosity;
23+
/** The maxTimeMS setting for the command. */
24+
maxTimeMS?: number;
25+
}
26+
27+
/**
28+
* @public
29+
*
30+
* When set, this configures an explain command. Valid values are boolean (for legacy compatibility,
31+
* see {@link ExplainVerbosityLike}), a string containing the explain verbosity, or an object containing the verbosity and
32+
* an optional maxTimeMS.
33+
*
34+
* Examples of valid usage:
35+
*
36+
* ```typescript
37+
* collection.find({ name: 'john doe' }, { explain: true });
38+
* collection.find({ name: 'john doe' }, { explain: false });
39+
* collection.find({ name: 'john doe' }, { explain: 'queryPlanner' });
40+
* collection.find({ name: 'john doe' }, { explain: { verbosity: 'queryPlanner' } });
41+
* ```
42+
*
43+
* maxTimeMS can be configured to limit the amount of time the server
44+
* spends executing an explain by providing an object:
45+
*
46+
* ```typescript
47+
* // limits the `explain` command to no more than 2 seconds
48+
* collection.find({ name: 'john doe' }, {
49+
* explain: {
50+
* verbosity: 'queryPlanner',
51+
* maxTimeMS: 2000
52+
* }
53+
* });
54+
* ```
55+
*/
2256
export interface ExplainOptions {
2357
/** Specifies the verbosity mode for the explain output. */
24-
explain?: ExplainVerbosityLike;
58+
explain?: ExplainVerbosityLike | ExplainCommandOptions;
2559
}
2660

2761
/** @internal */
2862
export class Explain {
29-
verbosity: ExplainVerbosity;
63+
readonly verbosity: ExplainVerbosity;
64+
readonly maxTimeMS?: number;
3065

31-
constructor(verbosity: ExplainVerbosityLike) {
66+
private constructor(verbosity: ExplainVerbosityLike, maxTimeMS?: number) {
3267
if (typeof verbosity === 'boolean') {
3368
this.verbosity = verbosity
3469
? ExplainVerbosity.allPlansExecution
3570
: ExplainVerbosity.queryPlanner;
3671
} else {
3772
this.verbosity = verbosity;
3873
}
74+
75+
this.maxTimeMS = maxTimeMS;
3976
}
4077

41-
static fromOptions(options?: ExplainOptions): Explain | undefined {
42-
if (options?.explain == null) return;
78+
static fromOptions({ explain }: ExplainOptions = {}): Explain | undefined {
79+
if (explain == null) return;
4380

44-
const explain = options.explain;
4581
if (typeof explain === 'boolean' || typeof explain === 'string') {
4682
return new Explain(explain);
4783
}
4884

49-
throw new MongoInvalidArgumentError('Field "explain" must be a string or a boolean');
85+
const { verbosity, maxTimeMS } = explain;
86+
return new Explain(verbosity, maxTimeMS);
5087
}
5188
}

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,12 @@ export type { RunCursorCommandOptions } from './cursor/run_command_cursor';
368368
export type { DbOptions, DbPrivate } from './db';
369369
export type { Encrypter, EncrypterOptions } from './encrypter';
370370
export type { AnyError, ErrorDescription, MongoNetworkErrorOptions } from './error';
371-
export type { Explain, ExplainOptions, ExplainVerbosityLike } from './explain';
371+
export type {
372+
Explain,
373+
ExplainCommandOptions,
374+
ExplainOptions,
375+
ExplainVerbosityLike
376+
} from './explain';
372377
export type {
373378
GridFSBucketReadStreamOptions,
374379
GridFSBucketReadStreamOptionsWithRevision,

src/utils.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
MongoParseError,
2626
MongoRuntimeError
2727
} from './error';
28-
import type { Explain } from './explain';
28+
import type { Explain, ExplainVerbosity } from './explain';
2929
import type { MongoClient } from './mongo_client';
3030
import type { CommandOperationOptions, OperationParent } from './operations/command';
3131
import type { Hint, OperationOptions } from './operations/operation';
@@ -251,12 +251,23 @@ export function decorateWithReadConcern(
251251
* @param command - the command on which to apply the explain
252252
* @param options - the options containing the explain verbosity
253253
*/
254-
export function decorateWithExplain(command: Document, explain: Explain): Document {
255-
if (command.explain) {
256-
return command;
254+
export function decorateWithExplain(
255+
command: Document,
256+
explain: Explain
257+
): {
258+
explain: Document;
259+
verbosity: ExplainVerbosity;
260+
maxTimeMS?: number;
261+
} {
262+
type ExplainCommand = ReturnType<typeof decorateWithExplain>;
263+
const { verbosity, maxTimeMS } = explain;
264+
const baseCommand: ExplainCommand = { explain: command, verbosity };
265+
266+
if (typeof maxTimeMS === 'number') {
267+
baseCommand.maxTimeMS = maxTimeMS;
257268
}
258269

259-
return { explain: command, verbosity: explain.verbosity };
270+
return baseCommand;
260271
}
261272

262273
/**

test/integration/crud/crud.prose.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { expect } from 'chai';
22
import { once } from 'events';
33

4-
import { MongoBulkWriteError, type MongoClient, MongoServerError } from '../../mongodb';
4+
import { type CommandStartedEvent } from '../../../mongodb';
5+
import {
6+
type Collection,
7+
MongoBulkWriteError,
8+
type MongoClient,
9+
MongoServerError
10+
} from '../../mongodb';
11+
import { filterForCommands } from '../shared';
512

613
describe('CRUD Prose Spec Tests', () => {
714
let client: MongoClient;
@@ -143,4 +150,42 @@ describe('CRUD Prose Spec Tests', () => {
143150
}
144151
});
145152
});
153+
154+
describe('14. `explain` helpers allow users to specify `maxTimeMS`', function () {
155+
let client: MongoClient;
156+
const commands: CommandStartedEvent[] = [];
157+
let collection: Collection;
158+
159+
beforeEach(async function () {
160+
client = this.configuration.newClient({}, { monitorCommands: true });
161+
await client.connect();
162+
163+
await client.db('explain-test').dropDatabase();
164+
collection = await client.db('explain-test').createCollection('collection');
165+
166+
client.on('commandStarted', filterForCommands('explain', commands));
167+
commands.length = 0;
168+
});
169+
170+
afterEach(async function () {
171+
await client.close();
172+
});
173+
174+
it('sets maxTimeMS on explain commands, when specified', async function () {
175+
await collection
176+
.find(
177+
{ name: 'john doe' },
178+
{
179+
explain: {
180+
maxTimeMS: 2000,
181+
verbosity: 'queryPlanner'
182+
}
183+
}
184+
)
185+
.toArray();
186+
187+
const [{ command }] = commands;
188+
expect(command).to.have.property('maxTimeMS', 2000);
189+
});
190+
});
146191
});

0 commit comments

Comments
 (0)