GroupDocumentSource.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. "use strict";
  2. var DocumentSource = require("./DocumentSource"),
  3. Accumulators = require("../accumulators/"),
  4. Document = require("../Document"),
  5. Expression = require("../expressions/Expression"),
  6. ConstantExpression = require("../expressions/ConstantExpression"),
  7. FieldPathExpression = require("../expressions/FieldPathExpression");
  8. /**
  9. * A class for grouping documents together
  10. * @class GroupDocumentSource
  11. * @namespace mungedb-aggregate.pipeline.documentSources
  12. * @module mungedb-aggregate
  13. * @constructor
  14. * @param [ctx] {ExpressionContext}
  15. **/
  16. var GroupDocumentSource = module.exports = function GroupDocumentSource(expCtx) {
  17. if (arguments.length > 1) throw new Error("up to one arg expected");
  18. base.call(this, expCtx);
  19. this.populated = false;
  20. this.idExpression = null;
  21. this.groups = {}; // GroupsType Value -> Accumulators[]
  22. this.groupsKeys = []; // This is to faciliate easier look up of groups
  23. this.originalGroupsKeys = []; // This stores the original group key un-hashed/stringified/whatever
  24. this.fieldNames = [];
  25. this.accumulatorFactories = [];
  26. this.expressions = [];
  27. this.currentDocument = null;
  28. this.currentGroupsKeysIndex = 0;
  29. }, klass = GroupDocumentSource, base = DocumentSource, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
  30. klass.groupOps = {
  31. "$addToSet": Accumulators.AddToSet,
  32. "$avg": Accumulators.Avg,
  33. "$first": Accumulators.First,
  34. "$last": Accumulators.Last,
  35. "$max": Accumulators.MinMax.createMax,
  36. "$min": Accumulators.MinMax.createMin,
  37. "$push": Accumulators.Push,
  38. "$sum": Accumulators.Sum
  39. };
  40. klass.groupName = "$group";
  41. proto.getSourceName = function getSourceName() {
  42. return klass.groupName;
  43. };
  44. /**
  45. * Create an object that represents the document source. The object
  46. * will have a single field whose name is the source's name. This
  47. * will be used by the default implementation of addToJsonArray()
  48. * to add this object to a pipeline being represented in JSON.
  49. *
  50. * @method sourceToJson
  51. * @param {Object} builder JSONObjBuilder: a blank object builder to write to
  52. * @param {Boolean} explain create explain output
  53. **/
  54. proto.sourceToJson = function sourceToJson(builder, explain) {
  55. var idExp = this.idExpression,
  56. insides = {
  57. _id: idExp ? idExp.toJSON() : {}
  58. },
  59. aFac = this.accumulatorFactories,
  60. aFacLen = aFac.length;
  61. for(var i=0; i < aFacLen; ++i) {
  62. var acc = new aFac[i](/*pExpCtx*/);
  63. acc.addOperand(this.expressions[i]);
  64. insides[this.fieldNames[i]] = acc.toJSON(true);
  65. }
  66. builder[this.getSourceName()] = insides;
  67. };
  68. klass.createFromJson = function createFromJson(groupObj, ctx) {
  69. if (!(groupObj instanceof Object && groupObj.constructor === Object)) throw new Error("a group's fields must be specified in an object");
  70. var idSet = false,
  71. group = new GroupDocumentSource(ctx);
  72. for (var groupFieldName in groupObj) {
  73. if (groupObj.hasOwnProperty(groupFieldName)) {
  74. var groupField = groupObj[groupFieldName];
  75. if (groupFieldName === "_id") {
  76. if(idSet) throw new Error("15948 a group's _id may only be specified once");
  77. if (groupField instanceof Object && groupField.constructor === Object) {
  78. var objCtx = new Expression.ObjectCtx({isDocumentOk:true});
  79. group.idExpression = Expression.parseObject(groupField, objCtx);
  80. idSet = true;
  81. } else if (typeof groupField === "string") {
  82. if (groupField[0] !== "$") {
  83. group.idExpression = new ConstantExpression(groupField);
  84. } else {
  85. var pathString = Expression.removeFieldPrefix(groupField);
  86. group.idExpression = new FieldPathExpression(pathString);
  87. }
  88. idSet = true;
  89. } else {
  90. var typeStr = group._getTypeStr(groupField);
  91. switch (typeStr) {
  92. case "number":
  93. case "string":
  94. case "boolean":
  95. case "Object":
  96. case "object": // null returns "object" Xp
  97. case "Array":
  98. group.idExpression = new ConstantExpression(groupField);
  99. idSet = true;
  100. break;
  101. default:
  102. throw new Error("a group's _id may not include fields of type " + typeStr + "");
  103. }
  104. }
  105. } else {
  106. if (groupFieldName.indexOf(".") !== -1) throw new Error("16414 the group aggregate field name '" + groupFieldName + "' cannot contain '.'");
  107. if (groupFieldName[0] === "$") throw new Error("15950 the group aggregate field name '" + groupFieldName + "' cannot be an operator name");
  108. if (group._getTypeStr(groupFieldName) === "Object") throw new Error("15951 the group aggregate field '" + groupFieldName + "' must be defined as an expression inside an object");
  109. var subFieldCount = 0;
  110. for (var subFieldName in groupField) {
  111. if (groupField.hasOwnProperty(subFieldName)) {
  112. var subField = groupField[subFieldName],
  113. op = klass.groupOps[subFieldName];
  114. if (!op) throw new Error("15952 unknown group operator '" + subFieldName + "'");
  115. var groupExpression,
  116. subFieldTypeStr = group._getTypeStr(subField);
  117. if (subFieldTypeStr === "Object") {
  118. var subFieldObjCtx = new Expression.ObjectCtx({isDocumentOk:true});
  119. groupExpression = Expression.parseObject(subField, subFieldObjCtx);
  120. } else if (subFieldTypeStr === "Array") {
  121. throw new Error("15953 aggregating group operators are unary (" + subFieldName + ")");
  122. } else {
  123. groupExpression = Expression.parseOperand(subField);
  124. }
  125. group.addAccumulator(groupFieldName,op, groupExpression);
  126. ++subFieldCount;
  127. }
  128. }
  129. if (subFieldCount != 1) throw new Error("15954 the computed aggregate '" + groupFieldName + "' must specify exactly one operator");
  130. }
  131. }
  132. }
  133. if (!idSet) throw new Error("15955 a group specification must include an _id");
  134. return group;
  135. };
  136. proto._getTypeStr = function _getTypeStr(obj) {
  137. var typeofStr = typeof obj,
  138. typeStr = (typeofStr == "object" && obj !== null) ? obj.constructor.name : typeofStr;
  139. return typeStr;
  140. };
  141. proto.advance = function advance() {
  142. base.prototype.advance.call(this); // Check for interupts ????
  143. if(!this.populated) this.populate();
  144. //verify(this.currentGroupsKeysIndex < this.groupsKeys.length);
  145. ++this.currentGroupsKeysIndex;
  146. if (this.currentGroupsKeysIndex >= this.groupsKeys.length) {
  147. this.currentDocument = null;
  148. return false;
  149. }
  150. this.currentDocument = this.makeDocument(this.currentGroupsKeysIndex);
  151. return true;
  152. };
  153. proto.eof = function eof() {
  154. if (!this.populated) this.populate();
  155. return this.currentGroupsKeysIndex === this.groupsKeys.length;
  156. };
  157. proto.getCurrent = function getCurrent() {
  158. if (!this.populated) this.populate();
  159. return this.currentDocument;
  160. };
  161. proto.getDependencies = function getDependencies(deps) {
  162. var self = this;
  163. // add _id
  164. this.idExpression.addDependencies(deps);
  165. // add the rest
  166. this.fieldNames.forEach(function (field, i) {
  167. self.expressions[i].addDependencies(deps);
  168. });
  169. return DocumentSource.GetDepsReturn.EXHAUSTIVE;
  170. };
  171. proto.addAccumulator = function addAccumulator(fieldName, accumulatorFactory, expression) {
  172. this.fieldNames.push(fieldName);
  173. this.accumulatorFactories.push(accumulatorFactory);
  174. this.expressions.push(expression);
  175. };
  176. proto.populate = function populate() {
  177. for (var hasNext = !this.source.eof(); hasNext; hasNext = this.source.advance()) {
  178. var group,
  179. currentDocument = this.source.getCurrent(),
  180. _id = this.idExpression.evaluate(currentDocument);
  181. if (undefined === _id) _id = null;
  182. var idHash = JSON.stringify(_id); //TODO: USE A REAL HASH. I didn't have time to take collision into account.
  183. if (idHash in this.groups) {
  184. group = this.groups[idHash];
  185. } else {
  186. this.groups[idHash] = group = [];
  187. this.groupsKeys[this.currentGroupsKeysIndex] = idHash;
  188. this.originalGroupsKeys[this.currentGroupsKeysIndex] = (_id && typeof _id === 'object') ? Document.clone(_id) : _id;
  189. ++this.currentGroupsKeysIndex;
  190. for (var ai = 0; ai < this.accumulatorFactories.length; ++ai) {
  191. var accumulator = new this.accumulatorFactories[ai]();
  192. accumulator.addOperand(this.expressions[ai]);
  193. group.push(accumulator);
  194. }
  195. }
  196. // tickle all the accumulators for the group we found
  197. for (var gi = 0; gi < group.length; ++gi) {
  198. group[gi].evaluate(currentDocument);
  199. }
  200. }
  201. this.currentGroupsKeysIndex = 0; // Start the group
  202. if (this.groupsKeys.length > 0) {
  203. this.currentDocument = this.makeDocument(this.currentGroupsKeysIndex);
  204. }
  205. this.populated = true;
  206. };
  207. proto.makeDocument = function makeDocument(groupKeyIndex) {
  208. var groupKey = this.groupsKeys[groupKeyIndex],
  209. originalGroupKey = this.originalGroupsKeys[groupKeyIndex],
  210. group = this.groups[groupKey],
  211. doc = {};
  212. doc[Document.ID_PROPERTY_NAME] = originalGroupKey;
  213. for (var i = 0; i < this.fieldNames.length; ++i) {
  214. var fieldName = this.fieldNames[i],
  215. item = group[i];
  216. if (item !== "null" && item !== undefined) {
  217. doc[fieldName] = item.getValue();
  218. }
  219. }
  220. return doc;
  221. };