tingodb
Version:
Embedded Node.js database upward compatible with MongoDB
1,364 lines (1,265 loc) • 35.2 kB
JavaScript
var safe = require('safe');
var _ = require('lodash');
var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var tcursor = require('./tcursor');
var wqueue = require('./wqueue');
var tindex = require('./tindex');
var tcache = require("./tcache");
var Code = require('./tcode').Code;
var tutils = require('./utils');
var Updater = require('./updater');
var Buffer = require('safe-buffer').Buffer;
function tcoll(tdb) {
this._tdb = null;
this._name = null;
this._store = Object.create(null);
this._fd = null;
this._fsize = null;
this._id = 1;
this._wq = new wqueue();
this._tq = null;
this._idx = Object.create(null);
this._cache = null;
// this._mc = {};
this._check1 = Math.random()*100+1;
// native mongo db compatibility attrs
this.collectionName = null;
if (tdb._stype=="mem") {
this.init = this.initM;
this._put = this._putM;
this._get = this._getM;
} else {
this.init = this.initFS;
this._put = this._putFS;
this._get = this._getFS;
}
}
module.exports = tcoll;
tcoll.prototype._tuneApi = function () {
if (this._tdb._gopts.apiLevel>=200) {
this.findAndModify=this._findAndModify200;
} else {
this.findAndModify=this._findAndModify140;
}
}
tcoll.prototype.initM = function (tdb, name, options, create, cb) {
var self= this;
this._tdb = tdb;
this._tuneApi();
tdb._mstore = tdb._mstore || Object.create(null);
this.collectionName = this._name = name;
if (options.strict) {
var exists = tdb._mstore[name];
if (exists && create) return cb(new Error("Collection " + self._name + " already exists. Currently in safe mode."));
else if (!exists && !create) return cb(new Error("Collection " + self._name + " does not exist. Currently in safe mode."));
}
tdb._mstore[name] = this._mstore = tdb._mstore[name] || [];
for (var k=0; k< this._mstore.length; k++) {
var o = this._mstore[k];
if (o) {
self._store[simplifyKey(o._id)]={pos:k+1};
}
}
this._tq = new wqueue(100, function (cb) {
// update indexes
safe.eachOfSeries(self._store, function (rec, k, cb) {
self._get(rec.pos, false, safe.sure(cb, function (obj) {
var id = simplifyKey(obj._id);
_.each(self._idx,function(v) {
v.set(obj, id);
});
cb();
}));
}, cb);
});
self.ensureIndex({_id: 1}, {name: '_id_', unique: true}, cb);
};
tcoll.prototype.initFS = function (tdb, name, options, create, cb) {
var self= this;
this._tdb = tdb;
this._tuneApi();
this._cache = new tcache(tdb, tdb._gopts.cacheSize);
this._cmaxobj = tdb._gopts.cacheMaxObjSize || 1024;
this.collectionName = this._name = name;
this._filename = path.join(this._tdb._path, this._name);
if (options.strict) {
var exists = fs.existsSync(self._filename);
if (exists && create) return cb(new Error("Collection " + self._name + " already exists. Currently in safe mode."));
else if (!exists && !create) return cb(new Error("Collection " + self._name + " does not exist. Currently in safe mode."));
}
var pos = 0;
var deleted = 0;
var found = 0;
this._tq = new wqueue(100, function (cb) {
safe.run(function (cb) {
fs.open(self._filename, "a+", safe.sure(cb, function (fd) {
self._fd = fd;
var b1 = new Buffer(45);
safe.whilst(function () { return self._fsize === null; }, function(cb) {
safe.run(function (cb) {
fs.read(fd, b1, 0, 45, pos, safe.sure(cb, function (bytes, data) {
if (bytes===0) {
self._fsize = pos;
return cb();
}
var h1 = JSON.parse(data.toString());
h1.o = parseInt(h1.o,10);
h1.k = parseInt(h1.k,10);
var b2 = new Buffer(h1.k);
fs.read(fd,b2,0,h1.k,pos+45+1, safe.sure(cb, function (bytes, data) {
var k = JSON.parse(data.toString());
self._id = k._uid;
if (k._a=='del') {
delete self._store[k._id];
deleted++;
} else {
if (self._store[k._id]) deleted++;
self._store[k._id] = { pos: pos, sum: k._s };
}
pos+=45+3+h1.o+h1.k;
found++;
cb();
}));
}));
}, function (err) {
if (err)
cb(new Error(self._name+": Error during load - "+err.toString()));
else
cb();
});
}, cb);
}));
}, function (err) {
if (!found && err)
return cb(err); // nothing read and error, just rise it
safe.run(function (cb) {
var size = _.size(self._store);
// autocompact on certain ratio or err
if (deleted > size || err) {
self._compact(function (errCompact) {
if (errCompact && err)
cb(errCompact);
else {
if (errCompact) console.log(err);
cb();
}
});
} else cb();
}, function () {
self._refreshIndexes(cb);
});
});
});
self.ensureIndex({_id: 1}, {name: '_id_', unique: true}, cb);
};
tcoll.prototype.compactCollection = function (cb) {
var self = this;
self._tq.add(function (cb) {
self._compact(safe.sure(cb, function () {
self._cache.clear();
self._refreshIndexes(cb);
}));
}, true, cb);
};
tcoll.prototype._refreshIndexes = function (cb) {
var self = this;
_.each(self._idx,function(v) {
v.clear();
});
safe.eachOfSeries(self._store, function (rec, k, cb) {
self._get(rec.pos, false, safe.sure(cb, function (obj) {
var id = simplifyKey(obj._id);
_.each(self._idx,function(v) {
v.set(obj, id);
});
cb();
}));
}, cb);
};
tcoll.prototype._compact = function (cb) {
var self = this;
var filename = self._filename + '.compact';
fs.open(filename, 'w+', safe.sure(cb, function (fd) {
var b1 = new Buffer(45);
function get(pos, cb) {
fs.read(self._fd, b1, 0, 45, pos, safe.sure(cb, function (bytes, data) {
var h1 = JSON.parse(data.toString());
h1.o = parseInt(h1.o, 10);
h1.k = parseInt(h1.k, 10);
var b2 = new Buffer(h1.k + h1.o + 3);
fs.read(self._fd, b2, 0, b2.length, pos + 45, safe.sure(cb, function (/*bytes*/) {
cb(null, Buffer.concat([ b1, b2 ]));
}));
}));
}
var wpos = 0;
var store = Object.create(null);
safe.eachOfSeries(self._store, function (rec, k, cb) {
get(rec.pos, safe.sure(cb, function (data) {
fs.write(fd, data, 0, data.length, wpos, safe.sure(cb, function (written) {
if (written != data.length) return cb(new Error('Insufficient disk space'));
store[k] = { pos: wpos, sum: rec.sum };
wpos += data.length;
cb();
}));
}));
}, safe.sure(function (err) {
fs.close(fd, safe.sure(cb, function () {
fs.unlink(filename, safe.sure(cb, function () {
cb(err);
}));
}));
}, function () {
if (process.platform.match(/^win/)) {
// WINDOWS: unsafe because if something fail while renaming file it will not
// restore automatically
fs.close(self._fd, safe.sure(cb, function () {
fs.close(fd, safe.sure(cb, function () {
fs.unlink(self._filename, safe.sure(cb, function () {
fs.rename(filename, self._filename, safe.sure(cb, function () {
fs.open(self._filename, 'a+', safe.sure(cb, function (fd) {
self._fd = fd;
self._fsize = wpos;
self._store = store;
cb();
}));
}));
}));
}));
}));
} else {
// safe way
fs.rename(filename, self._filename, safe.sure(cb, function () {
fs.close(self._fd, safe.sure(cb, function () {
self._fd = fd;
self._fsize = wpos;
self._store = store;
cb();
}));
}));
}
}));
}));
};
tcoll.prototype.drop = function (cb) {
this._tdb.dropCollection(this._name,cb);
};
tcoll.prototype.rename = function (nname, opts, cb) {
var self = this;
if (_.isFunction(opts)) {
cb = opts;
opts = {};
}
var err = self._tdb._nameCheck(nname);
if (err)
return safe.back(cb,err);
if (self._tdb._stype=="mem") {
delete self._tdb._cols[self._name];
self._tdb._cols[nname] = self;
delete self._tdb._mstore[self._name];
self._tdb._mstore[nname] = self._mstore;
safe.back(cb,null);
} else {
self._tq.add(function (cb) {
fs.rename(path.join(self._tdb._path,self._name),path.join(self._tdb._path,nname),safe.sure(cb, function () {
delete self._tdb._cols[self._name];
self._tdb._cols[nname] = self;
self.collectionName = self._name = nname;
cb();
}));
},true,cb);
}
};
tcoll.prototype._stop = function (cb) {
var self = this;
self._tq.add(function (cb) {
// this will prevent any tasks processed on this instance
self._tq._stoped = true;
if (self._fd) {
fs.close(self._fd,safe.sure(cb, function () {
cb(null,true);
}));
} else
cb(null,false);
},true,cb);
};
tcoll.prototype.createIndex = tcoll.prototype.ensureIndex = function (obj, options, cb) {
var self = this;
if (_.isFunction(options)) {
cb = options;
options = {};
}
cb = cb || function () {};
options = options || {};
var c = new tcursor(this,{},{},{});
c.sort(obj);
if (c._err)
return safe.back(cb,c._err);
var key = c._sort;
if (key===null)
return safe.back(cb,new Error("No fields are specified"));
var index = self._idx[key];
if (index)
return safe.back(cb,null, index.name);
// force array support when global option is set
if (_.isUndefined(options._tiarr) && self._tdb._gopts.searchInArray)
options._tiarr = true;
var name = options.name || _.map(key, function (v) { return v[0] + '_' + v[1]; }).join('_');
index = new tindex(key, self, options, name);
if (self._tq._tc==-1) {
// if no operation is pending just register index
self._idx[key] = index;
safe.back(cb, null, index.name);
}
else {
// overwise register index operation
this._tq.add(function (cb) {
var range = _.values(self._store);
safe.eachOfSeries(range, function (rec, k, cb) {
self._get(rec.pos, false, safe.sure(cb, function (obj) {
index.set(obj,simplifyKey(obj._id));
cb();
}));
}, safe.sure(cb, function () {
self._idx[key] = index;
cb();
}));
}, true, function (err) {
if (err) cb(err);
else cb(null, index.name);
});
}
};
tcoll.prototype.indexExists = function (idx, cb) {
if (!_.isArray(idx))
idx = [idx];
var i = _.intersection(idx, _.map(this._idx, 'name'));
cb(null,i.length == idx.length);
};
tcoll.prototype.indexes = function (cb) {
var self = this;
this._tq.add(function (cb) {
cb(null, _.values(self._idx));
},false,cb);
};
tcoll.prototype._getM = function (pos, unsafe, cb) {
safe.back(cb,null,unsafe?this._mstore[pos-1]:this._tdb._cloneDeep(this._mstore[pos-1]));
};
tcoll.prototype._getFS = function (pos, unsafe, cb) {
var self = this;
var cached = self._cache.get(pos,unsafe);
if (cached)
return safe.back(cb,null,cached);
var b1 = new Buffer(45);
fs.read(self._fd, b1, 0, 45, pos, safe.sure(cb, function (bytes, data) {
var h1 = JSON.parse(data.toString());
h1.o = parseInt(h1.o,10);
h1.k = parseInt(h1.k,10);
var b2 = new Buffer(h1.o);
fs.read(self._fd, b2, 0, h1.o, pos + 45 + 2 + h1.k, safe.sure(cb, function (bytes, data) {
var obj = self._unwrapTypes(JSON.parse(data.toString()));
if (bytes <= self._cmaxobj)
self._cache.set(pos, obj);
cb(null,obj);
}));
}));
};
tcoll.prototype.insert = function (docs, opts, cb ) {
var self = this;
if (_.isFunction(opts) && cb == null) {
cb = opts;
opts = {};
}
opts = opts || {};
if (opts.w>0 && !_.isFunction(cb))
throw new Error("Callback is required for safe update");
cb = cb || function () {};
if (!_.isArray(docs))
docs = [docs];
this._tq.add(function (cb) {
safe.eachOfSeries(docs, function (doc, k, cb) {
if (_.isUndefined(doc._id)) {
doc._id = new self._tdb.ObjectID();
}
self._put(doc, false, cb);
}, safe.sure(cb, function () {
cb(null, docs);
}));
}, true, cb);
};
tcoll.prototype._wrapTypes = function(obj) {
var self = this;
_.each(obj, function (v,k) {
if (_.isDate(v))
obj[k] = {$wrap:"$date",v:v.valueOf(),h:v};
else if (v instanceof self._tdb.ObjectID)
obj[k] = {$wrap:"$oid",v:v.toJSON()};
else if (v instanceof self._tdb.Binary)
obj[k] = {$wrap: "$bin", v: v.toJSON()};
else if (_.isObject(v))
self._wrapTypes(v);
});
return obj;
};
tcoll.prototype._ensureIds = function(obj) {
var self = this;
if (_.isFunction(obj.toBSON)) obj = obj.toBSON();
_.each(obj, function (v,k) {
if (k.length >0) {
if (k[0]=='$')
throw new Error("key "+k+" must not start with '$'");
if (k.indexOf('.')!=-1)
throw new Error("key "+k+" must not contain '.'");
}
if (_.isObject(v) && !self._tdb._gopts.nativeObjectID) {
if (v instanceof self._tdb.ObjectID) {
if (v.id<0) {
v._persist(++self._id);
}
}
else
self._ensureIds(v);
}
});
return obj;
};
tcoll.prototype._unwrapTypes = function(obj) {
var self = this;
_.each(obj, function (v,k) {
if (_.isObject(v)) {
switch (v.$wrap) {
case "$date": obj[k] = new Date(v.v); break;
case "$oid":
var oid = new self._tdb.ObjectID(v.v);
obj[k]=oid;
break;
case "$bin":
var bin = new self._tdb.Binary(new Buffer(v.v, 'base64'));
obj[k] = bin;
break;
default: self._unwrapTypes(v);
}
}
});
return obj;
};
tcoll.prototype._putM = function (item_, remove, cb) {
var item = this._tdb._cloneDeep(item_);
var self = this;
self._wq.add(function (cb) {
var err = _.attempt(function () {
item = self._ensureIds(item);
});
if (err) {
err.errmsg = err.toString();
return cb(err);
}
if (_.isUndefined(item._id))
return cb(new Error("Invalid object key (_id)"));
var key = {_id:simplifyKey(item._id)};
// check index update
if (item && !remove) {
err = _.attempt(function () {
_.each(self._idx,function(v) {
v.set(item,key._id,true);
});
});
if (err) {
err.errmsg = err.toString();
return cb(err);
}
}
if (remove) {
self._mstore[self._store[key._id].pos-1]=null;
delete self._store[key._id];
}
else {
if (self._store[key._id]) {
self._mstore[self._store[key._id].pos-1] = item;
} else {
self._mstore.push(item);
self._store[key._id] = {pos: self._mstore.length};
}
}
// update index
_.each(self._idx,function(v) {
if (!remove)
v.set(item,key._id);
else
v.del(item,key._id);
});
cb(null);
}, true, cb);
};
tcoll.prototype._putFS = function (item, remove, cb) {
var self = this;
self._wq.add(function (cb) {
var err = _.attempt(function () {
item = self._ensureIds(item);
});
if (err) {
err.errmsg = err.toString();
return cb(err);
}
if (_.isUndefined(item._id))
return cb(new Error("Invalid object key (_id)"));
item = self._wrapTypes(item);
var sobj = new Buffer(remove?"":JSON.stringify(item));
item = self._unwrapTypes(item);
var key = {_id:simplifyKey(item._id),_uid:self._id,_dt:(new Date()).valueOf()};
if (remove) key._a = "del";
else {
var hash = crypto.createHash('md5');
hash.update(sobj, 'utf8');
key._s = hash.digest('hex');
}
var skey = new Buffer(JSON.stringify(key));
var zeros = "0000000000";
var lobj = sobj.length.toString();
var lkey = skey.length.toString();
lobj = zeros.substr(0,zeros.length - lobj.length)+lobj;
lkey = zeros.substr(0,zeros.length - lkey.length)+lkey;
var h1={k:lkey,o:lobj,v:"001"};
var buf = new Buffer(JSON.stringify(h1)+"\n"+skey+"\n"+sobj+"\n");
// check index update
if (item && !remove) {
err = _.attempt(function () {
_.each(self._idx,function(v) {
v.set(item,key._id,true);
});
});
if (err) {
err.errmsg = err.toString();
return cb(err);
}
}
safe.run(function (cb) {
var rec = self._store[key._id];
if (rec && rec.sum == key._s) return safe.back(cb);
fs.write(self._fd, buf, 0, buf.length, self._fsize, safe.sure(cb, function (written) {
if (remove)
delete self._store[key._id];
else
self._store[key._id] = { pos: self._fsize, sum: key._s };
if (remove || sobj.length > self._cmaxobj)
self._cache.unset(self._fsize);
else
self._cache.set(self._fsize,item);
self._fsize+=written;
// randomly check for non exclusive file usage
// which is growth of file that we are nor aware
// randomly to avoid overhead
if (self._check1==0) {
this._check1 = Math.random()*100+1;
fs.fstat(self._fd, safe.sure(cb, function (stat) {
if (self._fsize!=stat.size)
cb(new Error("File size mismatch. Are you use db/collection exclusively?"));
else
cb();
}));
} else {
self._check1--;
cb();
}
}));
},
function () {
// update index
_.each(self._idx,function(v) {
if (!remove)
v.set(item,key._id);
else
v.del(item,key._id);
});
cb(null);
});
}, true, cb);
};
tcoll.prototype.count = function (query, options, cb) {
var self = this;
if (arguments.length == 1) {
cb = arguments[0];
options = null;
query = null;
}
if (arguments.length == 2) {
query = arguments[0];
cb = arguments[1];
options = null;
}
if (query==null || _.size(query)==0) {
this._tq.add(function (cb) {
cb(null, _.size(self._store));
},false,cb);
} else
self.find(query, options).count(cb);
};
tcoll.prototype.stats = function (cb) {
var self = this;
this._tq.add(function (cb) {
cb(null, {count:_.size(self._store)});
},false,cb);
};
var findOpts = ['limit','sort','fields','skip','hint','timeout','batchSize','safe','w'];
tcoll.prototype.findOne = _.rest(function (args) {
var cb = args.pop();
this.find.apply(this,args).limit(1).nextObject(cb);
});
tcoll.prototype.find = function () {
var cb = null, query = {}, opts = {}, fields = null, skip = null, limit = null, sort = null;
var argc = arguments.length;
if (argc>0) {
// guess callback, it is always latest
cb = arguments[argc-1];
if (!_.isFunction(cb))
cb=null;
else
argc--;
if (argc>0) {
// query should always exist
query = arguments[0];
if (argc>1) {
if (argc==2) {
var val = arguments[1];
// worst case we get either options either fiels
if (_(val).keys().intersection(findOpts).size() > 0)
opts = val;
else
fields = val;
} else {
fields = arguments[1];
if (argc == 3)
opts = arguments[2];
else {
skip = arguments[2];
limit = arguments[3];
}
}
}
}
}
opts = opts || {};
skip = skip || opts.skip || null;
limit = limit || opts.limit || null;
fields = fields || opts.fields || null;
sort = sort || opts.sort || null;
var c = new tcursor(this,query, fields, opts);
if (skip) c.skip(skip);
if (limit) c.limit(limit);
if (sort) c.sort(sort);
if (cb)
cb(null, c);
else
return c;
};
function simplifyKey(key) {
var k = key;
if (key.toJSON)
k = key.toJSON();
if (_.isNumber(k)||_.isString(k))
return k;
return k.toString();
}
tcoll.prototype.update = function (query, doc, opts, cb) {
var self = this;
if (_.isFunction(opts) && cb == null) {
cb = opts;
}
opts = opts || {};
if (opts.w>0 && !_.isFunction(cb))
throw new Error("Callback is required for safe update");
cb = cb || function () {};
if (!_.isObject(query))
throw new Error("selector must be a valid JavaScript object");
if (!_.isObject(doc))
throw new Error("document must be a valid JavaScript object");
var multi = opts.multi || false;
var updater = new Updater(doc, self._tdb);
var $doc = updater.hasAtomic()?null:doc;
this._tq.add(function (cb) {
self.__find(query, null, 0, multi ? null : 1, null, opts.hint, {}, safe.sure(cb, function (res) {
if (res.length==0) {
if (opts.upsert) {
$doc = $doc || query;
$doc = self._tdb._cloneDeep($doc);
updater.update($doc,true);
if (_.isUndefined($doc._id))
$doc._id = new self._tdb.ObjectID();
self._put($doc, false, safe.sure(cb, function () {
cb(null, 1,{updatedExisting:false,upserted:$doc._id,n:1});
}));
} else
cb(null,0);
} else {
safe.eachOfSeries(res, function (pos, k, cb) {
self._get(pos, false, safe.sure(cb, function (obj) {
// remove current version of doc from indexes
_.each(self._idx,function(v) {
v.del(obj,simplifyKey(obj._id));
});
var udoc = $doc;
if (!$doc) {
udoc = obj;
updater.update(udoc);
}
udoc._id = obj._id;
// put will add it back to indexes
self._put(udoc, false, cb);
}));
}, safe.sure(cb,function () {
cb(null, res.length, {updatedExisting:true,n:res.length});
}));
}
}));
},true,cb);
};
tcoll.prototype._findAndModify200 = function () {
var cb = arguments[arguments.length-1];
var args = Array.prototype.slice.call(arguments)
args[args.length-1] = function (err,res) {
if (err) cb(err)
else cb(null,{value:res,ok:1});
}
this._findAndModify140.apply(this, args)
}
tcoll.prototype._findAndModify140 = function (query, sort, doc, opts, cb) {
var self = this;
if (_.isFunction(opts) && cb == null) {
cb = opts;
opts = {};
}
opts = opts || {};
doc = doc || {};
var updater = new Updater(doc, self._tdb);
var $doc = updater.hasAtomic()?null:doc;
var c = new tcursor(this,{}, opts.fields || {},{});
c.sort(sort);
if (c._err)
return safe.back(cb,c._err);
this._tq.add(function (cb) {
self.__find(query, null, 0, 1, c._sort, opts.hint, {}, safe.sure(cb, function (res) {
if (res.length==0) {
if (opts.upsert) {
$doc = $doc || query;
$doc = self._tdb._cloneDeep($doc);
updater.update($doc,true);
if (_.isUndefined($doc._id))
$doc._id = new self._tdb.ObjectID();
self._put($doc, false, safe.sure(cb, function () {
cb(null,opts.new?c._projectFields($doc):{});
}));
} else
cb();
} else {
self._get(res[0], false, safe.sure(cb, function (obj) {
var robj = (opts.new && !opts.remove) ? obj : self._tdb._cloneDeep(obj);
// remove current version of doc from indexes
_.each(self._idx,function(v) {
v.del(obj,simplifyKey(obj._id));
});
var udoc = $doc;
if (!$doc) {
udoc = obj;
updater.update(udoc);
}
udoc._id = obj._id;
// put will add it back to indexes
self._put(udoc, opts.remove?true:false, safe.sure(cb,function () {
cb(null,c._projectFields(robj));
}));
}));
}
}));
},true,cb);
};
tcoll.prototype.save = function (doc, opts, cb) {
var self = this;
cb = _.isFunction(doc)?doc:_.isFunction(opts)?opts:cb;
cb = cb || function () {};
doc = doc || {};
opts = opts || {};
this._tq.add(function (cb) {
var res = doc;
(function(cb) {
if (_.isUndefined(doc._id)) {
doc._id = new self._tdb.ObjectID();
cb();
} else {
var id = simplifyKey(doc._id);
var pos = self._store[id];
// check if document with this id already exist
if (pos) {
// if so we need to fetch it to update index
self._get(pos.pos, false, safe.sure(cb, function (oldDoc) {
// remove current version of doc from indexes
_.each(self._idx,function(v) {
v.del(oldDoc,id);
});
res = 1;
cb();
}));
} else cb();
}
})(safe.sure(cb, function () {
self._put(doc, false, safe.sure(cb, function () {
cb(null,res); // when update return 1 when new save return obj
}));
}));
},true,cb);
};
tcoll.prototype.remove = function (query, opts, cb) {
var self = this;
if (_.isFunction(query)) {
cb = query;
query = opts = {};
} else if (_.isFunction(opts)) {
cb = opts;
opts = {};
}
opts = opts || {};
if (opts.w>0 && !_.isFunction(cb))
throw new Error("Callback is required");
cb = cb || function () {};
var single = opts.single || false;
this._tq.add(function (cb) {
self.__find(query, null, 0, single ? 1 : null, null, opts.hint, {}, safe.sure(cb, function (res) {
safe.eachOfSeries(res, function (pos, k, cb) {
self._get(pos, false, safe.sure(cb, function (obj) {
self._put(obj,true,cb);
}));
}, safe.sure(cb, function () {
cb(null,res.length);
}));
}));
},true,cb);
};
tcoll.prototype.findAndRemove = function (query,sort,opts,cb) {
var self = this;
if (_.isFunction(sort) && cb == null && opts==null) {
cb = sort;
sort = {};
opts = {};
} else if (_.isFunction(opts) && cb == null) {
cb = opts;
opts = {};
}
opts = opts || {};
sort = sort || {};
var c = new tcursor(this,{},{},{});
// Fix for mongoouse/tungus they pass sort as undefined
c.sort(sort);
if (c._err)
return safe.back(cb,c._err);
this._tq.add(function (cb) {
self.__find(query, null, 0, 1, c._sort, opts.hint, {}, safe.sure(cb, function (res) {
if (res.length==0)
return cb();
self._get(res[0], false, safe.sure(cb, function (obj) {
self._put(obj,true,safe.sure(cb, function () {
cb(null,obj);
}));
}));
}));
},true,cb);
};
tcoll.prototype._bestSortIndex = function (sort) {
// no sort
if (!sort) return null;
// exact match
if (this._idx[sort]) return this._idx[sort];
// find potential sort indexes
var pi = [];
_.each(this._idx,function (idx) {
var fields = idx.fields();
var match = _.takeWhile(fields, function (kv, i) {
return i < sort.length ? kv[0] == sort[i][0] : false;
});
if (match.length == sort.length) {
var score = fields.length;
_.each(sort,function (kv, i) {
if (kv[1] != fields[i][1]) score += 1;
});
pi.push({ value: idx, score: score });
}
});
if (pi.length === 0) return null;
// select best index
pi = pi.sort(function (l, r) { return l.score < r.score; });
return pi[0].value;
};
function reduceIndexSet(pi) {
var hit;
do {
hit = false;
// compare each potential index with each other
_.each(pi,function (v1, i1) {
_.each(pi,function (v2, i2) {
if (i1 == i2) return;
// compare the set of possible keys for both indexes
if (_.union(v1.k, v2.k).length == v1.k.length) {
// key for v2 is a subset of key for v1, check equality
if (v1.k.length == v2.k.length && v1.i.depth() > v2.i.depth()) {
// keys are equal, but the depth of v2 is lower;
// v2 is preferable, strike out v1
pi.splice(i1, 1);
} else {
// in other two cases v1 is preferable, strike out v2
pi.splice(i2, 1);
}
hit = true;
return false;
}
});
if (hit) return false;
});
} while (hit);
}
tcoll.prototype.__find = function (query, fields, skip, limit, sort, hint, arFields, cb) {
var self = this;
var range;
// find sort index
var si = this._bestSortIndex(sort);
// for non empty query check indexes that we can use
var qt = self._tdb.Finder.matcher(query);
var pi = [];
if (_.size(qt)>0) {
_.each(self._idx,function (i) {
var f = _.map(i.fields(), 0);
var e = _.takeWhile(f, function (k) {
return qt._ex(k) == 1 && (!hint || hint[k]);
});
if (e.length > 0) pi.push({ i: i, k: e, e: f.length > e.length });
});
}
// if possible indexes found split the query and process
// indexes separately
if (!_.isEmpty(pi)) {
// choose the most appropriate indexes
reduceIndexSet(pi);
// split query
var io = Object.create(null);
_.each(pi,function (v) {
_.each(v.k,function (k) {
if (!io[k]) io[k] = qt.split(k);
});
});
// process indexes
var p = _.map(pi,function (st) {
// this action applies to all indexes
var r = io[st.k[0]]._index(st.i);
// process subfields of compound index
_.each(st.k.slice(1),function (k) {
var v = io[k];
r = _(r).map(function (si) { return v._index(si); }).flatten().value();
});
// expand subindexes to plain ids
if (st.e) r = _(r).map(function (si) { return si.all(); }).flatten().value();
// store result of index search
return r;
});
if (p.length == 1) {
p = p[0];
// optimization for the case when search and sorting indexes are the same
if (si && pi[0].i === si) {
var sif = si.fields();
if (_.every(sort, function (v, i) { return sif[i][1] == v[1]; })) {
// sort order exactly matches index order,
// so the result is already sorted
sort = null;
} else if (_.every(sort, function (v, i) { return sif[i][1] == -v[1]; })) {
// sort order is exactly opposite to index order,
// so the result is sorted, but in reverse direction
p.reverse();
sort = null;
}
}
} else {
// TODO: use sort index as intersect base to speedup sorting
p = tutils.intersectIndexes(p);
}
// nowe we have ids, need to convert them to positions
range = _.map(p,function (_id) {
return self._store[_id].pos;
});
} else {
if (si) {
range = _.map(si.all(_.map(sort, 1)), function (_id) {
return self._store[_id].pos;
});
//if (order==-1)
// range.reverse();
sort = null;
} else
range = _.map(self._store, 'pos');
}
if (sort && si) {
var ps = Object.create(null);
_.each(range, function (pos) {
ps[pos] = true;
});
range = [];
_.each(si.all(_.map(sort, 1)), function (_id) {
var pos = self._store[_id].pos;
if (_.has(ps, pos))
range.push(pos);
});
//if (order == -1)
// range.reverse();
sort = null;
}
// no sort, no query then return right away
if (sort==null && (_.size(qt)==0 || qt._args.length==0)) {
if (skip!=0 || limit!=null) {
var c = Math.min(range.length-skip,limit?limit:range.length-skip);
range = range.splice(skip,c);
}
return safe.back(cb,null,range);
}
// check if we can use simple match or array match function
var arrayMatch = false;
if (self._tdb._gopts.searchInArray)
arrayMatch = true;
else {
_.each(qt.fields(), function (v,k) {
if (_.get(arFields, k)) {
arrayMatch = true;
return false;
}
});
}
var matcher = new Function("obj", "return " + (arrayMatch ? qt.native3() : qt.native()));
// create sort index
if (sort) {
si = new tindex(sort,self);
}
// now simple non-index search
var res = [];
var found = 0;
safe.eachOfSeries(range, function (pos, k, cb) {
if (sort==null && limit && res.length>=limit)
return safe.back(cb);
self._get(pos, true, safe.sure(cb, function (obj) {
if (matcher(obj)) {
if (sort!=null || found>=skip) {
if (sort==null)
res.push(pos);
else
si.set(obj,pos);
}
found++;
}
cb();
}));
}, safe.sure(cb, function () {
if (sort) {
res = si.all();
//if (order==-1) {
// res.reverse();
//}
if (skip!=0 || limit!=null) {
var c = Math.min(res.length-skip,limit?limit:res.length-skip);
res = res.splice(skip,c);
}
}
cb(null, res);
}));
};
tcoll.prototype._find = function (query, fields, skip, limit, sort_, hint, arFields, cb) {
var self = this;
this._tq.add(function (cb) {
self.__find(query, fields, skip, limit, sort_, hint, arFields, cb);
}, false, cb);
};
function code2fn(obj) {
if (_.isObject(obj)) {
_.each(obj,function (value, key) {
if (value instanceof Code) {
with (value.scope) {
obj[key] = eval('(' + value.code + ')');
}
}
else code2fn(value);
});
}
}
tcoll.prototype.mapReduce = function (map, reduce, opts, cb) {
var self = this;
if (_.isFunction(opts)) {
cb = opts;
opts = {};
}
if (!opts.out) return safe.back(cb, new Error('the out option parameter must be defined'));
if (!opts.out.inline && !opts.out.replace) {
return safe.back(cb, new Error('the only supported out options are inline and replace'));
}
code2fn(opts.scope);
var m = Object.create(null);
var finalize = opts.finalize;
function emit(k, v) {
var values = m[k];
if (!values) m[k] = [ v ];
else {
values.push(v);
if (values.length > 1000) values = [ reduce(k, values) ];
}
}
with(opts.scope || {}) {
try {
if (map instanceof Code) {
with(map.scope) {
map = eval('(' + map.code + ')');
}
} else {
map = eval('(' + map + ')');
}
if (reduce instanceof Code) {
with(reduce.scope) {
reduce = eval('(' + reduce.code + ')');
}
} else {
reduce = eval('(' + reduce + ')');
}
if (finalize instanceof Code) {
with(finalize.scope) {
finalize = eval('(' + finalize.code + ')');
}
} else if (finalize) {
finalize = eval('(' + finalize + ')');
}
} catch (e) {
return safe.back(cb, e);
}
}
self.find(opts.query, null, { limit: opts.limit, sort: opts.sort }, safe.sure(cb, function (c) {
var doc;
safe.doUntil(
function (cb) {
c.nextObject(safe.sure(cb, function (_doc) {
doc = _doc;
if (doc) map.call(doc);
cb();
}));
},
function () {
return doc === null;
},
safe.sure(cb, function () {
_.each(m,function (v, k) {
v = v.length > 1 ? reduce(k, v) : v[0];
if (finalize) v = finalize(k, v);
m[k] = v;
});
var stats = {};
if (opts.out.inline)
return safe.back(cb, null, _.values(m), stats); // execute outside of trap
// write results to collection
safe.waterfall([
function (cb) {
self._tdb.collection(opts.out.replace, { strict: 1 }, function (err, col) {
if (err) return cb(null, null);
col.drop(cb);
});
},
function (arg, cb) {
self._tdb.collection(opts.out.replace, {}, cb);
},
function (col, cb) {
var docs = _.map(m, function (value, key) {
return {
_id: key,
value: value
};
});
col.insert(docs, safe.sure(cb, function () {
if (opts.verbose) cb(null, col, stats);
else cb(null, col);
}));
}
], cb);
}
)); // doUntil
}));
};
tcoll.prototype.group = _.rest(function (keys, condition, initial, args) {
var self = this;
var callback = args.pop(),
reduce = args.length ? args.shift() : null,
finalize = args.length ? args.shift() : null,
command = args.length ? args.shift() : null,
options = args.length ? args.shift() : {};
if (!_.isFunction(finalize)) {
command = finalize;
finalize = null;
}
code2fn(options.scope);
with (options.scope || {}) {
try {
if (_.isFunction(keys)) keys = eval('(' + keys + ')');
else if (keys instanceof Code) {
with (keys.scope) {
keys = eval('(' + keys.code + ')');
}
}
if (reduce instanceof Code) {
with (reduce.scope) {
reduce = eval('(' + reduce.code + ')');
}
} else reduce = eval('(' + reduce + ')');
if (finalize instanceof Code) {
with (finalize.scope) {
finalize = eval('(' + finalize.code + ')');
}
} else finalize = eval('(' + finalize + ')');
} catch (e) {
return callback(e);
}
}
var m = Object.create(null);
self.find(condition, safe.sure(callback, function (c) {
var doc;
safe.doUntil(
function (cb) {
c.nextObject(safe.sure(cb, function (_doc) {
doc = _doc;
if (!doc) return cb();
var keys2 = keys;
if (_.isFunction(keys)) keys2 = keys(doc);
if (!_.isArray(keys2)) {
var keys3 = [];
_.each(keys2,function (v, k) {
if (v) keys3.push(k);
});
keys2 = keys3;
}
var key = Object.create(null);
_.each(keys2,function (k) {
key[k] = doc[k];
});
var skey = JSON.stringify(key);
var obj = m[skey];
if (!obj) obj = m[skey] = _.extend({}, key, initial);
try {
reduce(doc, obj);
} catch (e) {
return cb(e);
}
cb();
}));
},
function () {
return doc === null;
},
safe.sure(callback, function () {
var result = _.values(m);
if (finalize) {
_.each(result,function (value) {
finalize(value);
});
}
callback(null, result);
})
);
}));
});