Skip to content

Commit bb1e081

Browse files
authored
feat: add explain support for cursor commands (#2622)
Explain support for find and aggregate is accessible via the `explain` option specified at the operation level or via the existing cursor `explain` method, which now takes an optional verbosity parameter (defaults to true for backwards compatibility). NODE-2853
1 parent b1e15a8 commit bb1e081

File tree

10 files changed

+358
-58
lines changed

10 files changed

+358
-58
lines changed

src/cmap/wire_protocol/query.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { command, CommandOptions } from './command';
22
import { Query } from '../commands';
33
import { MongoError } from '../../error';
4-
import { maxWireVersion, collectionNamespace, Callback } from '../../utils';
4+
import { maxWireVersion, collectionNamespace, Callback, decorateWithExplain } from '../../utils';
55
import { getReadPreference, isSharded, applyCommonQueryOptions } from './shared';
66
import { Document, pluckBSONSerializeOptions } from '../../bson';
77
import type { Server } from '../../sdam/server';
88
import type { ReadPreferenceLike } from '../../read_preference';
99
import type { FindOptions } from '../../operations/find';
10+
import { Explain } from '../../explain';
1011

1112
/** @internal */
1213
export interface QueryOptions extends CommandOptions {
@@ -43,7 +44,14 @@ export function query(
4344
}
4445

4546
const readPreference = getReadPreference(cmd, options);
46-
const findCmd = prepareFindCommand(server, ns, cmd);
47+
let findCmd = prepareFindCommand(server, ns, cmd);
48+
49+
// If we have explain, we need to rewrite the find command
50+
// to wrap it in the explain command
51+
const explain = Explain.fromOptions(options);
52+
if (explain) {
53+
findCmd = decorateWithExplain(findCmd, explain);
54+
}
4755

4856
// NOTE: This actually modifies the passed in cmd, and our code _depends_ on this
4957
// side-effect. Change this ASAP
@@ -62,7 +70,7 @@ export function query(
6270
}
6371

6472
function prepareFindCommand(server: Server, ns: string, cmd: Document) {
65-
let findCmd: Document = {
73+
const findCmd: Document = {
6674
find: collectionNamespace(ns)
6775
};
6876

@@ -146,14 +154,6 @@ function prepareFindCommand(server: Server, ns: string, cmd: Document) {
146154
if (cmd.collation) findCmd.collation = cmd.collation;
147155
if (cmd.readConcern) findCmd.readConcern = cmd.readConcern;
148156

149-
// If we have explain, we need to rewrite the find command
150-
// to wrap it in the explain command
151-
if (cmd.explain) {
152-
findCmd = {
153-
explain: findCmd
154-
};
155-
}
156-
157157
return findCmd;
158158
}
159159

@@ -195,7 +195,7 @@ function prepareLegacyFindQuery(
195195
if (typeof cmd.showDiskLoc !== 'undefined') findCmd['$showDiskLoc'] = cmd.showDiskLoc;
196196
if (cmd.comment) findCmd['$comment'] = cmd.comment;
197197
if (cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS;
198-
if (cmd.explain) {
198+
if (options.explain !== undefined) {
199199
// nToReturn must be 0 (match all) or negative (match N and close cursor)
200200
// nToReturn > 0 will give explain results equivalent to limit(0)
201201
numberToReturn = -Math.abs(cmd.limit || 0);

src/cursor/cursor.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import { PromiseProvider } from '../promise_provider';
1313
import type { OperationTime, ResumeToken } from '../change_stream';
1414
import type { CloseOptions } from '../cmap/connection_pool';
1515
import type { CollationOptions } from '../cmap/wire_protocol/write_command';
16-
import type { Hint, OperationBase } from '../operations/operation';
16+
import { Aspect, Hint, OperationBase } from '../operations/operation';
1717
import type { Topology } from '../sdam/topology';
18-
import type { CommandOperationOptions } from '../operations/command';
18+
import { CommandOperation, CommandOperationOptions } from '../operations/command';
1919
import type { ReadConcern } from '../read_concern';
2020
import type { Server } from '../sdam/server';
2121
import type { ClientSession } from '../sessions';
22+
import { Explain, ExplainVerbosityLike } from '../explain';
2223

2324
const kCursor = Symbol('cursor');
2425

@@ -1300,26 +1301,22 @@ export class Cursor<
13001301
/**
13011302
* Execute the explain for the cursor
13021303
*
1304+
* @param verbosity - The mode in which to run the explain.
13031305
* @param callback - The result callback.
13041306
*/
1305-
explain(): Promise<unknown>;
1306-
explain(callback: Callback): void;
1307-
explain(callback?: Callback): Promise<unknown> | void {
1308-
// NOTE: the next line includes a special case for operations which do not
1309-
// subclass `CommandOperationV2`. To be removed asap.
1310-
// TODO NODE-2853: This had to be removed during NODE-2852; fix while re-implementing
1311-
// cursor explain
1312-
// if (this.operation && this.operation.cmd == null) {
1313-
// this.operation.options.explain = true;
1314-
// return executeOperation(this.topology, this.operation as any, callback);
1315-
// }
1316-
1317-
this.cmd.explain = true;
1318-
1319-
// Do we have a readConcern
1320-
if (this.cmd.readConcern) {
1321-
delete this.cmd['readConcern'];
1322-
}
1307+
explain(verbosity?: ExplainVerbosityLike): Promise<unknown>;
1308+
explain(verbosity?: ExplainVerbosityLike, callback?: Callback): Promise<unknown> | void {
1309+
if (typeof verbosity === 'function') (callback = verbosity), (verbosity = true);
1310+
if (verbosity === undefined) verbosity = true;
1311+
1312+
// TODO: For now, we need to manually do these checks. This will change after cursor refactor.
1313+
if (
1314+
!(this.operation instanceof CommandOperation) ||
1315+
!this.operation.hasAspect(Aspect.EXPLAINABLE)
1316+
) {
1317+
throw new MongoError('This command cannot be explained');
1318+
}
1319+
this.operation.explain = new Explain(verbosity);
13231320

13241321
return maybePromise(callback, cb => nextFunction(this, cb));
13251322
}

src/explain.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ export const ExplainVerbosity = {
99
} as const;
1010

1111
/**
12-
* For backwards compatibility, true is interpreted as
13-
* "allPlansExecution" and false as "queryPlanner".
12+
* For backwards compatibility, true is interpreted as "allPlansExecution"
13+
* and false as "queryPlanner". Prior to server version 3.6, aggregate()
14+
* ignores the verbosity parameter and executes in "queryPlanner".
1415
* @public
1516
*/
1617
export type ExplainVerbosityLike = keyof typeof ExplainVerbosity | boolean;

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export type {
163163
export type { DbPrivate, DbOptions } from './db';
164164
export type { AutoEncryptionOptions, AutoEncryptionLoggerLevels, AutoEncrypter } from './deps';
165165
export type { AnyError, ErrorDescription } from './error';
166-
export type { ExplainOptions, ExplainVerbosity, ExplainVerbosityLike } from './explain';
166+
export type { Explain, ExplainOptions, ExplainVerbosity, ExplainVerbosityLike } from './explain';
167167
export type {
168168
GridFSBucketReadStream,
169169
GridFSBucketReadStreamOptions,

src/operations/aggregate.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,8 @@ export class AggregateOperation<T = Document> extends CommandOperation<Aggregate
6565
this.readPreference = ReadPreference.primary;
6666
}
6767

68-
if (options?.explain && (this.readConcern || this.writeConcern)) {
69-
throw new MongoError(
70-
'"explain" cannot be used on an aggregate call with readConcern/writeConcern'
71-
);
68+
if (this.explain && this.writeConcern) {
69+
throw new MongoError('"explain" cannot be used on an aggregate call with writeConcern');
7270
}
7371

7472
if (options?.cursor != null && typeof options.cursor !== 'object') {
@@ -111,10 +109,6 @@ export class AggregateOperation<T = Document> extends CommandOperation<Aggregate
111109
command.hint = options.hint;
112110
}
113111

114-
if (options.explain) {
115-
command.explain = options.explain;
116-
}
117-
118112
command.cursor = options.cursor || {};
119113
if (options.batchSize && !this.hasWriteStage) {
120114
command.cursor.batchSize = options.batchSize;
@@ -124,4 +118,4 @@ export class AggregateOperation<T = Document> extends CommandOperation<Aggregate
124118
}
125119
}
126120

127-
defineAspects(AggregateOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE]);
121+
defineAspects(AggregateOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE, Aspect.EXPLAINABLE]);

src/operations/command.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,12 @@ export abstract class CommandOperation<
145145
}
146146

147147
if (this.hasAspect(Aspect.EXPLAINABLE) && this.explain) {
148-
cmd = decorateWithExplain(cmd, this.explain);
148+
if (serverWireVersion < 6 && cmd.aggregate) {
149+
// Prior to 3.6, with aggregate, verbosity is ignored, and we must pass in "explain: true"
150+
cmd.explain = true;
151+
} else {
152+
cmd = decorateWithExplain(cmd, this.explain);
153+
}
149154
}
150155

151156
server.command(

src/operations/find.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@ export class FindOperation extends CommandOperation<FindOptions, Document> {
212212
findCommand.allowDiskUse = options.allowDiskUse;
213213
}
214214

215+
if (this.explain) {
216+
// TODO: For now, we need to manually ensure explain is in the options. This will change after cursor refactor.
217+
this.options.explain = this.explain.verbosity;
218+
}
219+
215220
// TODO: use `MongoDBNamespace` through and through
216221
server.query(
217222
this.ns.toString(),
@@ -222,4 +227,4 @@ export class FindOperation extends CommandOperation<FindOptions, Document> {
222227
}
223228
}
224229

225-
defineAspects(FindOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE]);
230+
defineAspects(FindOperation, [Aspect.READ_OPERATION, Aspect.RETRYABLE, Aspect.EXPLAINABLE]);

src/operations/find_one.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { FindOptions } from './find';
55
import { MongoError } from '../error';
66
import type { Server } from '../sdam/server';
77
import { CommandOperation } from './command';
8+
import { Aspect, defineAspects } from './operation';
89

910
/** @internal */
1011
export class FindOneOperation extends CommandOperation<FindOptions, Document> {
@@ -36,3 +37,5 @@ export class FindOneOperation extends CommandOperation<FindOptions, Document> {
3637
}
3738
}
3839
}
40+
41+
defineAspects(FindOneOperation, [Aspect.EXPLAINABLE]);

test/functional/aggregation.test.js

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -386,12 +386,7 @@ describe('Aggregation', function () {
386386
* @example-class Collection
387387
* @example-method aggregate
388388
*/
389-
it.skip('should correctly return a cursor and call explain', {
390-
// TODO NODE-2853: This had to be skipped during NODE-2852; un-skip while re-implementing
391-
// cursor explain
392-
393-
// Add a tag that our runner can trigger on
394-
// in this case we are setting that node needs to be higher than 0.10.X to run
389+
it('should correctly return a cursor and call explain', {
395390
metadata: {
396391
requires: {
397392
mongodb: '>2.5.3',
@@ -461,7 +456,7 @@ describe('Aggregation', function () {
461456
cursor.explain(function (err, result) {
462457
expect(err).to.not.exist;
463458
expect(result.stages).to.have.lengthOf.at.least(1);
464-
expect(result.stages[0]).to.have.key('$cursor');
459+
expect(result.stages[0]).to.have.property('$cursor');
465460

466461
client.close(done);
467462
});
@@ -928,7 +923,7 @@ describe('Aggregation', function () {
928923
}
929924
});
930925

931-
it('should fail if you try to use explain flag with readConcern/writeConcern', {
926+
it('should fail if you try to use explain flag with writeConcern', {
932927
metadata: {
933928
requires: {
934929
mongodb: '>3.6.0',
@@ -938,12 +933,9 @@ describe('Aggregation', function () {
938933

939934
test: function (done) {
940935
var databaseName = this.configuration.db;
941-
var client = this.configuration.newClient(this.configuration.writeConcernMax(), {
942-
poolSize: 1
943-
});
936+
var client = this.configuration.newClient({ poolSize: 1 });
944937

945938
const testCases = [
946-
{ readConcern: { level: 'local' } },
947939
{ writeConcern: { j: true } },
948940
{ readConcern: { level: 'local' }, writeConcern: { j: true } }
949941
];

0 commit comments

Comments
 (0)