Skip to content

Commit ed3c15b

Browse files
author
George Wangensteen
committed
SERVER-34373 add aggregation expression to test if type is numeric
1 parent 7d28044 commit ed3c15b

File tree

5 files changed

+240
-0
lines changed

5 files changed

+240
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Tests for the $isNumber aggregation expression.
3+
*/
4+
(function() {
5+
'use strict';
6+
const coll = db.isNumber_expr;
7+
coll.drop();
8+
9+
function testIsNumber(inputExprPath, expectedOutput, inputId) {
10+
const result = coll.aggregate([
11+
{"$match": {_id: inputId}},
12+
{"$project": {_id: 0, "isNum": {"$isNumber": inputExprPath}}},
13+
])
14+
.toArray();
15+
assert.eq(result, expectedOutput);
16+
}
17+
18+
// Test when $isNumber evaluates to an integer, when the input expression is a document field.
19+
assert.commandWorked(coll.insert({_id: 1, integerFieldPath: NumberInt(56072)}));
20+
testIsNumber("$integerFieldPath", [{"isNum": true}], 1);
21+
// Same as above, but when the input expression is a nested document field.
22+
assert.commandWorked(coll.insert({_id: 11, nestedPath: {nestedNumber: NumberInt(500)}}));
23+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": true}], 11);
24+
25+
// Test when $isNumber evaluates to a double, when the input expression is a document field.
26+
assert.commandWorked(coll.insert({_id: 2, doubleFieldPath: 56072.2}));
27+
testIsNumber("$doubleFieldPath", [{"isNum": true}], 2);
28+
// Same as above, but when the input expression is a nested document field.
29+
assert.commandWorked(coll.insert({_id: 12, nestedPath: {nestedNumber: 56072.2}}));
30+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": true}], 12);
31+
32+
// Test when $isNumber evaluates to a decimal, when the input expression is a document field.
33+
assert.commandWorked(coll.insert({_id: 3, decimalFieldPath: NumberDecimal("1.234")}));
34+
testIsNumber("$decimalFieldPath", [{"isNum": true}], 3);
35+
// Same as above, but when the input expression is a nested document field.
36+
assert.commandWorked(coll.insert({_id: 13, nestedPath: {nestedNumber: NumberDecimal("1.234")}}));
37+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": true}], 13);
38+
39+
// Test when $isNumber evaluates to a long when the input expression is a document field.
40+
assert.commandWorked(coll.insert({_id: 4, longFieldPath: NumberLong("123456789")}));
41+
testIsNumber("$longFieldPath", [{"isNum": true}], 4);
42+
// Same as above, but when the input expression is a nested document field.
43+
assert.commandWorked(coll.insert({_id: 14, nestedPath: {nestedNumber: NumberLong("123456789")}}));
44+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": true}], 14);
45+
46+
// Test when $isNumber evaluates to null, when the input expression is a document field.
47+
assert.commandWorked(coll.insert({_id: 5, nullFieldPath: null}));
48+
testIsNumber("$nullFieldPath", [{"isNum": false}], 5);
49+
// Same as above, but when the input expression is a nested document field.
50+
assert.commandWorked(coll.insert({_id: 15, nestedPath: {nestedNull: null}}));
51+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": false}], 15);
52+
53+
// Test when $isNumber evaluates to missing, when the input expression is a document field.
54+
assert.commandWorked(coll.insert({_id: 6}));
55+
testIsNumber("$missingFieldPath", [{"isNum": false}], 6);
56+
// Same as above, but when the input expression is a nested document field.
57+
assert.commandWorked(coll.insert({_id: 16, nestedPath: {}}));
58+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": false}], 16);
59+
60+
// Test when $isNumber evaluates to an array, when the input expression is a document field.
61+
assert.commandWorked(coll.insert({_id: 7, arrayFieldPath: [1]}));
62+
testIsNumber("$arrayFieldPath", [{"isNum": false}], 7);
63+
// Same as above, but when the input expression is a nested document field.
64+
assert.commandWorked(coll.insert({_id: 17, nestedPath: {nestedArray: [1]}}));
65+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": false}], 17);
66+
67+
// Test when $isNumber evaluates to a string, when the input expression is a document field.
68+
assert.commandWorked(coll.insert({_id: 8, stringFieldPath: "1234"}));
69+
testIsNumber("$stringFieldPath", [{"isNum": false}], 8);
70+
// Same as above, but when the input expression is a nested document field.
71+
assert.commandWorked(coll.insert({_id: 18, nestedPath: {nestedString: "12345"}}));
72+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": false}], 18);
73+
74+
// Test when $isNumber evaluates to a Date, when the input expression is a document field.
75+
assert.commandWorked(coll.insert({_id: 9, dateFieldPath: new Date()}));
76+
testIsNumber("$dateFieldPath", [{"isNum": false}], 9);
77+
// Same as above, but when the input expression is a nested document field.
78+
assert.commandWorked(coll.insert({_id: 19, nestedPath: {nestedDate: new Date()}}));
79+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": false}], 19);
80+
81+
// Test when $isNumber evaluates to a BinData, when the input expression is a document field.
82+
// (UUID's are encoded as binary data)
83+
assert.commandWorked(coll.insert({_id: 10, binDataFieldPath: UUID()}));
84+
testIsNumber("$binDataFieldPath", [{"isNum": false}], 10);
85+
// Same as above, but when the input expression is a nested document field.
86+
assert.commandWorked(coll.insert({_id: 20, nestedPath: {nestedBinData: UUID()}}));
87+
testIsNumber("$nestedPath.nestedNumber", [{"isNum": false}], 20);
88+
89+
// Test a few literal expressions, rather than ones retrieved from a document.
90+
assert.commandWorked(coll.insert({_id: 21}));
91+
92+
// Test when $isNumber's input expression is a literal long.
93+
testIsNumber(NumberLong("12345678"), [{"isNum": true}], 21);
94+
95+
// Test when $isNumber's input expression is a literal decimal.
96+
testIsNumber(NumberDecimal("1.2345678"), [{"isNum": true}], 21);
97+
98+
// Test when $isNumber's input expression is a literal int.
99+
testIsNumber(NumberInt(18), [{"isNum": true}], 21);
100+
101+
// Test when $isNumber's input expression is a literal double.
102+
testIsNumber(5.34, [{"isNum": true}], 21);
103+
104+
// Test when $isNumber's input expression is null literal.
105+
testIsNumber(null, [{"isNum": false}], 21);
106+
107+
// Test when $isNumber's input expression is a literal array.
108+
testIsNumber([[2]], [{"isNum": false}], 21);
109+
110+
// Test when $isNumber's input expression is a literal string.
111+
testIsNumber("a literal string", [{"isNum": false}], 21);
112+
113+
// Test when $isNumber's input expression is a literal Date.
114+
testIsNumber(new Date(), [{"isNum": false}], 21);
115+
116+
// Test when $isNumber's input expression is a literal BinData.
117+
testIsNumber(UUID(), [{"isNum": false}], 21);
118+
}());

src/mongo/db/pipeline/expression.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4944,6 +4944,18 @@ const char* ExpressionType::getOpName() const {
49444944
return "$type";
49454945
}
49464946

4947+
/* ------------------------ ExpressionIsNumber --------------------------- */
4948+
4949+
Value ExpressionIsNumber::evaluate(const Document& root, Variables* variables) const {
4950+
Value val(_children[0]->evaluate(root, variables));
4951+
return Value(val.numeric());
4952+
}
4953+
4954+
REGISTER_EXPRESSION(isNumber, ExpressionIsNumber::parse);
4955+
const char* ExpressionIsNumber::getOpName() const {
4956+
return "$isNumber";
4957+
}
4958+
49474959
/* -------------------------- ExpressionZip ------------------------------ */
49484960

49494961
REGISTER_EXPRESSION(zip, ExpressionZip::parse);

src/mongo/db/pipeline/expression.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2378,6 +2378,18 @@ class ExpressionType final : public ExpressionFixedArity<ExpressionType, 1> {
23782378
}
23792379
};
23802380

2381+
class ExpressionIsNumber final : public ExpressionFixedArity<ExpressionIsNumber, 1> {
2382+
public:
2383+
explicit ExpressionIsNumber(const boost::intrusive_ptr<ExpressionContext>& expCtx)
2384+
: ExpressionFixedArity<ExpressionIsNumber, 1>(expCtx) {}
2385+
2386+
Value evaluate(const Document& root, Variables* variables) const final;
2387+
const char* getOpName() const final;
2388+
2389+
void acceptVisitor(ExpressionVisitor* visitor) final {
2390+
return visitor->visit(this);
2391+
}
2392+
};
23812393

23822394
class ExpressionWeek final : public DateExpressionAcceptingTimeZone<ExpressionWeek> {
23832395
public:

src/mongo/db/pipeline/expression_test.cpp

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5520,6 +5520,102 @@ TEST(ExpressionTypeTest, WithMaxKeyValue) {
55205520

55215521
} // namespace Type
55225522

5523+
namespace IsNumber {
5524+
5525+
TEST(ExpressionIsNumberTest, WithMinKeyValue) {
5526+
assertExpectedResults("$isNumber", {{{Value(MINKEY)}, Value(false)}});
5527+
}
5528+
5529+
TEST(ExpressionIsNumberTest, WithDoubleValue) {
5530+
assertExpectedResults("$isNumber", {{{Value(1.0)}, Value(true)}});
5531+
}
5532+
5533+
TEST(ExpressionIsNumberTest, WithStringValue) {
5534+
assertExpectedResults("$isNumber", {{{Value("stringValue"_sd)}, Value(false)}});
5535+
}
5536+
5537+
TEST(ExpressionIsNumberTest, WithNumericStringValue) {
5538+
assertExpectedResults("$isNumber", {{{Value("5"_sd)}, Value(false)}});
5539+
}
5540+
5541+
TEST(ExpressionIsNumberTest, WithObjectValue) {
5542+
BSONObj objectVal = fromjson("{a: {$literal: 1}}");
5543+
assertExpectedResults("$isNumber", {{{Value(objectVal)}, Value(false)}});
5544+
}
5545+
5546+
TEST(ExpressionIsNumberTest, WithArrayValue) {
5547+
assertExpectedResults("$isNumber", {{{Value(BSON_ARRAY(1 << 2))}, Value(false)}});
5548+
}
5549+
5550+
TEST(ExpressionIsNumberTest, WithBinDataValue) {
5551+
BSONBinData binDataVal = BSONBinData("", 0, BinDataGeneral);
5552+
assertExpectedResults("$isNumber", {{{Value(binDataVal)}, Value(false)}});
5553+
}
5554+
5555+
TEST(ExpressionIsNumberTest, WithUndefinedValue) {
5556+
assertExpectedResults("$isNumber", {{{Value(BSONUndefined)}, Value(false)}});
5557+
}
5558+
5559+
TEST(ExpressionIsNumberTest, WithOIDValue) {
5560+
assertExpectedResults("$isNumber", {{{Value(OID())}, Value(false)}});
5561+
}
5562+
5563+
TEST(ExpressionIsNumberTest, WithBoolValue) {
5564+
assertExpectedResults("$isNumber", {{{Value(true)}, Value(false)}});
5565+
}
5566+
5567+
TEST(ExpressionIsNumberTest, WithDateValue) {
5568+
Date_t dateVal = BSON("" << DATENOW).firstElement().Date();
5569+
assertExpectedResults("$isNumber", {{{Value(dateVal)}, Value(false)}});
5570+
}
5571+
5572+
TEST(ExpressionIsNumberTest, WithNullValue) {
5573+
assertExpectedResults("$isNumber", {{{Value(BSONNULL)}, Value(false)}});
5574+
}
5575+
5576+
TEST(ExpressionIsNumberTest, WithRegexValue) {
5577+
assertExpectedResults("$isNumber", {{{Value(BSONRegEx("a.b"))}, Value(false)}});
5578+
}
5579+
5580+
TEST(ExpressionIsNumberTest, WithSymbolValue) {
5581+
assertExpectedResults("$isNumber", {{{Value(BSONSymbol("a"))}, Value(false)}});
5582+
}
5583+
5584+
TEST(ExpressionIsNumberTest, WithDBRefValue) {
5585+
assertExpectedResults("$isNumber", {{{Value(BSONDBRef("", OID()))}, Value(false)}});
5586+
}
5587+
5588+
TEST(ExpressionIsNumberTest, WithCodeWScopeValue) {
5589+
assertExpectedResults("$isNumber",
5590+
{{{Value(BSONCodeWScope("var x = 3", BSONObj()))}, Value(false)}});
5591+
}
5592+
5593+
TEST(ExpressionIsNumberTest, WithCodeValue) {
5594+
assertExpectedResults("$isNumber", {{{Value(BSONCode("var x = 3"))}, Value(false)}});
5595+
}
5596+
5597+
TEST(ExpressionIsNumberTest, WithIntValue) {
5598+
assertExpectedResults("$isNumber", {{{Value(1)}, Value(true)}});
5599+
}
5600+
5601+
TEST(ExpressionIsNumberTest, WithDecimalValue) {
5602+
assertExpectedResults("$isNumber", {{{Value(Decimal128(0.3))}, Value(true)}});
5603+
}
5604+
5605+
TEST(ExpressionIsNumberTest, WithLongValue) {
5606+
assertExpectedResults("$isNumber", {{{Value(1LL)}, Value(true)}});
5607+
}
5608+
5609+
TEST(ExpressionIsNumberTest, WithTimestampValue) {
5610+
assertExpectedResults("$isNumber", {{{Value(Timestamp(0, 0))}, Value(false)}});
5611+
}
5612+
5613+
TEST(ExpressionIsNumberTest, WithMaxKeyValue) {
5614+
assertExpectedResults("$isNumber", {{{Value(MAXKEY)}, Value(false)}});
5615+
}
5616+
5617+
} // namespace IsNumber
5618+
55235619
namespace BuiltinRemoveVariable {
55245620

55255621
TEST(BuiltinRemoveVariableTest, TypeOfRemoveIsMissing) {

src/mongo/db/pipeline/expression_visitor.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class ExpressionIn;
7171
class ExpressionIndexOfArray;
7272
class ExpressionIndexOfBytes;
7373
class ExpressionIndexOfCP;
74+
class ExpressionIsNumber;
7475
class ExpressionLet;
7576
class ExpressionLn;
7677
class ExpressionLog;
@@ -194,6 +195,7 @@ class ExpressionVisitor {
194195
virtual void visit(ExpressionIndexOfArray*) = 0;
195196
virtual void visit(ExpressionIndexOfBytes*) = 0;
196197
virtual void visit(ExpressionIndexOfCP*) = 0;
198+
virtual void visit(ExpressionIsNumber*) = 0;
197199
virtual void visit(ExpressionLet*) = 0;
198200
virtual void visit(ExpressionLn*) = 0;
199201
virtual void visit(ExpressionLog*) = 0;

0 commit comments

Comments
 (0)