summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMindaugas Malinauskas <mindaugas.malinauskas@mongodb.com>2021-06-29 17:55:00 +0100
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-07-20 10:37:07 +0000
commit23e56121a014719f0dd29d27a60a72a0e11dac10 (patch)
treef8ba7f2741ad051f9745544409e83554bb51af3d
parenta3b9fc2924e1b5be041716038ba81759a667a5a3 (diff)
downloadmongo-23e56121a014719f0dd29d27a60a72a0e11dac10.tar.gz
SERVER-57321 Make $mod match expression handle NaN, Infinity and large values
-rw-r--r--jstests/core/mod_special_values.js104
-rw-r--r--jstests/libs/sbe_assert_error_override.js9
-rw-r--r--src/mongo/bson/bsonelement.h35
-rw-r--r--src/mongo/bson/bsonelement_test.cpp49
-rw-r--r--src/mongo/db/exec/sbe/expressions/expression.cpp2
-rw-r--r--src/mongo/db/exec/sbe/values/value.cpp5
-rw-r--r--src/mongo/db/exec/sbe/values/value.h2
-rw-r--r--src/mongo/db/exec/sbe/vm/vm.cpp17
-rw-r--r--src/mongo/db/exec/sbe/vm/vm.h2
-rw-r--r--src/mongo/db/matcher/expression_leaf.cpp34
-rw-r--r--src/mongo/db/matcher/expression_leaf.h7
-rw-r--r--src/mongo/db/matcher/expression_parser.cpp21
-rw-r--r--src/mongo/db/query/sbe_stage_builder_filter.cpp49
-rw-r--r--src/mongo/db/query/sbe_stage_builder_helpers.cpp4
-rw-r--r--src/mongo/db/query/sbe_stage_builder_helpers.h5
-rw-r--r--src/mongo/shell/assert.js12
16 files changed, 314 insertions, 43 deletions
diff --git a/jstests/core/mod_special_values.js b/jstests/core/mod_special_values.js
new file mode 100644
index 00000000000..84c7605ae50
--- /dev/null
+++ b/jstests/core/mod_special_values.js
@@ -0,0 +1,104 @@
+/**
+ * Tests $mod match expression with NaN, Infinity and large value inputs.
+ * @tags: [
+ * # This test exercises a changed behavior, thus prevent it running in multi-version variants.
+ * requires_fcv_50
+ * ]
+ */
+(function() {
+"use strict";
+load("jstests/libs/sbe_assert_error_override.js"); // Override error-code-checking APIs.
+const testDB = db.getSiblingDB(jsTestName());
+
+function executeTestCase(collection, testCase) {
+ collection.drop();
+
+ // Insert some documents into the collection.
+ const documents = testCase.inputDocuments || [];
+ assert.commandWorked(collection.insert(documents));
+
+ // Issue a 'find' command with $mod and verify the result.
+ const findCommand = () =>
+ collection.find({attribute: {$mod: [testCase.divisor, testCase.remainder]}}, {_id: 1})
+ .sort({_id: 1})
+ .toArray();
+ if (testCase.hasOwnProperty("expectedError")) {
+ assert.throwsWithCode(findCommand, testCase.expectedError, [], testCase);
+ } else {
+ assert.docEq(findCommand(), testCase.expectedResults, testCase);
+ }
+}
+
+const testCases = [];
+
+// Generate a set of test cases by combining input values, input types for divisor/remainder.
+const numberConverters = [NumberDecimal, Number];
+for (const value of ["NaN", "+Inf", "1e19"]) {
+ for (const numberConverter of numberConverters) {
+ testCases.push({
+ divisor: numberConverter(value),
+ remainder: NumberInt("1"),
+ expectedError: ErrorCodes.BadValue,
+ });
+ testCases.push({
+ divisor: NumberInt("1"),
+ remainder: numberConverter(value),
+ expectedError: ErrorCodes.BadValue,
+ });
+ }
+}
+
+// Tests for dividend parameter.
+// Double dividend value is too large.
+testCases.push({
+ inputDocuments: [{attribute: -1e19}],
+ divisor: 1,
+ remainder: 0,
+ expectedError: 5732100,
+});
+
+// Decimal dividend value is too large.
+testCases.push({
+ inputDocuments: [{attribute: NumberDecimal("1e19")}],
+ divisor: 1,
+ remainder: 0,
+ expectedError: 5732101,
+});
+
+// Verify that dividend value is truncated.
+testCases.push({
+ inputDocuments: [
+ {_id: 1, attribute: 14.5},
+ {_id: 2, attribute: 14.50001},
+ {_id: 3, attribute: 15.01},
+ {_id: 4, attribute: NumberDecimal("14.5")},
+ {_id: 5, attribute: NumberDecimal("14.50001")},
+ {_id: 6, attribute: NumberDecimal("15.01")},
+ {_id: 7, attribute: NumberInt("24")},
+ {_id: 8, attribute: NumberLong("34")},
+ ],
+ divisor: 10,
+ remainder: 4,
+ expectedResults: [{_id: 1}, {_id: 2}, {_id: 4}, {_id: 5}, {_id: 7}, {_id: 8}],
+});
+
+// Verify that NaN, Infinity decimal/double dividend values are not matched.
+testCases.push({
+ inputDocuments: [
+ {attribute: NumberDecimal("-NaN")},
+ {attribute: NumberDecimal("NaN")},
+ {attribute: NumberDecimal("-Inf")},
+ {attribute: NumberDecimal("Inf")},
+ {attribute: +NaN},
+ {attribute: -NaN},
+ {attribute: Infinity},
+ {attribute: -Infinity},
+ ],
+ divisor: 1,
+ remainder: 0,
+ expectedResults: [],
+});
+
+testCases.forEach((testCase, testCaseIdx) =>
+ executeTestCase(testDB.getCollection("coll" + testCaseIdx), testCase));
+})(); \ No newline at end of file
diff --git a/jstests/libs/sbe_assert_error_override.js b/jstests/libs/sbe_assert_error_override.js
index d42eddd5442..80eee699e44 100644
--- a/jstests/libs/sbe_assert_error_override.js
+++ b/jstests/libs/sbe_assert_error_override.js
@@ -113,6 +113,8 @@ const equivalentErrorCodesList = [
[5338802, 5439016],
[5687301, 5687400],
[5687302, 5687401],
+ [5732100, 5732102],
+ [5732101, 5732102],
];
// This map is generated based on the contents of 'equivalentErrorCodesList'. This map should _not_
@@ -180,6 +182,13 @@ assert.writeErrorWithCode = function(res, expectedCode, msg) {
return assertWriteErrorWithCodeOriginal(res, lookupEquivalentErrorCodes(expectedCode), msg);
};
+// Override the assert.throwsWithCode() function.
+const assertThrowsWithCodeOriginal = assert.throwsWithCode;
+assert.throwsWithCode = function(func, expectedCode, params, msg) {
+ return assertThrowsWithCodeOriginal(
+ func, lookupEquivalentErrorCodes(expectedCode), params, msg);
+};
+
// NOTE: Consider using 'assert.commandFailedWithCode' and 'assert.writeErrorWithCode' instead.
// This function should be only used when error code does not come from command. For instance, when
// validating explain output, which includes error code in the execution stats.
diff --git a/src/mongo/bson/bsonelement.h b/src/mongo/bson/bsonelement.h
index 15968765420..84d8f7fb909 100644
--- a/src/mongo/bson/bsonelement.h
+++ b/src/mongo/bson/bsonelement.h
@@ -789,7 +789,21 @@ public:
std::string _asCode() const;
bool coerce(std::string* out) const;
+
+ /**
+ * Coerces the value to an int. If the value type is NumberDouble, the value is rounded to
+ * a closest integer towards zero. If the value type is NumberDecimal, the value is rounded to a
+ * closest integer, but ties are rounded to an even integer. Returns false, if the value cannot
+ * be coerced.
+ */
bool coerce(int* out) const;
+
+ /**
+ * Coerces the value to a long long. If the value type is NumberDouble, the value is rounded to
+ * a closest integer towards zero. If the value type is NumberDecimal, the value is rounded to a
+ * closest integer, but ties are rounded to an even integer. Returns false, if the value cannot
+ * be coerced.
+ */
bool coerce(long long* out) const;
bool coerce(double* out) const;
bool coerce(bool* out) const;
@@ -1028,14 +1042,15 @@ inline long long BSONElement::safeNumberLong() const {
}
/**
- * Attempt to coerce the BSONElement to a primitive type.
- * For integral targets, we do additional checking that the
- * source file is a finite real number and fits within the
- * target type.
+ * Attempt to coerce the BSONElement to a primitive type. For integral targets, we do additional
+ * checking that the source number is a finite real number and fits within the target type after
+ * rounding to the closest integer towards zero. Note that for NumberDecimal types the real number
+ * rounding behavior of this method is different from one employed by 'coerce'.
*/
template <typename T>
Status BSONElement::tryCoerce(T* out) const {
if constexpr (std::is_integral<T>::value && !std::is_same<bool, T>::value) {
+ long long val;
if (type() == NumberDouble) {
double d = numberDouble();
if (!std::isfinite(d)) {
@@ -1044,22 +1059,26 @@ Status BSONElement::tryCoerce(T* out) const {
if ((d > std::numeric_limits<T>::max()) || (d < std::numeric_limits<T>::lowest())) {
return {ErrorCodes::BadValue, "Out of bounds coercing to integral value"};
}
+ val = static_cast<long long>(d);
} else if (type() == NumberDecimal) {
Decimal128 d = numberDecimal();
if (!d.isFinite()) {
return {ErrorCodes::BadValue, "Unable to coerce NaN/Inf to integral type"};
}
+ d = d.round(Decimal128::RoundingMode::kRoundTowardZero);
if (d.isGreater(Decimal128(std::numeric_limits<T>::max())) ||
d.isLess(Decimal128(std::numeric_limits<T>::lowest()))) {
return {ErrorCodes::BadValue, "Out of bounds coercing to integral value"};
}
+ uint32_t signalingFlags = Decimal128::SignalingFlag::kNoFlag;
+ val = d.toLongExact(&signalingFlags);
+ tassert(5732103,
+ "decimal128 number exact conversion to long failed",
+ Decimal128::SignalingFlag::kNoFlag == signalingFlags);
} else if (type() == mongo::Bool) {
*out = Bool();
return Status::OK();
- }
-
- long long val;
- if (!coerce(&val)) {
+ } else if (!coerce(&val)) {
return {ErrorCodes::BadValue, "Unable to coerce value to integral type"};
}
diff --git a/src/mongo/bson/bsonelement_test.cpp b/src/mongo/bson/bsonelement_test.cpp
index 6bcd5abd820..8a2a7c3fc52 100644
--- a/src/mongo/bson/bsonelement_test.cpp
+++ b/src/mongo/bson/bsonelement_test.cpp
@@ -30,7 +30,9 @@
#include "mongo/platform/basic.h"
#include <array>
+#include <cmath>
#include <fmt/format.h>
+#include <limits>
#include "mongo/bson/bsonelement.h"
#include "mongo/bson/bsonobj.h"
@@ -378,5 +380,52 @@ TEST(BSONElementIntegerParseTest, ParseIntegerElementToLongAcceptsThree) {
ASSERT_EQ(result.getValue(), 3LL);
}
+TEST(BSONElementTryCoeceToLongLongTest, CoerceSucceeds) {
+ struct TestCase {
+ BSONObj inputDocument;
+ long long expectedResult;
+ };
+ std::vector<TestCase> testCases{
+ {BSON("" << 3.0), 3},
+ {BSON("" << 4.5), 4},
+ {BSON("" << 5.5000001), 5},
+ {BSON("" << -5.5), -5},
+ {BSON("" << -5.500001), -5},
+ {BSON("" << (-std::exp2(63))), std::numeric_limits<long long>::min()},
+ {BSON("" << 36028797018963968.0), 36028797018963968LL}, // Representable integer in double.
+ {BSON("" << Decimal128{"5.6"}), 5},
+ {BSON("" << Decimal128{"5.5"}), 5},
+ {BSON("" << Decimal128{"-1.5"}), -1},
+ {BSON("" << Decimal128{"-1.500001"}), -1},
+ {BSON("" << Decimal128{"9223372036854775807.5"}), std::numeric_limits<long long>::max()},
+ {BSON("" << Decimal128{"-9223372036854775808.99"}), std::numeric_limits<long long>::min()}};
+ for (auto&& testCase : testCases) {
+ long long number;
+ auto result = testCase.inputDocument.firstElement().tryCoerce(&number);
+ ASSERT_OK(result);
+ ASSERT_EQUALS(number, testCase.expectedResult)
+ << " for input document " << testCase.inputDocument.toString();
+ }
+}
+
+TEST(BSONElementTryCoeceToLongLongTest, CoerceFails) {
+ std::vector<BSONObj> testCases{BSON("" << std::numeric_limits<double>::quiet_NaN()),
+ BSON("" << std::numeric_limits<double>::infinity()),
+ BSON("" << -std::numeric_limits<double>::quiet_NaN()),
+ BSON("" << -std::numeric_limits<double>::infinity()),
+ BSON("" << (std::exp2(63) + (1 << 11))),
+ BSON("" << -std::exp2(63) - (1 << 11)),
+ BSON("" << Decimal128::kPositiveNaN),
+ BSON("" << Decimal128::kPositiveInfinity),
+ BSON("" << Decimal128::kNegativeNaN),
+ BSON("" << Decimal128::kNegativeInfinity),
+ BSON("" << Decimal128{"9223372036854775808.5"}),
+ BSON("" << Decimal128{"-9223372036854775809.99"})};
+ for (auto&& testCase : testCases) {
+ long long number;
+ auto result = testCase.firstElement().tryCoerce(&number);
+ ASSERT_NOT_OK(result) << " for input document " << testCase.toString();
+ }
+}
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/exec/sbe/expressions/expression.cpp b/src/mongo/db/exec/sbe/expressions/expression.cpp
index 958f5bfcce5..dd283728f56 100644
--- a/src/mongo/db/exec/sbe/expressions/expression.cpp
+++ b/src/mongo/db/exec/sbe/expressions/expression.cpp
@@ -499,6 +499,8 @@ static stdx::unordered_map<std::string, InstrFn> kInstrFunctions = {
InstrFn{[](size_t n) { return n == 1; }, &vm::CodeFragment::appendIsBinData, false}},
{"isDate", InstrFn{[](size_t n) { return n == 1; }, &vm::CodeFragment::appendIsDate, false}},
{"isNaN", InstrFn{[](size_t n) { return n == 1; }, &vm::CodeFragment::appendIsNaN, false}},
+ {"isInfinity",
+ InstrFn{[](size_t n) { return n == 1; }, &vm::CodeFragment::appendIsInfinity, false}},
{"isRecordId",
InstrFn{[](size_t n) { return n == 1; }, &vm::CodeFragment::appendIsRecordId, false}},
{"isMinKey",
diff --git a/src/mongo/db/exec/sbe/values/value.cpp b/src/mongo/db/exec/sbe/values/value.cpp
index 68997e1a97f..b0f719cfbc2 100644
--- a/src/mongo/db/exec/sbe/values/value.cpp
+++ b/src/mongo/db/exec/sbe/values/value.cpp
@@ -1164,6 +1164,11 @@ bool isNaN(TypeTags tag, Value val) {
(tag == TypeTags::NumberDecimal && bitcastTo<Decimal128>(val).isNaN());
}
+bool isInfinity(TypeTags tag, Value val) {
+ return (tag == TypeTags::NumberDouble && std::isinf(bitcastTo<double>(val))) ||
+ (tag == TypeTags::NumberDecimal && bitcastTo<Decimal128>(val).isInfinite());
+}
+
void ArraySet::push_back(TypeTags tag, Value val) {
if (tag != TypeTags::Nothing) {
ValueGuard guard{tag, val};
diff --git a/src/mongo/db/exec/sbe/values/value.h b/src/mongo/db/exec/sbe/values/value.h
index bc01d193f90..85b902f784c 100644
--- a/src/mongo/db/exec/sbe/values/value.h
+++ b/src/mongo/db/exec/sbe/values/value.h
@@ -250,6 +250,8 @@ std::pair<TypeTags, Value> compareValue(
bool isNaN(TypeTags tag, Value val);
+bool isInfinity(TypeTags tag, Value val);
+
/**
* A simple hash combination.
*/
diff --git a/src/mongo/db/exec/sbe/vm/vm.cpp b/src/mongo/db/exec/sbe/vm/vm.cpp
index 921c9ec7fa8..c38b046d66e 100644
--- a/src/mongo/db/exec/sbe/vm/vm.cpp
+++ b/src/mongo/db/exec/sbe/vm/vm.cpp
@@ -122,6 +122,7 @@ int Instruction::stackOffset[Instruction::Tags::lastInstruction] = {
0, // isBinData
0, // isDate
0, // isNaN
+ 0, // isInfinity
0, // isRecordId
0, // isMinKey
0, // isMaxKey
@@ -384,6 +385,10 @@ void CodeFragment::appendIsNaN() {
appendSimpleInstruction(Instruction::isNaN);
}
+void CodeFragment::appendIsInfinity() {
+ appendSimpleInstruction(Instruction::isInfinity);
+}
+
void CodeFragment::appendIsRecordId() {
appendSimpleInstruction(Instruction::isRecordId);
}
@@ -4165,6 +4170,18 @@ std::tuple<uint8_t, value::TypeTags, value::Value> ByteCode::run(const CodeFragm
}
break;
}
+ case Instruction::isInfinity: {
+ auto [owned, tag, val] = getFromStack(0);
+ if (tag != value::TypeTags::Nothing) {
+ topStack(false,
+ value::TypeTags::Boolean,
+ value::bitcastFrom<bool>(value::isInfinity(tag, val)));
+ }
+ if (owned) {
+ value::releaseValue(tag, val);
+ }
+ break;
+ }
case Instruction::isRecordId: {
auto [owned, tag, val] = getFromStack(0);
diff --git a/src/mongo/db/exec/sbe/vm/vm.h b/src/mongo/db/exec/sbe/vm/vm.h
index a4c3b470078..fee363944fc 100644
--- a/src/mongo/db/exec/sbe/vm/vm.h
+++ b/src/mongo/db/exec/sbe/vm/vm.h
@@ -257,6 +257,7 @@ struct Instruction {
isBinData,
isDate,
isNaN,
+ isInfinity,
isRecordId,
isMinKey,
isMaxKey,
@@ -468,6 +469,7 @@ public:
void appendIsBinData();
void appendIsDate();
void appendIsNaN();
+ void appendIsInfinity();
void appendIsRecordId();
void appendIsMinKey() {
appendSimpleInstruction(Instruction::isMinKey);
diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp
index 761bfeb434a..c1f9a9ebb44 100644
--- a/src/mongo/db/matcher/expression_leaf.cpp
+++ b/src/mongo/db/matcher/expression_leaf.cpp
@@ -45,6 +45,7 @@
#include "mongo/db/matcher/path.h"
#include "mongo/db/query/collation/collator_interface.h"
#include "mongo/util/regex_util.h"
+#include "mongo/util/represent_as.h"
#include "mongo/util/str.h"
namespace mongo {
@@ -305,7 +306,38 @@ ModMatchExpression::ModMatchExpression(StringData path,
bool ModMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetails* details) const {
if (!e.isNumber())
return false;
- return overflow::safeMod(truncateToLong(e), _divisor) == _remainder;
+ long long dividend;
+ if (e.type() == BSONType::NumberDouble) {
+ auto dividendDouble = e.Double();
+
+ // If dividend is NaN or Infinity, then there is no match.
+ if (!std::isfinite(dividendDouble)) {
+ return false;
+ }
+ auto dividendLong = representAs<long long>(std::trunc(dividendDouble));
+ uassert(5732100,
+ str::stream() << "dividend value cannot be represented as a 64-bit integer: "
+ << e.toString(false),
+ dividendLong);
+ dividend = *dividendLong;
+ } else if (e.type() == BSONType::NumberDecimal) {
+ auto dividendDecimal = e.Decimal();
+
+ // If dividend is NaN or Infinity, then there is no match.
+ if (!dividendDecimal.isFinite()) {
+ return false;
+ }
+ auto dividendLong =
+ representAs<long long>(dividendDecimal.round(Decimal128::kRoundTowardZero));
+ uassert(5732101,
+ str::stream() << "dividend value cannot be represented as a 64-bit integer: "
+ << e.toString(false),
+ dividendLong);
+ dividend = *dividendLong;
+ } else {
+ dividend = e.numberLong();
+ }
+ return overflow::safeMod(dividend, _divisor) == _remainder;
}
void ModMatchExpression::debugString(StringBuilder& debug, int indentationLevel) const {
diff --git a/src/mongo/db/matcher/expression_leaf.h b/src/mongo/db/matcher/expression_leaf.h
index 3c35445bc0b..248e68f1f44 100644
--- a/src/mongo/db/matcher/expression_leaf.h
+++ b/src/mongo/db/matcher/expression_leaf.h
@@ -508,13 +508,6 @@ public:
visitor->visit(this);
}
- static long long truncateToLong(const BSONElement& element) {
- if (element.type() == BSONType::NumberDecimal) {
- return element.numberDecimal().toLong(Decimal128::kRoundTowardZero);
- }
- return element.numberLong();
- }
-
private:
ExpressionOptimizerFunc getOptimizer() const final {
return [](std::unique_ptr<MatchExpression> expression) { return expression; };
diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp
index 38fa24b6dc6..3a2a1bc63b6 100644
--- a/src/mongo/db/matcher/expression_parser.cpp
+++ b/src/mongo/db/matcher/expression_parser.cpp
@@ -539,23 +539,32 @@ StatusWithMatchExpression parseMOD(StringData name,
if (!iter.more())
return {Status(ErrorCodes::BadValue, "malformed mod, not enough elements")};
- auto divisor = iter.next();
- if (!divisor.isNumber())
+ auto divisorElement = iter.next();
+ if (!divisorElement.isNumber())
return {Status(ErrorCodes::BadValue, "malformed mod, divisor not a number")};
if (!iter.more())
return {Status(ErrorCodes::BadValue, "malformed mod, not enough elements")};
- auto remainder = iter.next();
- if (!remainder.isNumber())
+ auto remainderElement = iter.next();
+ if (!remainderElement.isNumber())
return {Status(ErrorCodes::BadValue, "malformed mod, remainder not a number")};
if (iter.more())
return {Status(ErrorCodes::BadValue, "malformed mod, too many elements")};
+ long long divisor;
+ if (auto status = divisorElement.tryCoerce(&divisor); !status.isOK()) {
+ return status.withContext("malformed mod, divisor value is invalid"_sd);
+ }
+
+ long long remainder;
+ if (auto status = remainderElement.tryCoerce(&remainder); !status.isOK()) {
+ return status.withContext("malformed mod, remainder value is invalid"_sd);
+ }
return {std::make_unique<ModMatchExpression>(
name,
- ModMatchExpression::truncateToLong(divisor),
- ModMatchExpression::truncateToLong(remainder),
+ divisor,
+ remainder,
doc_validation_error::createAnnotation(
expCtx, elem.fieldNameStringData().toString(), BSON(name << elem.wrap())))};
}
diff --git a/src/mongo/db/query/sbe_stage_builder_filter.cpp b/src/mongo/db/query/sbe_stage_builder_filter.cpp
index f58ee002164..1beac3ae456 100644
--- a/src/mongo/db/query/sbe_stage_builder_filter.cpp
+++ b/src/mongo/db/query/sbe_stage_builder_filter.cpp
@@ -1644,25 +1644,38 @@ public:
// The mod function returns the result of the mod operation between the operand and
// given divisor, so construct an expression to then compare the result of the operation
// to the given remainder.
- auto makePredicate = [expr](sbe::value::SlotId inputSlot,
- EvalStage inputStage) -> EvalExprStagePair {
+ auto makePredicate = [expr, context = _context](sbe::value::SlotId inputSlot,
+ EvalStage inputStage) -> EvalExprStagePair {
+ auto frameId = context->state.frameId();
+ sbe::EVariable dividend{inputSlot};
+ sbe::EVariable dividendConvertedToNumberInt64{frameId, 0};
auto truncatedArgument = sbe::makeE<sbe::ENumericConvert>(
- sbe::makeE<sbe::EFunction>("trunc",
- sbe::makeEs(sbe::makeE<sbe::EVariable>(inputSlot))),
- sbe::value::TypeTags::NumberInt64);
-
- return {makeFillEmptyFalse(makeBinaryOp(
- sbe::EPrimBinary::eq,
- sbe::makeE<sbe::EFunction>(
- "mod",
- sbe::makeEs(std::move(truncatedArgument),
- sbe::makeE<sbe::EConstant>(
- sbe::value::TypeTags::NumberInt64,
- sbe::value::bitcastFrom<int64_t>(expr->getDivisor())))),
- sbe::makeE<sbe::EConstant>(
- sbe::value::TypeTags::NumberInt64,
- sbe::value::bitcastFrom<int64_t>(expr->getRemainder())))),
- std::move(inputStage)};
+ makeFunction("trunc"_sd, dividend.clone()), sbe::value::TypeTags::NumberInt64);
+ auto modExpression = buildMultiBranchConditional(
+ CaseValuePair{
+ generateNullOrMissing(dividendConvertedToNumberInt64),
+ sbe::makeE<sbe::EFail>(ErrorCodes::Error{5732102},
+ "cannot represent dividend as a 64-bit integer"_sd)},
+ makeFillEmptyFalse(makeBinaryOp(
+ sbe::EPrimBinary::eq,
+ makeFunction(
+ "mod"_sd,
+ dividendConvertedToNumberInt64.clone(),
+ makeConstant(sbe::value::TypeTags::NumberInt64,
+ sbe::value::bitcastFrom<int64_t>(expr->getDivisor()))),
+ makeConstant(sbe::value::TypeTags::NumberInt64,
+ sbe::value::bitcastFrom<int64_t>(expr->getRemainder())))));
+ return {
+ makeBinaryOp(sbe::EPrimBinary::logicAnd,
+ makeNot(makeBinaryOp(sbe::EPrimBinary::logicOr,
+ generateNonNumericCheck(dividend),
+ makeBinaryOp(sbe::EPrimBinary::logicOr,
+ generateNaNCheck(dividend),
+ generateInfinityCheck(dividend)))),
+ sbe::makeE<sbe::ELocalBind>(frameId,
+ sbe::makeEs(std::move(truncatedArgument)),
+ std::move(modExpression))),
+ std::move(inputStage)};
};
generatePredicate(_context,
diff --git a/src/mongo/db/query/sbe_stage_builder_helpers.cpp b/src/mongo/db/query/sbe_stage_builder_helpers.cpp
index 932f565afb9..b6594aebe9a 100644
--- a/src/mongo/db/query/sbe_stage_builder_helpers.cpp
+++ b/src/mongo/db/query/sbe_stage_builder_helpers.cpp
@@ -138,6 +138,10 @@ std::unique_ptr<sbe::EExpression> generateNaNCheck(const sbe::EVariable& var) {
return makeFunction("isNaN", var.clone());
}
+std::unique_ptr<sbe::EExpression> generateInfinityCheck(const sbe::EVariable& var) {
+ return makeFunction("isInfinity"_sd, var.clone());
+}
+
std::unique_ptr<sbe::EExpression> generateNonPositiveCheck(const sbe::EVariable& var) {
return makeBinaryOp(sbe::EPrimBinary::EPrimBinary::lessEq,
var.clone(),
diff --git a/src/mongo/db/query/sbe_stage_builder_helpers.h b/src/mongo/db/query/sbe_stage_builder_helpers.h
index 75d105edbed..f79ff0911c9 100644
--- a/src/mongo/db/query/sbe_stage_builder_helpers.h
+++ b/src/mongo/db/query/sbe_stage_builder_helpers.h
@@ -95,6 +95,11 @@ std::unique_ptr<sbe::EExpression> generateLongLongMinCheck(const sbe::EVariable&
std::unique_ptr<sbe::EExpression> generateNaNCheck(const sbe::EVariable& var);
/**
+ * Generates an EExpression that checks if the input expression is a numeric Infinity.
+ */
+std::unique_ptr<sbe::EExpression> generateInfinityCheck(const sbe::EVariable& var);
+
+/**
* Generates an EExpression that checks if the input expression is a non-positive number (i.e. <= 0)
* _assuming that_ it has already been verified to be numeric.
*/
diff --git a/src/mongo/shell/assert.js b/src/mongo/shell/assert.js
index 10e4567394a..b80d9042e8d 100644
--- a/src/mongo/shell/assert.js
+++ b/src/mongo/shell/assert.js
@@ -580,7 +580,7 @@ assert = (function() {
return error;
};
- assert.throwsWithCode = function(func, code, params, msg) {
+ assert.throwsWithCode = function(func, expectedCode, params, msg) {
if (arguments.length < 2) {
throw new Error("assert.throwsWithCode expects at least 2 arguments");
}
@@ -588,8 +588,14 @@ assert = (function() {
// Use .apply() to preserve the length of the 'arguments' object.
const newArgs = [func, params, msg].filter(element => element !== undefined);
const error = assert.throws.apply(null, newArgs);
-
- assert.eq(error.code, code);
+ if (!Array.isArray(expectedCode)) {
+ expectedCode = [expectedCode];
+ }
+ if (!expectedCode.some((ec) => error.code == ec)) {
+ doassert(_buildAssertionMessage(
+ msg,
+ "[" + tojson(error.code) + "] != [" + tojson(expectedCode) + "] are not equal"));
+ }
};
assert.doesNotThrow = function(func, params, msg) {