Skip to content

Commit 3347d44

Browse files
initial POC
1 parent b6e66d3 commit 3347d44

File tree

10 files changed

+335
-52
lines changed

10 files changed

+335
-52
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';
@@ -64,7 +64,7 @@ export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
6464
}
6565

6666
/** Execute the explain for the cursor */
67-
async explain(verbosity?: ExplainVerbosityLike): Promise<Document> {
67+
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
6868
return (
6969
await executeOperation(
7070
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: 40 additions & 31 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,32 +17,63 @@ export type ExplainVerbosity = string;
1917
*/
2018
export type ExplainVerbosityLike = ExplainVerbosity | boolean;
2119

22-
/**
23-
* @public
24-
*/
20+
/** @public */
2521
export interface ExplainCommandOptions {
26-
verbosity: ExplainVerbosityLike;
22+
/** The explain verbosity for the command. */
23+
verbosity: ExplainVerbosity;
24+
/** The maxTimeMS setting for the command. */
2725
maxTimeMS?: number;
2826
}
2927

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

3662
/** @internal */
3763
export class Explain {
38-
verbosity: ExplainVerbosity;
64+
readonly verbosity: ExplainVerbosity;
65+
readonly maxTimeMS?: number;
3966

40-
constructor(verbosity: ExplainVerbosityLike) {
67+
private constructor(verbosity: ExplainVerbosityLike, maxTimeMS?: number) {
4168
if (typeof verbosity === 'boolean') {
4269
this.verbosity = verbosity
4370
? ExplainVerbosity.allPlansExecution
4471
: ExplainVerbosity.queryPlanner;
4572
} else {
4673
this.verbosity = verbosity;
4774
}
75+
76+
this.maxTimeMS = maxTimeMS;
4877
}
4978

5079
static fromOptions({ explain }: ExplainOptions = {}): Explain | undefined {
@@ -54,27 +83,7 @@ export class Explain {
5483
return new Explain(explain);
5584
}
5685

57-
if (typeof explain === 'object') {
58-
const { verbosity } = explain;
59-
return new Explain(verbosity);
60-
}
61-
62-
throw new MongoInvalidArgumentError(
63-
'Field "explain" must be a string, a boolean or an ExplainCommandOptions object.'
64-
);
65-
}
66-
}
67-
68-
export class ExplainCommandOptions2 {
69-
private constructor(
70-
public readonly explain: Explain,
71-
public readonly maxTimeMS: number | undefined
72-
) {}
73-
74-
static fromOptions(options: ExplainOptions = {}): ExplainCommandOptions2 | undefined {
75-
const explain = Explain.fromOptions(options);
76-
const maxTimeMS = typeof options.explain === 'object' ? options.explain.maxTimeMS : undefined;
77-
78-
return explain ? new ExplainCommandOptions2(explain, maxTimeMS) : undefined;
86+
const { verbosity, maxTimeMS } = explain;
87+
return new Explain(verbosity, maxTimeMS);
7988
}
8089
}

src/operations/command.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { BSONSerializeOptions, Document } from '../bson';
22
import { type MongoDBResponseConstructor } from '../cmap/wire_protocol/responses';
33
import { MongoInvalidArgumentError } from '../error';
4-
import { ExplainCommandOptions2, type ExplainOptions } from '../explain';
4+
import { Explain, type ExplainOptions } from '../explain';
55
import { ReadConcern } from '../read_concern';
66
import type { ReadPreference } from '../read_preference';
77
import type { Server } from '../sdam/server';
@@ -72,7 +72,7 @@ export abstract class CommandOperation<T> extends AbstractOperation<T> {
7272
override options: CommandOperationOptions;
7373
readConcern?: ReadConcern;
7474
writeConcern?: WriteConcern;
75-
explain?: ExplainCommandOptions2;
75+
explain?: Explain;
7676

7777
constructor(parent?: OperationParent, options?: CommandOperationOptions) {
7878
super(options);
@@ -94,7 +94,7 @@ export abstract class CommandOperation<T> extends AbstractOperation<T> {
9494
this.writeConcern = WriteConcern.fromOptions(options);
9595

9696
if (this.hasAspect(Aspect.EXPLAINABLE)) {
97-
this.explain = ExplainCommandOptions2.fromOptions(options);
97+
this.explain = Explain.fromOptions(options);
9898
} else if (options?.explain != null) {
9999
throw new MongoInvalidArgumentError(`Option "explain" is not supported on this command`);
100100
}

src/utils.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
MongoParseError,
2626
MongoRuntimeError
2727
} from './error';
28-
import type { ExplainCommandOptions2, ExplainVerbosity } 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';
@@ -254,24 +254,26 @@ export function decorateWithReadConcern(
254254
*/
255255
export function decorateWithExplain(
256256
command: Document,
257-
explainOptions: ExplainCommandOptions2
257+
explain: Explain
258258
): {
259259
explain: Document;
260260
verbosity: ExplainVerbosity;
261261
maxTimeMS?: number;
262262
} {
263263
type ExplainCommand = ReturnType<typeof decorateWithExplain>;
264-
if ('explain' in command && 'verbosity' in command) {
265-
return command as ExplainCommand;
266-
}
264+
const isExplainCommand = (doc: Document): doc is ExplainCommand => 'explain' in command;
267265

268-
const { explain, maxTimeMS } = explainOptions;
269-
const { verbosity } = explain;
266+
if (isExplainCommand(command)) {
267+
return command;
268+
}
270269

270+
const { verbosity, maxTimeMS } = explain;
271271
const baseCommand: ExplainCommand = { explain: command, verbosity };
272+
272273
if (typeof maxTimeMS === 'number') {
273274
baseCommand.maxTimeMS = maxTimeMS;
274275
}
276+
275277
return baseCommand;
276278
}
277279

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

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

4+
import { type CommandStartedEvent } from '../../../mongodb';
45
import { MongoBulkWriteError, type MongoClient, MongoServerError } from '../../mongodb';
6+
import { filterForCommands } from '../shared';
57

68
describe('CRUD Prose Spec Tests', () => {
79
let client: MongoClient;
@@ -143,4 +145,41 @@ describe('CRUD Prose Spec Tests', () => {
143145
}
144146
});
145147
});
148+
149+
describe('14. `explain` helpers allow users to specify `maxTimeMS`', function () {
150+
let client: MongoClient;
151+
const commands: CommandStartedEvent[] = [];
152+
153+
beforeEach(async function () {
154+
client = this.configuration.newClient({}, { monitorCommands: true });
155+
await client.connect();
156+
157+
client.on('commandStarted', filterForCommands('explain', commands));
158+
commands.length = 0;
159+
});
160+
161+
afterEach(async function () {
162+
await client.close();
163+
});
164+
165+
it('sets maxTimeMS on explain commands, when specfied', async function () {
166+
// Create a collection, referred to as `collection`, with the namespace `explain-test.collection`.
167+
const collection = client.db('explain-test').collection('collection');
168+
169+
await collection
170+
.find(
171+
{ name: 'john doe' },
172+
{
173+
explain: {
174+
maxTimeMS: 2000,
175+
verbosity: 'queryPlanner'
176+
}
177+
}
178+
)
179+
.toArray();
180+
181+
const [{ command }] = commands;
182+
expect(command).to.have.property('maxTimeMS', 2000);
183+
});
184+
});
146185
});

test/integration/crud/explain.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect } from 'chai';
22
import { once } from 'events';
3+
import { test } from 'mocha';
34

45
import {
56
type Collection,
@@ -8,6 +9,7 @@ import {
89
type MongoClient,
910
MongoServerError
1011
} from '../../mongodb';
12+
import { filterForCommands } from '../shared';
1113

1214
const explain = [true, false, 'queryPlanner', 'allPlansExecution', 'executionStats', 'invalid'];
1315

@@ -117,6 +119,122 @@ describe('CRUD API explain option', function () {
117119
});
118120
}
119121
}
122+
123+
describe('explain helpers w/ maxTimeMS', function () {
124+
let client: MongoClient;
125+
const commands: CommandStartedEvent[] = [];
126+
127+
beforeEach(async function () {
128+
client = this.configuration.newClient({}, { monitorCommands: true });
129+
await client.connect();
130+
131+
client.on('commandStarted', filterForCommands('explain', commands));
132+
commands.length = 0;
133+
});
134+
135+
afterEach(async function () {
136+
await client.close();
137+
});
138+
139+
describe('cursor explain commands', function () {
140+
describe('when maxTimeMS is specified via a cursor explain method, it sets the property on the command', function () {
141+
test('find()', async function () {
142+
const collection = client.db('explain-test').collection('collection');
143+
await collection
144+
.find({ name: 'john doe' })
145+
.explain({ maxTimeMS: 2000, verbosity: 'queryPlanner' });
146+
147+
const [{ command }] = commands;
148+
expect(command).to.have.property('maxTimeMS', 2000);
149+
});
150+
151+
test('aggregate()', async function () {
152+
const collection = client.db('explain-test').collection('collection');
153+
154+
await collection
155+
.aggregate([{ $match: { name: 'john doe' } }])
156+
.explain({ maxTimeMS: 2000, verbosity: 'queryPlanner' });
157+
158+
const [{ command }] = commands;
159+
expect(command).to.have.property('maxTimeMS', 2000);
160+
});
161+
});
162+
163+
it('when maxTimeMS is not specified, it is not attached to the explain command', async function () {
164+
// Create a collection, referred to as `collection`, with the namespace `explain-test.collection`.
165+
const collection = client.db('explain-test').collection('collection');
166+
167+
await collection.find({ name: 'john doe' }).explain({ verbosity: 'queryPlanner' });
168+
169+
const [{ command }] = commands;
170+
expect(command).not.to.have.property('maxTimeMS');
171+
});
172+
173+
it('when maxTimeMS is specified as an explain option and a command-level option, the explain option takes precedence', async function () {
174+
// Create a collection, referred to as `collection`, with the namespace `explain-test.collection`.
175+
const collection = client.db('explain-test').collection('collection');
176+
177+
await collection
178+
.find(
179+
{},
180+
{
181+
maxTimeMS: 1000,
182+
explain: {
183+
verbosity: 'queryPlanner',
184+
maxTimeMS: 2000
185+
}
186+
}
187+
)
188+
.toArray();
189+
190+
const [{ command }] = commands;
191+
expect(command).to.have.property('maxTimeMS', 2000);
192+
});
193+
});
194+
195+
describe('regular commands w/ explain', function () {
196+
it('when maxTimeMS is specified as an explain option and a command-level option, the explain option takes precedence', async function () {
197+
// Create a collection, referred to as `collection`, with the namespace `explain-test.collection`.
198+
const collection = client.db('explain-test').collection('collection');
199+
200+
await collection.deleteMany(
201+
{},
202+
{
203+
maxTimeMS: 1000,
204+
explain: {
205+
verbosity: true,
206+
maxTimeMS: 2000
207+
}
208+
}
209+
);
210+
211+
const [{ command }] = commands;
212+
expect(command).to.have.property('maxTimeMS', 2000);
213+
});
214+
215+
describe('when maxTimeMS is specified as an explain option', function () {
216+
it('attaches maxTimeMS to the explain command', async function () {
217+
const collection = client.db('explain-test').collection('collection');
218+
await collection.deleteMany(
219+
{},
220+
{ explain: { maxTimeMS: 2000, verbosity: 'queryPlanner' } }
221+
);
222+
223+
const [{ command }] = commands;
224+
expect(command).to.have.property('maxTimeMS', 2000);
225+
});
226+
});
227+
228+
it('when maxTimeMS is not specified, it is not attached to the explain command', async function () {
229+
const collection = client.db('explain-test').collection('collection');
230+
231+
await collection.deleteMany({}, { explain: { verbosity: 'queryPlanner' } });
232+
233+
const [{ command }] = commands;
234+
expect(command).not.to.have.property('maxTimeMS');
235+
});
236+
});
237+
});
120238
});
121239

122240
function explainValueToExpectation(explainValue: boolean | string) {

0 commit comments

Comments
 (0)