1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
// Test the behavior of user-defined (Javascript) accumulators.
(function() {
"use strict";
load('jstests/aggregation/extras/utils.js');
db.accumulator_js.drop();
for (const word of ["hello", "world", "world", "hello", "hi"]) {
db.accumulator_js.insert({word: word, val: 1});
}
const command = {
aggregate: 'accumulator_js',
cursor: {},
pipeline: [{
$group: {
_id: "$word",
wordCount: {
$accumulator: {
init: function() {
return 0;
},
accumulateArgs: ["$val"],
accumulate: function(state, val) {
return state + val;
},
merge: function(state1, state2) {
return state1 + state2;
},
finalize: function(state) {
return state;
}
}
}
}
}],
};
const expectedResults = [
{_id: "hello", wordCount: 2},
{_id: "world", wordCount: 2},
{_id: "hi", wordCount: 1},
];
let res = assert.commandWorked(db.runCommand(command));
assert(resultsEq(res.cursor.firstBatch, expectedResults), res.cursor);
// Test that the functions can be passed as strings.
{
const accumulatorSpec = command.pipeline[0].$group.wordCount.$accumulator;
accumulatorSpec.init = accumulatorSpec.init.toString();
accumulatorSpec.accumulate = accumulatorSpec.accumulate.toString();
accumulatorSpec.merge = accumulatorSpec.merge.toString();
accumulatorSpec.finalize = accumulatorSpec.finalize.toString();
}
res = assert.commandWorked(db.runCommand(command));
assert(resultsEq(res.cursor.firstBatch, expectedResults), res.cursor);
// Test that finalize is optional.
delete command.pipeline[0].$group.wordCount.$accumulator.finalize;
res = assert.commandWorked(db.runCommand(command));
assert(resultsEq(res.cursor.firstBatch, expectedResults), res.cursor);
// Test a finalizer other than the identity function. Finalizers are useful when the intermediate
// state needs to be a different format from the final result.
res = assert.commandWorked(db.runCommand(Object.merge(command, {
pipeline: [{
$group: {
_id: 1,
avgWordLen: {
$accumulator: {
init: function() {
return {count: 0, sum: 0};
},
accumulateArgs: [{$strLenCP: "$word"}],
accumulate: function({count, sum}, wordLen) {
return {count: count + 1, sum: sum + wordLen};
},
merge: function(s1, s2) {
return {count: s1.count + s2.count, sum: s1.sum + s2.sum};
},
finalize: function({count, sum}) {
return sum / count;
},
lang: 'js',
}
},
}
}],
})));
assert(resultsEq(res.cursor.firstBatch, [{_id: 1, avgWordLen: 22 / 5}]), res.cursor);
// Test that a null word is considered a valid value.
assert.commandWorked(db.accumulator_js.insert({word: null, val: 1}));
expectedResults.push({_id: null, wordCount: 1});
res = assert.commandWorked(db.runCommand(command));
assert(resultsEq(res.cursor.firstBatch, expectedResults), res.cursor);
// Test that missing fields become JS null.
// This is similar to how most other agg operators work.
// TODO SERVER-45450 is this a problem for mapreduce?
assert(db.accumulator_js.drop());
assert.commandWorked(db.accumulator_js.insert({sentinel: 1}));
command.pipeline = [{
$group: {
_id: 1,
value: {
$accumulator: {
init: function() {
return [];
},
accumulateArgs: ["$no_such_field"],
accumulate: function(state, value) {
return state.concat([value]);
},
merge: function(s1, s2) {
return s1.concat(s2);
},
lang: 'js',
}
}
}
}];
res = assert.commandWorked(db.runCommand(command));
assert(resultsEq(res.cursor.firstBatch, [{_id: 1, value: [null]}]), res.cursor);
})();
|