GroupDocumentSource.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. var DocumentSource = require("./DocumentSource"),
  2. Document = require("../Document"),
  3. Expression = require("../expressions/Expression"),
  4. Accumulators = require("../accumulators/"),
  5. GroupDocumentSource = module.exports = (function(){
  6. // CONSTRUCTOR
  7. /**
  8. * A base class for all document sources
  9. * @param {ExpressionContext}
  10. **/
  11. var klass = module.exports = GroupDocumentSource = function GroupDocumentSource(groupElement){
  12. if(!(groupElement instanceof Object && groupElement.constructor.name === "Object") || Object.keys(groupElement).length < 1)
  13. throw new Error("a group's fields must be specified in an object");
  14. this.populated = false;
  15. this.idExpression = null;
  16. this.groups = {}; // GroupsType Value -> Accumulators[]
  17. this.groupsKeys = []; // This is to faciliate easier look up of groups
  18. this.fieldNames = [];
  19. this.accumulatorFactories = [];
  20. this.expressions = [];
  21. this.currentDocument = null;
  22. this.groupCurrentIndex = 0;
  23. var groupObj = groupElement[this.getSourceName()];
  24. for(var groupFieldName in groupObj){
  25. if(groupObj.hasOwnProperty(groupFieldName)){
  26. var groupField = groupObj[groupFieldName];
  27. if(groupFieldName === "_id"){
  28. if(groupField instanceof Object && groupField.constructor.name === "Object"){
  29. var objCtx = new Expression.ObjectCtx({isDocumentOk:true});
  30. this.idExpression = Expression.parseObject(groupField, objCtx);
  31. }else if( typeof groupField === "string"){
  32. if(groupField[0] !== "$")
  33. this.idExpression = new ConstantExpression(groupField);
  34. var pathString = Expression.removeFieldPrefix(groupField);
  35. this.idExpression = new FieldPathExpression(pathString);
  36. }else{
  37. var typeStr = this._getTypeStr(groupField);
  38. switch(typeStr){
  39. case "number":
  40. case "string":
  41. case "boolean":
  42. case "object":
  43. this.idExpression = new ConstantExpression(groupField);
  44. break;
  45. default:
  46. throw new Error("a group's _id may not include fields of type " + typeStr + "");
  47. }
  48. }
  49. }else{
  50. if(groupFieldName.indexOf(".") !== -1)
  51. throw new Error("16414 the group aggregate field name '" + groupFieldName + "' cannot contain '.'");
  52. if(groupFieldName[0] === "$")
  53. throw new Error("15950 the group aggregate field name '" + groupFieldName + "' cannot be an operator name");
  54. if(this._getTypeStr(groupFieldName) === "object")
  55. throw new Error("15951 the group aggregate field '" + groupFieldName + "' must be defined as an expression inside an object");
  56. var subFieldCount = 0;
  57. for(var subFieldName in groupField){
  58. if(groupField.hasOwnProperty(subFieldName)){
  59. var subField = groupField[subField],
  60. op = DocumentSource.GroupOps[subFieldName];
  61. if(!op)
  62. throw new Exception("15952 unknown group operator '" + subFieldName + "'");
  63. var groupExpression,
  64. subFieldTypeStr = this._getTypeStr(subField);
  65. if(subFieldTypeStr === "object"){
  66. var subFieldObjCtx = new Expression.ObjectCtx({isDocumentOk:true});
  67. groupExpression = Expression.parseObject(groupField, subFieldObjCtx);
  68. }else if(subFieldTypeStr === "Array"){
  69. throw new Exception("15953 aggregating group operators are unary (" + subFieldName + ")");
  70. }else{
  71. groupExpression = Expression.parseOperand(subField);
  72. }
  73. this.addAccumulator(groupFieldName,op, groupExpression);
  74. ++subFieldCount;
  75. }
  76. if(subFieldCount != 1)
  77. throw new Error("15954 the computed aggregate '" + groupFieldName + "' must specify exactly one operator");
  78. }
  79. }
  80. }
  81. }
  82. }, base = DocumentSource, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
  83. klass.GroupOps = {
  84. "$addToSet": Accumulators.AddToSet,
  85. "$avg": Accumulators.Avg,
  86. "$first": Accumulators.First,
  87. "$last": Accumulators.Last,
  88. "$max": Accumulators.MinMax.bind(null, 1),
  89. "$min": Accumulators.MinMax.bind(null, -1),
  90. "$push": Accumulators.Push,
  91. "$sum": Accumulators.Sum
  92. };
  93. proto._getTypeStr = function _getTypeStr(obj){
  94. var typeofStr=typeof groupField, typeStr = (typeofStr == "object" ? groupField.constructor.name : typeStr);
  95. return typeofStr;
  96. };
  97. proto.getSourceName = function getSourceName(){
  98. return "$group";
  99. };
  100. proto.advance = function advance(){
  101. base.prototype.advance.call(this); // Check for interupts ????
  102. if(!this.populated)
  103. this.populate();
  104. ++this.currentGroupsKeysIndex;
  105. if(this.currentGroupsKeysIndex === this.groupKeys.length){
  106. this.currentDocument = null;
  107. return false;
  108. }
  109. return true;
  110. };
  111. proto.eof = function eof(){
  112. if(!this.populated)
  113. this.populate();
  114. return this.currentGroupsKeysIndex === this.groupsKeys.length;
  115. };
  116. proto.getCurrent = function getCurrent(){
  117. if(!this.populated)
  118. this.populate();
  119. return this.currentDocument;
  120. };
  121. proto.addAccumulator = function addAccumulator(fieldName, accumulatorFactory, expression){
  122. this.fieldNames.push(fieldName);
  123. this.accumulatorFactories.push(accumulatorFactory);
  124. this.expressions.push(expression);
  125. };
  126. proto.populate = function populate(){
  127. for(var hasNext = !this.pSource.eof(); hasNext; hasNext = this.pSource.advance()){
  128. var currentDocument = this.pSource.getCurrent(),
  129. _id = this.idExpression.evaluate(currentDocument) || null,
  130. group;
  131. if(_id in this.groups){
  132. group = this.groups[_id];
  133. }else{
  134. this.groups[_id] = group = [];
  135. this.groupsKeys[this.currentGroupsKeysIndex] = _id;
  136. for(var ai =0; ai < this.accumulators.length; ++ai){
  137. var accumulator = new this.accumulatorFactories[ai]();
  138. accumulators.addOperand(this.expressions[ai]);
  139. group.push(accumulator);
  140. }
  141. }
  142. // tickle all the accumulators for the group we found
  143. for(var gi=0; gi < group.length; ++gi)
  144. group[gi].evaluate(currentDocument);
  145. this.currentGroupsKeysIndex = 0; // Start the group
  146. if(this.currentGroupsKeysIndex === this.groups.length-1)
  147. this.currentDocument = makeDocument(this.currentGroupsKeysIndex);
  148. this.populated = true;
  149. }
  150. };
  151. proto.makeDocument = function makeDocument(groupKeyIndex){
  152. var groupKey = this.groupKeys[groupKeyIndex],
  153. group = this.groups[groupKey],
  154. doc = {};
  155. doc[Document.ID_PROPERTY_NAME] = groupKey;
  156. for(var i = 0; i < this.fieldNames.length; ++i){
  157. var fieldName = this.fieldNames[i],
  158. value = this.group[i].getValue();
  159. if(typeof value !== "undefined"){
  160. doc[fieldName] = value;
  161. }
  162. }
  163. return doc;
  164. };
  165. return klass;
  166. })();