Skip to content

Commit a827807

Browse files
authored
feat: add explain support (#2626)
Explain support for specific commands is accessible via a new `explain` option, either a boolean or a string specifying the requested verbosity, at the operation level. Explainable cursor operations can also be explained via the existing cursor `explain` method, which now takes an optional verbosity parameter (defaults to true for backwards compatibility). NODE-2853
1 parent 0516d93 commit a827807

27 files changed

+1100
-123
lines changed

lib/aggregation_cursor.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,13 @@ AggregationCursor.prototype.get = AggregationCursor.prototype.toArray;
322322

323323
/**
324324
* Execute the explain for the cursor
325+
*
326+
* For backwards compatibility, a verbosity of true is interpreted as "allPlansExecution"
327+
* and false as "queryPlanner". Prior to server version 3.6, aggregate()
328+
* ignores the verbosity parameter and executes in "queryPlanner".
329+
*
325330
* @method AggregationCursor.prototype.explain
331+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [verbosity=true] - An optional mode in which to run the explain.
326332
* @param {AggregationCursor~resultCallback} [callback] The result callback.
327333
* @return {Promise} returns Promise if no callback passed
328334
*/

lib/collection.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,6 @@ const DEPRECATED_FIND_OPTIONS = ['maxScan', 'fields', 'snapshot', 'oplogReplay']
287287
* @param {object} [options.fields] **Deprecated** Use `options.projection` instead
288288
* @param {number} [options.skip=0] Set to skip N documents ahead in your query (useful for pagination).
289289
* @param {Object} [options.hint] Tell the query to use specific indexes in the query. Object of indexes to use, {'_id':1}
290-
* @param {boolean} [options.explain=false] Explain the query instead of returning the data.
291290
* @param {boolean} [options.snapshot=false] DEPRECATED: Snapshot query.
292291
* @param {boolean} [options.timeout=false] Specify if the cursor can timeout.
293292
* @param {boolean} [options.tailable=false] Specify if the cursor is tailable.
@@ -310,6 +309,7 @@ const DEPRECATED_FIND_OPTIONS = ['maxScan', 'fields', 'snapshot', 'oplogReplay']
310309
* @param {boolean} [options.noCursorTimeout] The server normally times out idle cursors after an inactivity period (10 minutes) to prevent excess memory use. Set this option to prevent that.
311310
* @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
312311
* @param {boolean} [options.allowDiskUse] Enables writing to temporary files on the server.
312+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
313313
* @param {ClientSession} [options.session] optional session to use for this operation
314314
* @throws {MongoError}
315315
* @return {Cursor}
@@ -744,6 +744,7 @@ Collection.prototype.insert = deprecate(function(docs, options, callback) {
744744
* @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value
745745
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
746746
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
747+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
747748
* @param {ClientSession} [options.session] optional session to use for this operation
748749
* @param {Collection~updateWriteOpCallback} [callback] The command result callback
749750
* @return {Promise} returns Promise if no callback passed
@@ -821,6 +822,7 @@ Collection.prototype.replaceOne = function(filter, doc, options, callback) {
821822
* @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value
822823
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
823824
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
825+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
824826
* @param {ClientSession} [options.session] optional session to use for this operation
825827
* @param {Collection~updateWriteOpCallback} [callback] The command result callback
826828
* @return {Promise<Collection~updateWriteOpResult>} returns Promise if no callback passed
@@ -912,6 +914,7 @@ Collection.prototype.update = deprecate(function(selector, update, options, call
912914
* @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value
913915
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
914916
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
917+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
915918
* @param {ClientSession} [options.session] optional session to use for this operation
916919
* @param {string|object} [options.hint] optional index hint for optimizing the filter query
917920
* @param {Collection~deleteWriteOpCallback} [callback] The command result callback
@@ -947,6 +950,7 @@ Collection.prototype.removeOne = Collection.prototype.deleteOne;
947950
* @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value
948951
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
949952
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
953+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
950954
* @param {ClientSession} [options.session] optional session to use for this operation
951955
* @param {string|object} [options.hint] optional index hint for optimizing the filter query
952956
* @param {Collection~deleteWriteOpCallback} [callback] The command result callback
@@ -1056,7 +1060,6 @@ Collection.prototype.save = deprecate(function(doc, options, callback) {
10561060
* @param {object} [options.fields] **Deprecated** Use `options.projection` instead
10571061
* @param {number} [options.skip=0] Set to skip N documents ahead in your query (useful for pagination).
10581062
* @param {Object} [options.hint] Tell the query to use specific indexes in the query. Object of indexes to use, {'_id':1}
1059-
* @param {boolean} [options.explain=false] Explain the query instead of returning the data.
10601063
* @param {boolean} [options.snapshot=false] DEPRECATED: Snapshot query.
10611064
* @param {boolean} [options.timeout=false] Specify if the cursor can timeout.
10621065
* @param {boolean} [options.tailable=false] Specify if the cursor is tailable.
@@ -1075,6 +1078,7 @@ Collection.prototype.save = deprecate(function(doc, options, callback) {
10751078
* @param {boolean} [options.partial=false] Specify if the cursor should return partial results when querying against a sharded system
10761079
* @param {number} [options.maxTimeMS] Number of milliseconds to wait before aborting the query.
10771080
* @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
1081+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
10781082
* @param {ClientSession} [options.session] optional session to use for this operation
10791083
* @param {Collection~resultCallback} [callback] The command result callback
10801084
* @return {Promise} returns Promise if no callback passed
@@ -1595,6 +1599,7 @@ Collection.prototype.countDocuments = function(query, options, callback) {
15951599
* @param {(ReadPreference|string)} [options.readPreference] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST).
15961600
* @param {number} [options.maxTimeMS] Number of milliseconds to wait before aborting the query.
15971601
* @param {object} [options.collation] Specify collation settings for operation. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}.
1602+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
15981603
* @param {ClientSession} [options.session] optional session to use for this operation
15991604
* @param {Collection~resultCallback} [callback] The command result callback
16001605
* @return {Promise} returns Promise if no callback passed
@@ -1674,6 +1679,7 @@ Collection.prototype.stats = function(options, callback) {
16741679
* @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value
16751680
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
16761681
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
1682+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
16771683
* @param {ClientSession} [options.session] optional session to use for this operation
16781684
* @param {Collection~findAndModifyCallback} [callback] The collection result callback
16791685
* @return {Promise<Collection~findAndModifyWriteOpResultObject>} returns Promise if no callback passed
@@ -1707,6 +1713,7 @@ Collection.prototype.findOneAndDelete = function(filter, options, callback) {
17071713
* @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value
17081714
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
17091715
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
1716+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
17101717
* @param {ClientSession} [options.session] optional session to use for this operation
17111718
* @param {Collection~findAndModifyCallback} [callback] The collection result callback
17121719
* @return {Promise<Collection~findAndModifyWriteOpResultObject>} returns Promise if no callback passed
@@ -1741,6 +1748,7 @@ Collection.prototype.findOneAndReplace = function(filter, replacement, options,
17411748
* @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value
17421749
* @param {boolean} [options.serializeFunctions=false] Serialize functions on any object.
17431750
* @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields.
1751+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
17441752
* @param {ClientSession} [options.session] An ptional session to use for this operation
17451753
* @param {Collection~findAndModifyCallback} [callback] The collection result callback
17461754
* @return {Promise<Collection~findAndModifyWriteOpResultObject>} returns Promise if no callback passed
@@ -1848,7 +1856,6 @@ Collection.prototype.findAndRemove = deprecate(function(query, sort, options, ca
18481856
* @param {number} [options.batchSize=1000] The number of documents to return per batch. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}.
18491857
* @param {object} [options.cursor] Return the query as cursor, on 2.6 > it returns as a real cursor on pre 2.6 it returns as an emulated cursor.
18501858
* @param {number} [options.cursor.batchSize=1000] Deprecated. Use `options.batchSize`
1851-
* @param {boolean} [options.explain=false] Explain returns the aggregation execution plan (requires mongodb 2.6 >).
18521859
* @param {boolean} [options.allowDiskUse=false] allowDiskUse lets the server know if it can use disk to store temporary results for the aggregation (requires mongodb 2.6 >).
18531860
* @param {number} [options.maxTimeMS] maxTimeMS specifies a cumulative time limit in milliseconds for processing operations on the cursor. MongoDB interrupts the operation at the earliest following interrupt point.
18541861
* @param {number} [options.maxAwaitTimeMS] The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query.
@@ -1860,6 +1867,7 @@ Collection.prototype.findAndRemove = deprecate(function(query, sort, options, ca
18601867
* @param {object} [options.collation] Specify collation settings for operation. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}.
18611868
* @param {string} [options.comment] Add a comment to an aggregation command
18621869
* @param {string|object} [options.hint] Add an index selection hint to an aggregation command
1870+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
18631871
* @param {ClientSession} [options.session] optional session to use for this operation
18641872
* @param {Collection~aggregationCallback} callback The command result callback
18651873
* @return {(null|AggregationCursor)}
@@ -2106,6 +2114,7 @@ Collection.prototype.group = deprecate(function(
21062114
* @param {boolean} [options.jsMode=false] It is possible to make the execution stay in JS. Provided in MongoDB > 2.0.X.
21072115
* @param {boolean} [options.verbose=false] Provide statistics on job execution time.
21082116
* @param {boolean} [options.bypassDocumentValidation=false] Allow driver to bypass schema validation in MongoDB 3.2 or higher.
2117+
* @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output.
21092118
* @param {ClientSession} [options.session] optional session to use for this operation
21102119
* @param {Collection~resultCallback} [callback] The command result callback
21112120
* @throws {MongoError}

lib/core/sdam/topology.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,8 @@ function executeWriteOperation(args, options, callback) {
930930
!!options.retryWrites &&
931931
options.session &&
932932
isRetryableWritesSupported(topology) &&
933-
!options.session.inTransaction();
933+
!options.session.inTransaction() &&
934+
options.explain === undefined;
934935

935936
topology.selectServer(writableServerSelector(), options, (err, server) => {
936937
if (err) {

lib/core/topologies/mongos.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,8 @@ function executeWriteOperation(args, options, callback) {
919919
!!options.retryWrites &&
920920
options.session &&
921921
isRetryableWritesSupported(self) &&
922-
!options.session.inTransaction();
922+
!options.session.inTransaction() &&
923+
options.explain === undefined;
923924

924925
const handler = (err, result) => {
925926
if (!err) return callback(null, result);

lib/core/topologies/replset.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,8 @@ function executeWriteOperation(args, options, callback) {
11931193
!!options.retryWrites &&
11941194
options.session &&
11951195
isRetryableWritesSupported(self) &&
1196-
!options.session.inTransaction();
1196+
!options.session.inTransaction() &&
1197+
options.explain === undefined;
11971198

11981199
if (!self.s.replicaSetState.hasPrimary()) {
11991200
if (self.s.disconnectHandler) {

lib/core/wireprotocol/query.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const isSharded = require('./shared').isSharded;
88
const maxWireVersion = require('../utils').maxWireVersion;
99
const applyCommonQueryOptions = require('./shared').applyCommonQueryOptions;
1010
const command = require('./command');
11+
const decorateWithExplain = require('../../utils').decorateWithExplain;
12+
const Explain = require('../../explain').Explain;
1113

1214
function query(server, ns, cmd, cursorState, options, callback) {
1315
options = options || {};
@@ -31,7 +33,14 @@ function query(server, ns, cmd, cursorState, options, callback) {
3133
}
3234

3335
const readPreference = getReadPreference(cmd, options);
34-
const findCmd = prepareFindCommand(server, ns, cmd, cursorState, options);
36+
let findCmd = prepareFindCommand(server, ns, cmd, cursorState, options);
37+
38+
// If we have explain, we need to rewrite the find command
39+
// to wrap it in the explain command
40+
const explain = Explain.fromOptions(options);
41+
if (explain) {
42+
findCmd = decorateWithExplain(findCmd, explain);
43+
}
3544

3645
// NOTE: This actually modifies the passed in cmd, and our code _depends_ on this
3746
// side-effect. Change this ASAP
@@ -59,7 +68,7 @@ function query(server, ns, cmd, cursorState, options, callback) {
5968

6069
function prepareFindCommand(server, ns, cmd, cursorState) {
6170
cursorState.batchSize = cmd.batchSize || cursorState.batchSize;
62-
let findCmd = {
71+
const findCmd = {
6372
find: collectionNamespace(ns)
6473
};
6574

@@ -143,14 +152,6 @@ function prepareFindCommand(server, ns, cmd, cursorState) {
143152
if (cmd.collation) findCmd.collation = cmd.collation;
144153
if (cmd.readConcern) findCmd.readConcern = cmd.readConcern;
145154

146-
// If we have explain, we need to rewrite the find command
147-
// to wrap it in the explain command
148-
if (cmd.explain) {
149-
findCmd = {
150-
explain: findCmd
151-
};
152-
}
153-
154155
return findCmd;
155156
}
156157

@@ -188,7 +189,7 @@ function prepareLegacyFindQuery(server, ns, cmd, cursorState, options) {
188189
if (typeof cmd.showDiskLoc !== 'undefined') findCmd['$showDiskLoc'] = cmd.showDiskLoc;
189190
if (cmd.comment) findCmd['$comment'] = cmd.comment;
190191
if (cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS;
191-
if (cmd.explain) {
192+
if (options.explain !== undefined) {
192193
// nToReturn must be 0 (match all) or negative (match N and close cursor)
193194
// nToReturn > 0 will give explain results equivalent to limit(0)
194195
numberToReturn = -Math.abs(cmd.limit || 0);

lib/core/wireprotocol/write_command.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
const MongoError = require('../error').MongoError;
44
const collectionNamespace = require('./shared').collectionNamespace;
55
const command = require('./command');
6+
const decorateWithExplain = require('../../utils').decorateWithExplain;
7+
const Explain = require('../../explain').Explain;
68

79
function writeCommand(server, type, opsField, ns, ops, options, callback) {
810
if (ops.length === 0) throw new MongoError(`${type} must contain at least one document`);
@@ -15,7 +17,7 @@ function writeCommand(server, type, opsField, ns, ops, options, callback) {
1517
const ordered = typeof options.ordered === 'boolean' ? options.ordered : true;
1618
const writeConcern = options.writeConcern;
1719

18-
const writeCommand = {};
20+
let writeCommand = {};
1921
writeCommand[type] = collectionNamespace(ns);
2022
writeCommand[opsField] = ops;
2123
writeCommand.ordered = ordered;
@@ -36,6 +38,13 @@ function writeCommand(server, type, opsField, ns, ops, options, callback) {
3638
writeCommand.bypassDocumentValidation = options.bypassDocumentValidation;
3739
}
3840

41+
// If a command is to be explained, we need to reformat the command after
42+
// the other command properties are specified.
43+
const explain = Explain.fromOptions(options);
44+
if (explain) {
45+
writeCommand = decorateWithExplain(writeCommand, explain);
46+
}
47+
3948
const commandOptions = Object.assign(
4049
{
4150
checkKeys: type === 'insert',

0 commit comments

Comments
 (0)