mongosum
Version:
Maintains summary tables on Mongo collections, on top of Mongolian
310 lines (255 loc) • 9.19 kB
text/coffeescript
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: (column, options) -> return true
ignored_collections: ['system.indexes'],
track_collection: (collection, options) -> return true
Server.prototype.getSummaryOptions = () ->
DB.prototype.getSummaryOptions = () ->
.summaryOptions
Collection.prototype.getSummaryOptions = () ->
.server.summaryOptions
Server.prototype.db = (name) ->
if not [name]?
[name] = new DB @, name
[name].summary = new Collection [name], collection_name
return [name]
DB.prototype.collection = (name) ->
return [name] or ([name] = new Collection @, name)
###
# Retrieve the summary for this collection
###
Collection.prototype.getSummary = (callback) ->
if is collection_name
throw 'MongoSum cannot get the summary of the summarys collection.'
criteria = _collection:
.summary.find(criteria).next (err, summary = {}) =>
summary._collection ?=
summary._length ?= 0
callback err, summary
###
# Set the summary for this collection (used internally)
###
Collection.prototype.setSummary = (summary, incs, callback) ->
if is collection_name
throw 'MongoSum cannot set the summary of the summarys collection'
criteria = _collection:
summary._collection =
summary._updated = +new Date
delete summary._length
.summary.update criteria, summary, true, (err) =>
.summary.update criteria, {$inc: incs}, true, callback
###
# Do a full-table update of the summary. This is expensive.
###
Collection.prototype.rebuildSummary = (callback) ->
options =
summary =
_collection:
_length: 0
each = (object) =>
summary._length++
summary, object
.forEach each, () =>
summary, callback
###
# INTERNAL. Merge summary, save it, and fire the callback.
###
Collection.prototype._merge_summarys = (err, data, callback, options, summary) ->
(err, full_summary) =>
incs = {
_length: data.length or 1
}
summary._length = 0
# full_summary = full_summary, summary, options
full_summary = full_summary, summary, options, (key, vals, types, full_key) ->
if vals[1] and vals[1].type is 'Number' and vals[1].sum
incs[full_key.join('.') + '.sum'] = vals[1].sum
if not vals[0] and vals[1]
vals[0] = JSON.parse JSON.stringify vals[1]
if vals[0].sum
vals[0].sum = vals[1].sum = 0
if vals[0] and vals[1]
if vals[0].type is 'Number' and vals[1].type is '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 and vals[1].example
vals[0].example = vals[1].example
return vals[0]
full_summary, incs, () ->
callback and callback err, data
Collection.prototype._drop = Collection.prototype.drop
Collection.prototype.drop = () ->
.summary.remove _collection:
.apply this, arguments
Collection.prototype._insert = Collection.prototype.insert
Collection.prototype.insert = (object, callback) ->
options =
if ( is collection_name) or ( in options.ignored_collections) or (not options.track_collection , options)
return Collection.prototype._insert.apply this, arguments
summary = _length: 0
update_summary = (err, data) =>
if not err
summary, data
if Object::toString.call(object) isnt '[object Array]'
object = [object]
complete = 0
for obj in object
obj, (err, data) =>
update_summary err, data
summary._length++
if ++complete is object.length
err, data, callback, {}, summary
Collection.prototype._update = Collection.prototype.update
Collection.prototype.update = (criteria, object, upsert, multi, callback) ->
options =
if ( is collection_name) or ( in options.ignored_collections) or (not options.track_collection , options)
return Collection.prototype._update.apply this, arguments
if not callback and typeof multi is 'function'
callback = multi
multi = false
if not callback and typeof upsert is 'function'
callback = upsert
upsert = false
if callback and typeof callback isnt 'function'
throw 'Callback is not a function!'
# Process for an update:
# Do a find on the criteria specified
# Do a findAndModify
# If the update returns, subtract original and add updated
summary = _length: 0
subtract_summary = (err, data) =>
if not err and data
summary, ( data), {
sum: (a, b) -> return (b is null and -a) or (a - b)
min: (a, b) -> a
max: (a, b) -> a
}
update_summary = (err, data) =>
if not err and data
summary, data
if Object::toString.call(object) isnt '[object Array]'
object = [object]
if multi isnt true
object = [object.shift()]
options =
remove: false
new: true
upsert: !! upsert
merge_opts =
min: (a, b) ->
if isNaN(parseInt(a)) or (b == a)
throw 'FULL UPDATE'
return Math.min a, b
max: (a, b) ->
if isNaN(parseInt(a)) or (b == a)
throw 'FULL UPDATE'
return Math.max a, b
.toArray (err, _originals = []) =>
originals = {}
originals[o._id.toString()] = o for o in _originals
for_merge = []
complete = 0
for obj in object
opts =
query: criteria
update: obj
options: options
remove: false
new: true
upsert: !!upsert
opts, (err, data) =>
if not err and data
subtract_summary err, originals[data._id.toString()]
if not err
for_merge.push data
if ++complete is object.length
try
update_summary null, data for data in for_merge
err, data, callback, merge_opts, summary
catch e
if e is 'FULL UPDATE'
callback
else
throw e
Collection.prototype._remove = Collection.prototype.remove
Collection.prototype.remove = (criteria, callback) ->
options =
if ( is collection_name) or ( in options.ignored_collections) or (not options.track_collection , options)
return Collection.prototype._remove.apply this, arguments
if not callback and typeof criteria is 'function'
callback = criteria
criteria = {}
summary = {_length: 0}
subtract_summary = (err, data) =>
if not err and data
summary, ( data), {
sum: (a, b) -> return (b is null and -a) or (a - b)
min: (a, b) -> a
max: (a, b) -> a
}
merge_opts =
min: (a, b) ->
if isNaN(parseInt(a)) or (b == a)
throw 'FULL UPDATE'
return Math.min a, b
max: (a, b) ->
if isNaN(parseInt(a)) or (b == a)
throw 'FULL UPDATE'
return Math.max a, b
.toArray (err, data) =>
data = data or []
for row in data
summary._length--
subtract_summary err, row
try
err, data, (() -> null), merge_opts, summary
criteria, callback
catch e
if e is 'FULL UPDATE'
callback
else
throw e
Collection.prototype._get_summary = (object) ->
object, {}, {}, (key, vals, types) ->
ret = {}
ret.type = types[0]
ret.example = vals[0]
if types[0] is 'Number' or vals[0] = parseInt vals[0], 10
ret.min = ret.max = ret.sum = vals[0]
return ret
Collection.prototype._merge_summary = (left, right, options = {}) ->
options.sum ?= (a, b) -> return (parseInt(a, 10) + parseInt(b, 10)) or a
options.min ?= Math.min
options.max ?= Math.max
left, right, {}, (key, vals, types) ->
if not vals[0] and vals[1]
vals[0] = JSON.parse JSON.stringify vals[1]
if vals[1].sum then vals[1].sum = null
if vals[0] and vals[0].type and vals[1] and vals[1].type
if vals[0].type is 'Number' and vals[1].type is '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] and vals[1].example) or vals[0].example
return vals[0]
Collection.prototype._walk_objects = (first = {}, second = {}, options, fn, full_key = []) ->
keys = (k for k,v of first)
(keys.push k for k,v of second when k not in keys)
sopts =
for key in keys when (key not in sopts.ignored_columns) or sopts.track_column key, sopts
v1 = first[key]
v2 = second[key]
type = (o) -> (o? and o.constructor and o.constructor.name) or 'Null'
if (type(v1) in ['Object', 'Array'] and not v1.type?) or (type(v2) in ['Object', 'Array'] and not v2.type?)
first[key] = 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, val of first when key in sopts.ignored_columns or not sopts.track_column key, sopts
delete first[key]
return first
module.exports = Server