1 //
  2 // A DBQuery which is explained rather than executed normally. Also could be thought of as
  3 // an "explainable cursor". Explains of .find() operations run through this abstraction.
  4 //
  5 
  6 var DBExplainQuery = (function() {
  7 
  8     //
  9     // Private methods.
 10     //
 11 
 12     /**
 13      * In 2.6 and before, .explain(), .explain(false), or .explain(<falsy value>) instructed the
 14      * shell to reduce the explain verbosity by removing certain fields from the output. This
 15      * is implemented here for backwards compatibility.
 16      */
 17     function removeVerboseFields(obj) {
 18         if (typeof(obj) !== "object") {
 19             return;
 20         }
 21 
 22         delete obj.allPlans;
 23         delete obj.oldPlan;
 24         delete obj.stats;
 25 
 26         if (typeof(obj.length) === "number") {
 27             for (var i = 0; i < obj.length; i++) {
 28                 removeVerboseFields(obj[i]);
 29             }
 30         }
 31 
 32         if (obj.shards) {
 33             for (var key in obj.shards) {
 34                 removeVerboseFields(obj.shards[key]);
 35             }
 36         }
 37 
 38         if (obj.clauses) {
 39             removeVerboseFields(obj.clauses);
 40         }
 41     }
 42 
 43     /**
 44      * Many of the methods of an explain query just pass through to the underlying
 45      * non-explained DBQuery. Use this to generate a function which calls function 'name' on
 46      * 'destObj' and then returns this.
 47      */
 48     function createDelegationFunc(explainQuery, dbQuery, name) {
 49         return function() {
 50             dbQuery[name].apply(dbQuery, arguments);
 51             return explainQuery;
 52         };
 53     }
 54 
 55     /**
 56      * Where possible, the explain query will be sent to the server as an explain command.
 57      * However, if one of the nodes we are talking to (either a standalone or a shard in
 58      * a sharded cluster) is of a version that doesn't have the explain command, we will
 59      * use this function to fall back on the $explain query option.
 60      */
 61     function explainWithLegacyQueryOption(explainQuery) {
 62         // The wire protocol version indicates that the server does not have the explain
 63         // command. Add $explain to the query and send it to the server.
 64         var clone = explainQuery._query.clone();
 65         clone._addSpecial("$explain", true);
 66         var result = clone.next();
 67 
 68         // Remove some fields from the explain if verbosity is
 69         // just "queryPlanner".
 70         if ("queryPlanner" === explainQuery._verbosity) {
 71             removeVerboseFields(result);
 72         }
 73 
 74         return Explainable.throwOrReturn(result);
 75     }
 76 
 77     function constructor(query, verbosity) {
 78         //
 79         // Private vars.
 80         //
 81 
 82         this._query = query;
 83         this._verbosity = Explainable.parseVerbosity(verbosity);
 84         this._mongo = query._mongo;
 85         this._finished = false;
 86 
 87         // Used if this query is a count, not a find.
 88         this._isCount = false;
 89         this._applySkipLimit = false;
 90 
 91         //
 92         // Public delegation methods. These just pass through to the underlying
 93         // DBQuery.
 94         //
 95 
 96         var delegationFuncNames = [
 97             "addOption",
 98             "batchSize",
 99             "collation",
100             "comment",
101             "hint",
102             "limit",
103             "max",
104             "maxTimeMS",
105             "min",
106             "readPref",
107             "showDiskLoc",
108             "skip",
109             "snapshot",
110             "sort",
111         ];
112 
113         // Generate the delegation methods from the list of their names.
114         var that = this;
115         delegationFuncNames.forEach(function(name) {
116             that[name] = createDelegationFunc(that, that._query, name);
117         });
118 
119         //
120         // Core public methods.
121         //
122 
123         /**
124          * Indicates that we are done building the query to explain, and sends the explain
125          * command or query to the server.
126          *
127          * Returns the result of running the explain.
128          */
129         this.finish = function() {
130             if (this._finished) {
131                 throw Error("query has already been explained");
132             }
133 
134             // Mark this query as finished. Shouldn't be used for another explain.
135             this._finished = true;
136 
137             // Explain always gets pretty printed.
138             this._query._prettyShell = true;
139 
140             if (this._mongo.hasExplainCommand()) {
141                 // The wire protocol version indicates that the server has the explain command.
142                 // Convert this explain query into an explain command, and send the command to
143                 // the server.
144                 var innerCmd;
145                 if (this._isCount) {
146                     // True means to always apply the skip and limit values.
147                     innerCmd = this._query._convertToCountCmd(this._applySkipLimit);
148                 } else {
149                     var canAttachReadPref = false;
150                     innerCmd = this._query._convertToCommand(canAttachReadPref);
151                 }
152 
153                 var explainCmd = {explain: innerCmd};
154                 explainCmd["verbosity"] = this._verbosity;
155 
156                 var explainDb = this._query._db;
157 
158                 if ("$readPreference" in this._query._query) {
159                     var prefObj = this._query._query.$readPreference;
160                     explainCmd = explainDb._attachReadPreferenceToCommand(explainCmd, prefObj);
161                 }
162 
163                 var explainResult =
164                     explainDb.runReadCommand(explainCmd, null, this._query._options);
165 
166                 if (!explainResult.ok && explainResult.code === ErrorCodes.CommandNotFound) {
167                     // One of the shards doesn't have the explain command available. Retry using
168                     // the legacy $explain format, which should be supported by all shards.
169                     return explainWithLegacyQueryOption(this);
170                 }
171 
172                 return Explainable.throwOrReturn(explainResult);
173             } else {
174                 return explainWithLegacyQueryOption(this);
175             }
176         };
177 
178         this.next = function() {
179             return this.finish();
180         };
181 
182         this.hasNext = function() {
183             return !this._finished;
184         };
185 
186         this.forEach = function(func) {
187             while (this.hasNext()) {
188                 func(this.next());
189             }
190         };
191 
192         /**
193          * Returns the explain resulting from running this query as a count operation.
194          *
195          * If 'applySkipLimit' is true, then the skip and limit values set on this query values are
196          * passed to the server; otherwise they are ignored.
197          */
198         this.count = function(applySkipLimit) {
199             this._isCount = true;
200             if (applySkipLimit) {
201                 this._applySkipLimit = true;
202             }
203             return this.finish();
204         };
205 
206         /**
207          * This gets called automatically by the shell in interactive mode. It should
208          * print the result of running the explain.
209          */
210         this.shellPrint = function() {
211             var result = this.finish();
212             return tojson(result);
213         };
214 
215         /**
216          * Display help text.
217          */
218         this.help = function() {
219             print("Explain query methods");
220             print("\t.finish() - sends explain command to the server and returns the result");
221             print("\t.forEach(func) - apply a function to the explain results");
222             print("\t.hasNext() - whether this explain query still has a result to retrieve");
223             print("\t.next() - alias for .finish()");
224             print("Explain query modifiers");
225             print("\t.addOption(n)");
226             print("\t.batchSize(n)");
227             print("\t.comment(comment)");
228             print("\t.collation(collationSpec)");
229             print("\t.count()");
230             print("\t.hint(hintSpec)");
231             print("\t.limit(n)");
232             print("\t.maxTimeMS(n)");
233             print("\t.max(idxDoc)");
234             print("\t.min(idxDoc)");
235             print("\t.readPref(mode, tagSet)");
236             print("\t.showDiskLoc()");
237             print("\t.skip(n)");
238             print("\t.snapshot()");
239             print("\t.sort(sortSpec)");
240             return __magicNoPrint;
241         };
242     }
243 
244     return constructor;
245 })();
246