diff options
Diffstat (limited to 'src/mongo/db/pipeline')
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 49 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_convert_test.cpp | 303 |
2 files changed, 347 insertions, 5 deletions
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index f0f6d4cd84f..33d3c03c94e 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -4773,6 +4773,7 @@ public: table[BSONType::NumberDouble][BSONType::Bool] = [](Value inputValue) { return Value(inputValue.coerceToBool()); }; + table[BSONType::NumberDouble][BSONType::Date] = &performCastNumberToDate; table[BSONType::NumberDouble][BSONType::NumberInt] = &performCastDoubleToInt; table[BSONType::NumberDouble][BSONType::NumberLong] = &performCastDoubleToLong; table[BSONType::NumberDouble][BSONType::NumberDecimal] = [](Value inputValue) { @@ -4780,6 +4781,13 @@ public: }; // + // Conversions from jstOID + // + table[BSONType::jstOID][BSONType::Date] = [](Value inputValue) { + return Value(inputValue.getOid().asDateT()); + }; + + // // Conversions from Bool // table[BSONType::Bool][BSONType::NumberDouble] = [](Value inputValue) { @@ -4797,6 +4805,24 @@ public: }; // + // Conversions from Date + // + table[BSONType::Date][BSONType::NumberDouble] = [](Value inputValue) { + return Value(static_cast<double>(inputValue.getDate().toMillisSinceEpoch())); + }; + table[BSONType::Date][BSONType::Bool] = [](Value inputValue) { + return Value(inputValue.coerceToBool()); + }; + table[BSONType::Date][BSONType::Date] = &performIdentityConversion; + table[BSONType::Date][BSONType::NumberLong] = [](Value inputValue) { + return Value(inputValue.getDate().toMillisSinceEpoch()); + }; + table[BSONType::Date][BSONType::NumberDecimal] = [](Value inputValue) { + return Value( + Decimal128(static_cast<int64_t>(inputValue.getDate().toMillisSinceEpoch()))); + }; + + // // Conversions from NumberInt // table[BSONType::NumberInt][BSONType::NumberDouble] = [](Value inputValue) { @@ -4822,6 +4848,7 @@ public: table[BSONType::NumberLong][BSONType::Bool] = [](Value inputValue) { return Value(inputValue.coerceToBool()); }; + table[BSONType::NumberLong][BSONType::Date] = &performCastNumberToDate; table[BSONType::NumberLong][BSONType::NumberInt] = &performCastLongToInt; table[BSONType::NumberLong][BSONType::NumberLong] = &performIdentityConversion; table[BSONType::NumberLong][BSONType::NumberDecimal] = [](Value inputValue) { @@ -4835,6 +4862,7 @@ public: table[BSONType::NumberDecimal][BSONType::Bool] = [](Value inputValue) { return Value(inputValue.coerceToBool()); }; + table[BSONType::NumberDecimal][BSONType::Date] = &performCastNumberToDate; table[BSONType::NumberDecimal][BSONType::NumberInt] = [](Value inputValue) { return performCastDecimalToInt(BSONType::NumberInt, inputValue); }; @@ -4968,6 +4996,27 @@ private: return Value(static_cast<int>(longValue)); } + static Value performCastNumberToDate(Value inputValue) { + long long millisSinceEpoch; + + switch (inputValue.getType()) { + case BSONType::NumberLong: + millisSinceEpoch = inputValue.getLong(); + break; + case BSONType::NumberDouble: + millisSinceEpoch = performCastDoubleToLong(inputValue).getLong(); + break; + case BSONType::NumberDecimal: + millisSinceEpoch = + performCastDecimalToInt(BSONType::NumberLong, inputValue).getLong(); + break; + default: + MONGO_UNREACHABLE; + } + + return Value(Date_t::fromMillisSinceEpoch(millisSinceEpoch)); + } + static Value performIdentityConversion(Value inputValue) { return inputValue; } diff --git a/src/mongo/db/pipeline/expression_convert_test.cpp b/src/mongo/db/pipeline/expression_convert_test.cpp index 3f8609b1289..23aa30319de 100644 --- a/src/mongo/db/pipeline/expression_convert_test.cpp +++ b/src/mongo/db/pipeline/expression_convert_test.cpp @@ -284,12 +284,10 @@ TEST_F(ExpressionConvertTest, UnsupportedConversionFails) { {Value(OID()), "int"}, {Value(OID()), "long"}, {Value(OID()), "decimal"}, - {Value(Date_t::fromMillisSinceEpoch(0)), "objectId"}, - {Value(0.0), "date"}, + {Value(Date_t{}), "objectId"}, + {Value(Date_t{}), "int"}, {Value(int{1}), "date"}, {Value(true), "date"}, - {Value(0LL), "date"}, - {Value(Decimal128("0")), "date"}, }; // Attempt every possible unsupported conversion. @@ -469,6 +467,19 @@ TEST_F(ExpressionConvertTest, BoolIdentityConversion) { ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(falseBoolInput), false, BSONType::Bool); } +TEST_F(ExpressionConvertTest, DateIdentityConversion) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$convert" << BSON("input" + << "$path1" + << "to" + << "date")); + auto convertExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + + Document dateInput{{"path1", Value(Date_t{})}}; + ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(dateInput), Date_t{}, BSONType::Date); +} + TEST_F(ExpressionConvertTest, IntIdentityConversion) { auto expCtx = getExpCtx(); @@ -523,6 +534,20 @@ TEST_F(ExpressionConvertTest, DecimalIdentityConversion) { BSONType::NumberDecimal); } +TEST_F(ExpressionConvertTest, ConvertDateToBool) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$convert" << BSON("input" + << "$path1" + << "to" + << "bool")); + auto convertExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + + // All date inputs evaluate as true. + Document dateInput{{"path1", Value(Date_t{})}}; + ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(dateInput), true, BSONType::Bool); +} + TEST_F(ExpressionConvertTest, ConvertIntToBool) { auto expCtx = getExpCtx(); @@ -667,7 +692,7 @@ TEST_F(ExpressionConvertTest, ConvertNumericToDouble) { // wide enough for the original long long value in its entirety. Document largeLongInput{{"path1", Value(0xf0000000000000fLL)}}; result = convertExp->evaluate(largeLongInput); - ASSERT_EQ(static_cast<long long>(result.getDouble()), 0xf00000000000000ll); + ASSERT_EQ(static_cast<long long>(result.getDouble()), 0xf00000000000000LL); // Again, some precision is lost in the conversion from Decimal128 to double. Document preciseDecimalInput{{"path1", Value(Decimal128("1.125000000000000000005"))}}; @@ -675,6 +700,25 @@ TEST_F(ExpressionConvertTest, ConvertNumericToDouble) { ASSERT_EQ(result.getDouble(), 1.125); } +TEST_F(ExpressionConvertTest, ConvertDateToDouble) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$convert" << BSON("input" + << "$path1" + << "to" + << "double")); + auto convertExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + + Document dateInput{{"path1", Value(Date_t::fromMillisSinceEpoch(123))}}; + ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(dateInput), 123.0, BSONType::NumberDouble); + + // Note that the least significant bits get lost, because the significand of a double is not + // wide enough for the original 64-bit Date_t value in its entirety. + Document largeDateInput{{"path1", Value(Date_t::fromMillisSinceEpoch(0xf0000000000000fLL))}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(largeDateInput), 0xf00000000000000LL, BSONType::NumberDouble); +} + TEST_F(ExpressionConvertTest, ConvertOutOfBoundsDecimalToDouble) { auto expCtx = getExpCtx(); @@ -773,6 +817,24 @@ TEST_F(ExpressionConvertTest, ConvertNumericToDecimal) { convertExp->evaluate(largeLongInput), Value(0xf0000000000000fLL), BSONType::NumberDecimal); } +TEST_F(ExpressionConvertTest, ConvertDateToDecimal) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$convert" << BSON("input" + << "$path1" + << "to" + << "decimal")); + auto convertExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + + Document dateInput{{"path1", Value(Date_t::fromMillisSinceEpoch(123))}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(dateInput), Decimal128(123), BSONType::NumberDecimal); + + Document largeDateInput{{"path1", Value(Date_t::fromMillisSinceEpoch(0xf0000000000000fLL))}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(largeDateInput), Value(0xf0000000000000fLL), BSONType::NumberDecimal); +} + TEST_F(ExpressionConvertTest, ConvertDoubleToInt) { auto expCtx = getExpCtx(); @@ -1317,6 +1379,19 @@ TEST_F(ExpressionConvertTest, ConvertOutOfBoundsDecimalToLongWithOnError) { convertExp->evaluate(decimalNegativeInfinity), "X"_sd, BSONType::String); } +TEST_F(ExpressionConvertTest, ConvertDateToLong) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$convert" << BSON("input" + << "$path1" + << "to" + << "long")); + auto convertExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + + Document dateInput{{"path1", Value(Date_t::fromMillisSinceEpoch(123LL))}}; + ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(dateInput), 123LL, BSONType::NumberLong); +} + TEST_F(ExpressionConvertTest, ConvertIntToLong) { auto expCtx = getExpCtx(); @@ -1442,6 +1517,224 @@ TEST_F(ExpressionConvertTest, ConvertBoolToLong) { ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(boolTrue), 1ll, BSONType::NumberLong); } +TEST_F(ExpressionConvertTest, ConvertNumberToDate) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$convert" << BSON("input" + << "$path1" + << "to" + << "date")); + auto convertExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + + Document longInput{{"path1", Value(0ll)}}; + ASSERT_EQ(dateToISOStringUTC(convertExp->evaluate(longInput).getDate()), + "1970-01-01T00:00:00.000Z"); + + Document doubleInput{{"path1", Value(431568000000.0)}}; + ASSERT_EQ(dateToISOStringUTC(convertExp->evaluate(doubleInput).getDate()), + "1983-09-05T00:00:00.000Z"); + + Document doubleInputWithFraction{{"path1", Value(431568000000.987)}}; + ASSERT_EQ(dateToISOStringUTC(convertExp->evaluate(doubleInputWithFraction).getDate()), + "1983-09-05T00:00:00.000Z"); + + Document decimalInput{{"path1", Value(Decimal128("872835240000"))}}; + ASSERT_EQ(dateToISOStringUTC(convertExp->evaluate(decimalInput).getDate()), + "1997-08-29T06:14:00.000Z"); + + Document decimalInputWithFraction{{"path1", Value(Decimal128("872835240000.987"))}}; + ASSERT_EQ(dateToISOStringUTC(convertExp->evaluate(decimalInputWithFraction).getDate()), + "1997-08-29T06:14:00.000Z"); +} + +TEST_F(ExpressionConvertTest, ConvertOutOfBoundsNumberToDate) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$convert" << BSON("input" + << "$path1" + << "to" + << "date")); + auto convertExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + + Document doubleOverflowInput{{"path1", Value(1.0e100)}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(doubleOverflowInput), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS(exception.reason(), + "Conversion would overflow target type"); + }); + + Document doubleNegativeOverflowInput{{"path1", Value(-1.0e100)}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(doubleNegativeOverflowInput), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS(exception.reason(), + "Conversion would overflow target type"); + }); + + Document doubleNaN{{"path1", Value(std::numeric_limits<double>::quiet_NaN())}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(doubleNaN), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS( + exception.reason(), + "Attempt to convert NaN value to integer type"); + }); + + Document doubleInfinity{{"path1", std::numeric_limits<double>::infinity()}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(doubleInfinity), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS( + exception.reason(), + "Attempt to convert infinity value to integer type"); + }); + + Document doubleNegativeInfinity{{"path1", -std::numeric_limits<double>::infinity()}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(doubleNegativeInfinity), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS( + exception.reason(), + "Attempt to convert infinity value to integer type"); + }); + + Document decimalOverflowInput{{"path1", Value(Decimal128("1.0e100"))}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(decimalOverflowInput), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS(exception.reason(), + "Conversion would overflow target type"); + }); + + Document decimalNegativeOverflowInput{{"path1", Value(Decimal128("1.0e100"))}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(decimalNegativeOverflowInput), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS(exception.reason(), + "Conversion would overflow target type"); + }); + + Document decimalNaN{{"path1", Decimal128::kPositiveNaN}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(decimalNaN), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS( + exception.reason(), + "Attempt to convert NaN value to integer type"); + }); + + Document decimalNegativeNaN{{"path1", Decimal128::kNegativeNaN}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(decimalNegativeNaN), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS( + exception.reason(), + "Attempt to convert NaN value to integer type"); + }); + + Document decimalInfinity{{"path1", Decimal128::kPositiveInfinity}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(decimalInfinity), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS( + exception.reason(), + "Attempt to convert infinity value to integer type"); + }); + + Document decimalNegativeInfinity{{"path1", Decimal128::kNegativeInfinity}}; + ASSERT_THROWS_WITH_CHECK(convertExp->evaluate(decimalNegativeInfinity), + AssertionException, + [](const AssertionException& exception) { + ASSERT_EQ(exception.code(), ErrorCodes::ConversionFailure); + ASSERT_STRING_CONTAINS( + exception.reason(), + "Attempt to convert infinity value to integer type"); + }); +} + +TEST_F(ExpressionConvertTest, ConvertOutOfBoundsNumberToDateWithOnError) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$convert" << BSON("input" + << "$path1" + << "to" + << "date" + << "onError" + << "X")); + auto convertExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + + // Int is explicitly disallowed for date conversions. Clients must use 64-bit long instead. + Document intInput{{"path1", Value(int{0})}}; + ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(intInput), "X"_sd, BSONType::String); + + Document doubleOverflowInput{{"path1", Value(1.0e100)}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(doubleOverflowInput), "X"_sd, BSONType::String); + + Document doubleNegativeOverflowInput{{"path1", Value(-1.0e100)}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(doubleNegativeOverflowInput), "X"_sd, BSONType::String); + + Document doubleNaN{{"path1", Value(std::numeric_limits<double>::quiet_NaN())}}; + ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(doubleNaN), "X"_sd, BSONType::String); + + Document doubleInfinity{{"path1", std::numeric_limits<double>::infinity()}}; + ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(doubleInfinity), "X"_sd, BSONType::String); + + Document doubleNegativeInfinity{{"path1", -std::numeric_limits<double>::infinity()}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(doubleNegativeInfinity), "X"_sd, BSONType::String); + + Document decimalOverflowInput{{"path1", Value(Decimal128("1.0e100"))}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(decimalOverflowInput), "X"_sd, BSONType::String); + + Document decimalNegativeOverflowInput{{"path1", Value(Decimal128("1.0e100"))}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(decimalNegativeOverflowInput), "X"_sd, BSONType::String); + + Document decimalNaN{{"path1", Decimal128::kPositiveNaN}}; + ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(decimalNaN), "X"_sd, BSONType::String); + + Document decimalNegativeNaN{{"path1", Decimal128::kNegativeNaN}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(decimalNegativeNaN), "X"_sd, BSONType::String); + + Document decimalInfinity{{"path1", Decimal128::kPositiveInfinity}}; + ASSERT_VALUE_CONTENTS_AND_TYPE(convertExp->evaluate(decimalInfinity), "X"_sd, BSONType::String); + + Document decimalNegativeInfinity{{"path1", Decimal128::kNegativeInfinity}}; + ASSERT_VALUE_CONTENTS_AND_TYPE( + convertExp->evaluate(decimalNegativeInfinity), "X"_sd, BSONType::String); +} + +TEST_F(ExpressionConvertTest, ConvertObjectIdToDate) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$convert" << BSON("input" + << "$path1" + << "to" + << "date")); + auto convertExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + convertExp = convertExp->optimize(); + + Document oidInput{{"path1", Value(OID("59E8A8D8FEDCBA9876543210"))}}; + + ASSERT_EQ(dateToISOStringUTC(convertExp->evaluate(oidInput).getDate()), + "2017-10-19T13:30:00.000Z"); +} + } // namespace ExpressionConvertTest } // namespace mongo |