Skip to content

Commit e05e34f

Browse files
committed
add explain functionality to ops
1 parent 2036fe7 commit e05e34f

21 files changed

+274
-92
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 {string|boolean=true} verbosity - 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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ 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.
290+
* @param {string|boolean} [options.explain] The verbosity mode for the explain output.
291291
* @param {boolean} [options.snapshot=false] DEPRECATED: Snapshot query.
292292
* @param {boolean} [options.timeout=false] Specify if the cursor can timeout.
293293
* @param {boolean} [options.tailable=false] Specify if the cursor is tailable.
@@ -1044,7 +1044,7 @@ Collection.prototype.save = deprecate(function(doc, options, callback) {
10441044
* @param {object} [options.fields] **Deprecated** Use `options.projection` instead
10451045
* @param {number} [options.skip=0] Set to skip N documents ahead in your query (useful for pagination).
10461046
* @param {Object} [options.hint] Tell the query to use specific indexes in the query. Object of indexes to use, {'_id':1}
1047-
* @param {boolean} [options.explain=false] Explain the query instead of returning the data.
1047+
* @param {string|boolean} [options.explain] The verbosity mode for the explain output.
10481048
* @param {boolean} [options.snapshot=false] DEPRECATED: Snapshot query.
10491049
* @param {boolean} [options.timeout=false] Specify if the cursor can timeout.
10501050
* @param {boolean} [options.tailable=false] Specify if the cursor is tailable.
@@ -1831,7 +1831,7 @@ Collection.prototype.findAndRemove = deprecate(function(query, sort, options, ca
18311831
* @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}.
18321832
* @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.
18331833
* @param {number} [options.cursor.batchSize=1000] Deprecated. Use `options.batchSize`
1834-
* @param {boolean} [options.explain=false] Explain returns the aggregation execution plan (requires mongodb 2.6 >).
1834+
* @param {string|boolean} [options.explain] The verbosity mode for the explain output.
18351835
* @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 >).
18361836
* @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.
18371837
* @param {number} [options.maxAwaitTimeMS] The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query.

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');
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');
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',

lib/cursor.js

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const Map = require('./core').BSON.Map;
1212
const maybePromise = require('./utils').maybePromise;
1313
const executeOperation = require('./operations/execute_operation');
1414
const formattedOrderClause = require('./utils').formattedOrderClause;
15+
const Explain = require('./explain');
16+
const Aspect = require('./operations/operation').Aspect;
1517

1618
const each = require('./operations/cursor_ops').each;
1719
const CountOperation = require('./operations/count');
@@ -999,25 +1001,25 @@ class Cursor extends CoreCursor {
9991001

10001002
/**
10011003
* Execute the explain for the cursor
1004+
*
1005+
* For backwards compatibility, a verbosity of true is interpreted as "allPlansExecution"
1006+
* and false as "queryPlanner". Prior to server version 3.6, aggregate()
1007+
* ignores the verbosity parameter and executes in "queryPlanner".
1008+
*
10021009
* @method
1010+
* @param {string|boolean=true} verbosity - An optional mode in which to run the explain.
10031011
* @param {Cursor~resultCallback} [callback] The result callback.
10041012
* @return {Promise} returns Promise if no callback passed
10051013
*/
1006-
explain(callback) {
1007-
// NOTE: the next line includes a special case for operations which do not
1008-
// subclass `CommandOperationV2`. To be removed asap.
1009-
if (this.operation && this.operation.cmd == null) {
1010-
this.operation.options.explain = true;
1011-
this.operation.fullResponse = false;
1012-
return executeOperation(this.topology, this.operation, callback);
1013-
}
1014-
1015-
this.cmd.explain = true;
1014+
explain(verbosity, callback) {
1015+
if (typeof verbosity === 'function') (callback = verbosity), (verbosity = true);
1016+
if (verbosity === undefined) verbosity = true;
10161017

1017-
// Do we have a readConcern
1018-
if (this.cmd.readConcern) {
1019-
delete this.cmd['readConcern'];
1018+
if (!this.operation || !this.operation.hasAspect(Aspect.EXPLAINABLE)) {
1019+
throw new MongoError('This command cannot be explained');
10201020
}
1021+
this.operation.explain = new Explain(verbosity);
1022+
10211023
return maybePromise(this, callback, cb => {
10221024
CoreCursor.prototype._next.apply(this, [cb]);
10231025
});

lib/db.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ Db.prototype.command = function(command, options, callback) {
312312
* @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}.
313313
* @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.
314314
* @param {number} [options.cursor.batchSize=1000] Deprecated. Use `options.batchSize`
315-
* @param {boolean} [options.explain=false] Explain returns the aggregation execution plan (requires mongodb 2.6 >).
315+
* @param {string|boolean} [options.explain] The verbosity mode for the explain output.
316316
* @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 >).
317317
* @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.
318318
* @param {number} [options.maxAwaitTimeMS] The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query.

lib/explain.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
3+
/**
4+
* @class
5+
* @property {string} verbosity The verbosity mode for the explain output
6+
*/
7+
class Explain {
8+
/**
9+
* Constructs an Explain from the explain verbosity.
10+
*
11+
* For backwards compatibility, true is interpreted as "allPlansExecution"
12+
* and false as "queryPlanner". Prior to server version 3.6, aggregate()
13+
* ignores the verbosity parameter and executes in "queryPlanner".
14+
*
15+
* @param {string|boolean} [verbosity] The verbosity mode for the explain output ({'queryPlanner'|'queryPlannerExtended'|'executionStats'|'allPlansExecution'|boolean})
16+
*/
17+
constructor(verbosity) {
18+
if (typeof verbosity === 'boolean') {
19+
this.verbosity = verbosity ? 'allPlansExecution' : 'queryPlanner';
20+
} else {
21+
this.verbosity = verbosity;
22+
}
23+
}
24+
25+
/**
26+
* Construct an Explain given an options object.
27+
*
28+
* @param {object} options The options object from which to extract the explain.
29+
* @return {Explain}
30+
*/
31+
static fromOptions(options) {
32+
if (options == null || options.explain === undefined) {
33+
return;
34+
}
35+
36+
return new Explain(options.explain);
37+
}
38+
}
39+
40+
module.exports = Explain;

lib/operations/aggregate.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,8 @@ class AggregateOperation extends CommandOperationV2 {
3737
this.readPreference = ReadPreference.primary;
3838
}
3939

40-
if (options.explain && (this.readConcern || this.writeConcern)) {
41-
throw new MongoError(
42-
'"explain" cannot be used on an aggregate call with readConcern/writeConcern'
43-
);
40+
if (this.explain && this.writeConcern) {
41+
throw new MongoError('"explain" cannot be used on an aggregate call with writeConcern');
4442
}
4543

4644
if (options.cursor != null && typeof options.cursor !== 'object') {
@@ -83,9 +81,8 @@ class AggregateOperation extends CommandOperationV2 {
8381
command.hint = options.hint;
8482
}
8583

86-
if (options.explain) {
84+
if (this.explain) {
8785
options.full = false;
88-
command.explain = options.explain;
8986
}
9087

9188
command.cursor = options.cursor || {};
@@ -100,7 +97,8 @@ class AggregateOperation extends CommandOperationV2 {
10097
defineAspects(AggregateOperation, [
10198
Aspect.READ_OPERATION,
10299
Aspect.RETRYABLE,
103-
Aspect.EXECUTE_WITH_SELECTION
100+
Aspect.EXECUTE_WITH_SELECTION,
101+
Aspect.EXPLAINABLE
104102
]);
105103

106104
module.exports = AggregateOperation;

lib/operations/command_v2.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const ReadPreference = require('../core').ReadPreference;
66
const ReadConcern = require('../read_concern');
77
const WriteConcern = require('../write_concern');
88
const maxWireVersion = require('../core/utils').maxWireVersion;
9+
const decorateWithExplain = require('../utils').decorateWithExplain;
910
const commandSupportsReadConcern = require('../core/sessions').commandSupportsReadConcern;
1011
const MongoError = require('../core/error').MongoError;
1112

@@ -22,7 +23,6 @@ class CommandOperationV2 extends OperationBase {
2223
: ReadPreference.resolve(propertyProvider, this.options);
2324
this.readConcern = resolveReadConcern(propertyProvider, this.options);
2425
this.writeConcern = resolveWriteConcern(propertyProvider, this.options);
25-
this.explain = false;
2626

2727
if (operationOptions && typeof operationOptions.fullResponse === 'boolean') {
2828
this.fullResponse = true;
@@ -79,6 +79,15 @@ class CommandOperationV2 extends OperationBase {
7979
cmd.comment = options.comment;
8080
}
8181

82+
if (this.hasAspect(Aspect.EXPLAINABLE) && this.explain) {
83+
if (serverWireVersion < 6 && cmd.aggregate) {
84+
// Prior to 3.6, with aggregate, verbosity is ignored, and we must pass in "explain: true"
85+
cmd.explain = true;
86+
} else {
87+
cmd = decorateWithExplain(cmd, this.explain);
88+
}
89+
}
90+
8291
if (this.logger && this.logger.isDebug()) {
8392
this.logger.debug(`executing command ${JSON.stringify(cmd)} against ${this.ns}`);
8493
}

lib/operations/common_functions.js

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const MongoError = require('../core').MongoError;
1111
const ReadPreference = require('../core').ReadPreference;
1212
const toError = require('../utils').toError;
1313
const CursorState = require('../core/cursor').CursorState;
14+
const maxWireVersion = require('../core/utils').maxWireVersion;
1415

1516
/**
1617
* Build the count command.
@@ -57,14 +58,6 @@ function buildCountCommand(collectionOrCursor, query, options) {
5758
return cmd;
5859
}
5960

60-
function deleteCallback(err, r, callback) {
61-
if (callback == null) return;
62-
if (err && callback) return callback(err);
63-
if (r == null) return callback(null, { result: { ok: 1 } });
64-
r.deletedCount = r.result.n;
65-
if (callback) callback(null, r);
66-
}
67-
6861
/**
6962
* Find and update a document.
7063
*
@@ -308,6 +301,12 @@ function removeDocuments(coll, selector, options, callback) {
308301
return callback(err, null);
309302
}
310303

304+
if (options.explain !== undefined && maxWireVersion(coll.s.topology) < 3) {
305+
return callback
306+
? callback(new MongoError(`server does not support explain on remove`))
307+
: undefined;
308+
}
309+
311310
// Execute the remove
312311
coll.s.topology.remove(coll.s.namespace, [op], finalOptions, (err, result) => {
313312
if (callback == null) return;
@@ -369,6 +368,12 @@ function updateDocuments(coll, selector, document, options, callback) {
369368
return callback(err, null);
370369
}
371370

371+
if (options.explain !== undefined && maxWireVersion(coll.s.topology) < 3) {
372+
return callback
373+
? callback(new MongoError(`server does not support explain on update`))
374+
: undefined;
375+
}
376+
372377
// Update options
373378
coll.s.topology.update(coll.s.namespace, [op], finalOptions, (err, result) => {
374379
if (callback == null) return;
@@ -382,31 +387,13 @@ function updateDocuments(coll, selector, document, options, callback) {
382387
});
383388
}
384389

385-
function updateCallback(err, r, callback) {
386-
if (callback == null) return;
387-
if (err) return callback(err);
388-
if (r == null) return callback(null, { result: { ok: 1 } });
389-
r.modifiedCount = r.result.nModified != null ? r.result.nModified : r.result.n;
390-
r.upsertedId =
391-
Array.isArray(r.result.upserted) && r.result.upserted.length > 0
392-
? r.result.upserted[0] // FIXME(major): should be `r.result.upserted[0]._id`
393-
: null;
394-
r.upsertedCount =
395-
Array.isArray(r.result.upserted) && r.result.upserted.length ? r.result.upserted.length : 0;
396-
r.matchedCount =
397-
Array.isArray(r.result.upserted) && r.result.upserted.length > 0 ? 0 : r.result.n;
398-
callback(null, r);
399-
}
400-
401390
module.exports = {
402391
buildCountCommand,
403-
deleteCallback,
404392
findAndModify,
405393
indexInformation,
406394
nextObject,
407395
prepareDocs,
408396
insertDocuments,
409397
removeDocuments,
410-
updateDocuments,
411-
updateCallback
398+
updateDocuments
412399
};

0 commit comments

Comments
 (0)