Skip to content

Commit 43cd5d0

Browse files
pmereditcswanson310
authored andcommitted
SERVER-32930 Add trigonometric expressions to aggregation
Closes mongodb#1287 Signed-off-by: Charlie Swanson <[email protected]>
1 parent 8195b17 commit 43cd5d0

File tree

9 files changed

+2666
-2
lines changed

9 files changed

+2666
-2
lines changed
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// SERVER-32930: Basic integration tests for trigonometric aggregation expressions.
2+
3+
(function() {
4+
"use strict";
5+
// For assertErrorCode.
6+
load("jstests/aggregation/extras/utils.js");
7+
8+
const coll = db.expression_trigonometric;
9+
coll.drop();
10+
// We need at least one document in the collection in order to test expressions, add it here.
11+
assert.commandWorked(coll.insert({}));
12+
13+
// Helper for testing that op returns expResult.
14+
function testOp(op, expResult) {
15+
const pipeline = [{$project: {_id: 0, result: op}}];
16+
assert.eq(coll.aggregate(pipeline).toArray(), [{result: expResult}]);
17+
}
18+
19+
// Helper for testing that the aggregation expression 'op' returns expResult, approximately,
20+
// since NumberDecimal has so many representations for a given number (0 versus 0e-40 for
21+
// instance).
22+
function testOpApprox(op, expResult) {
23+
const pipeline = [{$project: {_id: 0, result: {$abs: {$subtract: [op, expResult]}}}}];
24+
assert.lt(coll.aggregate(pipeline).toArray(), [{result: NumberDecimal("0.00000005")}]);
25+
}
26+
27+
// Simple successful int input.
28+
testOp({$acos: NumberInt(1)}, 0);
29+
testOp({$acosh: NumberInt(1)}, 0);
30+
testOp({$asin: NumberInt(0)}, 0);
31+
testOp({$asinh: NumberInt(0)}, 0);
32+
testOp({$atan: NumberInt(0)}, 0);
33+
testOp({$atan2: [NumberInt(0), NumberInt(1)]}, 0);
34+
testOp({$atan2: [NumberInt(0), NumberInt(0)]}, 0);
35+
testOp({$atanh: NumberInt(0)}, 0);
36+
testOp({$cos: NumberInt(0)}, 1);
37+
testOp({$cosh: NumberInt(0)}, 1);
38+
testOp({$sin: NumberInt(0)}, 0);
39+
testOp({$sinh: NumberInt(0)}, 0);
40+
testOp({$tan: NumberInt(0)}, 0);
41+
testOp({$tanh: NumberInt(0)}, 0);
42+
testOp({$degreesToRadians: NumberInt(0)}, 0);
43+
testOp({$radiansToDegrees: NumberInt(0)}, 0);
44+
45+
// Simple successful long input.
46+
testOp({$acos: NumberLong(1)}, 0);
47+
testOp({$acosh: NumberLong(1)}, 0);
48+
testOp({$asin: NumberLong(0)}, 0);
49+
testOp({$asinh: NumberLong(0)}, 0);
50+
testOp({$atan: NumberLong(0)}, 0);
51+
testOp({$atan2: [NumberLong(0), NumberLong(1)]}, 0);
52+
testOp({$atan2: [NumberLong(0), NumberLong(0)]}, 0);
53+
testOp({$atanh: NumberLong(0)}, 0);
54+
testOp({$cos: NumberLong(0)}, 1);
55+
testOp({$cosh: NumberLong(0)}, 1);
56+
testOp({$sin: NumberLong(0)}, 0);
57+
testOp({$sinh: NumberLong(0)}, 0);
58+
testOp({$tan: NumberLong(0)}, 0);
59+
testOp({$tanh: NumberLong(0)}, 0);
60+
testOp({$degreesToRadians: NumberLong(0)}, 0);
61+
testOp({$radiansToDegrees: NumberLong(0)}, 0);
62+
63+
// Simple successful double input.
64+
testOp({$acos: 1}, 0);
65+
testOp({$acosh: 1}, 0);
66+
testOp({$asin: 0}, 0);
67+
testOp({$asinh: 0}, 0);
68+
testOp({$atan: 0}, 0);
69+
testOp({$atan2: [0, 1]}, 0);
70+
testOp({$atan2: [0, 0]}, 0);
71+
testOp({$atanh: 0}, 0);
72+
testOp({$cos: 0}, 1);
73+
testOp({$cosh: 0}, 1);
74+
testOp({$sin: 0}, 0);
75+
testOp({$sinh: 0}, 0);
76+
testOp({$tan: 0}, 0);
77+
testOp({$tanh: 0}, 0);
78+
testOp({$degreesToRadians: 0}, 0);
79+
testOp({$radiansToDegrees: 0}, 0);
80+
81+
// Simple successful decimal input.
82+
testOpApprox({$acos: NumberDecimal(1)}, NumberDecimal(0));
83+
testOpApprox({$acosh: NumberDecimal(1)}, NumberDecimal(0));
84+
testOpApprox({$asin: NumberDecimal(0)}, NumberDecimal(0));
85+
testOpApprox({$asinh: NumberDecimal(0)}, NumberDecimal(0));
86+
testOpApprox({$atan: NumberDecimal(0)}, NumberDecimal(0));
87+
testOpApprox({$atan2: [NumberDecimal(0), 1]}, NumberDecimal(0));
88+
testOpApprox({$atan2: [NumberDecimal(0), 0]}, NumberDecimal(0));
89+
testOpApprox({$atanh: NumberDecimal(0)}, NumberDecimal(0));
90+
testOpApprox({$cos: NumberDecimal(0)}, NumberDecimal(1));
91+
testOpApprox({$cosh: NumberDecimal(0)}, NumberDecimal(1));
92+
testOpApprox({$sin: NumberDecimal(0)}, NumberDecimal(0));
93+
testOpApprox({$sinh: NumberDecimal(0)}, NumberDecimal(0));
94+
testOpApprox({$tan: NumberDecimal(0)}, NumberDecimal(0));
95+
testOpApprox({$tanh: NumberDecimal(0)}, NumberDecimal(0));
96+
testOpApprox({$degreesToRadians: NumberDecimal(0)}, NumberDecimal(0));
97+
testOpApprox({$radiansToDegrees: NumberDecimal(0)}, NumberDecimal(0));
98+
99+
// Infinity input produces out of bounds error.
100+
assertErrorCode(coll, [{$project: {a: {$acos: -Infinity}}}], 50989);
101+
assertErrorCode(coll, [{$project: {a: {$acos: NumberDecimal('-Infinity')}}}], 50989);
102+
assertErrorCode(coll, [{$project: {a: {$acos: Infinity}}}], 50989);
103+
assertErrorCode(coll, [{$project: {a: {$acos: NumberDecimal('Infinity')}}}], 50989);
104+
105+
assertErrorCode(coll, [{$project: {a: {$acosh: -Infinity}}}], 50989);
106+
assertErrorCode(coll, [{$project: {a: {$acosh: NumberDecimal('-Infinity')}}}], 50989);
107+
108+
assertErrorCode(coll, [{$project: {a: {$asin: -Infinity}}}], 50989);
109+
assertErrorCode(coll, [{$project: {a: {$asin: NumberDecimal('-Infinity')}}}], 50989);
110+
assertErrorCode(coll, [{$project: {a: {$asin: Infinity}}}], 50989);
111+
assertErrorCode(coll, [{$project: {a: {$asin: NumberDecimal('Infinity')}}}], 50989);
112+
113+
assertErrorCode(coll, [{$project: {a: {$atanh: -Infinity}}}], 50989);
114+
assertErrorCode(coll, [{$project: {a: {$atanh: NumberDecimal('-Infinity')}}}], 50989);
115+
assertErrorCode(coll, [{$project: {a: {$atanh: Infinity}}}], 50989);
116+
assertErrorCode(coll, [{$project: {a: {$atanh: NumberDecimal('Infinity')}}}], 50989);
117+
118+
assertErrorCode(coll, [{$project: {a: {$cos: -Infinity}}}], 50989);
119+
assertErrorCode(coll, [{$project: {a: {$cos: NumberDecimal('-Infinity')}}}], 50989);
120+
assertErrorCode(coll, [{$project: {a: {$cos: Infinity}}}], 50989);
121+
assertErrorCode(coll, [{$project: {a: {$cos: NumberDecimal('Infinity')}}}], 50989);
122+
123+
assertErrorCode(coll, [{$project: {a: {$sin: -Infinity}}}], 50989);
124+
assertErrorCode(coll, [{$project: {a: {$sin: NumberDecimal('-Infinity')}}}], 50989);
125+
assertErrorCode(coll, [{$project: {a: {$sin: Infinity}}}], 50989);
126+
assertErrorCode(coll, [{$project: {a: {$sin: NumberDecimal('Infinity')}}}], 50989);
127+
128+
assertErrorCode(coll, [{$project: {a: {$tan: -Infinity}}}], 50989);
129+
assertErrorCode(coll, [{$project: {a: {$tan: NumberDecimal('-Infinity')}}}], 50989);
130+
assertErrorCode(coll, [{$project: {a: {$tan: Infinity}}}], 50989);
131+
assertErrorCode(coll, [{$project: {a: {$tan: NumberDecimal('Infinity')}}}], 50989);
132+
133+
// Infinity input produces Infinity as output.
134+
testOp({$acosh: NumberDecimal('Infinity')}, NumberDecimal('Infinity'));
135+
testOp({$acosh: Infinity}, Infinity);
136+
137+
testOp({$asinh: NumberDecimal('Infinity')}, NumberDecimal('Infinity'));
138+
testOp({$asinh: NumberDecimal('-Infinity')}, NumberDecimal('-Infinity'));
139+
testOp({$asinh: Infinity}, Infinity);
140+
testOp({$asinh: -Infinity}, -Infinity);
141+
testOp({$cosh: NumberDecimal('Infinity')}, NumberDecimal('Infinity'));
142+
testOp({$cosh: NumberDecimal('-Infinity')}, NumberDecimal('Infinity'));
143+
testOp({$cosh: Infinity}, Infinity);
144+
testOp({$cosh: -Infinity}, Infinity);
145+
testOp({$sinh: NumberDecimal('Infinity')}, NumberDecimal('Infinity'));
146+
testOp({$sinh: NumberDecimal('-Infinity')}, NumberDecimal('-Infinity'));
147+
testOp({$sinh: Infinity}, Infinity);
148+
testOp({$sinh: -Infinity}, -Infinity);
149+
150+
// Infinity produces finite output (due to asymptotic bounds).
151+
testOpApprox({$atan: NumberDecimal('Infinity')}, NumberDecimal(Math.PI / 2));
152+
testOpApprox({$atan: NumberDecimal('-Infinity')}, NumberDecimal(Math.Pi / 2));
153+
testOpApprox({$atan: Infinity}, Math.PI / 2);
154+
testOpApprox({$atan: -Infinity}, -Math.PI / 2);
155+
156+
testOpApprox({$atan2: [NumberDecimal('Infinity'), 0]}, NumberDecimal(Math.PI / 2));
157+
testOpApprox({$atan2: [NumberDecimal('-Infinity'), 0]}, NumberDecimal(-Math.PI / 2));
158+
testOpApprox({$atan2: [NumberDecimal('-Infinity'), NumberDecimal("Infinity")]},
159+
NumberDecimal(-Math.PI / 4));
160+
testOpApprox({$atan2: [NumberDecimal('-Infinity'), NumberDecimal("-Infinity")]},
161+
NumberDecimal(-3 * Math.PI / 4));
162+
testOpApprox({$atan2: [NumberDecimal('0'), NumberDecimal("-Infinity")]},
163+
NumberDecimal(Math.PI));
164+
testOpApprox({$atan2: [NumberDecimal('0'), NumberDecimal("Infinity")]}, NumberDecimal(0));
165+
166+
testOp({$tanh: NumberDecimal('Infinity')}, NumberDecimal('1'));
167+
testOp({$tanh: NumberDecimal('-Infinity')}, NumberDecimal('-1'));
168+
169+
// Finite input produces infinite outputs.
170+
testOp({$atanh: NumberDecimal(1)}, NumberDecimal('Infinity'));
171+
testOp({$atanh: NumberDecimal(-1)}, NumberDecimal('-Infinity'));
172+
testOp({$atanh: 1}, Infinity);
173+
testOp({$atanh: -1}, -Infinity);
174+
175+
testOp({$tanh: Infinity}, 1);
176+
testOp({$tanh: -Infinity}, -1);
177+
178+
// Int argument out of bounds.
179+
assertErrorCode(coll, [{$project: {a: {$acos: NumberInt(-2)}}}], 50989);
180+
assertErrorCode(coll, [{$project: {a: {$acos: NumberInt(2)}}}], 50989);
181+
assertErrorCode(coll, [{$project: {a: {$asin: NumberInt(-2)}}}], 50989);
182+
assertErrorCode(coll, [{$project: {a: {$asin: NumberInt(2)}}}], 50989);
183+
assertErrorCode(coll, [{$project: {a: {$acosh: NumberInt(0)}}}], 50989);
184+
assertErrorCode(coll, [{$project: {a: {$atanh: NumberInt(2)}}}], 50989);
185+
assertErrorCode(coll, [{$project: {a: {$atanh: NumberInt(-2)}}}], 50989);
186+
187+
// Long argument out of bounds.
188+
assertErrorCode(coll, [{$project: {a: {$acos: NumberLong(-2)}}}], 50989);
189+
assertErrorCode(coll, [{$project: {a: {$acos: NumberLong(2)}}}], 50989);
190+
assertErrorCode(coll, [{$project: {a: {$asin: NumberLong(-2)}}}], 50989);
191+
assertErrorCode(coll, [{$project: {a: {$asin: NumberLong(2)}}}], 50989);
192+
assertErrorCode(coll, [{$project: {a: {$acosh: NumberLong(0)}}}], 50989);
193+
assertErrorCode(coll, [{$project: {a: {$atanh: NumberLong(2)}}}], 50989);
194+
assertErrorCode(coll, [{$project: {a: {$atanh: NumberLong(-2)}}}], 50989);
195+
196+
// Double argument out of bounds.
197+
assertErrorCode(coll, [{$project: {a: {$acos: -1.1}}}], 50989);
198+
assertErrorCode(coll, [{$project: {a: {$acos: 1.1}}}], 50989);
199+
assertErrorCode(coll, [{$project: {a: {$asin: -1.1}}}], 50989);
200+
assertErrorCode(coll, [{$project: {a: {$asin: 1.1}}}], 50989);
201+
assertErrorCode(coll, [{$project: {a: {$acosh: 0.9}}}], 50989);
202+
assertErrorCode(coll, [{$project: {a: {$atanh: -1.00001}}}], 50989);
203+
assertErrorCode(coll, [{$project: {a: {$atanh: 1.00001}}}], 50989);
204+
205+
// Decimal argument out of bounds.
206+
assertErrorCode(coll, [{$project: {a: {$acos: NumberDecimal(-1.1)}}}], 50989);
207+
assertErrorCode(coll, [{$project: {a: {$acos: NumberDecimal(1.1)}}}], 50989);
208+
assertErrorCode(coll, [{$project: {a: {$asin: NumberDecimal(-1.1)}}}], 50989);
209+
assertErrorCode(coll, [{$project: {a: {$asin: NumberDecimal(1.1)}}}], 50989);
210+
assertErrorCode(coll, [{$project: {a: {$acosh: NumberDecimal(0.9)}}}], 50989);
211+
assertErrorCode(coll, [{$project: {a: {$atanh: NumberDecimal(-1.00001)}}}], 50989);
212+
assertErrorCode(coll, [{$project: {a: {$atanh: NumberDecimal(1.000001)}}}], 50989);
213+
214+
// Check NaN is preserved.
215+
["$acos", "$asin", "$atan", "$cos", "$sin", "$tan"].forEach(op => {
216+
testOp({[op]: NaN}, NaN);
217+
testOp({[op]: NumberDecimal(NaN)}, NumberDecimal(NaN));
218+
// Check the hyperbolic version of each function.
219+
testOp({[op + 'h']: NaN}, NaN);
220+
testOp({[op + 'h']: NumberDecimal(NaN)}, NumberDecimal(NaN));
221+
});
222+
223+
["$radiansToDegrees", "$degreesToRadians"].forEach(op => {
224+
testOp({[op]: NaN}, NaN);
225+
testOp({[op]: NumberDecimal(NaN)}, NumberDecimal(NaN));
226+
testOp({[op]: -Infinity}, -Infinity);
227+
testOp({[op]: NumberDecimal(-Infinity)}, NumberDecimal(-Infinity));
228+
testOp({[op]: Infinity}, Infinity);
229+
testOp({[op]: NumberDecimal(Infinity)}, NumberDecimal(Infinity));
230+
});
231+
232+
testOp({$atan2: [NumberDecimal('NaN'), NumberDecimal('NaN')]}, NumberDecimal('NaN'));
233+
testOp({$atan2: [NumberDecimal('NaN'), NumberDecimal('0')]}, NumberDecimal('NaN'));
234+
testOp({$atan2: [NumberDecimal('0'), NumberDecimal('NaN')]}, NumberDecimal('NaN'));
235+
236+
// Non-numeric input.
237+
assertErrorCode(coll, [{$project: {a: {$acos: "string"}}}], 28765);
238+
assertErrorCode(coll, [{$project: {a: {$acosh: "string"}}}], 28765);
239+
assertErrorCode(coll, [{$project: {a: {$asin: "string"}}}], 28765);
240+
assertErrorCode(coll, [{$project: {a: {$asinh: "string"}}}], 28765);
241+
assertErrorCode(coll, [{$project: {a: {$atan: "string"}}}], 28765);
242+
assertErrorCode(coll, [{$project: {a: {$atan2: ["string", "string"]}}}], 51044);
243+
assertErrorCode(coll, [{$project: {a: {$atan2: ["string", 0.0]}}}], 51044);
244+
assertErrorCode(coll, [{$project: {a: {$atan2: [0.0, "string"]}}}], 51045);
245+
assertErrorCode(coll, [{$project: {a: {$atanh: "string"}}}], 28765);
246+
assertErrorCode(coll, [{$project: {a: {$cos: "string"}}}], 28765);
247+
assertErrorCode(coll, [{$project: {a: {$cosh: "string"}}}], 28765);
248+
assertErrorCode(coll, [{$project: {a: {$sin: "string"}}}], 28765);
249+
assertErrorCode(coll, [{$project: {a: {$sinh: "string"}}}], 28765);
250+
assertErrorCode(coll, [{$project: {a: {$tan: "string"}}}], 28765);
251+
assertErrorCode(coll, [{$project: {a: {$tanh: "string"}}}], 28765);
252+
assertErrorCode(coll, [{$project: {a: {$degreesToRadians: "string"}}}], 28765);
253+
assertErrorCode(coll, [{$project: {a: {$radiansToDegrees: "string"}}}], 28765);
254+
}());

src/mongo/db/pipeline/SConscript

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ env.Library(
135135
target='expression',
136136
source=[
137137
'expression.cpp',
138+
'expression_trigonometric.cpp',
138139
],
139140
LIBDEPS=[
140141
'$BUILD_DIR/mongo/db/query/datetime/date_time_support',
@@ -445,6 +446,7 @@ env.CppUnitTest(
445446
'expression_convert_test.cpp',
446447
'expression_date_test.cpp',
447448
'expression_test.cpp',
449+
'expression_trigonometric_test.cpp',
448450
],
449451
LIBDEPS=[
450452
'$BUILD_DIR/mongo/db/query/query_test_service_context',

src/mongo/db/pipeline/expression.h

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ class ExpressionSingleNumericArg : public ExpressionFixedArity<SubClass, 1> {
435435
explicit ExpressionSingleNumericArg(const boost::intrusive_ptr<ExpressionContext>& expCtx)
436436
: ExpressionFixedArity<SubClass, 1>(expCtx) {}
437437

438-
virtual ~ExpressionSingleNumericArg() {}
438+
virtual ~ExpressionSingleNumericArg() = default;
439439

440440
Value evaluate(const Document& root) const final {
441441
Value arg = this->vpOperand[0]->evaluate(root);
@@ -453,6 +453,49 @@ class ExpressionSingleNumericArg : public ExpressionFixedArity<SubClass, 1> {
453453
virtual Value evaluateNumericArg(const Value& numericArg) const = 0;
454454
};
455455

456+
/**
457+
* Inherit from this class if your expression takes exactly two numeric arguments.
458+
*/
459+
template <typename SubClass>
460+
class ExpressionTwoNumericArgs : public ExpressionFixedArity<SubClass, 2> {
461+
public:
462+
explicit ExpressionTwoNumericArgs(const boost::intrusive_ptr<ExpressionContext>& expCtx)
463+
: ExpressionFixedArity<SubClass, 2>(expCtx) {}
464+
465+
virtual ~ExpressionTwoNumericArgs() = default;
466+
467+
/**
468+
* Evaluate performs the type checking necessary to make sure that both arguments are numeric,
469+
* then calls the evaluateNumericArgs on the two numeric args:
470+
* 1. If either input is nullish, it returns null.
471+
* 2. If either input is not numeric, it throws an error.
472+
* 3. Call evaluateNumericArgs on the two numeric args.
473+
*/
474+
Value evaluate(const Document& root) const final {
475+
Value arg1 = this->vpOperand[0]->evaluate(root);
476+
if (arg1.nullish())
477+
return Value(BSONNULL);
478+
uassert(51044,
479+
str::stream() << this->getOpName() << " only supports numeric types, not "
480+
<< typeName(arg1.getType()),
481+
arg1.numeric());
482+
Value arg2 = this->vpOperand[1]->evaluate(root);
483+
if (arg2.nullish())
484+
return Value(BSONNULL);
485+
uassert(51045,
486+
str::stream() << this->getOpName() << " only supports numeric types, not "
487+
<< typeName(arg2.getType()),
488+
arg2.numeric());
489+
490+
return evaluateNumericArgs(arg1, arg2);
491+
}
492+
493+
/**
494+
* Evaluate the expression on exactly two numeric arguments.
495+
*/
496+
virtual Value evaluateNumericArgs(const Value& numericArg1, const Value& numericArg2) const = 0;
497+
};
498+
456499
/**
457500
* A constant expression. Repeated calls to evaluate() will always return the same thing.
458501
*/
@@ -665,7 +708,6 @@ class ExpressionAbs final : public ExpressionSingleNumericArg<ExpressionAbs> {
665708
const char* getOpName() const final;
666709
};
667710

668-
669711
class ExpressionAdd final : public ExpressionVariadic<ExpressionAdd> {
670712
public:
671713
explicit ExpressionAdd(const boost::intrusive_ptr<ExpressionContext>& expCtx)

0 commit comments

Comments
 (0)