diff options
author | Mindaugas Malinauskas <mindaugas.malinauskas@mongodb.com> | 2021-06-29 17:55:00 +0100 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-07-20 10:37:07 +0000 |
commit | 23e56121a014719f0dd29d27a60a72a0e11dac10 (patch) | |
tree | f8ba7f2741ad051f9745544409e83554bb51af3d | |
parent | a3b9fc2924e1b5be041716038ba81759a667a5a3 (diff) | |
download | mongo-23e56121a014719f0dd29d27a60a72a0e11dac10.tar.gz |
SERVER-57321 Make $mod match expression handle NaN, Infinity and large values
-rw-r--r-- | jstests/core/mod_special_values.js | 104 | ||||
-rw-r--r-- | jstests/libs/sbe_assert_error_override.js | 9 | ||||
-rw-r--r-- | src/mongo/bson/bsonelement.h | 35 | ||||
-rw-r--r-- | src/mongo/bson/bsonelement_test.cpp | 49 | ||||
-rw-r--r-- | src/mongo/db/exec/sbe/expressions/expression.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/exec/sbe/values/value.cpp | 5 | ||||
-rw-r--r-- | src/mongo/db/exec/sbe/values/value.h | 2 | ||||
-rw-r--r-- | src/mongo/db/exec/sbe/vm/vm.cpp | 17 | ||||
-rw-r--r-- | src/mongo/db/exec/sbe/vm/vm.h | 2 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_leaf.cpp | 34 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_leaf.h | 7 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser.cpp | 21 | ||||
-rw-r--r-- | src/mongo/db/query/sbe_stage_builder_filter.cpp | 49 | ||||
-rw-r--r-- | src/mongo/db/query/sbe_stage_builder_helpers.cpp | 4 | ||||
-rw-r--r-- | src/mongo/db/query/sbe_stage_builder_helpers.h | 5 | ||||
-rw-r--r-- | src/mongo/shell/assert.js | 12 |
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) { |