aggregate_test.js 14 KB

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