Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-6342): support maxTimeMS for explain commands #4207

Merged
merged 13 commits into from
Sep 16, 2024
4 changes: 2 additions & 2 deletions src/cursor/aggregation_cursor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Document } from '../bson';
import type { ExplainVerbosityLike } from '../explain';
import type { ExplainCommandOptions, ExplainVerbosityLike } from '../explain';
import type { MongoClient } from '../mongo_client';
import { AggregateOperation, type AggregateOptions } from '../operations/aggregate';
import { executeOperation } from '../operations/execute_operation';
Expand Down Expand Up @@ -66,7 +66,7 @@ export class AggregationCursor<TSchema = any> extends AbstractCursor<TSchema> {
}

/** Execute the explain for the cursor */
async explain(verbosity?: ExplainVerbosityLike): Promise<Document> {
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
return (
await executeOperation(
this.client,
Expand Down
4 changes: 2 additions & 2 deletions src/cursor/find_cursor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Document } from '../bson';
import { CursorResponse } from '../cmap/wire_protocol/responses';
import { MongoInvalidArgumentError, MongoTailableCursorError } from '../error';
import { type ExplainVerbosityLike } from '../explain';
import { type ExplainCommandOptions, type ExplainVerbosityLike } from '../explain';
import type { MongoClient } from '../mongo_client';
import type { CollationOptions } from '../operations/command';
import { CountOperation, type CountOptions } from '../operations/count';
Expand Down Expand Up @@ -133,7 +133,7 @@ export class FindCursor<TSchema = any> extends AbstractCursor<TSchema> {
}

/** Execute the explain for the cursor */
async explain(verbosity?: ExplainVerbosityLike): Promise<Document> {
async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise<Document> {
return (
await executeOperation(
this.client,
Expand Down
55 changes: 46 additions & 9 deletions src/explain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { MongoInvalidArgumentError } from './error';

/** @public */
export const ExplainVerbosity = Object.freeze({
queryPlanner: 'queryPlanner',
Expand All @@ -20,33 +18,72 @@ export type ExplainVerbosity = string;
export type ExplainVerbosityLike = ExplainVerbosity | boolean;

/** @public */
export interface ExplainCommandOptions {
/** The explain verbosity for the command. */
verbosity: ExplainVerbosity;
/** The maxTimeMS setting for the command. */
maxTimeMS?: number;
}

/**
* @public
*
* When set, this configures an explain command. Valid values are boolean (for legacy compatibility,
* see {@link ExplainVerbosityLike}), a string containing the explain verbosity, or an object containing the verbosity and
* an optional maxTimeMS.
*
* Examples of valid usage:
*
* ```typescript
* collection.find({ name: 'john doe' }, { explain: true });
* collection.find({ name: 'john doe' }, { explain: false });
* collection.find({ name: 'john doe' }, { explain: 'queryPlanner' });
* collection.find({ name: 'john doe' }, { explain: { verbosity: 'queryPlanner' } });
* ```
*
* maxTimeMS can be configured to limit the amount of time the server
* spends executing an explain by providing an object:
*
* ```typescript
* // limits the `explain` command to no more than 2 seconds
* collection.find({ name: 'john doe' }, {
* explain: {
* verbosity: 'queryPlanner',
* maxTimeMS: 2000
* }
* });
* ```
*/
export interface ExplainOptions {
/** Specifies the verbosity mode for the explain output. */
explain?: ExplainVerbosityLike;
explain?: ExplainVerbosityLike | ExplainCommandOptions;
}

/** @internal */
export class Explain {
verbosity: ExplainVerbosity;
readonly verbosity: ExplainVerbosity;
readonly maxTimeMS?: number;

constructor(verbosity: ExplainVerbosityLike) {
private constructor(verbosity: ExplainVerbosityLike, maxTimeMS?: number) {
if (typeof verbosity === 'boolean') {
this.verbosity = verbosity
? ExplainVerbosity.allPlansExecution
: ExplainVerbosity.queryPlanner;
} else {
this.verbosity = verbosity;
}

this.maxTimeMS = maxTimeMS;
}

static fromOptions(options?: ExplainOptions): Explain | undefined {
if (options?.explain == null) return;
static fromOptions({ explain }: ExplainOptions = {}): Explain | undefined {
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
if (explain == null) return;

const explain = options.explain;
if (typeof explain === 'boolean' || typeof explain === 'string') {
return new Explain(explain);
}

throw new MongoInvalidArgumentError('Field "explain" must be a string or a boolean');
const { verbosity, maxTimeMS } = explain;
return new Explain(verbosity, maxTimeMS);
}
}
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,12 @@ export type { RunCursorCommandOptions } from './cursor/run_command_cursor';
export type { DbOptions, DbPrivate } from './db';
export type { Encrypter, EncrypterOptions } from './encrypter';
export type { AnyError, ErrorDescription, MongoNetworkErrorOptions } from './error';
export type { Explain, ExplainOptions, ExplainVerbosityLike } from './explain';
export type {
Explain,
ExplainCommandOptions,
ExplainOptions,
ExplainVerbosityLike
} from './explain';
export type {
GridFSBucketReadStreamOptions,
GridFSBucketReadStreamOptionsWithRevision,
Expand Down
21 changes: 16 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
MongoParseError,
MongoRuntimeError
} from './error';
import type { Explain } from './explain';
import type { Explain, ExplainVerbosity } from './explain';
import type { MongoClient } from './mongo_client';
import type { CommandOperationOptions, OperationParent } from './operations/command';
import type { Hint, OperationOptions } from './operations/operation';
Expand Down Expand Up @@ -251,12 +251,23 @@ export function decorateWithReadConcern(
* @param command - the command on which to apply the explain
* @param options - the options containing the explain verbosity
*/
export function decorateWithExplain(command: Document, explain: Explain): Document {
if (command.explain) {
return command;
export function decorateWithExplain(
command: Document,
explain: Explain
): {
explain: Document;
verbosity: ExplainVerbosity;
maxTimeMS?: number;
} {
type ExplainCommand = ReturnType<typeof decorateWithExplain>;
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
const { verbosity, maxTimeMS } = explain;
const baseCommand: ExplainCommand = { explain: command, verbosity };

if (typeof maxTimeMS === 'number') {
baseCommand.maxTimeMS = maxTimeMS;
}

return { explain: command, verbosity: explain.verbosity };
return baseCommand;
}

/**
Expand Down
47 changes: 46 additions & 1 deletion test/integration/crud/crud.prose.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { expect } from 'chai';
import { once } from 'events';

import { MongoBulkWriteError, type MongoClient, MongoServerError } from '../../mongodb';
import { type CommandStartedEvent } from '../../../mongodb';
import {
type Collection,
MongoBulkWriteError,
type MongoClient,
MongoServerError
} from '../../mongodb';
import { filterForCommands } from '../shared';

describe('CRUD Prose Spec Tests', () => {
let client: MongoClient;
Expand Down Expand Up @@ -143,4 +150,42 @@ describe('CRUD Prose Spec Tests', () => {
}
});
});

describe('14. `explain` helpers allow users to specify `maxTimeMS`', function () {
let client: MongoClient;
const commands: CommandStartedEvent[] = [];
let collection: Collection;

beforeEach(async function () {
client = this.configuration.newClient({}, { monitorCommands: true });
await client.connect();

await client.db('explain-test').dropDatabase();
collection = await client.db('explain-test').createCollection('collection');

client.on('commandStarted', filterForCommands('explain', commands));
commands.length = 0;
});

afterEach(async function () {
await client.close();
});

it('sets maxTimeMS on explain commands, when specified', async function () {
await collection
.find(
{ name: 'john doe' },
{
explain: {
maxTimeMS: 2000,
verbosity: 'queryPlanner'
}
}
)
.toArray();

const [{ command }] = commands;
expect(command).to.have.property('maxTimeMS', 2000);
});
});
});
Loading