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 }
 25 
 26 DBCollection.prototype.getName = function(){
 27     return this._shortName;
 28 }
 29 
 30 DBCollection.prototype.help = function () {
 31     var shortName = this.getName();
 32     print("DBCollection help");
 33     print("\tdb." + shortName + ".find().help() - show DBCursor help");
 34     print("\tdb." + shortName + ".count()");
 35     print("\tdb." + shortName + ".dataSize()");
 36     print("\tdb." + shortName + ".distinct( key ) - eg. db." + shortName + ".distinct( 'x' )");
 37     print("\tdb." + shortName + ".drop() drop the collection");
 38     print("\tdb." + shortName + ".dropIndex(name)");
 39     print("\tdb." + shortName + ".dropIndexes()");
 40     print("\tdb." + shortName + ".ensureIndex(keypattern[,options]) - options is an object with these possible fields: name, unique, dropDups");
 41     print("\tdb." + shortName + ".reIndex()");
 42     print("\tdb." + shortName + ".find([query],[fields]) - query is an optional query filter. fields is optional set of fields to return.");
 43     print("\t                                              e.g. db." + shortName + ".find( {x:77} , {name:1, x:1} )");
 44     print("\tdb." + shortName + ".find(...).count()");
 45     print("\tdb." + shortName + ".find(...).limit(n)");
 46     print("\tdb." + shortName + ".find(...).skip(n)");
 47     print("\tdb." + shortName + ".find(...).sort(...)");
 48     print("\tdb." + shortName + ".findOne([query])");
 49     print("\tdb." + shortName + ".findAndModify( { update : ... , remove : bool [, query: {}, sort: {}, 'new': false] } )");
 50     print("\tdb." + shortName + ".getDB() get DB object associated with collection");
 51     print("\tdb." + shortName + ".getIndexes()");
 52     print("\tdb." + shortName + ".group( { key : ..., initial: ..., reduce : ...[, cond: ...] } )");
 53     print("\tdb." + shortName + ".mapReduce( mapFunction , reduceFunction , <optional params> )");
 54     print("\tdb." + shortName + ".remove(query)");
 55     print("\tdb." + shortName + ".renameCollection( newName , <dropTarget> ) renames the collection.");
 56     print("\tdb." + shortName + ".runCommand( name , <options> ) runs a db command with the given name where the first param is the collection name");
 57     print("\tdb." + shortName + ".save(obj)");
 58     print("\tdb." + shortName + ".stats()");
 59     print("\tdb." + shortName + ".storageSize() - includes free space allocated to this collection");
 60     print("\tdb." + shortName + ".totalIndexSize() - size in bytes of all the indexes");
 61     print("\tdb." + shortName + ".totalSize() - storage allocated for all data and indexes");
 62     print("\tdb." + shortName + ".update(query, object[, upsert_bool, multi_bool])");
 63     print("\tdb." + shortName + ".validate() - SLOW");
 64     print("\tdb." + shortName + ".getShardVersion() - only for use with sharding");
 65     return __magicNoPrint;
 66 }
 67 
 68 DBCollection.prototype.getFullName = function(){
 69     return this._fullName;
 70 }
 71 DBCollection.prototype.getMongo = function(){
 72     return this._db.getMongo();
 73 }
 74 DBCollection.prototype.getDB = function(){
 75     return this._db;
 76 }
 77 
 78 DBCollection.prototype._dbCommand = function( cmd , params ){
 79     if ( typeof( cmd ) == "object" )
 80         return this._db._dbCommand( cmd );
 81     
 82     var c = {};
 83     c[cmd] = this.getName();
 84     if ( params )
 85         Object.extend( c , params );
 86     return this._db._dbCommand( c );    
 87 }
 88 
 89 DBCollection.prototype.runCommand = DBCollection.prototype._dbCommand;
 90 
 91 DBCollection.prototype._massageObject = function( q ){
 92     if ( ! q )
 93         return {};
 94 
 95     var type = typeof q;
 96 
 97     if ( type == "function" )
 98         return { $where : q };
 99 
100     if ( q.isObjectId )
101         return { _id : q };
102 
103     if ( type == "object" )
104         return q;
105 
106     if ( type == "string" ){
107         if ( q.length == 24 )
108             return { _id : q };
109 
110         return { $where : q };
111     }
112 
113     throw "don't know how to massage : " + type;
114 
115 }
116 
117 
118 DBCollection.prototype._validateObject = function( o ){
119     if ( o._ensureSpecial && o._checkModify )
120         throw "can't save a DBQuery object";
121 }
122 
123 DBCollection._allowedFields = { $id : 1 , $ref : 1 };
124 
125 DBCollection.prototype._validateForStorage = function( o ){
126     this._validateObject( o );
127     for ( var k in o ){
128         if ( k.indexOf( "." ) >= 0 ) {
129             throw "can't have . in field names [" + k + "]" ;
130         }
131 
132         if ( k.indexOf( "$" ) == 0 && ! DBCollection._allowedFields[k] ) {
133             throw "field names cannot start with $ [" + k + "]";
134         }
135 
136         if ( o[k] !== null && typeof( o[k] ) === "object" ) {
137             this._validateForStorage( o[k] );
138         }
139     }
140 };
141 
142 
143 DBCollection.prototype.find = function( query , fields , limit , skip ){
144     return new DBQuery( this._mongo , this._db , this ,
145                         this._fullName , this._massageObject( query ) , fields , limit , skip );
146 }
147 
148 DBCollection.prototype.findOne = function( query , fields ){
149     var cursor = this._mongo.find( this._fullName , this._massageObject( query ) || {} , fields , 
150         -1 /* limit */ , 0 /* skip*/, 0 /* batchSize */ , 0 /* options */ );
151     if ( ! cursor.hasNext() )
152         return null;
153     var ret = cursor.next();
154     if ( cursor.hasNext() ) throw "findOne has more than 1 result!";
155     if ( ret.$err )
156         throw "error " + tojson( ret );
157     return ret;
158 }
159 
160 DBCollection.prototype.insert = function( obj , _allow_dot ){
161     if ( ! obj )
162         throw "no object passed to insert!";
163     if ( ! _allow_dot ) {
164         this._validateForStorage( obj );
165     }
166     if ( typeof( obj._id ) == "undefined" ){
167         var tmp = obj; // don't want to modify input
168         obj = {_id: new ObjectId()};
169         for (var key in tmp){
170             obj[key] = tmp[key];
171         }
172     }
173     this._mongo.insert( this._fullName , obj );
174     this._lastID = obj._id;
175 }
176 
177 DBCollection.prototype.remove = function( t , justOne ){
178     for ( var k in t ){
179         if ( k == "_id" && typeof( t[k] ) == "undefined" ){
180             throw "can't have _id set to undefined in a remove expression"
181         }
182     }
183     this._mongo.remove( this._fullName , this._massageObject( t ) , justOne ? true : false );
184 }
185 
186 DBCollection.prototype.update = function( query , obj , upsert , multi ){
187     assert( query , "need a query" );
188     assert( obj , "need an object" );
189 
190     var firstKey = null;
191     for (var k in obj) { firstKey = k; break; }
192 
193     if (firstKey != null && firstKey[0] == '$') {
194         // for mods we only validate partially, for example keys may have dots
195         this._validateObject( obj );
196     } else {
197         // we're basically inserting a brand new object, do full validation
198         this._validateForStorage( obj );
199     }
200     this._mongo.update( this._fullName , query , obj , upsert ? true : false , multi ? true : false );
201 }
202 
203 DBCollection.prototype.save = function( obj ){
204     if ( obj == null || typeof( obj ) == "undefined" ) 
205         throw "can't save a null";
206 
207     if ( typeof( obj ) == "number" || typeof( obj) == "string" )
208         throw "can't save a number or string"
209 
210     if ( typeof( obj._id ) == "undefined" ){
211         obj._id = new ObjectId();
212         return this.insert( obj );
213     }
214     else {
215         return this.update( { _id : obj._id } , obj , true );
216     }
217 }
218 
219 DBCollection.prototype._genIndexName = function( keys ){
220     var name = "";
221     for ( var k in keys ){
222         var v = keys[k];
223         if ( typeof v == "function" )
224             continue;
225         
226         if ( name.length > 0 )
227             name += "_";
228         name += k + "_";
229 
230         if ( typeof v == "number" )
231             name += v;
232     }
233     return name;
234 }
235 
236 DBCollection.prototype._indexSpec = function( keys, options ) {
237     var ret = { ns : this._fullName , key : keys , name : this._genIndexName( keys ) };
238 
239     if ( ! options ){
240     }
241     else if ( typeof ( options ) == "string" )
242         ret.name = options;
243     else if ( typeof ( options ) == "boolean" )
244         ret.unique = true;
245     else if ( typeof ( options ) == "object" ){
246         if ( options.length ){
247             var nb = 0;
248             for ( var i=0; i<options.length; i++ ){
249                 if ( typeof ( options[i] ) == "string" )
250                     ret.name = options[i];
251                 else if ( typeof( options[i] ) == "boolean" ){
252                     if ( options[i] ){
253                         if ( nb == 0 )
254                             ret.unique = true;
255                         if ( nb == 1 )
256                             ret.dropDups = true;
257                     }
258                     nb++;
259                 }
260             }
261         }
262         else {
263             Object.extend( ret , options );
264         }
265     }
266     else {
267         throw "can't handle: " + typeof( options );
268     }
269     /*
270         return ret;
271 
272     var name;
273     var nTrue = 0;
274     
275     if ( ! isObject( options ) ) {
276         options = [ options ];
277     }
278     
279     if ( options.length ){
280         for( var i = 0; i < options.length; ++i ) {
281             var o = options[ i ];
282             if ( isString( o ) ) {
283                 ret.name = o;
284             } else if ( typeof( o ) == "boolean" ) {
285 	        if ( o ) {
286 		    ++nTrue;
287 	        }
288             }
289         }
290         if ( nTrue > 0 ) {
291 	    ret.unique = true;
292         }
293         if ( nTrue > 1 ) {
294 	    ret.dropDups = true;
295         }
296     }
297 */
298     return ret;
299 }
300 
301 DBCollection.prototype.createIndex = function( keys , options ){
302     var o = this._indexSpec( keys, options );
303     this._db.getCollection( "system.indexes" ).insert( o , true );
304 }
305 
306 DBCollection.prototype.ensureIndex = function( keys , options ){
307     var name = this._indexSpec( keys, options ).name;
308     this._indexCache = this._indexCache || {};
309     if ( this._indexCache[ name ] ){
310         return;
311     }
312 
313     this.createIndex( keys , options );
314     if ( this.getDB().getLastError() == "" ) {
315 	this._indexCache[name] = true;
316     }
317 }
318 
319 DBCollection.prototype.resetIndexCache = function(){
320     this._indexCache = {};
321 }
322 
323 DBCollection.prototype.reIndex = function() {
324     return this._db.runCommand({ reIndex: this.getName() });
325 }
326 
327 DBCollection.prototype.dropIndexes = function(){
328     this.resetIndexCache();
329 
330     var res = this._db.runCommand( { deleteIndexes: this.getName(), index: "*" } );
331     assert( res , "no result from dropIndex result" );
332     if ( res.ok )
333         return res;
334 
335     if ( res.errmsg.match( /not found/ ) )
336         return res;
337 
338     throw "error dropping indexes : " + tojson( res );
339 }
340 
341 
342 DBCollection.prototype.drop = function(){
343     if ( arguments.length > 0 )
344         throw "drop takes no argument";
345     this.resetIndexCache();
346     var ret = this._db.runCommand( { drop: this.getName() } );
347     if ( ! ret.ok ){
348         if ( ret.errmsg == "ns not found" )
349             return false;
350         throw "drop failed: " + tojson( ret );
351     }
352     return true;
353 }
354 
355 DBCollection.prototype.findAndModify = function(args){
356     var cmd = { findandmodify: this.getName() };
357     for (var key in args){
358         cmd[key] = args[key];
359     }
360 
361     var ret = this._db.runCommand( cmd );
362     if ( ! ret.ok ){
363         if (ret.errmsg == "No matching object found"){
364             return null;
365         }
366         throw "findAndModifyFailed failed: " + tojson( ret.errmsg );
367     }
368     return ret.value;
369 }
370 
371 DBCollection.prototype.renameCollection = function( newName , dropTarget ){
372     return this._db._adminCommand( { renameCollection : this._fullName , 
373                                      to : this._db._name + "." + newName , 
374                                      dropTarget : dropTarget } )
375 }
376 
377 DBCollection.prototype.validate = function() {
378     var res = this._db.runCommand( { validate: this.getName() } );
379 
380     res.valid = false;
381 
382     var raw = res.result || res.raw;
383 
384     if ( raw ){
385         var str = "-" + tojson( raw );
386         res.valid = ! ( str.match( /exception/ ) || str.match( /corrupt/ ) );
387 
388         var p = /lastExtentSize:(\d+)/;
389         var r = p.exec( str );
390         if ( r ){
391             res.lastExtentSize = Number( r[1] );
392         }
393     }
394 
395     return res;
396 }
397 
398 DBCollection.prototype.getShardVersion = function(){
399     return this._db._adminCommand( { getShardVersion : this._fullName } );
400 }
401 
402 DBCollection.prototype.getIndexes = function(){
403     return this.getDB().getCollection( "system.indexes" ).find( { ns : this.getFullName() } ).toArray();
404 }
405 
406 DBCollection.prototype.getIndices = DBCollection.prototype.getIndexes;
407 DBCollection.prototype.getIndexSpecs = DBCollection.prototype.getIndexes;
408 
409 DBCollection.prototype.getIndexKeys = function(){
410     return this.getIndexes().map(
411         function(i){
412             return i.key;
413         }
414     );
415 }
416 
417 
418 DBCollection.prototype.count = function( x ){
419     return this.find( x ).count();
420 }
421 
422 /**
423  *  Drop free lists. Normally not used.
424  *  Note this only does the collection itself, not the namespaces of its indexes (see cleanAll).
425  */
426 DBCollection.prototype.clean = function() {
427     return this._dbCommand( { clean: this.getName() } );
428 }
429 
430 
431 
432 /**
433  * <p>Drop a specified index.</p>
434  *
435  * <p>
436  * Name is the name of the index in the system.indexes name field. (Run db.system.indexes.find() to
437  *  see example data.)
438  * </p>
439  *
440  * <p>Note :  alpha: space is not reclaimed </p>
441  * @param {String} name of index to delete.
442  * @return A result object.  result.ok will be true if successful.
443  */
444 DBCollection.prototype.dropIndex =  function(index) {
445     assert(index , "need to specify index to dropIndex" );
446 
447     if ( ! isString( index ) && isObject( index ) )
448     	index = this._genIndexName( index );
449 
450     var res = this._dbCommand( "deleteIndexes" ,{ index: index } );
451     this.resetIndexCache();
452     return res;
453 }
454 
455 DBCollection.prototype.copyTo = function( newName ){
456     return this.getDB().eval(
457         function( collName , newName ){
458             var from = db[collName];
459             var to = db[newName];
460             to.ensureIndex( { _id : 1 } );
461             var count = 0;
462 
463             var cursor = from.find();
464             while ( cursor.hasNext() ){
465                 var o = cursor.next();
466                 count++;
467                 to.save( o );
468             }
469 
470             return count;
471         } , this.getName() , newName
472     );
473 }
474 
475 DBCollection.prototype.getCollection = function( subName ){
476     return this._db.getCollection( this._shortName + "." + subName );
477 }
478 
479 DBCollection.prototype.stats = function( scale ){
480     return this._db.runCommand( { collstats : this._shortName , scale : scale } );
481 }
482 
483 DBCollection.prototype.dataSize = function(){
484     return this.stats().size;
485 }
486 
487 DBCollection.prototype.storageSize = function(){
488     return this.stats().storageSize;
489 }
490 
491 DBCollection.prototype.totalIndexSize = function( verbose ){
492     var stats = this.stats();
493     if (verbose){
494         for (var ns in stats.indexSizes){
495             print( ns + "\t" + stats.indexSizes[ns] );
496         }
497     }
498     return stats.totalIndexSize;
499 }
500 
501 
502 DBCollection.prototype.totalSize = function(){
503     var total = this.storageSize();
504     var mydb = this._db;
505     var shortName = this._shortName;
506     this.getIndexes().forEach(
507         function( spec ){
508             var coll = mydb.getCollection( shortName + ".$" + spec.name );
509             var mysize = coll.storageSize();
510             //print( coll + "\t" + mysize + "\t" + tojson( coll.validate() ) );
511             total += coll.dataSize();
512         }
513     );
514     return total;
515 }
516 
517 
518 DBCollection.prototype.convertToCapped = function( bytes ){
519     if ( ! bytes )
520         throw "have to specify # of bytes";
521     return this._dbCommand( { convertToCapped : this._shortName , size : bytes } )
522 }
523 
524 DBCollection.prototype.exists = function(){
525     return this._db.system.namespaces.findOne( { name : this._fullName } );
526 }
527 
528 DBCollection.prototype.isCapped = function(){
529     var e = this.exists();
530     return ( e && e.options && e.options.capped ) ? true : false;
531 }
532 
533 DBCollection.prototype.distinct = function( keyString , query ){
534     var res = this._dbCommand( { distinct : this._shortName , key : keyString , query : query || {} } );
535     if ( ! res.ok )
536         throw "distinct failed: " + tojson( res );
537     return res.values;
538 }
539 
540 DBCollection.prototype.group = function( params ){
541     params.ns = this._shortName;
542     return this._db.group( params );
543 }
544 
545 DBCollection.prototype.groupcmd = function( params ){
546     params.ns = this._shortName;
547     return this._db.groupcmd( params );
548 }
549 
550 MapReduceResult = function( db , o ){
551     Object.extend( this , o );
552     this._o = o;
553     this._keys = Object.keySet( o );
554     this._db = db;
555     if ( this.result != null ) {
556         this._coll = this._db.getCollection( this.result );
557     }
558 }
559 
560 MapReduceResult.prototype._simpleKeys = function(){
561     return this._o;
562 }
563 
564 MapReduceResult.prototype.find = function(){
565     if ( this.results )
566         return this.results;
567     return DBCollection.prototype.find.apply( this._coll , arguments );
568 }
569 
570 MapReduceResult.prototype.drop = function(){
571     if ( this._coll ) {
572         return this._coll.drop();
573     }
574 }
575 
576 /**
577 * just for debugging really
578 */
579 MapReduceResult.prototype.convertToSingleObject = function(){
580     var z = {};
581     this._coll.find().forEach( function(a){ z[a._id] = a.value; } );
582     return z;
583 }
584 
585 DBCollection.prototype.convertToSingleObject = function(valueField){
586     var z = {};
587     this.find().forEach( function(a){ z[a._id] = a[valueField]; } );
588     return z;
589 }
590 
591 /**
592 * @param optional object of optional fields;
593 */
594 DBCollection.prototype.mapReduce = function( map , reduce , optionsOrOutString ){
595     var c = { mapreduce : this._shortName , map : map , reduce : reduce };
596     assert( optionsOrOutString , "need to an optionsOrOutString" )
597 
598     if ( typeof( optionsOrOutString ) == "string" )
599         c["out"] = optionsOrOutString;
600     else
601         Object.extend( c , optionsOrOutString );
602 
603     var raw = this._db.runCommand( c );
604     if ( ! raw.ok ){
605         __mrerror__ = raw;
606         throw "map reduce failed:" + tojson(raw);
607     }
608     return new MapReduceResult( this._db , raw );
609 
610 }
611 
612 DBCollection.prototype.toString = function(){
613     return this.getFullName();
614 }
615 
616 DBCollection.prototype.toString = function(){
617     return this.getFullName();
618 }
619 
620 
621 DBCollection.prototype.tojson = DBCollection.prototype.toString;
622 
623 DBCollection.prototype.shellPrint = DBCollection.prototype.toString;
624 
625 DBCollection.autocomplete = function(obj){
626     var colls = DB.autocomplete(obj.getDB());
627     var ret = [];
628     for (var i=0; i<colls.length; i++){
629         var c = colls[i];
630         if (c.length <= obj.getName().length) continue;
631         if (c.slice(0,obj.getName().length+1) != obj.getName()+'.') continue;
632 
633         ret.push(c.slice(obj.getName().length+1));
634     }
635     return ret;
636 }
637