From 7e968346badd06471211e6543a7a853d7d40effa Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 22 Aug 2024 14:42:46 -0600 Subject: [PATCH 01/12] POC --- src/explain.ts | 38 +++++++++++++++++++++++++++++++++----- src/operations/command.ts | 6 +++--- src/utils.ts | 25 ++++++++++++++++++++----- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/explain.ts b/src/explain.ts index 0d08e694a6..b0256e8c92 100644 --- a/src/explain.ts +++ b/src/explain.ts @@ -19,10 +19,18 @@ export type ExplainVerbosity = string; */ export type ExplainVerbosityLike = ExplainVerbosity | boolean; +/** + * @public + */ +export interface ExplainCommandOptions { + verbosity: ExplainVerbosityLike; + maxTimeMS?: number; +} + /** @public */ export interface ExplainOptions { /** Specifies the verbosity mode for the explain output. */ - explain?: ExplainVerbosityLike; + explain?: ExplainVerbosityLike | ExplainCommandOptions; } /** @internal */ @@ -39,14 +47,34 @@ export class Explain { } } - static fromOptions(options?: ExplainOptions): Explain | undefined { - if (options?.explain == null) return; + static fromOptions({ explain }: ExplainOptions = {}): Explain | undefined { + 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'); + if (typeof explain === 'object') { + const { verbosity } = explain; + return new Explain(verbosity); + } + + throw new MongoInvalidArgumentError( + 'Field "explain" must be a string, a boolean or an ExplainCommandOptions object.' + ); + } +} + +export class ExplainCommandOptions2 { + private constructor( + public readonly explain: Explain, + public readonly maxTimeMS: number | undefined + ) {} + + static fromOptions(options: ExplainOptions = {}): ExplainCommandOptions2 | undefined { + const explain = Explain.fromOptions(options); + const maxTimeMS = typeof options.explain === 'object' ? options.explain.maxTimeMS : undefined; + + return explain ? new ExplainCommandOptions2(explain, maxTimeMS) : undefined; } } diff --git a/src/operations/command.ts b/src/operations/command.ts index 94ccc6ceaf..5ff7fe8e6c 100644 --- a/src/operations/command.ts +++ b/src/operations/command.ts @@ -1,7 +1,7 @@ import type { BSONSerializeOptions, Document } from '../bson'; import { type MongoDBResponseConstructor } from '../cmap/wire_protocol/responses'; import { MongoInvalidArgumentError } from '../error'; -import { Explain, type ExplainOptions } from '../explain'; +import { ExplainCommandOptions2, type ExplainOptions } from '../explain'; import { ReadConcern } from '../read_concern'; import type { ReadPreference } from '../read_preference'; import type { Server } from '../sdam/server'; @@ -72,7 +72,7 @@ export abstract class CommandOperation extends AbstractOperation { override options: CommandOperationOptions; readConcern?: ReadConcern; writeConcern?: WriteConcern; - explain?: Explain; + explain?: ExplainCommandOptions2; constructor(parent?: OperationParent, options?: CommandOperationOptions) { super(options); @@ -94,7 +94,7 @@ export abstract class CommandOperation extends AbstractOperation { this.writeConcern = WriteConcern.fromOptions(options); if (this.hasAspect(Aspect.EXPLAINABLE)) { - this.explain = Explain.fromOptions(options); + this.explain = ExplainCommandOptions2.fromOptions(options); } else if (options?.explain != null) { throw new MongoInvalidArgumentError(`Option "explain" is not supported on this command`); } diff --git a/src/utils.ts b/src/utils.ts index 07fe8e56a6..a2b1705d79 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,7 +25,7 @@ import { MongoParseError, MongoRuntimeError } from './error'; -import type { Explain } from './explain'; +import type { ExplainCommandOptions2, ExplainVerbosity } from './explain'; import type { MongoClient } from './mongo_client'; import type { CommandOperationOptions, OperationParent } from './operations/command'; import type { Hint, OperationOptions } from './operations/operation'; @@ -251,12 +251,27 @@ 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, + explainOptions: ExplainCommandOptions2 +): { + explain: Document; + verbosity: ExplainVerbosity; + maxTimeMS?: number; +} { + type ExplainCommand = ReturnType; + if ('explain' in command && 'verbosity' in command) { + return command as ExplainCommand; } - return { explain: command, verbosity: explain.verbosity }; + const { explain, maxTimeMS } = explainOptions; + const { verbosity } = explain; + + const baseCommand: ExplainCommand = { explain: command, verbosity }; + if (typeof maxTimeMS === 'number') { + baseCommand.maxTimeMS = maxTimeMS; + } + return baseCommand; } /** From 178d8f9b3e05d41eb27cc11c9b9dc74188112eff Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 23 Aug 2024 09:29:09 -0600 Subject: [PATCH 02/12] initial POC --- src/cursor/aggregation_cursor.ts | 4 +- src/cursor/find_cursor.ts | 4 +- src/explain.ts | 71 ++++++++------ src/operations/command.ts | 6 +- src/utils.ts | 16 +-- test/integration/crud/crud.prose.test.ts | 39 ++++++++ test/integration/crud/explain.test.ts | 118 +++++++++++++++++++++++ test/integration/shared.js | 35 +++++-- test/unit/explain.test.ts | 53 ++++++++++ test/unit/utils.test.ts | 41 ++++++++ 10 files changed, 335 insertions(+), 52 deletions(-) create mode 100644 test/unit/explain.test.ts diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index c843f6c47e..872681042e 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -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'; @@ -66,7 +66,7 @@ export class AggregationCursor extends AbstractCursor { } /** Execute the explain for the cursor */ - async explain(verbosity?: ExplainVerbosityLike): Promise { + async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise { return ( await executeOperation( this.client, diff --git a/src/cursor/find_cursor.ts b/src/cursor/find_cursor.ts index e4b3dbc03c..450a4c2e97 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -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'; @@ -133,7 +133,7 @@ export class FindCursor extends AbstractCursor { } /** Execute the explain for the cursor */ - async explain(verbosity?: ExplainVerbosityLike): Promise { + async explain(verbosity?: ExplainVerbosityLike | ExplainCommandOptions): Promise { return ( await executeOperation( this.client, diff --git a/src/explain.ts b/src/explain.ts index b0256e8c92..58abb3c1a7 100644 --- a/src/explain.ts +++ b/src/explain.ts @@ -1,5 +1,3 @@ -import { MongoInvalidArgumentError } from './error'; - /** @public */ export const ExplainVerbosity = Object.freeze({ queryPlanner: 'queryPlanner', @@ -19,15 +17,43 @@ export type ExplainVerbosity = string; */ export type ExplainVerbosityLike = ExplainVerbosity | boolean; -/** - * @public - */ +/** @public */ export interface ExplainCommandOptions { - verbosity: ExplainVerbosityLike; + /** The explain verbosity for the command. */ + verbosity: ExplainVerbosity; + /** The maxTimeMS setting for the command. */ maxTimeMS?: number; } -/** @public */ +/** + * @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 | ExplainCommandOptions; @@ -35,9 +61,10 @@ export interface ExplainOptions { /** @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 @@ -45,6 +72,8 @@ export class Explain { } else { this.verbosity = verbosity; } + + this.maxTimeMS = maxTimeMS; } static fromOptions({ explain }: ExplainOptions = {}): Explain | undefined { @@ -54,27 +83,7 @@ export class Explain { return new Explain(explain); } - if (typeof explain === 'object') { - const { verbosity } = explain; - return new Explain(verbosity); - } - - throw new MongoInvalidArgumentError( - 'Field "explain" must be a string, a boolean or an ExplainCommandOptions object.' - ); - } -} - -export class ExplainCommandOptions2 { - private constructor( - public readonly explain: Explain, - public readonly maxTimeMS: number | undefined - ) {} - - static fromOptions(options: ExplainOptions = {}): ExplainCommandOptions2 | undefined { - const explain = Explain.fromOptions(options); - const maxTimeMS = typeof options.explain === 'object' ? options.explain.maxTimeMS : undefined; - - return explain ? new ExplainCommandOptions2(explain, maxTimeMS) : undefined; + const { verbosity, maxTimeMS } = explain; + return new Explain(verbosity, maxTimeMS); } } diff --git a/src/operations/command.ts b/src/operations/command.ts index 5ff7fe8e6c..94ccc6ceaf 100644 --- a/src/operations/command.ts +++ b/src/operations/command.ts @@ -1,7 +1,7 @@ import type { BSONSerializeOptions, Document } from '../bson'; import { type MongoDBResponseConstructor } from '../cmap/wire_protocol/responses'; import { MongoInvalidArgumentError } from '../error'; -import { ExplainCommandOptions2, type ExplainOptions } from '../explain'; +import { Explain, type ExplainOptions } from '../explain'; import { ReadConcern } from '../read_concern'; import type { ReadPreference } from '../read_preference'; import type { Server } from '../sdam/server'; @@ -72,7 +72,7 @@ export abstract class CommandOperation extends AbstractOperation { override options: CommandOperationOptions; readConcern?: ReadConcern; writeConcern?: WriteConcern; - explain?: ExplainCommandOptions2; + explain?: Explain; constructor(parent?: OperationParent, options?: CommandOperationOptions) { super(options); @@ -94,7 +94,7 @@ export abstract class CommandOperation extends AbstractOperation { this.writeConcern = WriteConcern.fromOptions(options); if (this.hasAspect(Aspect.EXPLAINABLE)) { - this.explain = ExplainCommandOptions2.fromOptions(options); + this.explain = Explain.fromOptions(options); } else if (options?.explain != null) { throw new MongoInvalidArgumentError(`Option "explain" is not supported on this command`); } diff --git a/src/utils.ts b/src/utils.ts index a2b1705d79..83351d6f0a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,7 +25,7 @@ import { MongoParseError, MongoRuntimeError } from './error'; -import type { ExplainCommandOptions2, ExplainVerbosity } 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'; @@ -253,24 +253,26 @@ export function decorateWithReadConcern( */ export function decorateWithExplain( command: Document, - explainOptions: ExplainCommandOptions2 + explain: Explain ): { explain: Document; verbosity: ExplainVerbosity; maxTimeMS?: number; } { type ExplainCommand = ReturnType; - if ('explain' in command && 'verbosity' in command) { - return command as ExplainCommand; - } + const isExplainCommand = (doc: Document): doc is ExplainCommand => 'explain' in command; - const { explain, maxTimeMS } = explainOptions; - const { verbosity } = explain; + if (isExplainCommand(command)) { + return command; + } + const { verbosity, maxTimeMS } = explain; const baseCommand: ExplainCommand = { explain: command, verbosity }; + if (typeof maxTimeMS === 'number') { baseCommand.maxTimeMS = maxTimeMS; } + return baseCommand; } diff --git a/test/integration/crud/crud.prose.test.ts b/test/integration/crud/crud.prose.test.ts index 5682c72043..d6143946a2 100644 --- a/test/integration/crud/crud.prose.test.ts +++ b/test/integration/crud/crud.prose.test.ts @@ -1,7 +1,9 @@ import { expect } from 'chai'; import { once } from 'events'; +import { type CommandStartedEvent } from '../../../mongodb'; import { MongoBulkWriteError, type MongoClient, MongoServerError } from '../../mongodb'; +import { filterForCommands } from '../shared'; describe('CRUD Prose Spec Tests', () => { let client: MongoClient; @@ -143,4 +145,41 @@ describe('CRUD Prose Spec Tests', () => { } }); }); + + describe('14. `explain` helpers allow users to specify `maxTimeMS`', function () { + let client: MongoClient; + const commands: CommandStartedEvent[] = []; + + beforeEach(async function () { + client = this.configuration.newClient({}, { monitorCommands: true }); + await client.connect(); + + client.on('commandStarted', filterForCommands('explain', commands)); + commands.length = 0; + }); + + afterEach(async function () { + await client.close(); + }); + + it('sets maxTimeMS on explain commands, when specfied', async function () { + // Create a collection, referred to as `collection`, with the namespace `explain-test.collection`. + const collection = client.db('explain-test').collection('collection'); + + await collection + .find( + { name: 'john doe' }, + { + explain: { + maxTimeMS: 2000, + verbosity: 'queryPlanner' + } + } + ) + .toArray(); + + const [{ command }] = commands; + expect(command).to.have.property('maxTimeMS', 2000); + }); + }); }); diff --git a/test/integration/crud/explain.test.ts b/test/integration/crud/explain.test.ts index 189a1ca387..1d0ec5b709 100644 --- a/test/integration/crud/explain.test.ts +++ b/test/integration/crud/explain.test.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import { once } from 'events'; +import { test } from 'mocha'; import { type Collection, @@ -8,6 +9,7 @@ import { type MongoClient, MongoServerError } from '../../mongodb'; +import { filterForCommands } from '../shared'; const explain = [true, false, 'queryPlanner', 'allPlansExecution', 'executionStats', 'invalid']; @@ -117,6 +119,122 @@ describe('CRUD API explain option', function () { }); } } + + describe('explain helpers w/ maxTimeMS', function () { + let client: MongoClient; + const commands: CommandStartedEvent[] = []; + + beforeEach(async function () { + client = this.configuration.newClient({}, { monitorCommands: true }); + await client.connect(); + + client.on('commandStarted', filterForCommands('explain', commands)); + commands.length = 0; + }); + + afterEach(async function () { + await client.close(); + }); + + describe('cursor explain commands', function () { + describe('when maxTimeMS is specified via a cursor explain method, it sets the property on the command', function () { + test('find()', async function () { + const collection = client.db('explain-test').collection('collection'); + await collection + .find({ name: 'john doe' }) + .explain({ maxTimeMS: 2000, verbosity: 'queryPlanner' }); + + const [{ command }] = commands; + expect(command).to.have.property('maxTimeMS', 2000); + }); + + test('aggregate()', async function () { + const collection = client.db('explain-test').collection('collection'); + + await collection + .aggregate([{ $match: { name: 'john doe' } }]) + .explain({ maxTimeMS: 2000, verbosity: 'queryPlanner' }); + + const [{ command }] = commands; + expect(command).to.have.property('maxTimeMS', 2000); + }); + }); + + it('when maxTimeMS is not specified, it is not attached to the explain command', async function () { + // Create a collection, referred to as `collection`, with the namespace `explain-test.collection`. + const collection = client.db('explain-test').collection('collection'); + + await collection.find({ name: 'john doe' }).explain({ verbosity: 'queryPlanner' }); + + const [{ command }] = commands; + expect(command).not.to.have.property('maxTimeMS'); + }); + + it('when maxTimeMS is specified as an explain option and a command-level option, the explain option takes precedence', async function () { + // Create a collection, referred to as `collection`, with the namespace `explain-test.collection`. + const collection = client.db('explain-test').collection('collection'); + + await collection + .find( + {}, + { + maxTimeMS: 1000, + explain: { + verbosity: 'queryPlanner', + maxTimeMS: 2000 + } + } + ) + .toArray(); + + const [{ command }] = commands; + expect(command).to.have.property('maxTimeMS', 2000); + }); + }); + + describe('regular commands w/ explain', function () { + it('when maxTimeMS is specified as an explain option and a command-level option, the explain option takes precedence', async function () { + // Create a collection, referred to as `collection`, with the namespace `explain-test.collection`. + const collection = client.db('explain-test').collection('collection'); + + await collection.deleteMany( + {}, + { + maxTimeMS: 1000, + explain: { + verbosity: true, + maxTimeMS: 2000 + } + } + ); + + const [{ command }] = commands; + expect(command).to.have.property('maxTimeMS', 2000); + }); + + describe('when maxTimeMS is specified as an explain option', function () { + it('attaches maxTimeMS to the explain command', async function () { + const collection = client.db('explain-test').collection('collection'); + await collection.deleteMany( + {}, + { explain: { maxTimeMS: 2000, verbosity: 'queryPlanner' } } + ); + + const [{ command }] = commands; + expect(command).to.have.property('maxTimeMS', 2000); + }); + }); + + it('when maxTimeMS is not specified, it is not attached to the explain command', async function () { + const collection = client.db('explain-test').collection('collection'); + + await collection.deleteMany({}, { explain: { verbosity: 'queryPlanner' } }); + + const [{ command }] = commands; + expect(command).not.to.have.property('maxTimeMS'); + }); + }); + }); }); function explainValueToExpectation(explainValue: boolean | string) { diff --git a/test/integration/shared.js b/test/integration/shared.js index b4ae4030b0..4a276edef9 100644 --- a/test/integration/shared.js +++ b/test/integration/shared.js @@ -42,15 +42,36 @@ function dropCollection(dbObj, collectionName, options = {}) { return dbObj.dropCollection(collectionName, options).catch(ignoreNsNotFound); } +/** + * Given a set of commands to look for when command monitoring and a destination to store them, returns an event handler + * that collects the specified events. + * + * ```typescript + * const commands = []; + * + * // one command + * client.on('commandStarted', filterForCommands('ping', commands)); + * // multiple commands + * client.on('commandStarted', filterForCommands(['ping', 'find'], commands)); + * // custom predicate + * client.on('commandStarted', filterForCommands((command) => command.commandName === 'find', commands)); + * ``` + * @param {string | string[] | (arg0: string) => boolean} commands A set of commands to look for. Either + * a single command name (string), a list of command names (string[]) or a predicate function that + * determines whether or not a command should be kept. + * @param {Array} bag the output for the filtered commands + * @returns a function that collects the specified comment events + */ function filterForCommands(commands, bag) { - if (typeof commands === 'function') { - return function (event) { - if (commands(event.commandName)) bag.push(event); - }; - } - commands = Array.isArray(commands) ? commands : [commands]; + const predicate = + typeof commands === 'function' + ? commands + : command => { + const specifiedCommandNames = [commands].flat(); + return specifiedCommandNames.includes(command.commandName); + }; return function (event) { - if (commands.indexOf(event.commandName) !== -1) bag.push(event); + if (predicate(event.commandName)) bag.push(event); }; } diff --git a/test/unit/explain.test.ts b/test/unit/explain.test.ts new file mode 100644 index 0000000000..a707c6b8d2 --- /dev/null +++ b/test/unit/explain.test.ts @@ -0,0 +1,53 @@ +import { expect } from 'chai'; +import { it, test } from 'mocha'; + +import { Explain, ExplainVerbosity } from '../mongodb'; + +describe('class Explain {}', function () { + describe('static .fromOptions()', function () { + test('when no options are provided, it returns undefined', function () { + expect(Explain.fromOptions()).to.be.undefined; + }); + + test('explain=true constructs an allPlansExecution explain', function () { + const explain = Explain.fromOptions({ explain: true }); + expect(explain).to.have.property('verbosity', ExplainVerbosity.allPlansExecution); + expect(explain).to.have.property('maxTimeMS').to.be.undefined; + }); + + test('explain=false constructs an allPlansExecution explain', function () { + const explain = Explain.fromOptions({ explain: false }); + expect(explain).to.have.property('verbosity', ExplainVerbosity.queryPlanner); + expect(explain).to.have.property('maxTimeMS').to.be.undefined; + }); + + test('explain= constructs an explain with verbosity set to the string', function () { + const explain = Explain.fromOptions({ explain: 'some random string' }); + expect(explain).to.have.property('verbosity', 'some random string'); + expect(explain).to.have.property('maxTimeMS').to.be.undefined; + }); + + describe('when explain is an object', function () { + it('uses the verbosity from the object', function () { + const explain = Explain.fromOptions({ + explain: { + verbosity: 'some random string' + } + }); + expect(explain).to.have.property('verbosity', 'some random string'); + expect(explain).to.have.property('maxTimeMS').to.be.undefined; + }); + + test('when a maxTimeMS is provided, it constructs an explain with the maxTImeMS value', function () { + const explain = Explain.fromOptions({ + explain: { + verbosity: 'some random string', + maxTimeMS: 2000 + } + }); + expect(explain).to.have.property('verbosity', 'some random string'); + expect(explain).to.have.property('maxTimeMS', 2000); + }); + }); + }); +}); diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index f796a65280..f4a00d44f7 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -1,9 +1,12 @@ import { expect } from 'chai'; +import { test } from 'mocha'; import { BufferPool, ByteUtils, compareObjectId, + decorateWithExplain, + Explain, HostAddress, hostMatchesWildcards, isHello, @@ -1003,4 +1006,42 @@ describe('driver utils', function () { describe('when given an object that does not respond to Symbol.toStringTag', () => it('returns false', () => expect(isUint8Array(Object.create(null))).to.be.false)); }); + + describe('decorateWithExplain()', function () { + test('when the command is a valid explain command, the command is returned unmodified', function () { + const command = Object.freeze({ explain: { hello: 'world' } }); + const result = decorateWithExplain(command, Explain.fromOptions({ explain: true })); + + expect(result).to.deep.equal(command); + }); + + test('when the options have a maxTimeMS, it is attached to the explain command', function () { + const command = { ping: 1 }; + const result = decorateWithExplain( + command, + Explain.fromOptions({ + explain: { verbosity: 'queryPlanner', maxTimeMS: 1000 } + }) + ); + expect(result).to.deep.equal({ + explain: { ping: 1 }, + verbosity: 'queryPlanner', + maxTimeMS: 1000 + }); + }); + + test('when the options have do not have a maxTimeMS, it is not attached to the explain command', function () { + const command = { ping: 1 }; + const result = decorateWithExplain( + command, + Explain.fromOptions({ + explain: { verbosity: 'queryPlanner' } + }) + ); + expect(result).to.deep.equal({ + explain: { ping: 1 }, + verbosity: 'queryPlanner' + }); + }); + }); }); From d0a7562d5e9a4bf8b5cdb5ec381bae3f3d63c343 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 23 Aug 2024 11:00:57 -0600 Subject: [PATCH 03/12] fix lint and tests --- src/index.ts | 7 ++++++- test/integration/shared.js | 15 +++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 19b2359379..900baf8dd0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -364,7 +364,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, diff --git a/test/integration/shared.js b/test/integration/shared.js index 4a276edef9..6bbbff6beb 100644 --- a/test/integration/shared.js +++ b/test/integration/shared.js @@ -63,15 +63,14 @@ function dropCollection(dbObj, collectionName, options = {}) { * @returns a function that collects the specified comment events */ function filterForCommands(commands, bag) { - const predicate = - typeof commands === 'function' - ? commands - : command => { - const specifiedCommandNames = [commands].flat(); - return specifiedCommandNames.includes(command.commandName); - }; + if (typeof commands === 'function') { + return function (event) { + if (commands(event.commandName)) bag.push(event); + }; + } + commands = Array.isArray(commands) ? commands : [commands]; return function (event) { - if (predicate(event.commandName)) bag.push(event); + if (commands.indexOf(event.commandName) !== -1) bag.push(event); }; } From 665e040b7143875ae9e32c95b026afe6276b81f1 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 23 Aug 2024 11:34:16 -0600 Subject: [PATCH 04/12] create collection --- test/integration/crud/crud.prose.test.ts | 13 +++++++++---- test/integration/crud/explain.test.ts | 20 ++++---------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/test/integration/crud/crud.prose.test.ts b/test/integration/crud/crud.prose.test.ts index d6143946a2..67a457f878 100644 --- a/test/integration/crud/crud.prose.test.ts +++ b/test/integration/crud/crud.prose.test.ts @@ -2,7 +2,12 @@ import { expect } from 'chai'; import { once } from 'events'; import { type CommandStartedEvent } from '../../../mongodb'; -import { MongoBulkWriteError, type MongoClient, MongoServerError } from '../../mongodb'; +import { + type Collection, + MongoBulkWriteError, + type MongoClient, + MongoServerError +} from '../../mongodb'; import { filterForCommands } from '../shared'; describe('CRUD Prose Spec Tests', () => { @@ -149,11 +154,14 @@ 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(); + collection = await client.db('explain-test').createCollection('collection'); + client.on('commandStarted', filterForCommands('explain', commands)); commands.length = 0; }); @@ -163,9 +171,6 @@ describe('CRUD Prose Spec Tests', () => { }); it('sets maxTimeMS on explain commands, when specfied', async function () { - // Create a collection, referred to as `collection`, with the namespace `explain-test.collection`. - const collection = client.db('explain-test').collection('collection'); - await collection .find( { name: 'john doe' }, diff --git a/test/integration/crud/explain.test.ts b/test/integration/crud/explain.test.ts index 1d0ec5b709..9ad103b688 100644 --- a/test/integration/crud/explain.test.ts +++ b/test/integration/crud/explain.test.ts @@ -123,11 +123,14 @@ describe('CRUD API explain option', function () { describe('explain helpers w/ maxTimeMS', function () { let client: MongoClient; const commands: CommandStartedEvent[] = []; + let collection: Collection; beforeEach(async function () { client = this.configuration.newClient({}, { monitorCommands: true }); await client.connect(); + collection = await client.db('explain-test').createCollection('bar'); + client.on('commandStarted', filterForCommands('explain', commands)); commands.length = 0; }); @@ -139,7 +142,6 @@ describe('CRUD API explain option', function () { describe('cursor explain commands', function () { describe('when maxTimeMS is specified via a cursor explain method, it sets the property on the command', function () { test('find()', async function () { - const collection = client.db('explain-test').collection('collection'); await collection .find({ name: 'john doe' }) .explain({ maxTimeMS: 2000, verbosity: 'queryPlanner' }); @@ -149,8 +151,6 @@ describe('CRUD API explain option', function () { }); test('aggregate()', async function () { - const collection = client.db('explain-test').collection('collection'); - await collection .aggregate([{ $match: { name: 'john doe' } }]) .explain({ maxTimeMS: 2000, verbosity: 'queryPlanner' }); @@ -161,9 +161,6 @@ describe('CRUD API explain option', function () { }); it('when maxTimeMS is not specified, it is not attached to the explain command', async function () { - // Create a collection, referred to as `collection`, with the namespace `explain-test.collection`. - const collection = client.db('explain-test').collection('collection'); - await collection.find({ name: 'john doe' }).explain({ verbosity: 'queryPlanner' }); const [{ command }] = commands; @@ -171,9 +168,6 @@ describe('CRUD API explain option', function () { }); it('when maxTimeMS is specified as an explain option and a command-level option, the explain option takes precedence', async function () { - // Create a collection, referred to as `collection`, with the namespace `explain-test.collection`. - const collection = client.db('explain-test').collection('collection'); - await collection .find( {}, @@ -194,15 +188,12 @@ describe('CRUD API explain option', function () { describe('regular commands w/ explain', function () { it('when maxTimeMS is specified as an explain option and a command-level option, the explain option takes precedence', async function () { - // Create a collection, referred to as `collection`, with the namespace `explain-test.collection`. - const collection = client.db('explain-test').collection('collection'); - await collection.deleteMany( {}, { maxTimeMS: 1000, explain: { - verbosity: true, + verbosity: 'queryPlanner', maxTimeMS: 2000 } } @@ -214,7 +205,6 @@ describe('CRUD API explain option', function () { describe('when maxTimeMS is specified as an explain option', function () { it('attaches maxTimeMS to the explain command', async function () { - const collection = client.db('explain-test').collection('collection'); await collection.deleteMany( {}, { explain: { maxTimeMS: 2000, verbosity: 'queryPlanner' } } @@ -226,8 +216,6 @@ describe('CRUD API explain option', function () { }); it('when maxTimeMS is not specified, it is not attached to the explain command', async function () { - const collection = client.db('explain-test').collection('collection'); - await collection.deleteMany({}, { explain: { verbosity: 'queryPlanner' } }); const [{ command }] = commands; From a75e9aa73b780f9e960234aa6322bb3c89368fd9 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 23 Aug 2024 13:20:01 -0600 Subject: [PATCH 05/12] force drop db --- test/integration/crud/crud.prose.test.ts | 1 + test/integration/crud/explain.test.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/integration/crud/crud.prose.test.ts b/test/integration/crud/crud.prose.test.ts index 67a457f878..045fef87be 100644 --- a/test/integration/crud/crud.prose.test.ts +++ b/test/integration/crud/crud.prose.test.ts @@ -160,6 +160,7 @@ describe('CRUD Prose Spec Tests', () => { 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)); diff --git a/test/integration/crud/explain.test.ts b/test/integration/crud/explain.test.ts index 9ad103b688..3bd0f383db 100644 --- a/test/integration/crud/explain.test.ts +++ b/test/integration/crud/explain.test.ts @@ -7,7 +7,8 @@ import { type CommandStartedEvent, type Db, type MongoClient, - MongoServerError + MongoServerError, + squashError } from '../../mongodb'; import { filterForCommands } from '../shared'; @@ -129,6 +130,7 @@ describe('CRUD API explain option', function () { client = this.configuration.newClient({}, { monitorCommands: true }); await client.connect(); + await client.db('explain-test').dropDatabase(); collection = await client.db('explain-test').createCollection('bar'); client.on('commandStarted', filterForCommands('explain', commands)); From 569e78106b5b995d6f3e9857658d91c412ab01d2 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 23 Aug 2024 13:41:38 -0600 Subject: [PATCH 06/12] lint --- test/integration/crud/explain.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/crud/explain.test.ts b/test/integration/crud/explain.test.ts index 3bd0f383db..271bbc3352 100644 --- a/test/integration/crud/explain.test.ts +++ b/test/integration/crud/explain.test.ts @@ -7,8 +7,7 @@ import { type CommandStartedEvent, type Db, type MongoClient, - MongoServerError, - squashError + MongoServerError } from '../../mongodb'; import { filterForCommands } from '../shared'; From 23c162954c6382a0750d2ce28db472b69cc34de7 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 12 Sep 2024 14:44:45 -0600 Subject: [PATCH 07/12] make tests more comprehaensive --- test/integration/crud/explain.test.ts | 187 ++++++++++++++++++-------- 1 file changed, 130 insertions(+), 57 deletions(-) diff --git a/test/integration/crud/explain.test.ts b/test/integration/crud/explain.test.ts index 271bbc3352..0acf5f86f3 100644 --- a/test/integration/crud/explain.test.ts +++ b/test/integration/crud/explain.test.ts @@ -140,89 +140,162 @@ describe('CRUD API explain option', function () { await client.close(); }); - describe('cursor explain commands', function () { - describe('when maxTimeMS is specified via a cursor explain method, it sets the property on the command', function () { - test('find()', async function () { - await collection - .find({ name: 'john doe' }) - .explain({ maxTimeMS: 2000, verbosity: 'queryPlanner' }); - - const [{ command }] = commands; - expect(command).to.have.property('maxTimeMS', 2000); + describe('maxTimeMS provided to explain, not to command', function () { + describe('cursor commands', function () { + describe('options API', function () { + beforeEach(async function () { + await collection + .find({}, { explain: { maxTimeMS: 1000, verbosity: 'queryPlanner' } }) + .toArray(); + }); + + it('attaches maxTimeMS to the explain command', expectOnExplain(1000)); + + it('does not attach maxTimeMS to the find command', expectNotOnCommand()); }); - test('aggregate()', async function () { - await collection - .aggregate([{ $match: { name: 'john doe' } }]) - .explain({ maxTimeMS: 2000, verbosity: 'queryPlanner' }); + describe('fluent API', function () { + beforeEach(async function () { + await collection.find({}).explain({ maxTimeMS: 1000, verbosity: 'queryPlanner' }); + }); - const [{ command }] = commands; - expect(command).to.have.property('maxTimeMS', 2000); + it('attaches maxTimeMS to the explain command', expectOnExplain(1000)); + + it('does not attach maxTimeMS to the find command', expectNotOnCommand()); }); }); - it('when maxTimeMS is not specified, it is not attached to the explain command', async function () { - await collection.find({ name: 'john doe' }).explain({ verbosity: 'queryPlanner' }); + describe('non-cursor commands', function () { + beforeEach(async function () { + await collection.deleteMany( + {}, + { explain: { maxTimeMS: 1000, verbosity: 'queryPlanner' } } + ); + }); - const [{ command }] = commands; - expect(command).not.to.have.property('maxTimeMS'); + it('attaches maxTimeMS to the explain command', expectOnExplain(1000)); + + it('does not attach maxTimeMS to the explained command', expectNotOnCommand()); }); + }); - it('when maxTimeMS is specified as an explain option and a command-level option, the explain option takes precedence', async function () { - await collection - .find( + describe('maxTimeMS provided to command, not explain', function () { + describe('cursor commands', function () { + describe('options API', function () { + beforeEach(async function () { + await collection + .find({}, { maxTimeMS: 1000, explain: { verbosity: 'queryPlanner' } }) + .toArray(); + }); + + it('does not attach maxTimeMS to the explain command', expectNotOnExplain()); + + it('attaches maxTimeMS to the find command', expectOnCommand(1000)); + }); + + describe('fluent API', function () { + beforeEach(async function () { + await collection.find({}, { maxTimeMS: 1000 }).explain({ verbosity: 'queryPlanner' }); + }); + + it('does not attach maxTimeMS to the explain command', expectNotOnExplain()); + + it('attaches maxTimeMS to the find command', expectOnCommand(1000)); + }); + }); + + describe('non-cursor commands', function () { + beforeEach(async function () { + await collection.deleteMany( {}, - { - maxTimeMS: 1000, - explain: { - verbosity: 'queryPlanner', - maxTimeMS: 2000 - } - } - ) - .toArray(); + { maxTimeMS: 1000, explain: { verbosity: 'queryPlanner' } } + ); + }); - const [{ command }] = commands; - expect(command).to.have.property('maxTimeMS', 2000); + it('does nto attach maxTimeMS to the explain command', expectNotOnExplain()); + + it('attaches maxTimeMS to the explained command', expectOnCommand(1000)); }); }); - describe('regular commands w/ explain', function () { - it('when maxTimeMS is specified as an explain option and a command-level option, the explain option takes precedence', async function () { - await collection.deleteMany( - {}, - { - maxTimeMS: 1000, - explain: { - verbosity: 'queryPlanner', - maxTimeMS: 2000 - } - } - ); + describe('maxTimeMS specified in command options and explain options', function () { + describe('cursor commands', function () { + describe('options API', function () { + beforeEach(async function () { + await collection + .find( + {}, + { maxTimeMS: 1000, explain: { maxTimeMS: 2000, verbosity: 'queryPlanner' } } + ) + .toArray(); + }); - const [{ command }] = commands; - expect(command).to.have.property('maxTimeMS', 2000); + it('attaches maxTimeMS from the explain options to explain', expectOnExplain(2000)); + + it('attaches maxTimeMS from the find options to the find command', expectOnCommand(1000)); + }); + + describe('fluent API', function () { + beforeEach(async function () { + await collection + .find({}, { maxTimeMS: 1000 }) + .explain({ maxTimeMS: 2000, verbosity: 'queryPlanner' }); + }); + + it('attaches maxTimeMS from the explain options to explain', expectOnExplain(2000)); + + it('attaches maxTimeMS from the find options to the find command', expectOnCommand(1000)); + }); }); - describe('when maxTimeMS is specified as an explain option', function () { - it('attaches maxTimeMS to the explain command', async function () { + describe('non-cursor commands', function () { + beforeEach(async function () { await collection.deleteMany( {}, - { explain: { maxTimeMS: 2000, verbosity: 'queryPlanner' } } + { maxTimeMS: 1000, explain: { maxTimeMS: 2000, verbosity: 'queryPlanner' } } ); - - const [{ command }] = commands; - expect(command).to.have.property('maxTimeMS', 2000); }); + + it('attaches maxTimeMS to the explain command', expectOnExplain(2000)); + + it('attaches maxTimeMS to the explained command', expectOnCommand(1000)); }); + }); - it('when maxTimeMS is not specified, it is not attached to the explain command', async function () { - await collection.deleteMany({}, { explain: { verbosity: 'queryPlanner' } }); + function expectOnExplain(value: number) { + return function () { + const [{ command }] = commands; + expect(command).to.have.property('maxTimeMS', value); + }; + } + function expectNotOnExplain() { + return function () { const [{ command }] = commands; expect(command).not.to.have.property('maxTimeMS'); - }); - }); + }; + } + + function expectOnCommand(value: number) { + return function () { + const [ + { + command: { explain } + } + ] = commands; + expect(explain).to.have.property('maxTimeMS', value); + }; + } + function expectNotOnCommand() { + return function () { + const [ + { + command: { explain } + } + ] = commands; + expect(explain).not.to.have.property('maxTimeMS'); + }; + } }); }); From dca8640c653e01d23454c537baeddf6eb6959e78 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 12 Sep 2024 16:09:03 -0600 Subject: [PATCH 08/12] fix lint --- test/integration/crud/explain.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/crud/explain.test.ts b/test/integration/crud/explain.test.ts index 0acf5f86f3..44fe381303 100644 --- a/test/integration/crud/explain.test.ts +++ b/test/integration/crud/explain.test.ts @@ -1,6 +1,5 @@ import { expect } from 'chai'; import { once } from 'events'; -import { test } from 'mocha'; import { type Collection, From aa96ea083b0a1f7a919a1d57b0f300f1f3d2e5b5 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 13 Sep 2024 11:47:13 -0600 Subject: [PATCH 09/12] comments --- test/unit/explain.test.ts | 12 ++++++------ test/unit/utils.test.ts | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/test/unit/explain.test.ts b/test/unit/explain.test.ts index a707c6b8d2..8d71197a81 100644 --- a/test/unit/explain.test.ts +++ b/test/unit/explain.test.ts @@ -1,27 +1,27 @@ import { expect } from 'chai'; -import { it, test } from 'mocha'; +import { it } from 'mocha'; import { Explain, ExplainVerbosity } from '../mongodb'; describe('class Explain {}', function () { describe('static .fromOptions()', function () { - test('when no options are provided, it returns undefined', function () { + it('when no options are provided, it returns undefined', function () { expect(Explain.fromOptions()).to.be.undefined; }); - test('explain=true constructs an allPlansExecution explain', function () { + it('explain=true constructs an allPlansExecution explain', function () { const explain = Explain.fromOptions({ explain: true }); expect(explain).to.have.property('verbosity', ExplainVerbosity.allPlansExecution); expect(explain).to.have.property('maxTimeMS').to.be.undefined; }); - test('explain=false constructs an allPlansExecution explain', function () { + it('explain=false constructs an allPlansExecution explain', function () { const explain = Explain.fromOptions({ explain: false }); expect(explain).to.have.property('verbosity', ExplainVerbosity.queryPlanner); expect(explain).to.have.property('maxTimeMS').to.be.undefined; }); - test('explain= constructs an explain with verbosity set to the string', function () { + it('explain= constructs an explain with verbosity set to the string', function () { const explain = Explain.fromOptions({ explain: 'some random string' }); expect(explain).to.have.property('verbosity', 'some random string'); expect(explain).to.have.property('maxTimeMS').to.be.undefined; @@ -38,7 +38,7 @@ describe('class Explain {}', function () { expect(explain).to.have.property('maxTimeMS').to.be.undefined; }); - test('when a maxTimeMS is provided, it constructs an explain with the maxTImeMS value', function () { + it('when a maxTimeMS is provided, it constructs an explain with the maxTImeMS value', function () { const explain = Explain.fromOptions({ explain: { verbosity: 'some random string', diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index f4a00d44f7..2975f4b2e2 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import { test } from 'mocha'; import { BufferPool, @@ -1008,14 +1007,14 @@ describe('driver utils', function () { }); describe('decorateWithExplain()', function () { - test('when the command is a valid explain command, the command is returned unmodified', function () { + it('when the command is a valid explain command, the command is returned unmodified', function () { const command = Object.freeze({ explain: { hello: 'world' } }); const result = decorateWithExplain(command, Explain.fromOptions({ explain: true })); expect(result).to.deep.equal(command); }); - test('when the options have a maxTimeMS, it is attached to the explain command', function () { + it('when the options have a maxTimeMS, it is attached to the explain command', function () { const command = { ping: 1 }; const result = decorateWithExplain( command, @@ -1030,7 +1029,7 @@ describe('driver utils', function () { }); }); - test('when the options have do not have a maxTimeMS, it is not attached to the explain command', function () { + it('when the options have do not have a maxTimeMS, it is not attached to the explain command', function () { const command = { ping: 1 }; const result = decorateWithExplain( command, From 64d0f46d150e3febc4b30611b770eb5566bd3959 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 13 Sep 2024 12:44:19 -0600 Subject: [PATCH 10/12] reformat comment --- src/explain.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/explain.ts b/src/explain.ts index 58abb3c1a7..c60a2e4628 100644 --- a/src/explain.ts +++ b/src/explain.ts @@ -46,8 +46,8 @@ export interface ExplainCommandOptions { * * ```typescript * // limits the `explain` command to no more than 2 seconds - * collection.find({ name: 'john doe' }, { explain: - * { + * collection.find({ name: 'john doe' }, { + * explain: { * verbosity: 'queryPlanner', * maxTimeMS: 2000 * } From 0e44b812a52e67cc7bac598776ccc0894515c338 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 13 Sep 2024 12:46:47 -0600 Subject: [PATCH 11/12] reformat comment --- test/integration/crud/crud.prose.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/crud/crud.prose.test.ts b/test/integration/crud/crud.prose.test.ts index 045fef87be..3ddc126d33 100644 --- a/test/integration/crud/crud.prose.test.ts +++ b/test/integration/crud/crud.prose.test.ts @@ -171,7 +171,7 @@ describe('CRUD Prose Spec Tests', () => { await client.close(); }); - it('sets maxTimeMS on explain commands, when specfied', async function () { + it('sets maxTimeMS on explain commands, when specified', async function () { await collection .find( { name: 'john doe' }, From 371271dbad115cb0013dfeb79e8027bb1a3ce428 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 13 Sep 2024 13:53:05 -0600 Subject: [PATCH 12/12] remove gaurd against double explain --- src/utils.ts | 6 ------ test/unit/utils.test.ts | 6 +++--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 83351d6f0a..5ad754c932 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -260,12 +260,6 @@ export function decorateWithExplain( maxTimeMS?: number; } { type ExplainCommand = ReturnType; - const isExplainCommand = (doc: Document): doc is ExplainCommand => 'explain' in command; - - if (isExplainCommand(command)) { - return command; - } - const { verbosity, maxTimeMS } = explain; const baseCommand: ExplainCommand = { explain: command, verbosity }; diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index 2975f4b2e2..7494fb080f 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -1007,11 +1007,11 @@ describe('driver utils', function () { }); describe('decorateWithExplain()', function () { - it('when the command is a valid explain command, the command is returned unmodified', function () { - const command = Object.freeze({ explain: { hello: 'world' } }); + it('when the command is a valid explain command, the command is still wrapped', function () { + const command = { explain: { hello: 'world' } }; const result = decorateWithExplain(command, Explain.fromOptions({ explain: true })); - expect(result).to.deep.equal(command); + expect(result).to.deep.equal({ explain: command, verbosity: 'allPlansExecution' }); }); it('when the options have a maxTimeMS, it is attached to the explain command', function () {