diff --git a/lib/aggregation_cursor.js b/lib/aggregation_cursor.js index aa9d207e7cf..a3afbcc460a 100644 --- a/lib/aggregation_cursor.js +++ b/lib/aggregation_cursor.js @@ -322,7 +322,13 @@ AggregationCursor.prototype.get = AggregationCursor.prototype.toArray; /** * Execute the explain for the cursor + * + * For backwards compatibility, a verbosity of true is interpreted as "allPlansExecution" + * and false as "queryPlanner". Prior to server version 3.6, aggregate() + * ignores the verbosity parameter and executes in "queryPlanner". + * * @method AggregationCursor.prototype.explain + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [verbosity=true] - An optional mode in which to run the explain. * @param {AggregationCursor~resultCallback} [callback] The result callback. * @return {Promise} returns Promise if no callback passed */ diff --git a/lib/collection.js b/lib/collection.js index 85cf1918414..d735fcc77c3 100644 --- a/lib/collection.js +++ b/lib/collection.js @@ -287,7 +287,6 @@ const DEPRECATED_FIND_OPTIONS = ['maxScan', 'fields', 'snapshot', 'oplogReplay'] * @param {object} [options.fields] **Deprecated** Use `options.projection` instead * @param {number} [options.skip=0] Set to skip N documents ahead in your query (useful for pagination). * @param {Object} [options.hint] Tell the query to use specific indexes in the query. Object of indexes to use, {'_id':1} - * @param {boolean} [options.explain=false] Explain the query instead of returning the data. * @param {boolean} [options.snapshot=false] DEPRECATED: Snapshot query. * @param {boolean} [options.timeout=false] Specify if the cursor can timeout. * @param {boolean} [options.tailable=false] Specify if the cursor is tailable. @@ -310,6 +309,7 @@ const DEPRECATED_FIND_OPTIONS = ['maxScan', 'fields', 'snapshot', 'oplogReplay'] * @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. * @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields). * @param {boolean} [options.allowDiskUse] Enables writing to temporary files on the server. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @throws {MongoError} * @return {Cursor} @@ -744,6 +744,7 @@ Collection.prototype.insert = deprecate(function(docs, options, callback) { * @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value * @param {boolean} [options.serializeFunctions=false] Serialize functions on any object. * @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {Collection~updateWriteOpCallback} [callback] The command result callback * @return {Promise} returns Promise if no callback passed @@ -821,6 +822,7 @@ Collection.prototype.replaceOne = function(filter, doc, options, callback) { * @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value * @param {boolean} [options.serializeFunctions=false] Serialize functions on any object. * @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {Collection~updateWriteOpCallback} [callback] The command result callback * @return {Promise} returns Promise if no callback passed @@ -912,6 +914,7 @@ Collection.prototype.update = deprecate(function(selector, update, options, call * @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value * @param {boolean} [options.serializeFunctions=false] Serialize functions on any object. * @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {string|object} [options.hint] optional index hint for optimizing the filter query * @param {Collection~deleteWriteOpCallback} [callback] The command result callback @@ -947,6 +950,7 @@ Collection.prototype.removeOne = Collection.prototype.deleteOne; * @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value * @param {boolean} [options.serializeFunctions=false] Serialize functions on any object. * @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {string|object} [options.hint] optional index hint for optimizing the filter query * @param {Collection~deleteWriteOpCallback} [callback] The command result callback @@ -1056,7 +1060,6 @@ Collection.prototype.save = deprecate(function(doc, options, callback) { * @param {object} [options.fields] **Deprecated** Use `options.projection` instead * @param {number} [options.skip=0] Set to skip N documents ahead in your query (useful for pagination). * @param {Object} [options.hint] Tell the query to use specific indexes in the query. Object of indexes to use, {'_id':1} - * @param {boolean} [options.explain=false] Explain the query instead of returning the data. * @param {boolean} [options.snapshot=false] DEPRECATED: Snapshot query. * @param {boolean} [options.timeout=false] Specify if the cursor can timeout. * @param {boolean} [options.tailable=false] Specify if the cursor is tailable. @@ -1075,6 +1078,7 @@ Collection.prototype.save = deprecate(function(doc, options, callback) { * @param {boolean} [options.partial=false] Specify if the cursor should return partial results when querying against a sharded system * @param {number} [options.maxTimeMS] Number of milliseconds to wait before aborting the query. * @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields). + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {Collection~resultCallback} [callback] The command result callback * @return {Promise} returns Promise if no callback passed @@ -1595,6 +1599,7 @@ Collection.prototype.countDocuments = function(query, options, callback) { * @param {(ReadPreference|string)} [options.readPreference] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). * @param {number} [options.maxTimeMS] Number of milliseconds to wait before aborting the query. * @param {object} [options.collation] Specify collation settings for operation. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {Collection~resultCallback} [callback] The command result callback * @return {Promise} returns Promise if no callback passed @@ -1674,6 +1679,7 @@ Collection.prototype.stats = function(options, callback) { * @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value * @param {boolean} [options.serializeFunctions=false] Serialize functions on any object. * @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {Collection~findAndModifyCallback} [callback] The collection result callback * @return {Promise} returns Promise if no callback passed @@ -1707,6 +1713,7 @@ Collection.prototype.findOneAndDelete = function(filter, options, callback) { * @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value * @param {boolean} [options.serializeFunctions=false] Serialize functions on any object. * @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {Collection~findAndModifyCallback} [callback] The collection result callback * @return {Promise} returns Promise if no callback passed @@ -1741,6 +1748,7 @@ Collection.prototype.findOneAndReplace = function(filter, replacement, options, * @param {boolean} [options.checkKeys=false] If true, will throw if bson documents start with `$` or include a `.` in any key value * @param {boolean} [options.serializeFunctions=false] Serialize functions on any object. * @param {boolean} [options.ignoreUndefined=false] Specify if the BSON serializer should ignore undefined fields. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] An ptional session to use for this operation * @param {Collection~findAndModifyCallback} [callback] The collection result callback * @return {Promise} returns Promise if no callback passed @@ -1848,7 +1856,6 @@ Collection.prototype.findAndRemove = deprecate(function(query, sort, options, ca * @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}. * @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. * @param {number} [options.cursor.batchSize=1000] Deprecated. Use `options.batchSize` - * @param {boolean} [options.explain=false] Explain returns the aggregation execution plan (requires mongodb 2.6 >). * @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 >). * @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. * @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 * @param {object} [options.collation] Specify collation settings for operation. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}. * @param {string} [options.comment] Add a comment to an aggregation command * @param {string|object} [options.hint] Add an index selection hint to an aggregation command + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {Collection~aggregationCallback} callback The command result callback * @return {(null|AggregationCursor)} @@ -2106,6 +2114,7 @@ Collection.prototype.group = deprecate(function( * @param {boolean} [options.jsMode=false] It is possible to make the execution stay in JS. Provided in MongoDB > 2.0.X. * @param {boolean} [options.verbose=false] Provide statistics on job execution time. * @param {boolean} [options.bypassDocumentValidation=false] Allow driver to bypass schema validation in MongoDB 3.2 or higher. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @param {ClientSession} [options.session] optional session to use for this operation * @param {Collection~resultCallback} [callback] The command result callback * @throws {MongoError} diff --git a/lib/core/sdam/topology.js b/lib/core/sdam/topology.js index 17fc442bd7c..82677692a12 100644 --- a/lib/core/sdam/topology.js +++ b/lib/core/sdam/topology.js @@ -930,7 +930,8 @@ function executeWriteOperation(args, options, callback) { !!options.retryWrites && options.session && isRetryableWritesSupported(topology) && - !options.session.inTransaction(); + !options.session.inTransaction() && + options.explain === undefined; topology.selectServer(writableServerSelector(), options, (err, server) => { if (err) { diff --git a/lib/core/topologies/mongos.js b/lib/core/topologies/mongos.js index 853059b82f7..b07d14ec691 100644 --- a/lib/core/topologies/mongos.js +++ b/lib/core/topologies/mongos.js @@ -919,7 +919,8 @@ function executeWriteOperation(args, options, callback) { !!options.retryWrites && options.session && isRetryableWritesSupported(self) && - !options.session.inTransaction(); + !options.session.inTransaction() && + options.explain === undefined; const handler = (err, result) => { if (!err) return callback(null, result); diff --git a/lib/core/topologies/replset.js b/lib/core/topologies/replset.js index 0be880f998a..ff39ccbb4dc 100644 --- a/lib/core/topologies/replset.js +++ b/lib/core/topologies/replset.js @@ -1193,7 +1193,8 @@ function executeWriteOperation(args, options, callback) { !!options.retryWrites && options.session && isRetryableWritesSupported(self) && - !options.session.inTransaction(); + !options.session.inTransaction() && + options.explain === undefined; if (!self.s.replicaSetState.hasPrimary()) { if (self.s.disconnectHandler) { diff --git a/lib/core/wireprotocol/query.js b/lib/core/wireprotocol/query.js index 945550830c1..66ba531ce6c 100644 --- a/lib/core/wireprotocol/query.js +++ b/lib/core/wireprotocol/query.js @@ -8,6 +8,8 @@ const isSharded = require('./shared').isSharded; const maxWireVersion = require('../utils').maxWireVersion; const applyCommonQueryOptions = require('./shared').applyCommonQueryOptions; const command = require('./command'); +const decorateWithExplain = require('../../utils').decorateWithExplain; +const Explain = require('../../explain').Explain; function query(server, ns, cmd, cursorState, options, callback) { options = options || {}; @@ -31,7 +33,14 @@ function query(server, ns, cmd, cursorState, options, callback) { } const readPreference = getReadPreference(cmd, options); - const findCmd = prepareFindCommand(server, ns, cmd, cursorState, options); + let findCmd = prepareFindCommand(server, ns, cmd, cursorState, options); + + // If we have explain, we need to rewrite the find command + // to wrap it in the explain command + const explain = Explain.fromOptions(options); + if (explain) { + findCmd = decorateWithExplain(findCmd, explain); + } // NOTE: This actually modifies the passed in cmd, and our code _depends_ on this // side-effect. Change this ASAP @@ -59,7 +68,7 @@ function query(server, ns, cmd, cursorState, options, callback) { function prepareFindCommand(server, ns, cmd, cursorState) { cursorState.batchSize = cmd.batchSize || cursorState.batchSize; - let findCmd = { + const findCmd = { find: collectionNamespace(ns) }; @@ -143,14 +152,6 @@ function prepareFindCommand(server, ns, cmd, cursorState) { if (cmd.collation) findCmd.collation = cmd.collation; if (cmd.readConcern) findCmd.readConcern = cmd.readConcern; - // If we have explain, we need to rewrite the find command - // to wrap it in the explain command - if (cmd.explain) { - findCmd = { - explain: findCmd - }; - } - return findCmd; } @@ -188,7 +189,7 @@ function prepareLegacyFindQuery(server, ns, cmd, cursorState, options) { if (typeof cmd.showDiskLoc !== 'undefined') findCmd['$showDiskLoc'] = cmd.showDiskLoc; if (cmd.comment) findCmd['$comment'] = cmd.comment; if (cmd.maxTimeMS) findCmd['$maxTimeMS'] = cmd.maxTimeMS; - if (cmd.explain) { + if (options.explain !== undefined) { // nToReturn must be 0 (match all) or negative (match N and close cursor) // nToReturn > 0 will give explain results equivalent to limit(0) numberToReturn = -Math.abs(cmd.limit || 0); diff --git a/lib/core/wireprotocol/write_command.js b/lib/core/wireprotocol/write_command.js index e334d5182c7..e6babc3b307 100644 --- a/lib/core/wireprotocol/write_command.js +++ b/lib/core/wireprotocol/write_command.js @@ -3,6 +3,8 @@ const MongoError = require('../error').MongoError; const collectionNamespace = require('./shared').collectionNamespace; const command = require('./command'); +const decorateWithExplain = require('../../utils').decorateWithExplain; +const Explain = require('../../explain').Explain; function writeCommand(server, type, opsField, ns, ops, options, callback) { 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) { const ordered = typeof options.ordered === 'boolean' ? options.ordered : true; const writeConcern = options.writeConcern; - const writeCommand = {}; + let writeCommand = {}; writeCommand[type] = collectionNamespace(ns); writeCommand[opsField] = ops; writeCommand.ordered = ordered; @@ -36,6 +38,13 @@ function writeCommand(server, type, opsField, ns, ops, options, callback) { writeCommand.bypassDocumentValidation = options.bypassDocumentValidation; } + // If a command is to be explained, we need to reformat the command after + // the other command properties are specified. + const explain = Explain.fromOptions(options); + if (explain) { + writeCommand = decorateWithExplain(writeCommand, explain); + } + const commandOptions = Object.assign( { checkKeys: type === 'insert', diff --git a/lib/cursor.js b/lib/cursor.js index b91756aa8e8..5c17f43d788 100644 --- a/lib/cursor.js +++ b/lib/cursor.js @@ -12,6 +12,8 @@ const Map = require('./core').BSON.Map; const maybePromise = require('./utils').maybePromise; const executeOperation = require('./operations/execute_operation'); const formattedOrderClause = require('./utils').formattedOrderClause; +const Explain = require('./explain').Explain; +const Aspect = require('./operations/operation').Aspect; const each = require('./operations/cursor_ops').each; const CountOperation = require('./operations/count'); @@ -999,25 +1001,25 @@ class Cursor extends CoreCursor { /** * Execute the explain for the cursor + * + * For backwards compatibility, a verbosity of true is interpreted as "allPlansExecution" + * and false as "queryPlanner". Prior to server version 3.6, aggregate() + * ignores the verbosity parameter and executes in "queryPlanner". + * * @method + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [verbosity=true] - An optional mode in which to run the explain. * @param {Cursor~resultCallback} [callback] The result callback. * @return {Promise} returns Promise if no callback passed */ - explain(callback) { - // NOTE: the next line includes a special case for operations which do not - // subclass `CommandOperationV2`. To be removed asap. - if (this.operation && this.operation.cmd == null) { - this.operation.options.explain = true; - this.operation.fullResponse = false; - return executeOperation(this.topology, this.operation, callback); - } - - this.cmd.explain = true; + explain(verbosity, callback) { + if (typeof verbosity === 'function') (callback = verbosity), (verbosity = true); + if (verbosity === undefined) verbosity = true; - // Do we have a readConcern - if (this.cmd.readConcern) { - delete this.cmd['readConcern']; + if (!this.operation || !this.operation.hasAspect(Aspect.EXPLAINABLE)) { + throw new MongoError('This command cannot be explained'); } + this.operation.explain = new Explain(verbosity); + return maybePromise(this, callback, cb => { CoreCursor.prototype._next.apply(this, [cb]); }); diff --git a/lib/db.js b/lib/db.js index ee48b5770d3..40c51f9e217 100644 --- a/lib/db.js +++ b/lib/db.js @@ -314,7 +314,7 @@ Db.prototype.command = function(command, options, callback) { * @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}. * @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. * @param {number} [options.cursor.batchSize=1000] Deprecated. Use `options.batchSize` - * @param {boolean} [options.explain=false] Explain returns the aggregation execution plan (requires mongodb 2.6 >). + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output. * @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 >). * @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. * @param {number} [options.maxAwaitTimeMS] The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query. diff --git a/lib/explain.js b/lib/explain.js new file mode 100644 index 00000000000..14ec6f438ae --- /dev/null +++ b/lib/explain.js @@ -0,0 +1,55 @@ +'use strict'; + +const MongoError = require('./core/error').MongoError; + +const ExplainVerbosity = { + queryPlanner: 'queryPlanner', + queryPlannerExtended: 'queryPlannerExtended', + executionStats: 'executionStats', + allPlansExecution: 'allPlansExecution' +}; + +/** + * @class + * @property {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'} verbosity The verbosity mode for the explain output. + */ +class Explain { + /** + * Constructs an Explain from the explain verbosity. + * + * For backwards compatibility, true is interpreted as "allPlansExecution" + * and false as "queryPlanner". Prior to server version 3.6, aggregate() + * ignores the verbosity parameter and executes in "queryPlanner". + * + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [verbosity] The verbosity mode for the explain output. + */ + constructor(verbosity) { + if (typeof verbosity === 'boolean') { + this.verbosity = verbosity ? 'allPlansExecution' : 'queryPlanner'; + } else { + this.verbosity = verbosity; + } + } + + /** + * Construct an Explain given an options object. + * + * @param {object} [options] The options object from which to extract the explain. + * @param {'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean} [options.explain] The verbosity mode for the explain output + * @return {Explain} + */ + static fromOptions(options) { + if (options == null || options.explain === undefined) { + return; + } + + const explain = options.explain; + if (typeof explain === 'boolean' || explain in ExplainVerbosity) { + return new Explain(options.explain); + } + + throw new MongoError(`explain must be one of ${Object.keys(ExplainVerbosity)} or a boolean`); + } +} + +module.exports = { Explain }; diff --git a/lib/operations/aggregate.js b/lib/operations/aggregate.js index e0f2da84e10..ca69a52e198 100644 --- a/lib/operations/aggregate.js +++ b/lib/operations/aggregate.js @@ -37,10 +37,8 @@ class AggregateOperation extends CommandOperationV2 { this.readPreference = ReadPreference.primary; } - if (options.explain && (this.readConcern || this.writeConcern)) { - throw new MongoError( - '"explain" cannot be used on an aggregate call with readConcern/writeConcern' - ); + if (this.explain && this.writeConcern) { + throw new MongoError('"explain" cannot be used on an aggregate call with writeConcern'); } if (options.cursor != null && typeof options.cursor !== 'object') { @@ -83,9 +81,8 @@ class AggregateOperation extends CommandOperationV2 { command.hint = options.hint; } - if (options.explain) { + if (this.explain) { options.full = false; - command.explain = options.explain; } command.cursor = options.cursor || {}; @@ -100,7 +97,8 @@ class AggregateOperation extends CommandOperationV2 { defineAspects(AggregateOperation, [ Aspect.READ_OPERATION, Aspect.RETRYABLE, - Aspect.EXECUTE_WITH_SELECTION + Aspect.EXECUTE_WITH_SELECTION, + Aspect.EXPLAINABLE ]); module.exports = AggregateOperation; diff --git a/lib/operations/command_v2.js b/lib/operations/command_v2.js index e59122a234f..52f982b885f 100644 --- a/lib/operations/command_v2.js +++ b/lib/operations/command_v2.js @@ -6,6 +6,7 @@ const ReadPreference = require('../core').ReadPreference; const ReadConcern = require('../read_concern'); const WriteConcern = require('../write_concern'); const maxWireVersion = require('../core/utils').maxWireVersion; +const decorateWithExplain = require('../utils').decorateWithExplain; const commandSupportsReadConcern = require('../core/sessions').commandSupportsReadConcern; const MongoError = require('../core/error').MongoError; @@ -22,7 +23,6 @@ class CommandOperationV2 extends OperationBase { : ReadPreference.resolve(propertyProvider, this.options); this.readConcern = resolveReadConcern(propertyProvider, this.options); this.writeConcern = resolveWriteConcern(propertyProvider, this.options); - this.explain = false; if (operationOptions && typeof operationOptions.fullResponse === 'boolean') { this.fullResponse = true; @@ -79,6 +79,15 @@ class CommandOperationV2 extends OperationBase { cmd.comment = options.comment; } + if (this.hasAspect(Aspect.EXPLAINABLE) && this.explain) { + if (serverWireVersion < 6 && cmd.aggregate) { + // Prior to 3.6, with aggregate, verbosity is ignored, and we must pass in "explain: true" + cmd.explain = true; + } else { + cmd = decorateWithExplain(cmd, this.explain); + } + } + if (this.logger && this.logger.isDebug()) { this.logger.debug(`executing command ${JSON.stringify(cmd)} against ${this.ns}`); } diff --git a/lib/operations/common_functions.js b/lib/operations/common_functions.js index a02c53dc587..5b9c172f1ec 100644 --- a/lib/operations/common_functions.js +++ b/lib/operations/common_functions.js @@ -11,6 +11,7 @@ const MongoError = require('../core').MongoError; const ReadPreference = require('../core').ReadPreference; const toError = require('../utils').toError; const CursorState = require('../core/cursor').CursorState; +const maxWireVersion = require('../core/utils').maxWireVersion; /** * Build the count command. @@ -57,14 +58,6 @@ function buildCountCommand(collectionOrCursor, query, options) { return cmd; } -function deleteCallback(err, r, callback) { - if (callback == null) return; - if (err && callback) return callback(err); - if (r == null) return callback(null, { result: { ok: 1 } }); - r.deletedCount = r.result.n; - if (callback) callback(null, r); -} - /** * Find and update a document. * @@ -308,6 +301,12 @@ function removeDocuments(coll, selector, options, callback) { return callback(err, null); } + if (options.explain !== undefined && maxWireVersion(coll.s.topology) < 3) { + return callback + ? callback(new MongoError(`server does not support explain on remove`)) + : undefined; + } + // Execute the remove coll.s.topology.remove(coll.s.namespace, [op], finalOptions, (err, result) => { if (callback == null) return; @@ -369,6 +368,12 @@ function updateDocuments(coll, selector, document, options, callback) { return callback(err, null); } + if (options.explain !== undefined && maxWireVersion(coll.s.topology) < 3) { + return callback + ? callback(new MongoError(`server does not support explain on update`)) + : undefined; + } + // Update options coll.s.topology.update(coll.s.namespace, [op], finalOptions, (err, result) => { if (callback == null) return; @@ -382,31 +387,13 @@ function updateDocuments(coll, selector, document, options, callback) { }); } -function updateCallback(err, r, callback) { - if (callback == null) return; - if (err) return callback(err); - if (r == null) return callback(null, { result: { ok: 1 } }); - r.modifiedCount = r.result.nModified != null ? r.result.nModified : r.result.n; - r.upsertedId = - Array.isArray(r.result.upserted) && r.result.upserted.length > 0 - ? r.result.upserted[0] // FIXME(major): should be `r.result.upserted[0]._id` - : null; - r.upsertedCount = - Array.isArray(r.result.upserted) && r.result.upserted.length ? r.result.upserted.length : 0; - r.matchedCount = - Array.isArray(r.result.upserted) && r.result.upserted.length > 0 ? 0 : r.result.n; - callback(null, r); -} - module.exports = { buildCountCommand, - deleteCallback, findAndModify, indexInformation, nextObject, prepareDocs, insertDocuments, removeDocuments, - updateDocuments, - updateCallback + updateDocuments }; diff --git a/lib/operations/delete_many.js b/lib/operations/delete_many.js index d881f67d9cd..32ae500ddab 100644 --- a/lib/operations/delete_many.js +++ b/lib/operations/delete_many.js @@ -1,8 +1,9 @@ 'use strict'; const OperationBase = require('./operation').OperationBase; -const deleteCallback = require('./common_functions').deleteCallback; const removeDocuments = require('./common_functions').removeDocuments; +const Aspect = require('./operation').Aspect; +const defineAspects = require('./operation').defineAspects; class DeleteManyOperation extends OperationBase { constructor(collection, filter, options) { @@ -18,8 +19,20 @@ class DeleteManyOperation extends OperationBase { const options = this.options; options.single = false; - removeDocuments(coll, filter, options, (err, r) => deleteCallback(err, r, callback)); + removeDocuments(coll, filter, options, (err, r) => { + if (callback == null) return; + if (err && callback) return callback(err); + if (r == null) return callback(null, { result: { ok: 1 } }); + + // If an explain operation was executed, don't process the server results + if (this.explain) return callback(undefined, r.result); + + r.deletedCount = r.result.n; + callback(null, r); + }); } } +defineAspects(DeleteManyOperation, [Aspect.EXPLAINABLE]); + module.exports = DeleteManyOperation; diff --git a/lib/operations/delete_one.js b/lib/operations/delete_one.js index b05597fd4ee..9aec05b99ed 100644 --- a/lib/operations/delete_one.js +++ b/lib/operations/delete_one.js @@ -1,8 +1,9 @@ 'use strict'; const OperationBase = require('./operation').OperationBase; -const deleteCallback = require('./common_functions').deleteCallback; const removeDocuments = require('./common_functions').removeDocuments; +const Aspect = require('./operation').Aspect; +const defineAspects = require('./operation').defineAspects; class DeleteOneOperation extends OperationBase { constructor(collection, filter, options) { @@ -18,8 +19,20 @@ class DeleteOneOperation extends OperationBase { const options = this.options; options.single = true; - removeDocuments(coll, filter, options, (err, r) => deleteCallback(err, r, callback)); + removeDocuments(coll, filter, options, (err, r) => { + if (callback == null) return; + if (err && callback) return callback(err); + if (r == null) return callback(null, { result: { ok: 1 } }); + + // If an explain operation was executed, don't process the server results + if (this.explain) return callback(undefined, r.result); + + r.deletedCount = r.result.n; + callback(null, r); + }); } } +defineAspects(DeleteOneOperation, [Aspect.EXPLAINABLE]); + module.exports = DeleteOneOperation; diff --git a/lib/operations/distinct.js b/lib/operations/distinct.js index dcf4f7e2b00..fcac930c6f4 100644 --- a/lib/operations/distinct.js +++ b/lib/operations/distinct.js @@ -5,6 +5,8 @@ const defineAspects = require('./operation').defineAspects; const CommandOperationV2 = require('./command_v2'); const decorateWithCollation = require('../utils').decorateWithCollation; const decorateWithReadConcern = require('../utils').decorateWithReadConcern; +const maxWireVersion = require('../core/utils').maxWireVersion; +const MongoError = require('../error').MongoError; /** * Return a list of distinct values for the given key across a collection. @@ -65,13 +67,18 @@ class DistinctOperation extends CommandOperationV2 { return callback(err, null); } + if (this.explain && maxWireVersion(server) < 4) { + callback(new MongoError(`server does not support explain on distinct`)); + return; + } + super.executeCommand(server, cmd, (err, result) => { if (err) { callback(err); return; } - callback(null, this.options.full ? result : result.values); + callback(null, this.options.full || this.explain ? result : result.values); }); } } @@ -79,7 +86,8 @@ class DistinctOperation extends CommandOperationV2 { defineAspects(DistinctOperation, [ Aspect.READ_OPERATION, Aspect.RETRYABLE, - Aspect.EXECUTE_WITH_SELECTION + Aspect.EXECUTE_WITH_SELECTION, + Aspect.EXPLAINABLE ]); module.exports = DistinctOperation; diff --git a/lib/operations/find.js b/lib/operations/find.js index 09cb9a9c89f..4cf697f5f59 100644 --- a/lib/operations/find.js +++ b/lib/operations/find.js @@ -25,6 +25,11 @@ class FindOperation extends OperationBase { return; } + if (this.explain) { + // We need to manually ensure explain is in the options. + this.options.explain = this.explain.verbosity; + } + // TOOD: use `MongoDBNamespace` through and through const cursorState = this.cursorState || {}; server.query(this.ns.toString(), this.cmd, cursorState, this.options, callback); @@ -34,7 +39,8 @@ class FindOperation extends OperationBase { defineAspects(FindOperation, [ Aspect.READ_OPERATION, Aspect.RETRYABLE, - Aspect.EXECUTE_WITH_SELECTION + Aspect.EXECUTE_WITH_SELECTION, + Aspect.EXPLAINABLE ]); module.exports = FindOperation; diff --git a/lib/operations/find_and_modify.js b/lib/operations/find_and_modify.js index 03711eeb492..3886688e977 100644 --- a/lib/operations/find_and_modify.js +++ b/lib/operations/find_and_modify.js @@ -10,6 +10,9 @@ const handleCallback = require('../utils').handleCallback; const ReadPreference = require('../core').ReadPreference; const maxWireVersion = require('../core/utils').maxWireVersion; const MongoError = require('../error').MongoError; +const Aspect = require('./operation').Aspect; +const defineAspects = require('./operation').defineAspects; +const decorateWithExplain = require('../utils').decorateWithExplain; class FindAndModifyOperation extends OperationBase { constructor(collection, query, sort, doc, options) { @@ -29,7 +32,7 @@ class FindAndModifyOperation extends OperationBase { let options = this.options; // Create findAndModify command object - const queryObject = { + let queryObject = { findAndModify: coll.collectionName, query: query }; @@ -103,6 +106,14 @@ class FindAndModifyOperation extends OperationBase { queryObject.hint = options.hint; } + if (this.explain) { + if (maxWireVersion(coll.s.topology) < 4) { + callback(new MongoError(`server does not support explain on findAndModify`)); + return; + } + queryObject = decorateWithExplain(queryObject, this.explain); + } + // Execute the command executeCommand(coll.s.db, queryObject, options, (err, result) => { if (err) return handleCallback(callback, err, null); @@ -112,4 +123,6 @@ class FindAndModifyOperation extends OperationBase { } } +defineAspects(FindAndModifyOperation, [Aspect.EXPLAINABLE]); + module.exports = FindAndModifyOperation; diff --git a/lib/operations/find_one.js b/lib/operations/find_one.js index b584db643d9..3e4b3cf8500 100644 --- a/lib/operations/find_one.js +++ b/lib/operations/find_one.js @@ -3,6 +3,8 @@ const handleCallback = require('../utils').handleCallback; const OperationBase = require('./operation').OperationBase; const toError = require('../utils').toError; +const Aspect = require('./operation').Aspect; +const defineAspects = require('./operation').defineAspects; class FindOneOperation extends OperationBase { constructor(collection, query, options) { @@ -34,4 +36,6 @@ class FindOneOperation extends OperationBase { } } +defineAspects(FindOneOperation, [Aspect.EXPLAINABLE]); + module.exports = FindOneOperation; diff --git a/lib/operations/map_reduce.js b/lib/operations/map_reduce.js index febba586f81..3a2cf261866 100644 --- a/lib/operations/map_reduce.js +++ b/lib/operations/map_reduce.js @@ -11,8 +11,14 @@ const loadDb = require('../dynamic_loaders').loadDb; const OperationBase = require('./operation').OperationBase; const ReadPreference = require('../core').ReadPreference; const toError = require('../utils').toError; +const Aspect = require('./operation').Aspect; +const defineAspects = require('./operation').defineAspects; +const decorateWithExplain = require('../utils').decorateWithExplain; +const maxWireVersion = require('../core/utils').maxWireVersion; +const MongoError = require('../error').MongoError; const exclusionList = [ + 'explain', 'readPreference', 'session', 'bypassDocumentValidation', @@ -59,7 +65,7 @@ class MapReduceOperation extends OperationBase { const reduce = this.reduce; let options = this.options; - const mapCommandHash = { + let mapCommandHash = { mapReduce: coll.collectionName, map: map, reduce: reduce @@ -110,6 +116,14 @@ class MapReduceOperation extends OperationBase { return callback(err, null); } + if (this.explain) { + if (maxWireVersion(coll.s.topology) < 9) { + callback(new MongoError(`server does not support explain on mapReduce`)); + return; + } + mapCommandHash = decorateWithExplain(mapCommandHash, this.explain); + } + // Execute command executeCommand(coll.s.db, mapCommandHash, options, (err, result) => { if (err) return handleCallback(callback, err); @@ -118,6 +132,9 @@ class MapReduceOperation extends OperationBase { return handleCallback(callback, toError(result)); } + // If an explain operation was executed, don't process the server results + if (this.explain) return callback(undefined, result); + // Create statistics value const stats = {}; if (result.timeMillis) stats['processtime'] = result.timeMillis; @@ -187,4 +204,6 @@ function processScope(scope) { return new_scope; } +defineAspects(MapReduceOperation, [Aspect.EXPLAINABLE]); + module.exports = MapReduceOperation; diff --git a/lib/operations/operation.js b/lib/operations/operation.js index 28a9d05fc8d..a8c86fbbaaf 100644 --- a/lib/operations/operation.js +++ b/lib/operations/operation.js @@ -1,11 +1,15 @@ 'use strict'; +const Explain = require('../explain').Explain; +const MongoError = require('../core').MongoError; + const Aspect = { READ_OPERATION: Symbol('READ_OPERATION'), WRITE_OPERATION: Symbol('WRITE_OPERATION'), RETRYABLE: Symbol('RETRYABLE'), EXECUTE_WITH_SELECTION: Symbol('EXECUTE_WITH_SELECTION'), - NO_INHERIT_OPTIONS: Symbol('NO_INHERIT_OPTIONS') + NO_INHERIT_OPTIONS: Symbol('NO_INHERIT_OPTIONS'), + EXPLAINABLE: Symbol('EXPLAINABLE') }; /** @@ -17,6 +21,12 @@ const Aspect = { class OperationBase { constructor(options) { this.options = Object.assign({}, options); + + if (this.hasAspect(Aspect.EXPLAINABLE)) { + this.explain = Explain.fromOptions(options); + } else if (this.options.explain !== undefined) { + throw new MongoError(`explain is not supported on this command`); + } } hasAspect(aspect) { diff --git a/lib/operations/update_many.js b/lib/operations/update_many.js index 725ed28d7df..6d2252d34be 100644 --- a/lib/operations/update_many.js +++ b/lib/operations/update_many.js @@ -1,9 +1,10 @@ 'use strict'; const OperationBase = require('./operation').OperationBase; -const updateCallback = require('./common_functions').updateCallback; const updateDocuments = require('./common_functions').updateDocuments; const hasAtomicOperators = require('../utils').hasAtomicOperators; +const Aspect = require('./operation').Aspect; +const defineAspects = require('./operation').defineAspects; class UpdateManyOperation extends OperationBase { constructor(collection, filter, update, options) { @@ -27,8 +28,28 @@ class UpdateManyOperation extends OperationBase { // Set single document update options.multi = true; // Execute update - updateDocuments(coll, filter, update, options, (err, r) => updateCallback(err, r, callback)); + updateDocuments(coll, filter, update, options, (err, r) => { + if (callback == null) return; + if (err) return callback(err); + if (r == null) return callback(null, { result: { ok: 1 } }); + + // If an explain operation was executed, don't process the server results + if (this.explain) return callback(undefined, r.result); + + r.modifiedCount = r.result.nModified != null ? r.result.nModified : r.result.n; + r.upsertedId = + Array.isArray(r.result.upserted) && r.result.upserted.length > 0 + ? r.result.upserted[0] // FIXME(major): should be `r.result.upserted[0]._id` + : null; + r.upsertedCount = + Array.isArray(r.result.upserted) && r.result.upserted.length ? r.result.upserted.length : 0; + r.matchedCount = + Array.isArray(r.result.upserted) && r.result.upserted.length > 0 ? 0 : r.result.n; + callback(null, r); + }); } } +defineAspects(UpdateManyOperation, [Aspect.EXPLAINABLE]); + module.exports = UpdateManyOperation; diff --git a/lib/operations/update_one.js b/lib/operations/update_one.js index eea12a8eab3..e05cb93a818 100644 --- a/lib/operations/update_one.js +++ b/lib/operations/update_one.js @@ -3,6 +3,8 @@ const OperationBase = require('./operation').OperationBase; const updateDocuments = require('./common_functions').updateDocuments; const hasAtomicOperators = require('../utils').hasAtomicOperators; +const Aspect = require('./operation').Aspect; +const defineAspects = require('./operation').defineAspects; class UpdateOneOperation extends OperationBase { constructor(collection, filter, update, options) { @@ -26,24 +28,28 @@ class UpdateOneOperation extends OperationBase { // Set single document update options.multi = false; // Execute update - updateDocuments(coll, filter, update, options, (err, r) => updateCallback(err, r, callback)); + updateDocuments(coll, filter, update, options, (err, r) => { + if (callback == null) return; + if (err) return callback(err); + if (r == null) return callback(null, { result: { ok: 1 } }); + + // If an explain operation was executed, don't process the server results + if (this.explain) return callback(undefined, r.result); + + r.modifiedCount = r.result.nModified != null ? r.result.nModified : r.result.n; + r.upsertedId = + Array.isArray(r.result.upserted) && r.result.upserted.length > 0 + ? r.result.upserted[0] // FIXME(major): should be `r.result.upserted[0]._id` + : null; + r.upsertedCount = + Array.isArray(r.result.upserted) && r.result.upserted.length ? r.result.upserted.length : 0; + r.matchedCount = + Array.isArray(r.result.upserted) && r.result.upserted.length > 0 ? 0 : r.result.n; + callback(null, r); + }); } } -function updateCallback(err, r, callback) { - if (callback == null) return; - if (err) return callback(err); - if (r == null) return callback(null, { result: { ok: 1 } }); - r.modifiedCount = r.result.nModified != null ? r.result.nModified : r.result.n; - r.upsertedId = - Array.isArray(r.result.upserted) && r.result.upserted.length > 0 - ? r.result.upserted[0] // FIXME(major): should be `r.result.upserted[0]._id` - : null; - r.upsertedCount = - Array.isArray(r.result.upserted) && r.result.upserted.length ? r.result.upserted.length : 0; - r.matchedCount = - Array.isArray(r.result.upserted) && r.result.upserted.length > 0 ? 0 : r.result.n; - callback(null, r); -} +defineAspects(UpdateOneOperation, [Aspect.EXPLAINABLE]); module.exports = UpdateOneOperation; diff --git a/lib/utils.js b/lib/utils.js index 9d1053ed61d..17f53850fae 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -518,6 +518,22 @@ function decorateWithReadConcern(command, coll, options) { } } +/** + * Applies an explain to a given command. + * @internal + * + * @param {object} command - the command on which to apply the explain + * @param {Explain} explain - the options containing the explain verbosity + * @return the new command + */ +function decorateWithExplain(command, explain) { + if (command.explain) { + return command; + } + + return { explain: command, verbosity: explain.verbosity }; +} + const emitProcessWarning = msg => process.emitWarning(msg, 'DeprecationWarning'); const emitConsoleWarning = msg => console.error(msg); const emitDeprecationWarning = process.emitWarning ? emitProcessWarning : emitConsoleWarning; @@ -820,6 +836,7 @@ module.exports = { isPromiseLike, decorateWithCollation, decorateWithReadConcern, + decorateWithExplain, deprecateOptions, SUPPORTS, MongoDBNamespace, diff --git a/test/functional/aggregation.test.js b/test/functional/aggregation.test.js index 1ab06ad801c..02e2701dced 100644 --- a/test/functional/aggregation.test.js +++ b/test/functional/aggregation.test.js @@ -484,7 +484,7 @@ describe('Aggregation', function() { cursor.explain(function(err, result) { expect(err).to.be.null; expect(result.stages).to.have.lengthOf.at.least(1); - expect(result.stages[0]).to.have.key('$cursor'); + expect(result.stages[0]).to.have.property('$cursor'); client.close(done); }); @@ -977,7 +977,7 @@ describe('Aggregation', function() { } }); - it('should fail if you try to use explain flag with readConcern/writeConcern', { + it('should fail if you try to use explain flag with writeConcern', { metadata: { requires: { mongodb: '>3.6.0', @@ -987,12 +987,9 @@ describe('Aggregation', function() { test: function(done) { var databaseName = this.configuration.db; - var client = this.configuration.newClient(this.configuration.writeConcernMax(), { - poolSize: 1 - }); + var client = this.configuration.newClient({ poolSize: 1 }); const testCases = [ - { readConcern: { level: 'local' } }, { writeConcern: { j: true } }, { readConcern: { level: 'local' }, writeConcern: { j: true } } ]; diff --git a/test/functional/crud_api.test.js b/test/functional/crud_api.test.js index 67cb5eacba1..3908fd46ffa 100644 --- a/test/functional/crud_api.test.js +++ b/test/functional/crud_api.test.js @@ -35,25 +35,29 @@ describe('CRUD API', function() { // // Cursor // -------------------------------------------------- - var cursor = db.collection('t').find({}); - // Possible methods on the the cursor instance - cursor - .filter({ a: 1 }) - .addCursorFlag('noCursorTimeout', true) - .addQueryModifier('$comment', 'some comment') - .batchSize(2) - .comment('some comment 2') - .limit(2) - .maxTimeMs(50) - .project({ a: 1 }) - .skip(0) - .sort({ a: 1 }); + const makeCursor = () => { + // Possible methods on the the cursor instance + return db + .collection('t') + .find({}) + .filter({ a: 1 }) + .addCursorFlag('noCursorTimeout', true) + .addQueryModifier('$comment', 'some comment') + .batchSize(2) + .comment('some comment 2') + .limit(2) + .maxTimeMs(50) + .project({ a: 1 }) + .skip(0) + .sort({ a: 1 }); + }; // // Exercise count method // ------------------------------------------------- var countMethod = function() { // Execute the different methods supported by the cursor + const cursor = makeCursor(); cursor.count(function(err, count) { test.equal(null, err); test.equal(2, count); @@ -67,6 +71,7 @@ describe('CRUD API', function() { var eachMethod = function() { var count = 0; + const cursor = makeCursor(); cursor.each(function(err, doc) { test.equal(null, err); if (doc) count = count + 1; @@ -81,6 +86,7 @@ describe('CRUD API', function() { // Exercise toArray // ------------------------------------------------- var toArrayMethod = function() { + const cursor = makeCursor(); cursor.toArray(function(err, docs) { test.equal(null, err); test.equal(2, docs.length); @@ -92,16 +98,16 @@ describe('CRUD API', function() { // Exercise next method // ------------------------------------------------- var nextMethod = function() { - var clonedCursor = cursor.clone(); - clonedCursor.next(function(err, doc) { + const cursor = makeCursor(); + cursor.next(function(err, doc) { test.equal(null, err); test.ok(doc != null); - clonedCursor.next(function(err, doc) { + cursor.next(function(err, doc) { test.equal(null, err); test.ok(doc != null); - clonedCursor.next(function(err, doc) { + cursor.next(function(err, doc) { test.equal(null, err); test.equal(null, doc); streamMethod(); @@ -115,12 +121,12 @@ describe('CRUD API', function() { // ------------------------------------------------- var streamMethod = function() { var count = 0; - var clonedCursor = cursor.clone(); - clonedCursor.on('data', function() { + const cursor = makeCursor(); + cursor.on('data', function() { count = count + 1; }); - clonedCursor.once('end', function() { + cursor.once('end', function() { test.equal(2, count); explainMethod(); }); @@ -130,8 +136,8 @@ describe('CRUD API', function() { // Explain method // ------------------------------------------------- var explainMethod = function() { - var clonedCursor = cursor.clone(); - clonedCursor.explain(function(err, result) { + const cursor = makeCursor(); + cursor.explain(function(err, result) { test.equal(null, err); test.ok(result != null); diff --git a/test/functional/explain.test.js b/test/functional/explain.test.js new file mode 100644 index 00000000000..c74c09d15a0 --- /dev/null +++ b/test/functional/explain.test.js @@ -0,0 +1,765 @@ +'use strict'; +const chai = require('chai'); +const expect = chai.expect; +const withClient = require('./shared').withClient; +const setupDatabase = require('./shared').setupDatabase; + +describe('Explain', function() { + before(function() { + return setupDatabase(this.configuration); + }); + + it('should honor boolean explain with delete one', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorBooleanExplainWithDeleteOne'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.deleteOne({ a: 1 }, { explain: true }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + }); + }); + }) + }); + + it('should honor boolean explain with delete many', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorBooleanExplainWithDeleteMany'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.deleteMany({ a: 1 }, { explain: true }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + }); + }); + }) + }); + + it('should honor boolean explain with update one', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorBooleanExplainWithUpdateOne'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.updateOne( + { a: 1 }, + { $inc: { a: 2 } }, + { explain: true }, + (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + } + ); + }); + }) + }); + + it('should honor boolean explain with update many', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorBooleanExplainWithUpdateMany'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.updateMany( + { a: 1 }, + { $inc: { a: 2 } }, + { explain: true }, + (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).nested.property('queryPlanner').to.exist; + done(); + } + ); + }); + }) + }); + + it('should honor boolean explain with remove one', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorBooleanExplainWithRemoveOne'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.removeOne({ a: 1 }, { explain: true }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + }); + }); + }) + }); + + it('should honor boolean explain with remove many', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorBooleanExplainWithRemoveMany'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.removeMany({ a: 1 }, { explain: true }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + }); + }); + }) + }); + + it('should honor boolean explain with distinct', { + metadata: { + requires: { + mongodb: '>=3.2' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorBooleanExplainWithDistinct'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.distinct('a', {}, { explain: true }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + }); + }); + }) + }); + + it('should honor boolean explain with findOneAndModify', { + metadata: { + requires: { + mongodb: '>=3.2' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorBooleanExplainWithFindOneAndModify'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.findOneAndDelete({ a: 1 }, { explain: true }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + }); + }); + }) + }); + + it('should honor boolean explain with mapReduce', { + metadata: { + requires: { + mongodb: '>=4.4' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorBooleanExplainWithMapReduce'); + var collection = db.collection('test'); + + collection.insertMany([{ user_id: 1 }, { user_id: 2 }], (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + var map = 'function() { emit(this.user_id, 1); }'; + var reduce = 'function(k,vals) { return 1; }'; + + collection.mapReduce( + map, + reduce, + { out: { replace: 'tempCollection' }, explain: true }, + (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('stages').to.exist; + done(); + } + ); + }); + }) + }); + + it('should use allPlansExecution as true explain verbosity', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldUseAllPlansExecutionAsTrueExplainVerbosity'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + // Verify explanation result contains properties of allPlansExecution output + collection.deleteOne({ a: 1 }, { explain: true }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + expect(explanation).nested.property('executionStats.allPlansExecution').to.exist; + done(); + }); + }); + }) + }); + + it('should use queryPlanner as false explain verbosity', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldUseQueryPlannerAsFalseExplainVerbosity'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + // Verify explanation result contains properties of queryPlanner output + collection.deleteOne({ a: 1 }, { explain: false }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + expect(explanation).to.not.have.property('executionStats'); + done(); + }); + }); + }) + }); + + it('should honor queryPlanner string explain', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorQueryPlannerStringExplain'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + // Verify explanation result contains properties of queryPlanner output + collection.deleteOne({ a: 1 }, { explain: 'queryPlanner' }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + expect(explanation).to.not.have.property('executionStats'); + done(); + }); + }); + }) + }); + + it('should honor executionStats string explain', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorExecutionStatsStringExplain'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + // Verify explanation result contains properties of executionStats output + collection.deleteMany({ a: 1 }, { explain: 'executionStats' }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + expect(explanation).property('executionStats').to.exist; + expect(explanation.executionStats).to.not.have.property('allPlansExecution'); + done(); + }); + }); + }) + }); + + it('should honor allPlansExecution string explain', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorAllPlansStringExplain'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + // Verify explanation result contains properties of allPlansExecution output + collection.removeOne({ a: 1 }, { explain: 'allPlansExecution' }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + expect(explanation).nested.property('executionStats.allPlansExecution').to.exist; + done(); + }); + }); + }) + }); + + it('should honor string explain with distinct', { + metadata: { + requires: { + mongodb: '>=3.2' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorStringExplainWithDistinct'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.distinct('a', {}, { explain: 'executionStats' }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + expect(explanation).property('executionStats').to.exist; + done(); + }); + }); + }) + }); + + it('should honor string explain with findOneAndModify', { + metadata: { + requires: { + mongodb: '>=3.2' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorStringExplainWithFindOneAndModify'); + var collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.findOneAndReplace( + { a: 1 }, + { a: 2 }, + { explain: 'queryPlanner' }, + (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + } + ); + }); + }) + }); + + it('should honor string explain with mapReduce', { + metadata: { + requires: { + mongodb: '>=4.4' + } + }, + test: withClient(function(client, done) { + var db = client.db('shouldHonorStringExplainWithMapReduce'); + var collection = db.collection('test'); + + collection.insertMany([{ user_id: 1 }, { user_id: 2 }], (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + var map = 'function() { emit(this.user_id, 1); }'; + var reduce = 'function(k,vals) { return 1; }'; + + collection.mapReduce( + map, + reduce, + { out: { replace: 'tempCollection' }, explain: 'executionStats' }, + (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('stages').to.exist; + done(); + } + ); + }); + }) + }); + + it('should honor boolean explain with find', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + const db = client.db('shouldHonorBooleanExplainWithFind'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.find({ a: 1 }, { explain: true }).toArray((err, docs) => { + expect(err).to.not.exist; + const explanation = docs[0]; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + }); + }); + }) + }); + + it('should honor string explain with find', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + const db = client.db('shouldHonorStringExplainWithFind'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.find({ a: 1 }, { explain: 'executionStats' }).toArray((err, docs) => { + expect(err).to.not.exist; + const explanation = docs[0]; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + expect(explanation).property('executionStats').to.exist; + done(); + }); + }); + }) + }); + + it('should honor boolean explain with findOne', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + const db = client.db('shouldHonorBooleanExplainWithFindOne'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.findOne({ a: 1 }, { explain: true }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + }); + }); + }) + }); + + it('should honor string explain with findOne', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + const db = client.db('shouldHonorStringExplainWithFindOne'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.findOne({ a: 1 }, { explain: 'executionStats' }, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + expect(explanation).property('executionStats').to.exist; + done(); + }); + }); + }) + }); + + it('should honor boolean explain specified on cursor with find', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + const db = client.db('shouldHonorBooleanExplainSpecifiedOnCursor'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.find({ a: 1 }).explain(false, (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + done(); + }); + }); + }) + }); + + it('should honor string explain specified on cursor with find', { + metadata: { + requires: { + mongodb: '>=3.0' + } + }, + test: withClient(function(client, done) { + const db = client.db('shouldHonorStringExplainSpecifiedOnCursor'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.find({ a: 1 }).explain('allPlansExecution', (err, explanation) => { + expect(err).to.not.exist; + expect(explanation).to.exist; + expect(explanation).property('queryPlanner').to.exist; + expect(explanation).property('executionStats').to.exist; + done(); + }); + }); + }) + }); + + it('should honor legacy explain with find', { + metadata: { + requires: { + mongodb: '<3.0' + } + }, + test: withClient(function(client, done) { + const db = client.db('shouldHonorLegacyExplainWithFind'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection.find({ a: 1 }).explain((err, result) => { + expect(err).to.not.exist; + expect(result).to.have.property('allPlans'); + done(); + }); + }); + }) + }); + + it( + 'should honor boolean explain with aggregate', + withClient(function(client, done) { + const db = client.db('shouldHonorBooleanExplainWithAggregate'); + const collection = db.collection('test'); + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection + .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }], { explain: true }) + .toArray((err, docs) => { + expect(err).to.not.exist; + const result = docs[0]; + expect(result).to.have.property('stages'); + expect(result.stages).to.have.lengthOf.at.least(1); + expect(result.stages[0]).to.have.property('$cursor'); + done(); + }); + }); + }) + ); + + it('should honor string explain with aggregate', { + metadata: { + requires: { + mongodb: '>=3.6.0' + } + }, + test: withClient(function(client, done) { + const db = client.db('shouldHonorStringExplainWithAggregate'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection + .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }], { + explain: 'executionStats' + }) + .toArray((err, docs) => { + expect(err).to.not.exist; + const result = docs[0]; + expect(result).to.have.property('stages'); + expect(result.stages).to.have.lengthOf.at.least(1); + expect(result.stages[0]).to.have.property('$cursor'); + expect(result.stages[0].$cursor).to.have.property('queryPlanner'); + expect(result.stages[0].$cursor).to.have.property('executionStats'); + done(); + }); + }); + }) + }); + + it( + 'should honor boolean explain specified on cursor with aggregate', + withClient(function(client, done) { + const db = client.db('shouldHonorBooleanExplainSpecifiedOnCursor'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection + .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }]) + .explain(false, (err, result) => { + expect(err).to.not.exist; + expect(result).to.have.property('stages'); + expect(result.stages).to.have.lengthOf.at.least(1); + expect(result.stages[0]).to.have.property('$cursor'); + done(); + }); + }); + }) + ); + + it('should honor string explain specified on cursor with aggregate', { + metadata: { + requires: { + mongodb: '>=3.6' + } + }, + test: withClient(function(client, done) { + const db = client.db('shouldHonorStringExplainSpecifiedOnCursor'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection + .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }]) + .explain('allPlansExecution', (err, result) => { + expect(err).to.not.exist; + expect(result).to.exist; + expect(result).to.have.property('stages'); + expect(result.stages).to.have.lengthOf.at.least(1); + expect(result.stages[0]).to.have.property('$cursor'); + expect(result.stages[0].$cursor).to.have.property('queryPlanner'); + expect(result.stages[0].$cursor).to.have.property('executionStats'); + done(); + }); + }); + }) + }); + + it( + 'should honor legacy explain with aggregate', + withClient(function(client, done) { + const db = client.db('shouldHonorLegacyExplainWithAggregate'); + const collection = db.collection('test'); + + collection.insertOne({ a: 1 }, (err, res) => { + expect(err).to.not.exist; + expect(res).to.exist; + + collection + .aggregate([{ $project: { a: 1 } }, { $group: { _id: '$a' } }]) + .explain((err, result) => { + expect(err).to.not.exist; + expect(result).to.have.property('stages'); + expect(result.stages).to.have.lengthOf.at.least(1); + expect(result.stages[0]).to.have.property('$cursor'); + done(); + }); + }); + }) + ); +});