mongosum
Version:
Maintains summary tables on Mongo collections, on top of Mongolian
499 lines (463 loc) • 14.8 kB
JavaScript
// Generated by CoffeeScript 1.3.3
(function() {
var Collection, DB, Server, collection_name,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
Server = require('mongolian');
DB = require('mongolian/lib/db.js');
Collection = require('mongolian/lib/collection.js');
collection_name = '_summaries';
Server.prototype.summaryOptions = {
ignored_columns: ['_id'],
track_column: function(column, options) {
return true;
},
ignored_collections: ['system.indexes'],
track_collection: function(collection, options) {
return true;
}
};
Server.prototype.getSummaryOptions = function() {
return this.summaryOptions;
};
DB.prototype.getSummaryOptions = function() {
return this.server.summaryOptions;
};
Collection.prototype.getSummaryOptions = function() {
return this.db.server.summaryOptions;
};
Server.prototype.db = function(name) {
if (!(this._dbs[name] != null)) {
this._dbs[name] = new DB(this, name);
this._dbs[name].summary = new Collection(this._dbs[name], collection_name);
}
return this._dbs[name];
};
DB.prototype.collection = function(name) {
return this._collections[name] || (this._collections[name] = new Collection(this, name));
};
/*
# Retrieve the summary for this collection
*/
Collection.prototype.getSummary = function(callback) {
var criteria,
_this = this;
if (this.name === collection_name) {
throw 'MongoSum cannot get the summary of the summarys collection.';
}
criteria = {
_collection: this.name
};
return this.db.summary.find(criteria).next(function(err, summary) {
var _ref, _ref1;
if (summary == null) {
summary = {};
}
if ((_ref = summary._collection) == null) {
summary._collection = _this.name;
}
if ((_ref1 = summary._length) == null) {
summary._length = 0;
}
return callback(err, summary);
});
};
/*
# Set the summary for this collection (used internally)
*/
Collection.prototype.setSummary = function(summary, incs, callback) {
var criteria,
_this = this;
if (this.name === collection_name) {
throw 'MongoSum cannot set the summary of the summarys collection';
}
criteria = {
_collection: this.name
};
summary._collection = this.name;
summary._updated = +(new Date);
delete summary._length;
return this.db.summary.update(criteria, summary, true, function(err) {
return _this.db.summary.update(criteria, {
$inc: incs
}, true, callback);
});
};
/*
# Do a full-table update of the summary. This is expensive.
*/
Collection.prototype.rebuildSummary = function(callback) {
var each, options, summary,
_this = this;
options = this.getSummaryOptions();
summary = {
_collection: this.name,
_length: 0
};
each = function(object) {
summary._length++;
return _this._merge_summary(summary, _this._get_summary(object));
};
return this.find().forEach(each, function() {
return _this.setSummary(summary, callback);
});
};
/*
# INTERNAL. Merge summary, save it, and fire the callback.
*/
Collection.prototype._merge_summarys = function(err, data, callback, options, summary) {
var _this = this;
return this.getSummary(function(err, full_summary) {
var incs;
incs = {
_length: data.length || 1
};
summary._length = 0;
full_summary = _this._walk_objects(full_summary, summary, options, function(key, vals, types, full_key) {
if (vals[1] && vals[1].type === 'Number' && vals[1].sum) {
incs[full_key.join('.') + '.sum'] = vals[1].sum;
}
if (!vals[0] && vals[1]) {
vals[0] = JSON.parse(JSON.stringify(vals[1]));
if (vals[0].sum) {
vals[0].sum = vals[1].sum = 0;
}
}
if (vals[0] && vals[1]) {
if (vals[0].type === 'Number' && vals[1].type === 'Number') {
vals[0].min = Math.min(vals[0].min, vals[1].min);
vals[0].max = Math.max(vals[0].max, vals[1].max);
}
if (vals[0].example && vals[1].example) {
vals[0].example = vals[1].example;
}
}
return vals[0];
});
return _this.setSummary(full_summary, incs, function() {
return callback && callback(err, data);
});
});
};
Collection.prototype._drop = Collection.prototype.drop;
Collection.prototype.drop = function() {
this.db.summary.remove({
_collection: this.name
});
return this._drop.apply(this, arguments);
};
Collection.prototype._insert = Collection.prototype.insert;
Collection.prototype.insert = function(object, callback) {
var complete, obj, options, summary, update_summary, _i, _len, _ref, _results,
_this = this;
options = this.getSummaryOptions();
if ((this.name === collection_name) || (_ref = this.name, __indexOf.call(options.ignored_collections, _ref) >= 0) || (!options.track_collection(this.name, options))) {
return Collection.prototype._insert.apply(this, arguments);
}
summary = {
_length: 0
};
update_summary = function(err, data) {
if (!err) {
return _this._merge_summary(summary, _this._get_summary(data));
}
};
if (Object.prototype.toString.call(object) !== '[object Array]') {
object = [object];
}
complete = 0;
_results = [];
for (_i = 0, _len = object.length; _i < _len; _i++) {
obj = object[_i];
_results.push(this._insert(obj, function(err, data) {
update_summary(err, data);
summary._length++;
if (++complete === object.length) {
return _this._merge_summarys(err, data, callback, {}, summary);
}
}));
}
return _results;
};
Collection.prototype._update = Collection.prototype.update;
Collection.prototype.update = function(criteria, object, upsert, multi, callback) {
var merge_opts, options, subtract_summary, summary, update_summary, _ref,
_this = this;
options = this.getSummaryOptions();
if ((this.name === collection_name) || (_ref = this.name, __indexOf.call(options.ignored_collections, _ref) >= 0) || (!options.track_collection(this.name, options))) {
return Collection.prototype._update.apply(this, arguments);
}
if (!callback && typeof multi === 'function') {
callback = multi;
multi = false;
}
if (!callback && typeof upsert === 'function') {
callback = upsert;
upsert = false;
}
if (callback && typeof callback !== 'function') {
throw 'Callback is not a function!';
}
summary = {
_length: 0
};
subtract_summary = function(err, data) {
if (!err && data) {
return _this._merge_summary(summary, _this._get_summary(data), {
sum: function(a, b) {
return (b === null && -a) || (a - b);
},
min: function(a, b) {
return a;
},
max: function(a, b) {
return a;
}
});
}
};
update_summary = function(err, data) {
if (!err && data) {
return _this._merge_summary(summary, _this._get_summary(data));
}
};
if (Object.prototype.toString.call(object) !== '[object Array]') {
object = [object];
}
if (multi !== true) {
object = [object.shift()];
}
options = {
remove: false,
"new": true,
upsert: !!upsert
};
merge_opts = {
min: function(a, b) {
if (isNaN(parseInt(a)) || (b === a)) {
throw 'FULL UPDATE';
}
return Math.min(a, b);
},
max: function(a, b) {
if (isNaN(parseInt(a)) || (b === a)) {
throw 'FULL UPDATE';
}
return Math.max(a, b);
}
};
return this.find(criteria).toArray(function(err, _originals) {
var complete, for_merge, o, obj, opts, originals, _i, _j, _len, _len1, _results;
if (_originals == null) {
_originals = [];
}
originals = {};
for (_i = 0, _len = _originals.length; _i < _len; _i++) {
o = _originals[_i];
originals[o._id.toString()] = o;
}
for_merge = [];
complete = 0;
_results = [];
for (_j = 0, _len1 = object.length; _j < _len1; _j++) {
obj = object[_j];
opts = {
query: criteria,
update: obj,
options: options,
remove: false,
"new": true,
upsert: !!upsert
};
_results.push(_this.findAndModify(opts, function(err, data) {
var _k, _len2;
if (!err && data) {
subtract_summary(err, originals[data._id.toString()]);
if (!err) {
for_merge.push(data);
}
if (++complete === object.length) {
try {
for (_k = 0, _len2 = for_merge.length; _k < _len2; _k++) {
data = for_merge[_k];
update_summary(null, data);
}
return _this._merge_summarys(err, data, callback, merge_opts, summary);
} catch (e) {
if (e === 'FULL UPDATE') {
return _this.updateSummary(callback);
} else {
throw e;
}
}
}
}
}));
}
return _results;
});
};
Collection.prototype._remove = Collection.prototype.remove;
Collection.prototype.remove = function(criteria, callback) {
var merge_opts, options, subtract_summary, summary, _ref,
_this = this;
options = this.getSummaryOptions();
if ((this.name === collection_name) || (_ref = this.name, __indexOf.call(options.ignored_collections, _ref) >= 0) || (!options.track_collection(this.name, options))) {
return Collection.prototype._remove.apply(this, arguments);
}
if (!callback && typeof criteria === 'function') {
callback = criteria;
criteria = {};
}
summary = {
_length: 0
};
subtract_summary = function(err, data) {
if (!err && data) {
return _this._merge_summary(summary, _this._get_summary(data), {
sum: function(a, b) {
return (b === null && -a) || (a - b);
},
min: function(a, b) {
return a;
},
max: function(a, b) {
return a;
}
});
}
};
merge_opts = {
min: function(a, b) {
if (isNaN(parseInt(a)) || (b === a)) {
throw 'FULL UPDATE';
}
return Math.min(a, b);
},
max: function(a, b) {
if (isNaN(parseInt(a)) || (b === a)) {
throw 'FULL UPDATE';
}
return Math.max(a, b);
}
};
return this.find(criteria).toArray(function(err, data) {
var row, _i, _len;
data = data || [];
for (_i = 0, _len = data.length; _i < _len; _i++) {
row = data[_i];
summary._length--;
subtract_summary(err, row);
}
try {
_this._merge_summarys(err, data, (function() {
return null;
}), merge_opts, summary);
return _this._remove(criteria, callback);
} catch (e) {
if (e === 'FULL UPDATE') {
return _this.updateSummary(callback);
} else {
throw e;
}
}
});
};
Collection.prototype._get_summary = function(object) {
return this._walk_objects(object, {}, {}, function(key, vals, types) {
var ret;
ret = {};
ret.type = types[0];
ret.example = vals[0];
if (types[0] === 'Number' || (vals[0] = parseInt(vals[0], 10))) {
ret.min = ret.max = ret.sum = vals[0];
}
return ret;
});
};
Collection.prototype._merge_summary = function(left, right, options) {
var _ref, _ref1, _ref2;
if (options == null) {
options = {};
}
if ((_ref = options.sum) == null) {
options.sum = function(a, b) {
return (parseInt(a, 10) + parseInt(b, 10)) || a;
};
}
if ((_ref1 = options.min) == null) {
options.min = Math.min;
}
if ((_ref2 = options.max) == null) {
options.max = Math.max;
}
return this._walk_objects(left, right, {}, function(key, vals, types) {
if (!vals[0] && vals[1]) {
vals[0] = JSON.parse(JSON.stringify(vals[1]));
if (vals[1].sum) {
vals[1].sum = null;
}
}
if (vals[0] && vals[0].type && vals[1] && vals[1].type) {
if (vals[0].type === 'Number' && vals[1].type === 'Number') {
vals[0].min = options.min(vals[0].min, vals[1].min);
vals[0].max = options.max(vals[0].max, vals[1].max);
vals[0].sum = options.sum(vals[0].sum, vals[1].sum);
}
vals[0].example = (vals[1] && vals[1].example) || vals[0].example;
}
return vals[0];
});
};
Collection.prototype._walk_objects = function(first, second, options, fn, full_key) {
var k, key, keys, sopts, type, v, v1, v2, val, _i, _len, _ref, _ref1;
if (first == null) {
first = {};
}
if (second == null) {
second = {};
}
if (full_key == null) {
full_key = [];
}
keys = (function() {
var _results;
_results = [];
for (k in first) {
v = first[k];
_results.push(k);
}
return _results;
})();
for (k in second) {
v = second[k];
if (__indexOf.call(keys, k) < 0) {
keys.push(k);
}
}
sopts = this.getSummaryOptions();
for (_i = 0, _len = keys.length; _i < _len; _i++) {
key = keys[_i];
if (!((__indexOf.call(sopts.ignored_columns, key) < 0) || sopts.track_column(key, sopts))) {
continue;
}
v1 = first[key];
v2 = second[key];
type = function(o) {
return ((o != null) && o.constructor && o.constructor.name) || 'Null';
};
if ((((_ref = type(v1)) === 'Object' || _ref === 'Array') && !(v1.type != null)) || (((_ref1 = type(v2)) === 'Object' || _ref1 === 'Array') && !(v2.type != null))) {
first[key] = this._walk_objects(v1, v2, options, fn, full_key.concat([key]));
} else {
first[key] = fn(key, [v1, v2], [type(v1), type(v2)], full_key.concat([key]));
}
}
for (key in first) {
val = first[key];
if (__indexOf.call(sopts.ignored_columns, key) >= 0 || !sopts.track_column(key, sopts)) {
delete first[key];
}
}
return first;
};
module.exports = Server;
}).call(this);