aggregate_test.js 14 KB

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