aggregate_test.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. "use strict";
  2. /*jshint camelcase:false*/
  3. if (!module.parent) return require.cache[__filename] = 0, (new(require("mocha"))()).addFile(__filename).ui("exports").run(process.exit);
  4. var assert = require("assert"),
  5. aggregate = require("../../");
  6. aggregate.cmdDefaults.batchSize = Infinity;
  7. // Utility to test the various use cases of `aggregate`
  8. function testAggregate(opts){
  9. if (!opts.asyncOnly){
  10. // SYNC: test one-off usage
  11. var results = aggregate(opts.pipeline, opts.inputs).toArray();
  12. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected));
  13. // SYNC: test one-off usage with context
  14. results = aggregate(opts.pipeline, {hi: "there"}, opts.inputs).toArray();
  15. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected));
  16. // SYNC: test use with context
  17. var aggregator = aggregate(opts.pipeline, {hi: "there"});
  18. results = aggregator(opts.inputs).toArray();
  19. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected));
  20. // SYNC: test reusable aggregator functionality
  21. aggregator = aggregate(opts.pipeline);
  22. results = aggregator(opts.inputs).toArray();
  23. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected));
  24. // SYNC: test that it is actually reusable
  25. results = aggregator(opts.inputs).toArray();
  26. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected), "should allow sync aggregator reuse");
  27. }
  28. // ASYNC: test one-off usage
  29. aggregate(opts.pipeline, opts.inputs, function(err, results){
  30. assert.ifError(err);
  31. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected));
  32. // ASYNC: test one-off usage with context
  33. aggregate(opts.pipeline, {hi: "there"}, opts.inputs, function(err, results){
  34. assert.ifError(err);
  35. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected));
  36. // ASYNC: test reusable aggregator functionality with context
  37. var aggregator = aggregate(opts.pipeline);
  38. aggregator({hi: "there"}, opts.inputs, function(err, results){
  39. assert.ifError(err);
  40. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected));
  41. // ASYNC: test reusable aggregator functionality
  42. var aggregator = aggregate(opts.pipeline);
  43. aggregator(opts.inputs, function(err, results){
  44. assert.ifError(err);
  45. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected));
  46. // ASYNC: test that it is actually reusable
  47. aggregator(opts.inputs, function(err, results){
  48. assert.ifError(err);
  49. assert.equal(JSON.stringify(results), JSON.stringify(opts.expected), "should allow async aggregator reuse");
  50. // success!
  51. return opts.next();
  52. });
  53. });
  54. });
  55. });
  56. });
  57. }
  58. exports.aggregate = {
  59. "should be able to use an empty pipeline (no-op)": function(next){
  60. testAggregate({
  61. inputs: [1, 2, 3],
  62. pipeline: [],
  63. expected: [1, 2, 3],
  64. next: next
  65. });
  66. },
  67. "should be able to use a limit operator": function(next){
  68. testAggregate({
  69. inputs: [{_id:0}, {_id:1}, {_id:2}, {_id:3}, {_id:4}, {_id:5}],
  70. pipeline: [{$limit:2}],
  71. expected: [{_id:0}, {_id:1}],
  72. next: next
  73. });
  74. },
  75. "should be able to use a match operator": function(next){
  76. testAggregate({
  77. inputs: [{_id:0, e:1}, {_id:1, e:0}, {_id:2, e:1}, {_id:3, e:0}, {_id:4, e:1}, {_id:5, e:0}],
  78. pipeline: [{$match:{e:1}}],
  79. expected: [{_id:0, e:1}, {_id:2, e:1}, {_id:4, e:1}],
  80. next: next
  81. });
  82. },
  83. "should be able to use a skip operator": function(next){
  84. testAggregate({
  85. inputs: [{_id:0}, {_id:1}, {_id:2}, {_id:3}, {_id:4}, {_id:5}],
  86. pipeline: [{$skip:2}, {$skip:1}], //testing w/ 2 ensures independent state variables
  87. expected: [{_id:3}, {_id:4}, {_id:5}],
  88. next: next
  89. });
  90. },
  91. "should be able to use a skip and then a limit operator together in the same pipeline": function(next){
  92. testAggregate({
  93. inputs: [{_id:0, e:1}, {_id:1, e:0}, {_id:2, e:1}, {_id:3, e:0}, {_id:4, e:1}, {_id:5, e:0}],
  94. pipeline: [{$skip:2}, {$limit:1}],
  95. expected: [{_id:2, e:1}],
  96. next: next
  97. });
  98. },
  99. "should be able to construct an instance with unwind operators properly": function(next){
  100. testAggregate({
  101. inputs: [
  102. {_id:0, nodes:[
  103. {one:[11], two:[2,2]},
  104. {one:[1,1], two:[22]}
  105. ]},
  106. {_id:1, nodes:[
  107. {two:[22], three:[333]},
  108. {one:[1], three:[3,3,3]}
  109. ]}
  110. ],
  111. pipeline: [{$unwind:"$nodes"}, {$unwind:"$nodes.two"}],
  112. expected: [
  113. {_id:0,nodes:{one:[11],two:2}},
  114. {_id:0,nodes:{one:[11],two:2}},
  115. {_id:0,nodes:{one:[1,1],two:22}},
  116. {_id:1,nodes:{two:22,three:[333]}}
  117. ],
  118. next: next
  119. });
  120. },
  121. "should be able to use a project operator": function(next){
  122. // NOTE: Test case broken until expression is fixed
  123. testAggregate({
  124. inputs: [{_id:0, e:1, f:23}, {_id:2, e:2, g:34}, {_id:4, e:3}],
  125. pipeline: [
  126. {$project:{
  127. e:1,
  128. a:{$add:["$e", "$e"]},
  129. b:{$cond:[{$eq:["$e", 2]}, "two", "not two"]}
  130. //TODO: high level test of all other expression operators
  131. }}
  132. ],
  133. expected: [{_id:0, e:1, a:2, b:"not two"}, {_id:2, e:2, a:4, b:"two"}, {_id:4, e:3, a:6, b:"not two"}],
  134. next: next
  135. });
  136. },
  137. "should be able to use a project operator to exclude the _id field": function(next){
  138. // NOTE: Test case broken until expression is fixed
  139. testAggregate({
  140. inputs: [{_id:0, e:1, f:23}, {_id:2, e:2, g:34}, {_id:4, e:3}],
  141. pipeline: [
  142. {$project:{
  143. _id:0,
  144. e:1
  145. //TODO: high level test of all other expression operators
  146. }}
  147. ],
  148. expected: [{e:1}, {e:2}, {e:3}],
  149. next: next
  150. });
  151. },
  152. "should be able to project out a whole document and leave an empty": function(next) {
  153. testAggregate({
  154. inputs: [{_id:0, a:1}, {_id:1, a:2, b:1}, {_id:2, b:2, c:1}],
  155. pipeline: [
  156. {$project:{
  157. _id:0,
  158. a:1
  159. //TODO: high level test of all other expression operators
  160. }}
  161. ],
  162. expected: [{a:1}, {a:2}, {}],
  163. next: next
  164. });
  165. },
  166. "should be able to construct an instance with sort operators properly (ascending)": function(next){
  167. testAggregate({
  168. inputs: [
  169. {_id:3.14159}, {_id:-273.15},
  170. {_id:42}, {_id:11}, {_id:1},
  171. {_id:null}, {_id:NaN}
  172. ],
  173. pipeline: [{$sort:{_id:1}}],
  174. expected: [
  175. {_id:null}, {_id:NaN},
  176. {_id:-273.15}, {_id:1}, {_id:3.14159}, {_id:11}, {_id:42}
  177. ],
  178. next: next
  179. });
  180. },
  181. "should be able to construct an instance with $group operators properly": function(next){
  182. testAggregate({
  183. inputs: [
  184. {_id:0, a:1},
  185. {_id:0, a:2},
  186. {_id:0, a:3},
  187. {_id:0, a:4},
  188. {_id:0, a:1.5},
  189. {_id:0, a:null},
  190. {_id:1, b:"a"},
  191. {_id:1, b:"b"},
  192. {_id:1, b:"b"},
  193. {_id:1, b:"c"}
  194. ],
  195. pipeline:[
  196. {$group:{
  197. _id:"$_id",
  198. sum_a:{$sum:"$a"},
  199. //min_a:{$min:"$a"}, //this is busted in this version of mongo
  200. max_a:{$max:"$a"},
  201. avg_a:{$avg:"$a"},
  202. first_b:{$first:"$b"},
  203. last_b:{$last:"$b"},
  204. addToSet_b:{$addToSet:"$b"},
  205. push_b:{$push:"$b"}
  206. }}
  207. ],
  208. expected: [
  209. {
  210. _id:0,
  211. sum_a:11.5,
  212. //min_a:1,
  213. max_a:4,
  214. avg_a:2.3,
  215. first_b:null,
  216. last_b:null,
  217. addToSet_b:[],
  218. push_b:[]
  219. },
  220. {
  221. _id:1,
  222. sum_a:0,
  223. //min_a:null,
  224. max_a:null,
  225. avg_a:0,
  226. first_b:"a",
  227. last_b:"c",
  228. addToSet_b:["a", "b", "c"],
  229. push_b:["a", "b", "b", "c"]
  230. }
  231. ],
  232. next: next
  233. });
  234. },
  235. "should be able to construct an instance with $group using concat": function(next){
  236. testAggregate({
  237. inputs: [
  238. {_id:0, a:null},
  239. {_id:1, a:"a"},
  240. {_id:1, a:"b"},
  241. {_id:1, a:"b"},
  242. {_id:1, a:"c"}
  243. ],
  244. pipeline: [
  245. {$group:{
  246. _id:{$concat:["$a"]}
  247. }}
  248. ],
  249. expected: [
  250. {_id: null},
  251. {_id: "a"},
  252. {_id: "b"},
  253. {_id: "c"}
  254. ],
  255. next: next
  256. });
  257. },
  258. "should be able to successfully use comparisions of objects to nulls without throwing an exception": function(next){
  259. testAggregate({
  260. inputs: [
  261. {
  262. cond:{$or:[
  263. {$eq:["$server","Starmetal.demo.com"]},
  264. ]},
  265. value:"PII"
  266. },
  267. {
  268. cond:{$or:[
  269. {$eq:["$server","Specium.demo.com"]},
  270. {$eq:["$server","Germanium.demo.com"]},
  271. {$eq:["$server","Runite.demo.com"]}
  272. ]},
  273. value:"PI"
  274. },
  275. {
  276. cond:{$or:[
  277. {$eq:["$server","Primal.demo.com"]}
  278. ]},
  279. value:"Confidential"
  280. },
  281. {
  282. cond:{$or:[
  283. {$eq:["$server","Polarite.demo.com"]},
  284. {$eq:["$server","Ryanium.demo.com"]}
  285. ]},
  286. value:"Proprietary"
  287. },
  288. {
  289. cond:{$or:[
  290. {$eq:["$server","Phazon.demo.com"]}
  291. ]},
  292. value:"PHI"
  293. },
  294. {
  295. cond:null,
  296. value:"Authorized"
  297. }
  298. ],
  299. pipeline: [
  300. {$skip:1},
  301. {$limit:1},
  302. {$project:{
  303. retValue:{$cond:[
  304. {$ne:["$cond", null]},
  305. null,
  306. "$value"
  307. ]}
  308. }}
  309. ],
  310. expected: [{"retValue":null}],
  311. next: next
  312. });
  313. },
  314. "should be able to successfully compare a null to a null": function(next){
  315. testAggregate({
  316. inputs: [
  317. {
  318. cond:null,
  319. value:"Authorized"
  320. }
  321. ],
  322. pipeline: [
  323. {$project:{
  324. retValue:{$cond:[
  325. {$eq:["$cond", null]},
  326. "$value",
  327. null
  328. ]}
  329. }}
  330. ],
  331. expected: [{"retValue":"Authorized"}],
  332. next: next
  333. });
  334. },
  335. "should be able to handle a large array of inputs": function(next){
  336. var inputs = [],
  337. expected = [];
  338. for(var i = 0; i < 10000; i++){
  339. inputs.push({a:i});
  340. expected.push({foo:i});
  341. }
  342. testAggregate({
  343. asyncOnly: true,
  344. inputs: inputs,
  345. pipeline: [
  346. {$project:{
  347. foo: "$a"
  348. }}
  349. ],
  350. expected: expected,
  351. next: next
  352. });
  353. },
  354. "should be able to explain an empty pipeline": function(){
  355. var pipeline = [],
  356. expected = [],
  357. actual = aggregate({
  358. pipeline: pipeline,
  359. explain: true
  360. });
  361. assert.deepEqual(actual, expected);
  362. },
  363. "should be able to explain a full pipeline": function(){
  364. var pipeline = [
  365. {$match:{e:1}},
  366. {$match:{d:1}},
  367. {$skip:2},
  368. {$limit:1},
  369. {$project:{
  370. foo: "$a"
  371. }},
  372. {$group:{
  373. _id:{$concat:["$foo"]}
  374. }}
  375. ],
  376. expected = [
  377. {$match:{$and:[{e:1},{d:1}]}},
  378. {$limit:3},
  379. {$skip:2},
  380. {$project:{
  381. foo: "$a"
  382. }},
  383. {$group:{
  384. _id:{$concat:["$foo"]}
  385. }}
  386. ],
  387. actual = aggregate({
  388. pipeline: pipeline,
  389. explain: true
  390. });
  391. assert.deepEqual(actual, expected);
  392. },
  393. "should be able to explain a full pipeline with inputs": function(){
  394. var pipeline = [
  395. {$match:{e:1}},
  396. {$match:{d:1}},
  397. {$skip:2},
  398. {$limit:1},
  399. {$project:{
  400. foo: "$a"
  401. }},
  402. {$group:{
  403. _id:{$concat:["$foo"]}
  404. }}
  405. ],
  406. expected = [
  407. {"$cursor":{
  408. "query":{"$and":[{"e":1},{"d":1}]},
  409. "fields":{"a":1,"_id":1},
  410. "plan":{
  411. "type":"ArrayRunner",
  412. "nDocs":1,
  413. "position":0,
  414. "state":"RUNNER_ADVANCED"
  415. }
  416. }},
  417. {"$match":{"$and":[{"e":1},{"d":1}]}},
  418. {"$limit":3},
  419. {"$skip":2},
  420. {"$project":{"foo":"$a"}},
  421. {"$group":{"_id":{"$concat":["$foo"]}}}
  422. ],
  423. actual = aggregate({
  424. pipeline: pipeline,
  425. explain: true
  426. }, [{e:1,d:2,a:4}]);
  427. assert.deepEqual(actual, expected);
  428. },
  429. "should throw parse errors if called sync-ly": function(){
  430. assert.throws(function(){
  431. aggregate([{"$project":{"foo":"bar"}}], [{"bar":1}]);
  432. });
  433. assert.throws(function(){
  434. aggregate([{"$project":{"foo":"bar"}}]);
  435. });
  436. },
  437. "should return parse errors in the callback if called async-ly": function(done){
  438. aggregate([{"$project":{"foo":"bar"}}], [{"bar":1}], function(err, results){
  439. assert(err, "Expected Error");
  440. done();
  441. });
  442. },
  443. "should throw pipeline errors if called sync-ly": function(){
  444. assert.throws(function(){
  445. aggregate([{"$project":{"sum":{"$add":["$foo", "$bar"]}}}], [{"foo":1, "bar":"baz"}]).toArray();
  446. });
  447. var agg = aggregate([{"$project":{"sum":{"$add":["$foo", "$bar"]}}}]);
  448. assert.throws(function(){
  449. agg([{"foo":1, "bar":"baz"}]).toArray();
  450. });
  451. assert.doesNotThrow(function(){
  452. agg([{"foo":1, "bar":2}]).toArray();
  453. });
  454. },
  455. "should return pipeline errors in the callback if called async-ly": function(done){
  456. aggregate([{"$project":{"sum":{"$add":["$foo", "$bar"]}}}], [{"foo":1, "bar":"baz"}], function(err, results){
  457. assert(err, "Expected Error");
  458. var agg = aggregate([{"$project":{"sum":{"$add":["$foo", "$bar"]}}}]);
  459. agg([{"foo":1, "bar":"baz"}], function(err, results){
  460. assert(err, "Expected Error");
  461. agg([{"foo":1, "bar":2}], function(err, results){
  462. assert.ifError(err, "UnExpected Error");
  463. done();
  464. });
  465. });
  466. });
  467. },
  468. "should be able to each over a cursor": function(done) {
  469. var docs = [{a:1}, {a:2}, {a:3}],
  470. expected = docs.slice(0,2),
  471. counter = 0,
  472. iterator = function(err, doc) {
  473. assert.ifError(err);
  474. assert.deepEqual(doc, expected[counter++]);
  475. if (doc === null) return done();
  476. };
  477. expected.push(null);
  478. aggregate([{$limit:2}], docs).each(iterator);
  479. },
  480. "should be able to forEach over a cursor": function(done) {
  481. var docs = [{a:1}, {a:2}, {a:3}],
  482. expected = docs.slice(0,2),
  483. counter = 0,
  484. iterator = function(doc) {
  485. assert.deepEqual(doc, expected[counter++]);
  486. },
  487. callback = function(err) {
  488. assert.ifError(err);
  489. done();
  490. };
  491. aggregate([{$limit:2}], docs).forEach(iterator, callback);
  492. }
  493. };