1 // @file collection.js - DBCollection support in the mongo shell
  2 // db.colName is a DBCollection object
  3 // or db["colName"]
  4 
  5 if ((typeof DBCollection) == "undefined") {
  6     DBCollection = function(mongo, db, shortName, fullName) {
  7         this._mongo = mongo;
  8         this._db = db;
  9         this._shortName = shortName;
 10         this._fullName = fullName;
 11 
 12         this.verify();
 13     };
 14 }
 15 
 16 DBCollection.prototype.verify = function() {
 17     assert(this._fullName, "no fullName");
 18     assert(this._shortName, "no shortName");
 19     assert(this._db, "no db");
 20 
 21     assert.eq(this._fullName, this._db._name + "." + this._shortName, "name mismatch");
 22 
 23     assert(this._mongo, "no mongo in DBCollection");
 24     assert(this.getMongo(), "no mongo from getMongo()");
 25 };
 26 
 27 DBCollection.prototype.getName = function() {
 28     return this._shortName;
 29 };
 30 
 31 DBCollection.prototype.help = function() {
 32     var shortName = this.getName();
 33     print("DBCollection help");
 34     print("\tdb." + shortName + ".find().help() - show DBCursor help");
 35     print(
 36         "\tdb." + shortName +
 37         ".bulkWrite( operations, <optional params> ) - bulk execute write operations, optional parameters are: w, wtimeout, j");
 38     print(
 39         "\tdb." + shortName +
 40         ".count( query = {}, <optional params> ) - count the number of documents that matches the query, optional parameters are: limit, skip, hint, maxTimeMS");
 41     print(
 42         "\tdb." + shortName +
 43         ".copyTo(newColl) - duplicates collection by copying all documents to newColl; no indexes are copied.");
 44     print("\tdb." + shortName + ".convertToCapped(maxBytes) - calls {convertToCapped:'" +
 45           shortName + "', size:maxBytes}} command");
 46     print("\tdb." + shortName + ".createIndex(keypattern[,options])");
 47     print("\tdb." + shortName + ".createIndexes([keypatterns], <options>)");
 48     print("\tdb." + shortName + ".dataSize()");
 49     print(
 50         "\tdb." + shortName +
 51         ".deleteOne( filter, <optional params> ) - delete first matching document, optional parameters are: w, wtimeout, j");
 52     print(
 53         "\tdb." + shortName +
 54         ".deleteMany( filter, <optional params> ) - delete all matching documents, optional parameters are: w, wtimeout, j");
 55     print("\tdb." + shortName + ".distinct( key, query, <optional params> ) - e.g. db." +
 56           shortName + ".distinct( 'x' ), optional parameters are: maxTimeMS");
 57     print("\tdb." + shortName + ".drop() drop the collection");
 58     print("\tdb." + shortName + ".dropIndex(index) - e.g. db." + shortName +
 59           ".dropIndex( \"indexName\" ) or db." + shortName + ".dropIndex( { \"indexKey\" : 1 } )");
 60     print("\tdb." + shortName + ".dropIndexes()");
 61     print("\tdb." + shortName +
 62           ".ensureIndex(keypattern[,options]) - DEPRECATED, use createIndex() instead");
 63     print("\tdb." + shortName + ".explain().help() - show explain help");
 64     print("\tdb." + shortName + ".reIndex()");
 65     print(
 66         "\tdb." + shortName +
 67         ".find([query],[fields]) - query is an optional query filter. fields is optional set of fields to return.");
 68     print("\t                                              e.g. db." + shortName +
 69           ".find( {x:77} , {name:1, x:1} )");
 70     print("\tdb." + shortName + ".find(...).count()");
 71     print("\tdb." + shortName + ".find(...).limit(n)");
 72     print("\tdb." + shortName + ".find(...).skip(n)");
 73     print("\tdb." + shortName + ".find(...).sort(...)");
 74     print("\tdb." + shortName + ".findOne([query], [fields], [options], [readConcern])");
 75     print(
 76         "\tdb." + shortName +
 77         ".findOneAndDelete( filter, <optional params> ) - delete first matching document, optional parameters are: projection, sort, maxTimeMS");
 78     print(
 79         "\tdb." + shortName +
 80         ".findOneAndReplace( filter, replacement, <optional params> ) - replace first matching document, optional parameters are: projection, sort, maxTimeMS, upsert, returnNewDocument");
 81     print(
 82         "\tdb." + shortName +
 83         ".findOneAndUpdate( filter, update, <optional params> ) - update first matching document, optional parameters are: projection, sort, maxTimeMS, upsert, returnNewDocument");
 84     print("\tdb." + shortName + ".getDB() get DB object associated with collection");
 85     print("\tdb." + shortName + ".getPlanCache() get query plan cache associated with collection");
 86     print("\tdb." + shortName + ".getIndexes()");
 87     print("\tdb." + shortName + ".group( { key : ..., initial: ..., reduce : ...[, cond: ...] } )");
 88     print("\tdb." + shortName + ".insert(obj)");
 89     print(
 90         "\tdb." + shortName +
 91         ".insertOne( obj, <optional params> ) - insert a document, optional parameters are: w, wtimeout, j");
 92     print(
 93         "\tdb." + shortName +
 94         ".insertMany( [objects], <optional params> ) - insert multiple documents, optional parameters are: w, wtimeout, j");
 95     print("\tdb." + shortName + ".mapReduce( mapFunction , reduceFunction , <optional params> )");
 96     print(
 97         "\tdb." + shortName +
 98         ".aggregate( [pipeline], <optional params> ) - performs an aggregation on a collection; returns a cursor");
 99     print("\tdb." + shortName + ".remove(query)");
100     print(
101         "\tdb." + shortName +
102         ".replaceOne( filter, replacement, <optional params> ) - replace the first matching document, optional parameters are: upsert, w, wtimeout, j");
103     print("\tdb." + shortName +
104           ".renameCollection( newName , <dropTarget> ) renames the collection.");
105     print(
106         "\tdb." + shortName +
107         ".runCommand( name , <options> ) runs a db command with the given name where the first param is the collection name");
108     print("\tdb." + shortName + ".save(obj)");
109     print("\tdb." + shortName + ".stats({scale: N, indexDetails: true/false, " +
110           "indexDetailsKey: <index key>, indexDetailsName: <index name>})");
111     // print("\tdb." + shortName + ".diskStorageStats({[extent: <num>,] [granularity: <bytes>,]
112     // ...}) - analyze record layout on disk");
113     // print("\tdb." + shortName + ".pagesInRAM({[extent: <num>,] [granularity: <bytes>,] ...}) -
114     // analyze resident memory pages");
115     print("\tdb." + shortName +
116           ".storageSize() - includes free space allocated to this collection");
117     print("\tdb." + shortName + ".totalIndexSize() - size in bytes of all the indexes");
118     print("\tdb." + shortName + ".totalSize() - storage allocated for all data and indexes");
119     print(
120         "\tdb." + shortName +
121         ".update( query, object[, upsert_bool, multi_bool] ) - instead of two flags, you can pass an object with fields: upsert, multi");
122     print(
123         "\tdb." + shortName +
124         ".updateOne( filter, update, <optional params> ) - update the first matching document, optional parameters are: upsert, w, wtimeout, j");
125     print(
126         "\tdb." + shortName +
127         ".updateMany( filter, update, <optional params> ) - update all matching documents, optional parameters are: upsert, w, wtimeout, j");
128     print("\tdb." + shortName + ".validate( <full> ) - SLOW");
129     print("\tdb." + shortName + ".getShardVersion() - only for use with sharding");
130     print("\tdb." + shortName +
131           ".getShardDistribution() - prints statistics about data distribution in the cluster");
132     print(
133         "\tdb." + shortName +
134         ".getSplitKeysForChunks( <maxChunkSize> ) - calculates split points over all chunks and returns splitter function");
135     print(
136         "\tdb." + shortName +
137         ".getWriteConcern() - returns the write concern used for any operations on this collection, inherited from server/db if set");
138     print(
139         "\tdb." + shortName +
140         ".setWriteConcern( <write concern doc> ) - sets the write concern for writes to the collection");
141     print(
142         "\tdb." + shortName +
143         ".unsetWriteConcern( <write concern doc> ) - unsets the write concern for writes to the collection");
144     print("\tdb." + shortName +
145           ".latencyStats() - display operation latency histograms for this collection");
146     // print("\tdb." + shortName + ".getDiskStorageStats({...}) - prints a summary of disk usage
147     // statistics");
148     // print("\tdb." + shortName + ".getPagesInRAM({...}) - prints a summary of storage pages
149     // currently in physical memory");
150     return __magicNoPrint;
151 };
152 
153 DBCollection.prototype.getFullName = function() {
154     return this._fullName;
155 };
156 DBCollection.prototype.getMongo = function() {
157     return this._db.getMongo();
158 };
159 DBCollection.prototype.getDB = function() {
160     return this._db;
161 };
162 
163 DBCollection.prototype._makeCommand = function(cmd, params) {
164     var c = {};
165     c[cmd] = this.getName();
166     if (params)
167         Object.extend(c, params);
168     return c;
169 };
170 
171 DBCollection.prototype._dbCommand = function(cmd, params) {
172     if (typeof(cmd) === "object")
173         return this._db._dbCommand(cmd, {}, this.getQueryOptions());
174 
175     return this._db._dbCommand(this._makeCommand(cmd, params), {}, this.getQueryOptions());
176 };
177 
178 // Like _dbCommand, but applies $readPreference
179 DBCollection.prototype._dbReadCommand = function(cmd, params) {
180     if (typeof(cmd) === "object")
181         return this._db._dbReadCommand(cmd, {}, this.getQueryOptions());
182 
183     return this._db._dbReadCommand(this._makeCommand(cmd, params), {}, this.getQueryOptions());
184 };
185 
186 DBCollection.prototype.runCommand = DBCollection.prototype._dbCommand;
187 
188 DBCollection.prototype.runReadCommand = DBCollection.prototype._dbReadCommand;
189 
190 DBCollection.prototype._massageObject = function(q) {
191     if (!q)
192         return {};
193 
194     var type = typeof q;
195 
196     if (type == "function")
197         return {$where: q};
198 
199     if (q.isObjectId)
200         return {_id: q};
201 
202     if (type == "object")
203         return q;
204 
205     if (type == "string") {
206         // If the string is 24 hex characters, it is most likely an ObjectId.
207         if (/^[0-9a-fA-F]{24}$/.test(q)) {
208             return {_id: ObjectId(q)};
209         }
210 
211         return {$where: q};
212     }
213 
214     throw Error("don't know how to massage : " + type);
215 
216 };
217 
218 DBCollection.prototype._validateObject = function(o) {
219     // Hidden property for testing purposes.
220     if (this.getMongo()._skipValidation)
221         return;
222 
223     if (typeof(o) != "object")
224         throw Error("attempted to save a " + typeof(o) + " value.  document expected.");
225 
226     if (o._ensureSpecial && o._checkModify)
227         throw Error("can't save a DBQuery object");
228 };
229 
230 DBCollection._allowedFields = {
231     $id: 1,
232     $ref: 1,
233     $db: 1
234 };
235 
236 DBCollection.prototype._validateForStorage = function(o) {
237     // Hidden property for testing purposes.
238     if (this.getMongo()._skipValidation)
239         return;
240 
241     this._validateObject(o);
242     for (var k in o) {
243         if (k.indexOf(".") >= 0) {
244             throw Error("can't have . in field names [" + k + "]");
245         }
246 
247         if (k.indexOf("$") == 0 && !DBCollection._allowedFields[k]) {
248             throw Error("field names cannot start with $ [" + k + "]");
249         }
250 
251         if (o[k] !== null && typeof(o[k]) === "object") {
252             this._validateForStorage(o[k]);
253         }
254     }
255 };
256 
257 DBCollection.prototype.find = function(query, fields, limit, skip, batchSize, options) {
258     var cursor = new DBQuery(this._mongo,
259                              this._db,
260                              this,
261                              this._fullName,
262                              this._massageObject(query),
263                              fields,
264                              limit,
265                              skip,
266                              batchSize,
267                              options || this.getQueryOptions());
268 
269     var connObj = this.getMongo();
270     var readPrefMode = connObj.getReadPrefMode();
271     if (readPrefMode != null) {
272         cursor.readPref(readPrefMode, connObj.getReadPrefTagSet());
273     }
274 
275     return cursor;
276 };
277 
278 DBCollection.prototype.findOne = function(query, fields, options, readConcern, collation) {
279     var cursor = this.find(query, fields, -1 /* limit */, 0 /* skip*/, 0 /* batchSize */, options);
280 
281     if (readConcern) {
282         cursor = cursor.readConcern(readConcern);
283     }
284 
285     if (collation) {
286         cursor = cursor.collation(collation);
287     }
288 
289     if (!cursor.hasNext())
290         return null;
291     var ret = cursor.next();
292     if (cursor.hasNext())
293         throw Error("findOne has more than 1 result!");
294     if (ret.$err)
295         throw _getErrorWithCode(ret, "error " + tojson(ret));
296     return ret;
297 };
298 
299 DBCollection.prototype.insert = function(obj, options, _allow_dot) {
300     if (!obj)
301         throw Error("no object passed to insert!");
302 
303     var flags = 0;
304 
305     var wc = undefined;
306     var allowDottedFields = false;
307     if (options === undefined) {
308         // do nothing
309     } else if (typeof(options) == 'object') {
310         if (options.ordered === undefined) {
311             // do nothing, like above
312         } else {
313             flags = options.ordered ? 0 : 1;
314         }
315 
316         if (options.writeConcern)
317             wc = options.writeConcern;
318         if (options.allowdotted)
319             allowDottedFields = true;
320     } else {
321         flags = options;
322     }
323 
324     // 1 = continueOnError, which is synonymous with unordered in the write commands/bulk-api
325     var ordered = ((flags & 1) == 0);
326 
327     if (!wc)
328         wc = this.getWriteConcern();
329 
330     var result = undefined;
331     var startTime =
332         (typeof(_verboseShell) === 'undefined' || !_verboseShell) ? 0 : new Date().getTime();
333 
334     if (this.getMongo().writeMode() != "legacy") {
335         // Bit 1 of option flag is continueOnError. Bit 0 (stop on error) is the default.
336         var bulk = ordered ? this.initializeOrderedBulkOp() : this.initializeUnorderedBulkOp();
337         var isMultiInsert = Array.isArray(obj);
338 
339         if (isMultiInsert) {
340             obj.forEach(function(doc) {
341                 bulk.insert(doc);
342             });
343         } else {
344             bulk.insert(obj);
345         }
346 
347         try {
348             result = bulk.execute(wc);
349             if (!isMultiInsert)
350                 result = result.toSingleResult();
351         } catch (ex) {
352             if (ex instanceof BulkWriteError) {
353                 result = isMultiInsert ? ex.toResult() : ex.toSingleResult();
354             } else if (ex instanceof WriteCommandError) {
355                 result = isMultiInsert ? ex : ex.toSingleResult();
356             } else {
357                 // Other exceptions thrown
358                 throw Error(ex);
359             }
360         }
361     } else {
362         if (!_allow_dot) {
363             this._validateForStorage(obj);
364         }
365 
366         if (typeof(obj._id) == "undefined" && !Array.isArray(obj)) {
367             var tmp = obj;  // don't want to modify input
368             obj = {_id: new ObjectId()};
369             for (var key in tmp) {
370                 obj[key] = tmp[key];
371             }
372         }
373 
374         this.getMongo().insert(this._fullName, obj, flags);
375 
376         // enforce write concern, if required
377         if (wc)
378             result = this.runCommand("getLastError", wc instanceof WriteConcern ? wc.toJSON() : wc);
379     }
380 
381     this._lastID = obj._id;
382     this._printExtraInfo("Inserted", startTime);
383     return result;
384 };
385 
386 DBCollection.prototype._validateRemoveDoc = function(doc) {
387     // Hidden property for testing purposes.
388     if (this.getMongo()._skipValidation)
389         return;
390 
391     for (var k in doc) {
392         if (k == "_id" && typeof(doc[k]) == "undefined") {
393             throw new Error("can't have _id set to undefined in a remove expression");
394         }
395     }
396 };
397 
398 /**
399  * Does validation of the remove args. Throws if the parse is not successful, otherwise
400  * returns a document {query: <query>, justOne: <limit>, wc: <writeConcern>}.
401  */
402 DBCollection.prototype._parseRemove = function(t, justOne) {
403     if (undefined === t)
404         throw Error("remove needs a query");
405 
406     var query = this._massageObject(t);
407 
408     var wc = undefined;
409     var collation = undefined;
410     if (typeof(justOne) === "object") {
411         var opts = justOne;
412         wc = opts.writeConcern;
413         justOne = opts.justOne;
414         collation = opts.collation;
415     }
416 
417     // Normalize "justOne" to a bool.
418     justOne = justOne ? true : false;
419 
420     // Handle write concern.
421     if (!wc) {
422         wc = this.getWriteConcern();
423     }
424 
425     return {"query": query, "justOne": justOne, "wc": wc, "collation": collation};
426 };
427 
428 DBCollection.prototype.remove = function(t, justOne) {
429     var parsed = this._parseRemove(t, justOne);
430     var query = parsed.query;
431     var justOne = parsed.justOne;
432     var wc = parsed.wc;
433     var collation = parsed.collation;
434 
435     var result = undefined;
436     var startTime =
437         (typeof(_verboseShell) === 'undefined' || !_verboseShell) ? 0 : new Date().getTime();
438 
439     if (this.getMongo().writeMode() != "legacy") {
440         var bulk = this.initializeOrderedBulkOp();
441         var removeOp = bulk.find(query);
442 
443         if (collation) {
444             removeOp.collation(collation);
445         }
446 
447         if (justOne) {
448             removeOp.removeOne();
449         } else {
450             removeOp.remove();
451         }
452 
453         try {
454             result = bulk.execute(wc).toSingleResult();
455         } catch (ex) {
456             if (ex instanceof BulkWriteError || ex instanceof WriteCommandError) {
457                 result = ex.toSingleResult();
458             } else {
459                 // Other exceptions thrown
460                 throw Error(ex);
461             }
462         }
463     } else {
464         if (collation) {
465             throw new Error("collation requires use of write commands");
466         }
467 
468         this._validateRemoveDoc(t);
469         this.getMongo().remove(this._fullName, query, justOne);
470 
471         // enforce write concern, if required
472         if (wc)
473             result = this.runCommand("getLastError", wc instanceof WriteConcern ? wc.toJSON() : wc);
474     }
475 
476     this._printExtraInfo("Removed", startTime);
477     return result;
478 };
479 
480 DBCollection.prototype._validateUpdateDoc = function(doc) {
481     // Hidden property for testing purposes.
482     if (this.getMongo()._skipValidation)
483         return;
484 
485     var firstKey = null;
486     for (var key in doc) {
487         firstKey = key;
488         break;
489     }
490 
491     if (firstKey != null && firstKey[0] == '$') {
492         // for mods we only validate partially, for example keys may have dots
493         this._validateObject(doc);
494     } else {
495         // we're basically inserting a brand new object, do full validation
496         this._validateForStorage(doc);
497     }
498 };
499 
500 /**
501  * Does validation of the update args. Throws if the parse is not successful, otherwise
502  * returns a document containing fields for query, obj, upsert, multi, and wc.
503  *
504  * Throws if the arguments are invalid.
505  */
506 DBCollection.prototype._parseUpdate = function(query, obj, upsert, multi) {
507     if (!query)
508         throw Error("need a query");
509     if (!obj)
510         throw Error("need an object");
511 
512     var wc = undefined;
513     var collation = undefined;
514     // can pass options via object for improved readability
515     if (typeof(upsert) === "object") {
516         if (multi) {
517             throw Error("Fourth argument must be empty when specifying " +
518                         "upsert and multi with an object.");
519         }
520 
521         var opts = upsert;
522         multi = opts.multi;
523         wc = opts.writeConcern;
524         upsert = opts.upsert;
525         collation = opts.collation;
526     }
527 
528     // Normalize 'upsert' and 'multi' to booleans.
529     upsert = upsert ? true : false;
530     multi = multi ? true : false;
531 
532     if (!wc) {
533         wc = this.getWriteConcern();
534     }
535 
536     return {
537         "query": query,
538         "obj": obj,
539         "upsert": upsert,
540         "multi": multi,
541         "wc": wc,
542         "collation": collation
543     };
544 };
545 
546 DBCollection.prototype.update = function(query, obj, upsert, multi) {
547     var parsed = this._parseUpdate(query, obj, upsert, multi);
548     var query = parsed.query;
549     var obj = parsed.obj;
550     var upsert = parsed.upsert;
551     var multi = parsed.multi;
552     var wc = parsed.wc;
553     var collation = parsed.collation;
554 
555     var result = undefined;
556     var startTime =
557         (typeof(_verboseShell) === 'undefined' || !_verboseShell) ? 0 : new Date().getTime();
558 
559     if (this.getMongo().writeMode() != "legacy") {
560         var bulk = this.initializeOrderedBulkOp();
561         var updateOp = bulk.find(query);
562 
563         if (upsert) {
564             updateOp = updateOp.upsert();
565         }
566 
567         if (collation) {
568             updateOp.collation(collation);
569         }
570 
571         if (multi) {
572             updateOp.update(obj);
573         } else {
574             updateOp.updateOne(obj);
575         }
576 
577         try {
578             result = bulk.execute(wc).toSingleResult();
579         } catch (ex) {
580             if (ex instanceof BulkWriteError || ex instanceof WriteCommandError) {
581                 result = ex.toSingleResult();
582             } else {
583                 // Other exceptions thrown
584                 throw Error(ex);
585             }
586         }
587     } else {
588         if (collation) {
589             throw new Error("collation requires use of write commands");
590         }
591 
592         this._validateUpdateDoc(obj);
593         this.getMongo().update(this._fullName, query, obj, upsert, multi);
594 
595         // Enforce write concern, if required
596         if (wc) {
597             result = this.runCommand("getLastError", wc instanceof WriteConcern ? wc.toJSON() : wc);
598         }
599     }
600 
601     this._printExtraInfo("Updated", startTime);
602     return result;
603 };
604 
605 DBCollection.prototype.save = function(obj, opts) {
606     if (obj == null)
607         throw Error("can't save a null");
608 
609     if (typeof(obj) == "number" || typeof(obj) == "string")
610         throw Error("can't save a number or string");
611 
612     if (typeof(obj._id) == "undefined") {
613         obj._id = new ObjectId();
614         return this.insert(obj, opts);
615     } else {
616         return this.update({_id: obj._id}, obj, Object.merge({upsert: true}, opts));
617     }
618 };
619 
620 DBCollection.prototype._genIndexName = function(keys) {
621     var name = "";
622     for (var k in keys) {
623         var v = keys[k];
624         if (typeof v == "function")
625             continue;
626 
627         if (name.length > 0)
628             name += "_";
629         name += k + "_";
630 
631         name += v;
632     }
633     return name;
634 };
635 
636 DBCollection.prototype._indexSpec = function(keys, options) {
637     var ret = {ns: this._fullName, key: keys, name: this._genIndexName(keys)};
638 
639     if (!options) {
640     } else if (typeof(options) == "string")
641         ret.name = options;
642     else if (typeof(options) == "boolean")
643         ret.unique = true;
644     else if (typeof(options) == "object") {
645         if (Array.isArray(options)) {
646             if (options.length > 3) {
647                 throw new Error("Index options that are supplied in array form may only specify" +
648                                 " three values: name, unique, dropDups");
649             }
650             var nb = 0;
651             for (var i = 0; i < options.length; i++) {
652                 if (typeof(options[i]) == "string")
653                     ret.name = options[i];
654                 else if (typeof(options[i]) == "boolean") {
655                     if (options[i]) {
656                         if (nb == 0)
657                             ret.unique = true;
658                         if (nb == 1)
659                             ret.dropDups = true;
660                     }
661                     nb++;
662                 }
663             }
664         } else {
665             Object.extend(ret, options);
666         }
667     } else {
668         throw Error("can't handle: " + typeof(options));
669     }
670 
671     return ret;
672 };
673 
674 DBCollection.prototype.createIndex = function(keys, options) {
675     return this.createIndexes([keys], options);
676 };
677 
678 DBCollection.prototype.createIndexes = function(keys, options) {
679     var indexSpecs = Array(keys.length);
680     for (var i = 0; i < indexSpecs.length; i++) {
681         indexSpecs[i] = this._indexSpec(keys[i], options);
682     }
683 
684     if (this.getMongo().writeMode() == "commands") {
685         for (var i = 0; i < indexSpecs.length; i++) {
686             delete (indexSpecs[i].ns);  // ns is passed to the first element in the command.
687         }
688         return this._db.runCommand({createIndexes: this.getName(), indexes: indexSpecs});
689     } else if (this.getMongo().writeMode() == "compatibility") {
690         // Use the downconversion machinery of the bulk api to do a safe write, report response as a
691         // command response
692         var result = this._db.getCollection("system.indexes").insert(indexSpecs, 0, true);
693 
694         if (result.hasWriteErrors() || result.hasWriteConcernError()) {
695             // Return the first error
696             var error = result.hasWriteErrors() ? result.getWriteErrors()[0]
697                                                 : result.getWriteConcernError();
698             return {ok: 0.0, code: error.code, errmsg: error.errmsg};
699         } else {
700             return {ok: 1.0};
701         }
702     } else {
703         this._db.getCollection("system.indexes").insert(indexSpecs, 0, true);
704     }
705 };
706 
707 DBCollection.prototype.ensureIndex = function(keys, options) {
708     var result = this.createIndex(keys, options);
709 
710     if (this.getMongo().writeMode() != "legacy") {
711         return result;
712     }
713 
714     err = this.getDB().getLastErrorObj();
715     if (err.err) {
716         return err;
717     }
718     // nothing returned on success
719 };
720 
721 DBCollection.prototype.reIndex = function() {
722     return this._db.runCommand({reIndex: this.getName()});
723 };
724 
725 DBCollection.prototype.dropIndexes = function() {
726     if (arguments.length)
727         throw Error("dropIndexes doesn't take arguments");
728 
729     var res = this._db.runCommand({deleteIndexes: this.getName(), index: "*"});
730     assert(res, "no result from dropIndex result");
731     if (res.ok)
732         return res;
733 
734     if (res.errmsg.match(/not found/))
735         return res;
736 
737     throw _getErrorWithCode(res, "error dropping indexes : " + tojson(res));
738 };
739 
740 DBCollection.prototype.drop = function() {
741     if (arguments.length > 0)
742         throw Error("drop takes no argument");
743     var ret = this._db.runCommand({drop: this.getName()});
744     if (!ret.ok) {
745         if (ret.errmsg == "ns not found")
746             return false;
747         throw _getErrorWithCode(ret, "drop failed: " + tojson(ret));
748     }
749     return true;
750 };
751 
752 DBCollection.prototype.findAndModify = function(args) {
753     var cmd = {findandmodify: this.getName()};
754     for (var key in args) {
755         cmd[key] = args[key];
756     }
757 
758     var ret = this._db.runCommand(cmd);
759     if (!ret.ok) {
760         if (ret.errmsg == "No matching object found") {
761             return null;
762         }
763         throw _getErrorWithCode(ret, "findAndModifyFailed failed: " + tojson(ret));
764     }
765     return ret.value;
766 };
767 
768 DBCollection.prototype.renameCollection = function(newName, dropTarget) {
769     return this._db._adminCommand({
770         renameCollection: this._fullName,
771         to: this._db._name + "." + newName,
772         dropTarget: dropTarget
773     });
774 };
775 
776 // Display verbose information about the operation
777 DBCollection.prototype._printExtraInfo = function(action, startTime) {
778     if (typeof _verboseShell === 'undefined' || !_verboseShell) {
779         __callLastError = true;
780         return;
781     }
782 
783     // explicit w:1 so that replset getLastErrorDefaults aren't used here which would be bad.
784     var res = this._db.getLastErrorCmd(1);
785     if (res) {
786         if (res.err != undefined && res.err != null) {
787             // error occurred, display it
788             print(res.err);
789             return;
790         }
791 
792         var info = action + " ";
793         // hack for inserted because res.n is 0
794         info += action != "Inserted" ? res.n : 1;
795         if (res.n > 0 && res.updatedExisting != undefined)
796             info += " " + (res.updatedExisting ? "existing" : "new");
797         info += " record(s)";
798         var time = new Date().getTime() - startTime;
799         info += " in " + time + "ms";
800         print(info);
801     }
802 };
803 
804 DBCollection.prototype.validate = function(full) {
805     var cmd = {validate: this.getName()};
806 
807     if (typeof(full) == 'object')  // support arbitrary options here
808         Object.extend(cmd, full);
809     else
810         cmd.full = full;
811 
812     var res = this._db.runCommand(cmd);
813 
814     if (typeof(res.valid) == 'undefined') {
815         // old-style format just put everything in a string. Now using proper fields
816 
817         res.valid = false;
818 
819         var raw = res.result || res.raw;
820 
821         if (raw) {
822             var str = "-" + tojson(raw);
823             res.valid = !(str.match(/exception/) || str.match(/corrupt/));
824 
825             var p = /lastExtentSize:(\d+)/;
826             var r = p.exec(str);
827             if (r) {
828                 res.lastExtentSize = Number(r[1]);
829             }
830         }
831     }
832 
833     return res;
834 };
835 
836 /**
837  * Invokes the storageDetails command to provide aggregate and (if requested) detailed information
838  * regarding the layout of records and deleted records in the collection extents.
839  * getDiskStorageStats provides a human-readable summary of the command output
840  */
841 DBCollection.prototype.diskStorageStats = function(opt) {
842     var cmd = {storageDetails: this.getName(), analyze: 'diskStorage'};
843     if (typeof(opt) == 'object')
844         Object.extend(cmd, opt);
845 
846     var res = this._db.runCommand(cmd);
847     if (!res.ok && res.errmsg.match(/no such cmd/)) {
848         print("this command requires starting mongod with --enableExperimentalStorageDetailsCmd");
849     }
850     return res;
851 };
852 
853 // Refer to diskStorageStats
854 DBCollection.prototype.getDiskStorageStats = function(params) {
855     var stats = this.diskStorageStats(params);
856     if (!stats.ok) {
857         print("error executing storageDetails command: " + stats.errmsg);
858         return;
859     }
860 
861     print("\n    " + "size".pad(9) + " " + "# recs".pad(10) + " " +
862           "[===occupied by BSON=== ---occupied by padding---       free           ]" + "  " +
863           "bson".pad(8) + " " + "rec".pad(8) + " " + "padding".pad(8));
864     print();
865 
866     var BAR_WIDTH = 70;
867 
868     var formatSliceData = function(data) {
869         var bar = _barFormat(
870             [
871               [data.bsonBytes / data.onDiskBytes, "="],
872               [(data.recBytes - data.bsonBytes) / data.onDiskBytes, "-"]
873             ],
874             BAR_WIDTH);
875 
876         return sh._dataFormat(data.onDiskBytes).pad(9) + " " + data.numEntries.toFixed(0).pad(10) +
877             " " + bar + "  " + (data.bsonBytes / data.onDiskBytes).toPercentStr().pad(8) + " " +
878             (data.recBytes / data.onDiskBytes).toPercentStr().pad(8) + " " +
879             (data.recBytes / data.bsonBytes).toFixed(4).pad(8);
880     };
881 
882     var printExtent = function(ex, rng) {
883         print("--- extent " + rng + " ---");
884         print("tot " + formatSliceData(ex));
885         print();
886         if (ex.slices) {
887             for (var c = 0; c < ex.slices.length; c++) {
888                 var slice = ex.slices[c];
889                 print(("" + c).pad(3) + " " + formatSliceData(slice));
890             }
891             print();
892         }
893     };
894 
895     if (stats.extents) {
896         print("--- extent overview ---\n");
897         for (var i = 0; i < stats.extents.length; i++) {
898             var ex = stats.extents[i];
899             print(("" + i).pad(3) + " " + formatSliceData(ex));
900         }
901         print();
902         if (params && (params.granularity || params.numberOfSlices)) {
903             for (var i = 0; i < stats.extents.length; i++) {
904                 printExtent(stats.extents[i], i);
905             }
906         }
907     } else {
908         printExtent(stats, "range " + stats.range);
909     }
910 
911 };
912 
913 /**
914  * Invokes the storageDetails command to report the percentage of virtual memory pages of the
915  * collection storage currently in physical memory (RAM).
916  * getPagesInRAM provides a human-readable summary of the command output
917  */
918 DBCollection.prototype.pagesInRAM = function(opt) {
919     var cmd = {storageDetails: this.getName(), analyze: 'pagesInRAM'};
920     if (typeof(opt) == 'object')
921         Object.extend(cmd, opt);
922 
923     var res = this._db.runCommand(cmd);
924     if (!res.ok && res.errmsg.match(/no such cmd/)) {
925         print("this command requires starting mongod with --enableExperimentalStorageDetailsCmd");
926     }
927     return res;
928 };
929 
930 // Refer to pagesInRAM
931 DBCollection.prototype.getPagesInRAM = function(params) {
932     var stats = this.pagesInRAM(params);
933     if (!stats.ok) {
934         print("error executing storageDetails command: " + stats.errmsg);
935         return;
936     }
937 
938     var BAR_WIDTH = 70;
939     var formatExtentData = function(data) {
940         return "size".pad(8) + " " + _barFormat([[data.inMem, '=']], BAR_WIDTH) + "  " +
941             data.inMem.toPercentStr().pad(7);
942     };
943 
944     var printExtent = function(ex, rng) {
945         print("--- extent " + rng + " ---");
946         print("tot " + formatExtentData(ex));
947         print();
948         if (ex.slices) {
949             print("\tslices, percentage of pages in memory (< .1% : ' ', <25% : '.', " +
950                   "<50% : '_', <75% : '=', >75% : '#')");
951             print();
952             print("\t" + "offset".pad(8) + "  [slices...] (each slice is " +
953                   sh._dataFormat(ex.sliceBytes) + ")");
954             line = "\t" + ("" + 0).pad(8) + "  [";
955             for (var c = 0; c < ex.slices.length; c++) {
956                 if (c % 80 == 0 && c != 0) {
957                     print(line + "]");
958                     line = "\t" + sh._dataFormat(ex.sliceBytes * c).pad(8) + "  [";
959                 }
960                 var inMem = ex.slices[c];
961                 if (inMem <= .001)
962                     line += " ";
963                 else if (inMem <= .25)
964                     line += ".";
965                 else if (inMem <= .5)
966                     line += "_";
967                 else if (inMem <= .75)
968                     line += "=";
969                 else
970                     line += "#";
971             }
972             print(line + "]");
973             print();
974         }
975     };
976 
977     if (stats.extents) {
978         print("--- extent overview ---\n");
979         for (var i = 0; i < stats.extents.length; i++) {
980             var ex = stats.extents[i];
981             print(("" + i).pad(3) + " " + formatExtentData(ex));
982         }
983         print();
984         if (params && (params.granularity || params.numberOfSlices)) {
985             for (var i = 0; i < stats.extents.length; i++) {
986                 printExtent(stats.extents[i], i);
987             }
988         } else {
989             print("use getPagesInRAM({granularity: _bytes_}) or " +
990                   "getPagesInRAM({numberOfSlices: _num_} for details");
991             print("use pagesInRAM(...) for json output, same parameters apply");
992         }
993     } else {
994         printExtent(stats, "range " + stats.range);
995     }
996 };
997 
998 DBCollection.prototype.getShardVersion = function() {
999     return this._db._adminCommand({getShardVersion: this._fullName});
1000 };
1001 
1002 DBCollection.prototype._getIndexesSystemIndexes = function(filter) {
1003     var si = this.getDB().getCollection("system.indexes");
1004     var query = {ns: this.getFullName()};
1005     if (filter)
1006         query = Object.extend(query, filter);
1007     return si.find(query).toArray();
1008 };
1009 
1010 DBCollection.prototype._getIndexesCommand = function(filter) {
1011     var res = this.runCommand("listIndexes", filter);
1012 
1013     if (!res.ok) {
1014         if (res.code == 59) {
1015             // command doesn't exist, old mongod
1016             return null;
1017         }
1018 
1019         if (res.code == 26) {
1020             // NamespaceNotFound, for compatability, return []
1021             return [];
1022         }
1023 
1024         if (res.errmsg && res.errmsg.startsWith("no such cmd")) {
1025             return null;
1026         }
1027 
1028         throw _getErrorWithCode(res, "listIndexes failed: " + tojson(res));
1029     }
1030 
1031     return new DBCommandCursor(this._mongo, res).toArray();
1032 };
1033 
1034 DBCollection.prototype.getIndexes = function(filter) {
1035     var res = this._getIndexesCommand(filter);
1036     if (res) {
1037         return res;
1038     }
1039     return this._getIndexesSystemIndexes(filter);
1040 };
1041 
1042 DBCollection.prototype.getIndices = DBCollection.prototype.getIndexes;
1043 DBCollection.prototype.getIndexSpecs = DBCollection.prototype.getIndexes;
1044 
1045 DBCollection.prototype.getIndexKeys = function() {
1046     return this.getIndexes().map(function(i) {
1047         return i.key;
1048     });
1049 };
1050 
1051 DBCollection.prototype.hashAllDocs = function() {
1052     var cmd = {dbhash: 1, collections: [this._shortName]};
1053     var res = this._dbCommand(cmd);
1054     var hash = res.collections[this._shortName];
1055     assert(hash);
1056     assert(typeof(hash) == "string");
1057     return hash;
1058 };
1059 
1060 /**
1061  * <p>Drop a specified index.</p>
1062  *
1063  * <p>
1064  * "index" is the name of the index in the system.indexes name field (run db.system.indexes.find()
1065  *to
1066  *  see example data), or an object holding the key(s) used to create the index.
1067  * For example:
1068  *  db.collectionName.dropIndex( "myIndexName" );
1069  *  db.collectionName.dropIndex( { "indexKey" : 1 } );
1070  * </p>
1071  *
1072  * @param {String} name or key object of index to delete.
1073  * @return A result object.  result.ok will be true if successful.
1074  */
1075 DBCollection.prototype.dropIndex = function(index) {
1076     assert(index, "need to specify index to dropIndex");
1077     var res = this._dbCommand("deleteIndexes", {index: index});
1078     return res;
1079 };
1080 
1081 DBCollection.prototype.copyTo = function(newName) {
1082     return this.getDB().eval(function(collName, newName) {
1083         var from = db[collName];
1084         var to = db[newName];
1085         to.ensureIndex({_id: 1});
1086         var count = 0;
1087 
1088         var cursor = from.find();
1089         while (cursor.hasNext()) {
1090             var o = cursor.next();
1091             count++;
1092             to.save(o);
1093         }
1094 
1095         return count;
1096     }, this.getName(), newName);
1097 };
1098 
1099 DBCollection.prototype.getCollection = function(subName) {
1100     return this._db.getCollection(this._shortName + "." + subName);
1101 };
1102 
1103 /**
1104   * scale: The scale at which to deliver results. Unless specified, this command returns all data
1105   *        in bytes.
1106   * indexDetails: Includes indexDetails field in results. Default: false.
1107   * indexDetailsKey: If indexDetails is true, filter contents in indexDetails by this index key.
1108   * indexDetailsname: If indexDetails is true, filter contents in indexDetails by this index name.
1109   *
1110   * It is an error to provide both indexDetailsKey and indexDetailsName.
1111   */
1112 DBCollection.prototype.stats = function(args) {
1113     'use strict';
1114 
1115     // For backwards compatibility with db.collection.stats(scale).
1116     var scale = isObject(args) ? args.scale : args;
1117 
1118     var options = isObject(args) ? args : {};
1119     if (options.indexDetailsKey && options.indexDetailsName) {
1120         throw new Error('Cannot filter indexDetails on both indexDetailsKey and ' +
1121                         'indexDetailsName');
1122     }
1123     // collStats can run on a secondary, so we need to apply readPreference
1124     var res = this._db.runReadCommand({collStats: this._shortName, scale: scale});
1125     if (!res.ok) {
1126         return res;
1127     }
1128 
1129     var getIndexName = function(collection, indexKey) {
1130         if (!isObject(indexKey))
1131             return undefined;
1132         var indexName;
1133         collection.getIndexes().forEach(function(spec) {
1134             if (friendlyEqual(spec.key, options.indexDetailsKey)) {
1135                 indexName = spec.name;
1136             }
1137         });
1138         return indexName;
1139     };
1140 
1141     var filterIndexName = options.indexDetailsName || getIndexName(this, options.indexDetailsKey);
1142 
1143     var updateStats = function(stats, keepIndexDetails, indexName) {
1144         if (!stats.indexDetails)
1145             return;
1146         if (!keepIndexDetails) {
1147             delete stats.indexDetails;
1148             return;
1149         }
1150         if (!indexName)
1151             return;
1152         for (var key in stats.indexDetails) {
1153             if (key == indexName)
1154                 continue;
1155             delete stats.indexDetails[key];
1156         }
1157     };
1158 
1159     updateStats(res, options.indexDetails, filterIndexName);
1160 
1161     if (res.sharded) {
1162         for (var shardName in res.shards) {
1163             updateStats(res.shards[shardName], options.indexDetails, filterIndexName);
1164         }
1165     }
1166 
1167     return res;
1168 };
1169 
1170 DBCollection.prototype.dataSize = function() {
1171     return this.stats().size;
1172 };
1173 
1174 DBCollection.prototype.storageSize = function() {
1175     return this.stats().storageSize;
1176 };
1177 
1178 DBCollection.prototype.totalIndexSize = function(verbose) {
1179     var stats = this.stats();
1180     if (verbose) {
1181         for (var ns in stats.indexSizes) {
1182             print(ns + "\t" + stats.indexSizes[ns]);
1183         }
1184     }
1185     return stats.totalIndexSize;
1186 };
1187 
1188 DBCollection.prototype.totalSize = function() {
1189     var total = this.storageSize();
1190     var totalIndexSize = this.totalIndexSize();
1191     if (totalIndexSize) {
1192         total += totalIndexSize;
1193     }
1194     return total;
1195 };
1196 
1197 DBCollection.prototype.convertToCapped = function(bytes) {
1198     if (!bytes)
1199         throw Error("have to specify # of bytes");
1200     return this._dbCommand({convertToCapped: this._shortName, size: bytes});
1201 };
1202 
1203 DBCollection.prototype.exists = function() {
1204     var res = this._db.runCommand("listCollections", {filter: {name: this._shortName}});
1205     if (res.ok) {
1206         var cursor = new DBCommandCursor(this._mongo, res);
1207         if (!cursor.hasNext())
1208             return null;
1209         return cursor.next();
1210     }
1211 
1212     if (res.errmsg && res.errmsg.startsWith("no such cmd")) {
1213         return this._db.system.namespaces.findOne({name: this._fullName});
1214     }
1215 
1216     throw _getErrorWithCode(res, "listCollections failed: " + tojson(res));
1217 };
1218 
1219 DBCollection.prototype.isCapped = function() {
1220     var e = this.exists();
1221     return (e && e.options && e.options.capped) ? true : false;
1222 };
1223 
1224 //
1225 // CRUD specification aggregation cursor extension
1226 //
1227 DBCollection.prototype.aggregate = function(pipeline, aggregateOptions) {
1228     if (!(pipeline instanceof Array)) {
1229         // support legacy varargs form. (Also handles db.foo.aggregate())
1230         pipeline = Array.from(arguments);
1231         aggregateOptions = {};
1232     } else if (aggregateOptions === undefined) {
1233         aggregateOptions = {};
1234     }
1235 
1236     // Copy the aggregateOptions
1237     var copy = Object.extend({}, aggregateOptions);
1238 
1239     // Ensure handle crud API aggregateOptions
1240     var keys = Object.keys(copy);
1241 
1242     for (var i = 0; i < keys.length; i++) {
1243         var name = keys[i];
1244 
1245         if (name == 'batchSize') {
1246             if (copy.cursor == null) {
1247                 copy.cursor = {};
1248             }
1249 
1250             copy.cursor.batchSize = copy['batchSize'];
1251             delete copy['batchSize'];
1252         } else if (name == 'useCursor') {
1253             if (copy.cursor == null) {
1254                 copy.cursor = {};
1255             }
1256 
1257             delete copy['useCursor'];
1258         }
1259     }
1260 
1261     // Assign the cleaned up options
1262     aggregateOptions = copy;
1263     // Create the initial command document
1264     var cmd = {pipeline: pipeline};
1265     Object.extend(cmd, aggregateOptions);
1266 
1267     if (!('cursor' in cmd)) {
1268         // implicitly use cursors
1269         cmd.cursor = {};
1270     }
1271 
1272     // in a well formed pipeline, $out must be the last stage. If it isn't then the server
1273     // will reject the pipeline anyway.
1274     var hasOutStage = pipeline.length >= 1 && pipeline[pipeline.length - 1].hasOwnProperty("$out");
1275 
1276     var doAgg = function(cmd) {
1277         // if we don't have an out stage, we could run on a secondary
1278         // so we need to attach readPreference
1279         return hasOutStage ? this.runCommand("aggregate", cmd)
1280                            : this.runReadCommand("aggregate", cmd);
1281     }.bind(this);
1282 
1283     var res = doAgg(cmd);
1284 
1285     if (!res.ok && (res.code == 17020 || res.errmsg == "unrecognized field \"cursor") &&
1286         !("cursor" in aggregateOptions)) {
1287         // If the command failed because cursors aren't supported and the user didn't explicitly
1288         // request a cursor, try again without requesting a cursor.
1289         delete cmd.cursor;
1290 
1291         res = doAgg(cmd);
1292 
1293         if ('result' in res && !("cursor" in res)) {
1294             // convert old-style output to cursor-style output
1295             res.cursor = {ns: '', id: NumberLong(0)};
1296             res.cursor.firstBatch = res.result;
1297             delete res.result;
1298         }
1299     }
1300 
1301     assert.commandWorked(res, "aggregate failed");
1302 
1303     if ("cursor" in res) {
1304         return new DBCommandCursor(this._mongo, res);
1305     }
1306 
1307     return res;
1308 };
1309 
1310 DBCollection.prototype.group = function(params) {
1311     params.ns = this._shortName;
1312     return this._db.group(params);
1313 };
1314 
1315 DBCollection.prototype.groupcmd = function(params) {
1316     params.ns = this._shortName;
1317     return this._db.groupcmd(params);
1318 };
1319 
1320 MapReduceResult = function(db, o) {
1321     Object.extend(this, o);
1322     this._o = o;
1323     this._keys = Object.keySet(o);
1324     this._db = db;
1325     if (this.result != null) {
1326         this._coll = this._db.getCollection(this.result);
1327     }
1328 };
1329 
1330 MapReduceResult.prototype._simpleKeys = function() {
1331     return this._o;
1332 };
1333 
1334 MapReduceResult.prototype.find = function() {
1335     if (this.results)
1336         return this.results;
1337     return DBCollection.prototype.find.apply(this._coll, arguments);
1338 };
1339 
1340 MapReduceResult.prototype.drop = function() {
1341     if (this._coll) {
1342         return this._coll.drop();
1343     }
1344 };
1345 
1346 /**
1347 * just for debugging really
1348 */
1349 MapReduceResult.prototype.convertToSingleObject = function() {
1350     var z = {};
1351     var it = this.results != null ? this.results : this._coll.find();
1352     it.forEach(function(a) {
1353         z[a._id] = a.value;
1354     });
1355     return z;
1356 };
1357 
1358 DBCollection.prototype.convertToSingleObject = function(valueField) {
1359     var z = {};
1360     this.find().forEach(function(a) {
1361         z[a._id] = a[valueField];
1362     });
1363     return z;
1364 };
1365 
1366 /**
1367 * @param optional object of optional fields;
1368 */
1369 DBCollection.prototype.mapReduce = function(map, reduce, optionsOrOutString) {
1370     var c = {mapreduce: this._shortName, map: map, reduce: reduce};
1371     assert(optionsOrOutString, "need to supply an optionsOrOutString");
1372 
1373     if (typeof(optionsOrOutString) == "string")
1374         c["out"] = optionsOrOutString;
1375     else
1376         Object.extend(c, optionsOrOutString);
1377 
1378     var raw;
1379 
1380     if (c["out"].hasOwnProperty("inline") && c["out"]["inline"] === 1) {
1381         // if inline output is specified, we need to apply readPreference on the command
1382         // as it could be run on a secondary
1383         raw = this._db.runReadCommand(c);
1384     } else {
1385         raw = this._db.runCommand(c);
1386     }
1387 
1388     if (!raw.ok) {
1389         __mrerror__ = raw;
1390         throw _getErrorWithCode(raw, "map reduce failed:" + tojson(raw));
1391     }
1392     return new MapReduceResult(this._db, raw);
1393 
1394 };
1395 
1396 DBCollection.prototype.toString = function() {
1397     return this.getFullName();
1398 };
1399 
1400 DBCollection.prototype.toString = function() {
1401     return this.getFullName();
1402 };
1403 
1404 DBCollection.prototype.tojson = DBCollection.prototype.toString;
1405 
1406 DBCollection.prototype.shellPrint = DBCollection.prototype.toString;
1407 
1408 DBCollection.autocomplete = function(obj) {
1409     var colls = DB.autocomplete(obj.getDB());
1410     var ret = [];
1411     for (var i = 0; i < colls.length; i++) {
1412         var c = colls[i];
1413         if (c.length <= obj.getName().length)
1414             continue;
1415         if (c.slice(0, obj.getName().length + 1) != obj.getName() + '.')
1416             continue;
1417 
1418         ret.push(c.slice(obj.getName().length + 1));
1419     }
1420     return ret;
1421 };
1422 
1423 // Sharding additions
1424 
1425 /*
1426 Usage :
1427 
1428 mongo <mongos>
1429 > load('path-to-file/shardingAdditions.js')
1430 Loading custom sharding extensions...
1431 true
1432 
1433 > var collection = db.getMongo().getCollection("foo.bar")
1434 > collection.getShardDistribution() // prints statistics related to the collection's data
1435 distribution
1436 
1437 > collection.getSplitKeysForChunks() // generates split points for all chunks in the collection,
1438 based on the
1439                                      // default maxChunkSize or alternately a specified chunk size
1440 > collection.getSplitKeysForChunks( 10 ) // Mb
1441 
1442 > var splitter = collection.getSplitKeysForChunks() // by default, the chunks are not split, the
1443 keys are just
1444                                                     // found.  A splitter function is returned which
1445 will actually
1446                                                     // do the splits.
1447 
1448 > splitter() // ! Actually executes the splits on the cluster !
1449 
1450 */
1451 
1452 DBCollection.prototype.getShardDistribution = function() {
1453 
1454     var stats = this.stats();
1455 
1456     if (!stats.sharded) {
1457         print("Collection " + this + " is not sharded.");
1458         return;
1459     }
1460 
1461     var config = this.getMongo().getDB("config");
1462 
1463     var numChunks = 0;
1464 
1465     for (var shard in stats.shards) {
1466         var shardDoc = config.shards.findOne({_id: shard});
1467 
1468         print("\nShard " + shard + " at " + shardDoc.host);
1469 
1470         var shardStats = stats.shards[shard];
1471 
1472         var chunks = config.chunks.find({_id: sh._collRE(this), shard: shard}).toArray();
1473 
1474         numChunks += chunks.length;
1475 
1476         var estChunkData = shardStats.size / chunks.length;
1477         var estChunkCount = Math.floor(shardStats.count / chunks.length);
1478 
1479         print(" data : " + sh._dataFormat(shardStats.size) + " docs : " + shardStats.count +
1480               " chunks : " + chunks.length);
1481         print(" estimated data per chunk : " + sh._dataFormat(estChunkData));
1482         print(" estimated docs per chunk : " + estChunkCount);
1483     }
1484 
1485     print("\nTotals");
1486     print(" data : " + sh._dataFormat(stats.size) + " docs : " + stats.count + " chunks : " +
1487           numChunks);
1488     for (var shard in stats.shards) {
1489         var shardStats = stats.shards[shard];
1490 
1491         var estDataPercent = Math.floor(shardStats.size / stats.size * 10000) / 100;
1492         var estDocPercent = Math.floor(shardStats.count / stats.count * 10000) / 100;
1493 
1494         print(" Shard " + shard + " contains " + estDataPercent + "% data, " + estDocPercent +
1495               "% docs in cluster, " + "avg obj size on shard : " +
1496               sh._dataFormat(stats.shards[shard].avgObjSize));
1497     }
1498 
1499     print("\n");
1500 
1501 };
1502 
1503 DBCollection.prototype.getSplitKeysForChunks = function(chunkSize) {
1504 
1505     var stats = this.stats();
1506 
1507     if (!stats.sharded) {
1508         print("Collection " + this + " is not sharded.");
1509         return;
1510     }
1511 
1512     var config = this.getMongo().getDB("config");
1513 
1514     if (!chunkSize) {
1515         chunkSize = config.settings.findOne({_id: "chunksize"}).value;
1516         print("Chunk size not set, using default of " + chunkSize + "MB");
1517     } else {
1518         print("Using chunk size of " + chunkSize + "MB");
1519     }
1520 
1521     var shardDocs = config.shards.find().toArray();
1522 
1523     var allSplitPoints = {};
1524     var numSplits = 0;
1525 
1526     for (var i = 0; i < shardDocs.length; i++) {
1527         var shardDoc = shardDocs[i];
1528         var shard = shardDoc._id;
1529         var host = shardDoc.host;
1530         var sconn = new Mongo(host);
1531 
1532         var chunks = config.chunks.find({_id: sh._collRE(this), shard: shard}).toArray();
1533 
1534         print("\nGetting split points for chunks on shard " + shard + " at " + host);
1535 
1536         var splitPoints = [];
1537 
1538         for (var j = 0; j < chunks.length; j++) {
1539             var chunk = chunks[j];
1540             var result = sconn.getDB("admin").runCommand(
1541                 {splitVector: this + "", min: chunk.min, max: chunk.max, maxChunkSize: chunkSize});
1542             if (!result.ok) {
1543                 print(" Had trouble getting split keys for chunk " + sh._pchunk(chunk) + " :\n");
1544                 printjson(result);
1545             } else {
1546                 splitPoints = splitPoints.concat(result.splitKeys);
1547 
1548                 if (result.splitKeys.length > 0)
1549                     print(" Added " + result.splitKeys.length + " split points for chunk " +
1550                           sh._pchunk(chunk));
1551             }
1552         }
1553 
1554         print("Total splits for shard " + shard + " : " + splitPoints.length);
1555 
1556         numSplits += splitPoints.length;
1557         allSplitPoints[shard] = splitPoints;
1558     }
1559 
1560     // Get most recent migration
1561     var migration = config.changelog.find({what: /^move.*/}).sort({time: -1}).limit(1).toArray();
1562     if (migration.length == 0)
1563         print("\nNo migrations found in changelog.");
1564     else {
1565         migration = migration[0];
1566         print("\nMost recent migration activity was on " + migration.ns + " at " + migration.time);
1567     }
1568 
1569     var admin = this.getMongo().getDB("admin");
1570     var coll = this;
1571     var splitFunction = function() {
1572 
1573         // Turn off the balancer, just to be safe
1574         print("Turning off balancer...");
1575         config.settings.update({_id: "balancer"}, {$set: {stopped: true}}, true);
1576         print(
1577             "Sleeping for 30s to allow balancers to detect change.  To be extra safe, check config.changelog" +
1578             " for recent migrations.");
1579         sleep(30000);
1580 
1581         for (var shard in allSplitPoints) {
1582             for (var i = 0; i < allSplitPoints[shard].length; i++) {
1583                 var splitKey = allSplitPoints[shard][i];
1584                 print("Splitting at " + tojson(splitKey));
1585                 printjson(admin.runCommand({split: coll + "", middle: splitKey}));
1586             }
1587         }
1588 
1589         print("Turning the balancer back on.");
1590         config.settings.update({_id: "balancer"}, {$set: {stopped: false}});
1591         sleep(1);
1592     };
1593 
1594     splitFunction.getSplitPoints = function() {
1595         return allSplitPoints;
1596     };
1597 
1598     print("\nGenerated " + numSplits + " split keys, run output function to perform splits.\n" +
1599           " ex : \n" + "  > var splitter = <collection>.getSplitKeysForChunks()\n" +
1600           "  > splitter() // Execute splits on cluster !\n");
1601 
1602     return splitFunction;
1603 
1604 };
1605 
1606 DBCollection.prototype.setSlaveOk = function(value) {
1607     if (value == undefined)
1608         value = true;
1609     this._slaveOk = value;
1610 };
1611 
1612 DBCollection.prototype.getSlaveOk = function() {
1613     if (this._slaveOk != undefined)
1614         return this._slaveOk;
1615     return this._db.getSlaveOk();
1616 };
1617 
1618 DBCollection.prototype.getQueryOptions = function() {
1619     // inherit this method from DB but use apply so
1620     // that slaveOk will be set if is overridden on this DBCollection
1621     return this._db.getQueryOptions.apply(this, arguments);
1622 };
1623 
1624 /**
1625  * Returns a PlanCache for the collection.
1626  */
1627 DBCollection.prototype.getPlanCache = function() {
1628     return new PlanCache(this);
1629 };
1630 
1631 // Overrides connection-level settings.
1632 //
1633 
1634 DBCollection.prototype.setWriteConcern = function(wc) {
1635     if (wc instanceof WriteConcern) {
1636         this._writeConcern = wc;
1637     } else {
1638         this._writeConcern = new WriteConcern(wc);
1639     }
1640 };
1641 
1642 DBCollection.prototype.getWriteConcern = function() {
1643     if (this._writeConcern)
1644         return this._writeConcern;
1645 
1646     if (this._db.getWriteConcern())
1647         return this._db.getWriteConcern();
1648 
1649     return null;
1650 };
1651 
1652 DBCollection.prototype.unsetWriteConcern = function() {
1653     delete this._writeConcern;
1654 };
1655 
1656 //
1657 // CRUD specification read methods
1658 //
1659 
1660 /**
1661 * Count number of matching documents in the db to a query.
1662 *
1663 * @method
1664 * @param {object} query The query for the count.
1665 * @param {object} [options=null] Optional settings.
1666 * @param {number} [options.limit=null] The limit of documents to count.
1667 * @param {number} [options.skip=null] The number of documents to skip for the count.
1668 * @param {string|object} [options.hint=null] An index name hint or specification for the query.
1669 * @param {number} [options.maxTimeMS=null] The maximum amount of time to allow the query to run.
1670 * @param {string} [options.readConcern=null] The level of readConcern passed to the count command
1671 * @param {object} [options.collation=null] The collation that should be used for string comparisons
1672 * for this count op.
1673 * @return {number}
1674 */
1675 DBCollection.prototype.count = function(query, options) {
1676     var opts = Object.extend({}, options || {});
1677 
1678     var query = this.find(query);
1679     if (typeof opts.skip == 'number') {
1680         query.skip(opts.skip);
1681     }
1682 
1683     if (typeof opts.limit == 'number') {
1684         query.limit(opts.limit);
1685     }
1686 
1687     if (typeof opts.maxTimeMS == 'number') {
1688         query.maxTimeMS(opts.maxTimeMS);
1689     }
1690 
1691     if (opts.hint) {
1692         query.hint(opts.hint);
1693     }
1694 
1695     if (typeof opts.readConcern == 'string') {
1696         query.readConcern(opts.readConcern);
1697     }
1698 
1699     if (typeof opts.collation == 'object') {
1700         query.collation(opts.collation);
1701     }
1702 
1703     // Return the result of the find
1704     return query.count(true);
1705 };
1706 
1707 /**
1708 * The distinct command returns returns a list of distinct values for the given key across a
1709 *collection.
1710 *
1711 * @method
1712 * @param {string} key Field of the document to find distinct values for.
1713 * @param {object} query The query for filtering the set of documents to which we apply the distinct
1714 *filter.
1715 * @param {object} [options=null] Optional settings.
1716 * @param {number} [options.maxTimeMS=null] The maximum amount of time to allow the query to run.
1717 * @return {object}
1718 */
1719 DBCollection.prototype.distinct = function(keyString, query, options) {
1720     var opts = Object.extend({}, options || {});
1721     var keyStringType = typeof keyString;
1722     var queryType = typeof query;
1723 
1724     if (keyStringType != "string") {
1725         throw new Error("The first argument to the distinct command must be a string but was a " +
1726                         keyStringType);
1727     }
1728 
1729     if (query != null && queryType != "object") {
1730         throw new Error("The query argument to the distinct command must be a document but was a " +
1731                         queryType);
1732     }
1733 
1734     // Distinct command
1735     var cmd = {distinct: this.getName(), key: keyString, query: query || {}};
1736 
1737     // Set maxTimeMS if provided
1738     if (opts.maxTimeMS) {
1739         cmd.maxTimeMS = opts.maxTimeMS;
1740     }
1741 
1742     if (opts.collation) {
1743         cmd.collation = opts.collation;
1744     }
1745 
1746     // Execute distinct command
1747     var res = this.runReadCommand(cmd);
1748     if (!res.ok) {
1749         throw new Error("distinct failed: " + tojson(res));
1750     }
1751 
1752     return res.values;
1753 };
1754 
1755 DBCollection.prototype._distinct = function(keyString, query) {
1756     return this._dbReadCommand({distinct: this._shortName, key: keyString, query: query || {}});
1757 };
1758 
1759 DBCollection.prototype.latencyStats = function() {
1760     return this.aggregate([{$collStats: {latencyStats: {}}}]);
1761 };
1762 
1763 /**
1764  * PlanCache
1765  * Holds a reference to the collection.
1766  * Proxy for planCache* commands.
1767  */
1768 if ((typeof PlanCache) == "undefined") {
1769     PlanCache = function(collection) {
1770         this._collection = collection;
1771     };
1772 }
1773 
1774 /**
1775  * Name of PlanCache.
1776  * Same as collection.
1777  */
1778 PlanCache.prototype.getName = function() {
1779     return this._collection.getName();
1780 };
1781 
1782 /**
1783  * toString prints the name of the collection
1784  */
1785 PlanCache.prototype.toString = function() {
1786     return "PlanCache for collection " + this.getName() + '. Type help() for more info.';
1787 };
1788 
1789 PlanCache.prototype.shellPrint = PlanCache.prototype.toString;
1790 
1791 /**
1792  * Displays help for a PlanCache object.
1793  */
1794 PlanCache.prototype.help = function() {
1795     var shortName = this.getName();
1796     print("PlanCache help");
1797     print("\tdb." + shortName + ".getPlanCache().help() - show PlanCache help");
1798     print("\tdb." + shortName + ".getPlanCache().listQueryShapes() - " +
1799           "displays all query shapes in a collection");
1800     print("\tdb." + shortName + ".getPlanCache().clear() - " +
1801           "drops all cached queries in a collection");
1802     print("\tdb." + shortName +
1803           ".getPlanCache().clearPlansByQuery(query[, projection, sort, collation]) - " +
1804           "drops query shape from plan cache");
1805     print("\tdb." + shortName +
1806           ".getPlanCache().getPlansByQuery(query[, projection, sort, collation]) - " +
1807           "displays the cached plans for a query shape");
1808     return __magicNoPrint;
1809 };
1810 
1811 /**
1812  * Internal function to parse query shape.
1813  */
1814 PlanCache.prototype._parseQueryShape = function(query, projection, sort, collation) {
1815     if (query == undefined) {
1816         throw new Error("required parameter query missing");
1817     }
1818 
1819     // Accept query shape object as only argument.
1820     // Query shape must contain 'query', 'projection', and 'sort', and may optionally contain
1821     // 'collation'. 'collation' must be non-empty if present.
1822     if (typeof(query) == 'object' && projection == undefined && sort == undefined &&
1823         collation == undefined) {
1824         var keysSorted = Object.keys(query).sort();
1825         // Expected keys must be sorted for the comparison to work.
1826         if (bsonWoCompare(keysSorted, ['projection', 'query', 'sort']) == 0) {
1827             return query;
1828         }
1829         if (bsonWoCompare(keysSorted, ['collation', 'projection', 'query', 'sort']) == 0) {
1830             if (Object.keys(query.collation).length === 0) {
1831                 throw new Error("collation object must not be empty");
1832             }
1833             return query;
1834         }
1835     }
1836 
1837     // Extract query shape, projection, sort and collation from DBQuery if it is the first
1838     // argument. If a sort or projection is provided in addition to DBQuery, do not
1839     // overwrite with the DBQuery value.
1840     if (query instanceof DBQuery) {
1841         if (projection != undefined) {
1842             throw new Error("cannot pass DBQuery with projection");
1843         }
1844         if (sort != undefined) {
1845             throw new Error("cannot pass DBQuery with sort");
1846         }
1847         if (collation != undefined) {
1848             throw new Error("cannot pass DBQuery with collation");
1849         }
1850 
1851         var queryObj = query._query["query"] || {};
1852         projection = query._fields || {};
1853         sort = query._query["orderby"] || {};
1854         collation = query._query["collation"] || undefined;
1855         // Overwrite DBQuery with the BSON query.
1856         query = queryObj;
1857     }
1858 
1859     var shape = {
1860         query: query,
1861         projection: projection == undefined ? {} : projection,
1862         sort: sort == undefined ? {} : sort,
1863     };
1864 
1865     if (collation !== undefined) {
1866         shape.collation = collation;
1867     }
1868 
1869     return shape;
1870 };
1871 
1872 /**
1873  * Internal function to run command.
1874  */
1875 PlanCache.prototype._runCommandThrowOnError = function(cmd, params) {
1876     var res = this._collection.runCommand(cmd, params);
1877     if (!res.ok) {
1878         throw new Error(res.errmsg);
1879     }
1880     return res;
1881 };
1882 
1883 /**
1884  * Lists query shapes in a collection.
1885  */
1886 PlanCache.prototype.listQueryShapes = function() {
1887     return this._runCommandThrowOnError("planCacheListQueryShapes", {}).shapes;
1888 };
1889 
1890 /**
1891  * Clears plan cache in a collection.
1892  */
1893 PlanCache.prototype.clear = function() {
1894     this._runCommandThrowOnError("planCacheClear", {});
1895     return;
1896 };
1897 
1898 /**
1899  * List plans for a query shape.
1900  */
1901 PlanCache.prototype.getPlansByQuery = function(query, projection, sort, collation) {
1902     return this
1903         ._runCommandThrowOnError("planCacheListPlans",
1904                                  this._parseQueryShape(query, projection, sort, collation))
1905         .plans;
1906 };
1907 
1908 /**
1909  * Drop query shape from the plan cache.
1910  */
1911 PlanCache.prototype.clearPlansByQuery = function(query, projection, sort, collation) {
1912     this._runCommandThrowOnError("planCacheClear",
1913                                  this._parseQueryShape(query, projection, sort, collation));
1914     return;
1915 };
1916