diff options
author | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2018-01-30 13:10:47 -0500 |
---|---|---|
committer | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2018-02-21 13:33:36 -0500 |
commit | 1e9e5f85d3d9ae42ea80a48b593be45306724831 (patch) | |
tree | 6ffaca9b84c1117fe6cf4b9e33e720463898b9db /src/mongo/db/pipeline | |
parent | f15200621c45cf27bc348eaa1e0573372fc6ff93 (diff) | |
download | mongo-1e9e5f85d3d9ae42ea80a48b593be45306724831.tar.gz |
SERVER-32736: Add "onError" and "onNull" options to $dateFromString
Diffstat (limited to 'src/mongo/db/pipeline')
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 114 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 6 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_date_test.cpp | 280 |
3 files changed, 355 insertions, 45 deletions
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 3db514b5222..f679616e720 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -1279,9 +1279,7 @@ intrusive_ptr<Expression> ExpressionDateFromString::parse( << typeName(expr.type()), expr.type() == BSONType::Object); - BSONElement dateStringElem; - BSONElement timeZoneElem; - BSONElement formatElem; + BSONElement dateStringElem, timeZoneElem, formatElem, onNullElem, onErrorElem; const BSONObj args = expr.embeddedObject(); for (auto&& arg : args) { @@ -1293,6 +1291,10 @@ intrusive_ptr<Expression> ExpressionDateFromString::parse( dateStringElem = arg; } else if (field == "timezone"_sd) { timeZoneElem = arg; + } else if (field == "onNull"_sd) { + onNullElem = arg; + } else if (field == "onError"_sd) { + onErrorElem = arg; } else { uasserted(40541, str::stream() << "Unrecognized argument to $dateFromString: " @@ -1306,29 +1308,45 @@ intrusive_ptr<Expression> ExpressionDateFromString::parse( expCtx, parseOperand(expCtx, dateStringElem, vps), timeZoneElem ? parseOperand(expCtx, timeZoneElem, vps) : nullptr, - formatElem ? parseOperand(expCtx, formatElem, vps) : nullptr); + formatElem ? parseOperand(expCtx, formatElem, vps) : nullptr, + onNullElem ? parseOperand(expCtx, onNullElem, vps) : nullptr, + onErrorElem ? parseOperand(expCtx, onErrorElem, vps) : nullptr); } ExpressionDateFromString::ExpressionDateFromString( const boost::intrusive_ptr<ExpressionContext>& expCtx, intrusive_ptr<Expression> dateString, intrusive_ptr<Expression> timeZone, - intrusive_ptr<Expression> format) + intrusive_ptr<Expression> format, + intrusive_ptr<Expression> onNull, + intrusive_ptr<Expression> onError) : Expression(expCtx), _dateString(std::move(dateString)), _timeZone(std::move(timeZone)), - _format(std::move(format)) {} + _format(std::move(format)), + _onNull(std::move(onNull)), + _onError(std::move(onError)) {} intrusive_ptr<Expression> ExpressionDateFromString::optimize() { _dateString = _dateString->optimize(); if (_timeZone) { _timeZone = _timeZone->optimize(); } + if (_format) { _format = _format->optimize(); } - if (ExpressionConstant::allNullOrConstant({_dateString, _timeZone, _format})) { + if (_onNull) { + _onNull = _onNull->optimize(); + } + + if (_onError) { + _onError = _onError->optimize(); + } + + if (ExpressionConstant::allNullOrConstant( + {_dateString, _timeZone, _format, _onNull, _onError})) { // Everything is a constant, so we can turn into a constant. return ExpressionConstant::create(getExpressionContext(), evaluate(Document{})); } @@ -1340,48 +1358,71 @@ Value ExpressionDateFromString::serialize(bool explain) const { Document{{"$dateFromString", Document{{"dateString", _dateString->serialize(explain)}, {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}, - {"format", _format ? _format->serialize(explain) : Value()}}}}); + {"format", _format ? _format->serialize(explain) : Value()}, + {"onNull", _onNull ? _onNull->serialize(explain) : Value()}, + {"onError", _onError ? _onError->serialize(explain) : Value()}}}}); } Value ExpressionDateFromString::evaluate(const Document& root) const { const Value dateString = _dateString->evaluate(root); + Value formatValue; + + // Eagerly validate the format parameter, ignoring if nullish since the input string nullish + // behavior takes precedence. + if (_format) { + formatValue = _format->evaluate(root); + if (!formatValue.nullish()) { + uassert(40684, + str::stream() << "$dateFromString requires that 'format' be a string, found: " + << typeName(formatValue.getType()) + << " with value " + << formatValue.toString(), + formatValue.getType() == BSONType::String); + + TimeZone::validateFromStringFormat(formatValue.getStringData()); + } + } + // Evaluate the timezone parameter before checking for nullish input, as this will throw an + // exception for an invalid timezone string. auto timeZone = makeTimeZone(getExpressionContext()->timeZoneDatabase, root, _timeZone.get()); - if (!timeZone || dateString.nullish()) { - return Value(BSONNULL); + // Behavior for nullish input takes precedence over other nullish elements. + if (dateString.nullish()) { + return _onNull ? _onNull->evaluate(root) : Value(BSONNULL); } - uassert(40543, - str::stream() << "$dateFromString requires that 'dateString' be a string, found: " - << typeName(dateString.getType()) - << " with value " - << dateString.toString(), - dateString.getType() == BSONType::String); - const auto& dateTimeString = dateString.getStringData(); + try { + uassert(ErrorCodes::ConversionFailure, + str::stream() << "$dateFromString requires that 'dateString' be a string, found: " + << typeName(dateString.getType()) + << " with value " + << dateString.toString(), + dateString.getType() == BSONType::String); - if (_format) { - const Value format = _format->evaluate(root); + const auto dateTimeString = dateString.getStringData(); - if (format.nullish()) { + if (!timeZone) { return Value(BSONNULL); } - uassert(40684, - str::stream() << "$dateFromString requires that 'format' be a string, found: " - << typeName(format.getType()) - << " with value " - << format.toString(), - format.getType() == BSONType::String); - const auto& formatString = format.getStringData(); + if (_format) { + if (formatValue.nullish()) { + return Value(BSONNULL); + } - TimeZone::validateFromStringFormat(formatString); + return Value(getExpressionContext()->timeZoneDatabase->fromString( + dateTimeString, timeZone, formatValue.getStringData())); + } - return Value(getExpressionContext()->timeZoneDatabase->fromString( - dateTimeString, timeZone, formatString)); + return Value( + getExpressionContext()->timeZoneDatabase->fromString(dateTimeString, timeZone)); + } catch (const ExceptionFor<ErrorCodes::ConversionFailure>&) { + if (_onError) { + return _onError->evaluate(root); + } + throw; } - - return Value(getExpressionContext()->timeZoneDatabase->fromString(dateTimeString, timeZone)); } void ExpressionDateFromString::_doAddDependencies(DepsTracker* deps) const { @@ -1389,9 +1430,18 @@ void ExpressionDateFromString::_doAddDependencies(DepsTracker* deps) const { if (_timeZone) { _timeZone->addDependencies(deps); } + if (_format) { _format->addDependencies(deps); } + + if (_onNull) { + _onNull->addDependencies(deps); + } + + if (_onError) { + _onError->addDependencies(deps); + } } /* ---------------------- ExpressionDateToParts ----------------------- */ diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index 7f843caffcc..b70457cc06f 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -874,11 +874,15 @@ private: ExpressionDateFromString(const boost::intrusive_ptr<ExpressionContext>& expCtx, boost::intrusive_ptr<Expression> dateString, boost::intrusive_ptr<Expression> timeZone, - boost::intrusive_ptr<Expression> format); + boost::intrusive_ptr<Expression> format, + boost::intrusive_ptr<Expression> onNull, + boost::intrusive_ptr<Expression> onError); boost::intrusive_ptr<Expression> _dateString; boost::intrusive_ptr<Expression> _timeZone; boost::intrusive_ptr<Expression> _format; + boost::intrusive_ptr<Expression> _onNull; + boost::intrusive_ptr<Expression> _onError; }; class ExpressionDateFromParts final : public Expression { diff --git a/src/mongo/db/pipeline/expression_date_test.cpp b/src/mongo/db/pipeline/expression_date_test.cpp index bfe72aefa3b..fbceb9c5349 100644 --- a/src/mongo/db/pipeline/expression_date_test.cpp +++ b/src/mongo/db/pipeline/expression_date_test.cpp @@ -772,11 +772,34 @@ TEST_F(ExpressionDateFromStringTest, SerializesToObjectSyntax) { ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); + + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:06:44Z" + << "timezone" + << "Europe/London" + << "format" + << "%Y-%d-%mT%H:%M:%S" + << "onNull" + << "nullDefault" + << "onError" + << "errorDefault")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + expectedSerialization = + Value(Document{{"$dateFromString", + Document{{"dateString", Document{{"$const", "2017-07-04T13:06:44Z"_sd}}}, + {"timezone", Document{{"$const", "Europe/London"_sd}}}, + {"format", Document{{"$const", "%Y-%d-%mT%H:%M:%S"_sd}}}, + {"onNull", Document{{"$const", "nullDefault"_sd}}}, + {"onError", Document{{"$const", "errorDefault"_sd}}}}}}); + + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); } TEST_F(ExpressionDateFromStringTest, OptimizesToConstantIfAllInputsAreConstant) { auto expCtx = getExpCtx(); - // Test that it becomes a constant with just the dateString. + + // Test that it becomes a constant if all parameters evaluate to a constant value. auto spec = BSON("$dateFromString" << BSON("dateString" << "2017-07-04T13:09:57Z")); auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); @@ -785,7 +808,6 @@ TEST_F(ExpressionDateFromStringTest, OptimizesToConstantIfAllInputsAreConstant) Date_t dateVal = Date_t::fromMillisSinceEpoch(1499173797000); ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); - // Test that it becomes a constant with the dateString and timezone being a constant. spec = BSON("$dateFromString" << BSON("dateString" << "2017-07-04T13:09:57" << "timezone" @@ -806,6 +828,29 @@ TEST_F(ExpressionDateFromStringTest, OptimizesToConstantIfAllInputsAreConstant) dateVal = Date_t::fromMillisSinceEpoch(1499170197000); ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:09:57" + << "onNull" + << "Null default")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:09:57" + << "onError" + << "Error default")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:09:57" + << "onError" + << "Error default" + << "onNull" + << "null default")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + // Test that it does *not* become a constant if dateString is not a constant. spec = BSON("$dateFromString" << BSON("dateString" << "$date")); @@ -829,6 +874,22 @@ TEST_F(ExpressionDateFromStringTest, OptimizesToConstantIfAllInputsAreConstant) << "$format")); dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if onNull is not a constant. + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:09:57Z" + << "onNull" + << "$onNull")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if onError is not a constant. + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:09:57Z" + << "onError" + << "$onError")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); } TEST_F(ExpressionDateFromStringTest, RejectsUnparsableString) { @@ -837,7 +898,7 @@ TEST_F(ExpressionDateFromStringTest, RejectsUnparsableString) { auto spec = BSON("$dateFromString" << BSON("dateString" << "60.Monday1770/06:59")); auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, ErrorCodes::ConversionFailure); } TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInString) { @@ -846,12 +907,12 @@ TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInString) { auto spec = BSON("$dateFromString" << BSON("dateString" << "2017-07-13T10:02:57 Europe/London")); auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, ErrorCodes::ConversionFailure); spec = BSON("$dateFromString" << BSON("dateString" << "July 4, 2017 Europe/London")); dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, ErrorCodes::ConversionFailure); } TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInStringAndArgument) { @@ -863,7 +924,7 @@ TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInStringAndArgument) { << "timezone" << "Europe/London")); auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40551); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, ErrorCodes::ConversionFailure); // Test with timezone abbreviation and timezone spec = BSON("$dateFromString" << BSON("dateString" @@ -871,7 +932,7 @@ TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInStringAndArgument) { << "timezone" << "Europe/London")); dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40551); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, ErrorCodes::ConversionFailure); // Test with GMT offset and timezone spec = BSON("$dateFromString" << BSON("dateString" @@ -879,7 +940,7 @@ TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInStringAndArgument) { << "timezone" << "Europe/London")); dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40554); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, ErrorCodes::ConversionFailure); // Test with GMT offset and GMT timezone spec = BSON("$dateFromString" << BSON("dateString" @@ -887,7 +948,7 @@ TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInStringAndArgument) { << "timezone" << "GMT")); dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40554); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, ErrorCodes::ConversionFailure); } TEST_F(ExpressionDateFromStringTest, RejectsNonStringFormat) { @@ -916,14 +977,14 @@ TEST_F(ExpressionDateFromStringTest, RejectsStringsThatDoNotMatchFormat) { << "format" << "%Y-%m-%d")); auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, ErrorCodes::ConversionFailure); spec = BSON("$dateFromString" << BSON("dateString" << "2017-07" << "format" << "%m-%Y")); dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, ErrorCodes::ConversionFailure); } TEST_F(ExpressionDateFromStringTest, EscapeCharacterAllowsPrefixUsage) { @@ -1050,6 +1111,201 @@ TEST_F(ExpressionDateFromStringTest, ConvertStringWithISODateFormat) { ASSERT_EQ("2017-01-08T00:00:00.000Z", dateExp->evaluate(Document{}).toString()); } -} // namespace ExpressionDateFromStringTest +TEST_F(ExpressionDateFromStringTest, ReturnsOnNullForNullishInput) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" << BSONNULL << "onNull" + << "Null default")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value("Null default"_sd), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromString" << BSON("dateString" + << "$missing" + << "onNull" + << "Null default")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value("Null default"_sd), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromString" << BSON("dateString" + << "$missing" + << "onNull" + << "$alsoMissing")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromString" << BSON("dateString" << BSONNULL << "onNull" << BSONNULL)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); +} + +TEST_F(ExpressionDateFromStringTest, InvalidFormatTakesPrecedenceOverOnNull) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" << BSONNULL << "onNull" + << "Null default" + << "format" + << 5)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40684); + + spec = BSON("$dateFromString" << BSON("dateString" << BSONNULL << "onNull" + << "Null default" + << "format" + << "%")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 18535); +} + +TEST_F(ExpressionDateFromStringTest, InvalidFormatTakesPrecedenceOverOnError) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "Invalid dateString" + << "onError" + << "Not used default" + << "format" + << 5)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40684); + + spec = BSON("$dateFromString" << BSON("dateString" << 5 << "onError" + << "Not used default" + << "format" + << "%")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 18535); +} + +TEST_F(ExpressionDateFromStringTest, InvalidTimezoneTakesPrecedenceOverOnNull) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" << BSONNULL << "onNull" + << "Null default" + << "timezone" + << 5)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40517); + spec = BSON("$dateFromString" << BSON("dateString" << BSONNULL << "onNull" + << "Null default" + << "timezone" + << "invalid timezone string")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40485); +} + +TEST_F(ExpressionDateFromStringTest, InvalidTimezoneTakesPrecedenceOverOnError) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "Invalid dateString" + << "onError" + << "On error default" + << "timezone" + << 5)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40517); + + spec = BSON("$dateFromString" << BSON("dateString" << 5 << "onError" + << "On error default" + << "timezone" + << "invalid timezone string")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40485); +} + +TEST_F(ExpressionDateFromStringTest, OnNullTakesPrecedenceOverOtherNullishParameters) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" << BSONNULL << "onNull" + << "Null default" + << "timezone" + << BSONNULL)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value("Null default"_sd), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromString" << BSON("dateString" << BSONNULL << "onNull" + << "Null default" + << "format" + << BSONNULL)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value("Null default"_sd), dateExp->evaluate(Document{})); +} + +TEST_F(ExpressionDateFromStringTest, OnNullOnlyUsedIfInputStringIsNullish) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "2018-02-14" + << "onNull" + << "Null default" + << "timezone" + << BSONNULL)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromString" << BSON("dateString" + << "2018-02-14" + << "onNull" + << "Null default" + << "format" + << BSONNULL)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); +} + +TEST_F(ExpressionDateFromStringTest, ReturnsOnErrorForParseFailures) { + auto expCtx = getExpCtx(); + + std::vector<std::string> invalidDates = { + "60.Monday1770/06:59", "July 4th", "12:50:53", "2017, 12:50:53"}; + for (auto date : invalidDates) { + auto spec = BSON("$dateFromString" << BSON("dateString" << date << "onError" + << "Error default")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value("Error default"_sd), dateExp->evaluate(Document{})); + } +} + +TEST_F(ExpressionDateFromStringTest, ReturnsOnErrorForFormatMismatch) { + auto expCtx = getExpCtx(); + + const std::string date = "2018/02/06"; + std::vector<std::string> unmatchedFormats = {"%Y", "%Y/%m/%d:%H", "Y/m/d"}; + for (auto format : unmatchedFormats) { + auto spec = + BSON("$dateFromString" << BSON("dateString" << date << "format" << format << "onError" + << "Error default")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value("Error default"_sd), dateExp->evaluate(Document{})); + } +} + +TEST_F(ExpressionDateFromStringTest, OnNullEvaluatedLazily) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "$date" + << "onNull" + << BSON("$divide" << BSON_ARRAY(1 << 0)))); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2018-02-14T00:00:00.000Z", + dateExp->evaluate(Document{{"date", "2018-02-14"_sd}}).toString()); + ASSERT_THROWS_CODE(dateExp->evaluate(Document{}), AssertionException, 16608); +} + +TEST_F(ExpressionDateFromStringTest, OnErrorEvaluatedLazily) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "$date" + << "onError" + << BSON("$divide" << BSON_ARRAY(1 << 0)))); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2018-02-14T00:00:00.000Z", + dateExp->evaluate(Document{{"date", "2018-02-14"_sd}}).toString()); + ASSERT_THROWS_CODE(dateExp->evaluate(Document{{"date", 5}}), AssertionException, 16608); +} + +} // namespace ExpressionDateFromStringTest } // namespace mongo |