aggregate_test.js 13 KB

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