diff options
-rw-r--r-- | jstests/aggregation/expressions/date_to_string_on_null.js | 77 | ||||
-rw-r--r-- | src/mongo/db/pipeline/SConscript | 5 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 56 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 8 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_date_test.cpp | 1056 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_test.cpp | 953 |
6 files changed, 1179 insertions, 976 deletions
diff --git a/jstests/aggregation/expressions/date_to_string_on_null.js b/jstests/aggregation/expressions/date_to_string_on_null.js new file mode 100644 index 00000000000..e5b3ec50f1b --- /dev/null +++ b/jstests/aggregation/expressions/date_to_string_on_null.js @@ -0,0 +1,77 @@ +/** + * Tests for the $dateToString expression with the optional 'onNull' parameter. + */ +(function() { + "use strict"; + + const onNullValue = ISODate("2017-07-04T11:56:02Z"); + const coll = db.date_to_string_on_null; + coll.drop(); + + assert.writeOK(coll.insert({_id: 0})); + + for (let nullishValue of[null, undefined, "$missing"]) { + // Test that the 'onNull' value is returned when the 'date' is nullish. + assert.eq([{_id: 0, date: onNullValue}], + coll.aggregate({ + $project: { + date: { + $dateToString: { + date: nullishValue, + format: "%Y-%m-%d %H:%M:%S", + onNull: onNullValue + } + } + } + }) + .toArray()); + + // Test that null is returned when the 'timezone' is nullish, regardless of the 'onNull' + // value. + assert.eq([{_id: 0, date: null}], + coll.aggregate({ + $project: { + date: { + $dateToString: { + date: "2018-02-06T11:56:02Z", + format: "%Y-%m-%d %H:%M:%S", + timezone: nullishValue, + onNull: onNullValue + } + } + } + }) + .toArray()); + } + + // Test that 'onNull' can be any type, not just an ISODate. + for (let onNullValue of[{}, 5, "Not a date", null, undefined]) { + assert.eq([{_id: 0, date: onNullValue}], + coll.aggregate({ + $project: { + date: { + $dateToString: { + date: "$missing", + format: "%Y-%m-%d %H:%M:%S", + onNull: onNullValue + } + } + } + }) + .toArray()); + } + + // Test that 'onNull' can be missing, resulting in no output field when used within a $project + // stage. + assert.eq( + [{_id: 0}], + coll.aggregate({ + $project: { + date: { + $dateToString: + {date: "$missing", format: "%Y-%m-%d %H:%M:%S", onNull: "$missing"} + } + } + }) + .toArray()); +})(); diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index 8163982ec19..5e06cd54358 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -391,7 +391,10 @@ env.CppUnitTest( env.CppUnitTest( target='agg_expression_test', - source='expression_test.cpp', + source=[ + 'expression_test.cpp', + 'expression_date_test.cpp', + ], LIBDEPS=[ '$BUILD_DIR/mongo/db/query/query_test_service_context', 'accumulator', diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 3ab0c300cd3..e015f7a0a78 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -1549,19 +1549,22 @@ intrusive_ptr<Expression> ExpressionDateToString::parse( const VariablesParseState& vps) { verify(str::equals(expr.fieldName(), "$dateToString")); - uassert(18629, "$dateToString only supports an object as its argument", expr.type() == Object); + uassert(18629, + "$dateToString only supports an object as its argument", + expr.type() == BSONType::Object); - BSONElement formatElem; - BSONElement dateElem; - BSONElement timeZoneElem; - const BSONObj args = expr.embeddedObject(); - BSONForEach(arg, args) { - if (str::equals(arg.fieldName(), "format")) { + BSONElement formatElem, dateElem, timeZoneElem, onNullElem; + for (auto&& arg : expr.embeddedObject()) { + auto field = arg.fieldNameStringData(); + + if (field == "format"_sd) { formatElem = arg; - } else if (str::equals(arg.fieldName(), "date")) { + } else if (field == "date"_sd) { dateElem = arg; - } else if (str::equals(arg.fieldName(), "timezone")) { + } else if (field == "timezone"_sd) { timeZoneElem = arg; + } else if (field == "onNull"_sd) { + onNullElem = arg; } else { uasserted(18534, str::stream() << "Unrecognized argument to $dateToString: " @@ -1574,7 +1577,7 @@ intrusive_ptr<Expression> ExpressionDateToString::parse( uassert(18533, "The 'format' parameter to $dateToString must be a string literal", - formatElem.type() == String); + formatElem.type() == BSONType::String); const string format = formatElem.str(); @@ -1584,15 +1587,21 @@ intrusive_ptr<Expression> ExpressionDateToString::parse( format, parseOperand(expCtx, dateElem, vps), timeZoneElem ? parseOperand(expCtx, timeZoneElem, vps) - : nullptr); + : nullptr, + onNullElem ? parseOperand(expCtx, onNullElem, vps) : nullptr); } ExpressionDateToString::ExpressionDateToString( const boost::intrusive_ptr<ExpressionContext>& expCtx, const string& format, intrusive_ptr<Expression> date, - intrusive_ptr<Expression> timeZone) - : Expression(expCtx), _format(format), _date(std::move(date)), _timeZone(std::move(timeZone)) {} + intrusive_ptr<Expression> timeZone, + intrusive_ptr<Expression> onNull) + : Expression(expCtx), + _format(format), + _date(std::move(date)), + _timeZone(std::move(timeZone)), + _onNull(std::move(onNull)) {} intrusive_ptr<Expression> ExpressionDateToString::optimize() { _date = _date->optimize(); @@ -1600,7 +1609,11 @@ intrusive_ptr<Expression> ExpressionDateToString::optimize() { _timeZone = _timeZone->optimize(); } - if (ExpressionConstant::allNullOrConstant({_date, _timeZone})) { + if (_onNull) { + _onNull = _onNull->optimize(); + } + + if (ExpressionConstant::allNullOrConstant({_date, _timeZone, _onNull})) { // Everything is a constant, so we can turn into a constant. return ExpressionConstant::create(getExpressionContext(), evaluate(Document{})); } @@ -1613,18 +1626,19 @@ Value ExpressionDateToString::serialize(bool explain) const { Document{{"$dateToString", Document{{"format", _format}, {"date", _date->serialize(explain)}, - {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}}}}); + {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}, + {"onNull", _onNull ? _onNull->serialize(explain) : Value()}}}}); } Value ExpressionDateToString::evaluate(const Document& root) const { const Value date = _date->evaluate(root); - auto timeZone = makeTimeZone(getExpressionContext()->timeZoneDatabase, root, _timeZone.get()); - if (!timeZone) { - return Value(BSONNULL); + if (date.nullish()) { + return _onNull ? _onNull->evaluate(root) : Value(BSONNULL); } - if (date.nullish()) { + auto timeZone = makeTimeZone(getExpressionContext()->timeZoneDatabase, root, _timeZone.get()); + if (!timeZone) { return Value(BSONNULL); } @@ -1636,6 +1650,10 @@ void ExpressionDateToString::_doAddDependencies(DepsTracker* deps) const { if (_timeZone) { _timeZone->addDependencies(deps); } + + if (_onNull) { + _onNull->addDependencies(deps); + } } /* ----------------------- ExpressionDivide ---------------------------- */ diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index 2e0d899ed12..b04ded945e7 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -986,13 +986,15 @@ protected: private: ExpressionDateToString(const boost::intrusive_ptr<ExpressionContext>& expCtx, - const std::string& format, // The format string. - boost::intrusive_ptr<Expression> date, // The date to format. - boost::intrusive_ptr<Expression> timeZone); // The optional timezone. + const std::string& format, // The format string. + boost::intrusive_ptr<Expression> date, // The date to format. + boost::intrusive_ptr<Expression> timeZone, // The optional timezone. + boost::intrusive_ptr<Expression> onNull); const std::string _format; boost::intrusive_ptr<Expression> _date; boost::intrusive_ptr<Expression> _timeZone; + boost::intrusive_ptr<Expression> _onNull; }; class ExpressionDayOfMonth final : public DateExpressionAcceptingTimeZone<ExpressionDayOfMonth> { diff --git a/src/mongo/db/pipeline/expression_date_test.cpp b/src/mongo/db/pipeline/expression_date_test.cpp new file mode 100644 index 00000000000..f41f2b4e857 --- /dev/null +++ b/src/mongo/db/pipeline/expression_date_test.cpp @@ -0,0 +1,1056 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/pipeline/aggregation_context_fixture.h" +#include "mongo/db/pipeline/document_value_test_util.h" +#include "mongo/db/pipeline/value_comparator.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { + +namespace ExpressionDateFromPartsTest { + +// This provides access to an ExpressionContext that has a valid ServiceContext with a +// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. +using ExpressionDateFromPartsTest = AggregationContextFixture; + +TEST_F(ExpressionDateFromPartsTest, SerializesToObjectSyntax) { + auto expCtx = getExpCtx(); + + // Test that it serializes to the full format if given an object specification. + BSONObj spec = + BSON("$dateFromParts" << BSON( + "year" << 2017 << "month" << 6 << "day" << 27 << "hour" << 14 << "minute" << 37 + << "second" + << 15 + << "millisecond" + << 414 + << "timezone" + << "America/Los_Angeles")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto expectedSerialization = + Value(Document{{"$dateFromParts", + Document{{"year", Document{{"$const", 2017}}}, + {"month", Document{{"$const", 6}}}, + {"day", Document{{"$const", 27}}}, + {"hour", Document{{"$const", 14}}}, + {"minute", Document{{"$const", 37}}}, + {"second", Document{{"$const", 15}}}, + {"millisecond", Document{{"$const", 414}}}, + {"timezone", Document{{"$const", "America/Los_Angeles"_sd}}}}}}); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); +} + +TEST_F(ExpressionDateFromPartsTest, OptimizesToConstantIfAllInputsAreConstant) { + auto expCtx = getExpCtx(); + auto spec = BSON("$dateFromParts" << BSON("year" << 2017)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both year, month and day are provided, and are both + // constants. + spec = BSON("$dateFromParts" << BSON("year" << 2017 << "month" << 6 << "day" << 27)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both year, hour and minute are provided, and are both + // expressions which evaluate to constants. + spec = BSON("$dateFromParts" << BSON("year" << BSON("$add" << BSON_ARRAY(1900 << 107)) << "hour" + << BSON("$add" << BSON_ARRAY(13 << 1)) + << "minute" + << BSON("$add" << BSON_ARRAY(40 << 3)))); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both year and milliseconds are provided, and year is an + // expressions which evaluate to a constant, with milliseconds a constant + spec = BSON("$dateFromParts" << BSON( + "year" << BSON("$add" << BSON_ARRAY(1900 << 107)) << "millisecond" << 514)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both isoWeekYear, and isoWeek are provided, and are both + // constants. + spec = BSON("$dateFromParts" << BSON("isoWeekYear" << 2017 << "isoWeek" << 26)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both isoWeekYear, isoWeek and isoDayOfWeek are provided, + // and are both expressions which evaluate to constants. + spec = BSON("$dateFromParts" << BSON("isoWeekYear" << BSON("$add" << BSON_ARRAY(1017 << 1000)) + << "isoWeek" + << BSON("$add" << BSON_ARRAY(20 << 6)) + << "isoDayOfWeek" + << BSON("$add" << BSON_ARRAY(3 << 2)))); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both year and month are provided, but + // year is not a constant. + spec = BSON("$dateFromParts" << BSON("year" + << "$year" + << "month" + << 6)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both year and day are provided, but + // day is not a constant. + spec = BSON("$dateFromParts" << BSON("year" << 2017 << "day" + << "$day")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both isoWeekYear and isoDayOfWeek are provided, + // but isoDayOfWeek is not a constant. + spec = BSON("$dateFromParts" << BSON("isoWeekYear" << 2017 << "isoDayOfWeek" + << "$isoDayOfWeekday")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); +} + +TEST_F(ExpressionDateFromPartsTest, TestThatOutOfRangeValuesRollOver) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromParts" << BSON("year" << 2017 << "month" << -1)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto dateVal = Date_t::fromMillisSinceEpoch(1477958400000); // 11/1/2016 in ms. + ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromParts" << BSON("year" << 2017 << "day" << -1)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + dateVal = Date_t::fromMillisSinceEpoch(1483056000000); // 12/30/2016 + ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromParts" << BSON("year" << 2017 << "hour" << 25)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + dateVal = Date_t::fromMillisSinceEpoch(1483318800000); // 1/2/2017 01:00:00 + ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromParts" << BSON("year" << 2017 << "minute" << 61)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + dateVal = Date_t::fromMillisSinceEpoch(1483232460000); // 1/1/2017 01:01:00 + ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromParts" << BSON("year" << 2017 << "second" << 61)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + dateVal = Date_t::fromMillisSinceEpoch(1483228861000); // 1/1/2017 00:01:01 + ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); +} + +} // namespace ExpressionDateFromPartsTest + +namespace ExpressionDateToPartsTest { + +// This provides access to an ExpressionContext that has a valid ServiceContext with a +// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. +using ExpressionDateToPartsTest = AggregationContextFixture; + +TEST_F(ExpressionDateToPartsTest, SerializesToObjectSyntax) { + auto expCtx = getExpCtx(); + + // Test that it serializes to the full format if given an object specification. + BSONObj spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "timezone" + << "Europe/London" + << "iso8601" + << false)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto expectedSerialization = + Value(Document{{"$dateToParts", + Document{{"date", Document{{"$const", Date_t{}}}}, + {"timezone", Document{{"$const", "Europe/London"_sd}}}, + {"iso8601", Document{{"$const", false}}}}}}); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); +} + +TEST_F(ExpressionDateToPartsTest, OptimizesToConstantIfAllInputsAreConstant) { + auto expCtx = getExpCtx(); + auto spec = BSON("$dateToParts" << BSON("date" << Date_t{})); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both date and timezone are provided, and are both + // constants. + spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "timezone" + << "UTC")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both date and timezone are provided, and are both + // expressions which evaluate to constants. + spec = BSON("$dateToParts" << BSON("date" << BSON("$add" << BSON_ARRAY(Date_t{} << 1000)) + << "timezone" + << BSON("$concat" << BSON_ARRAY("Europe" + << "/" + << "London")))); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both date and iso8601 are provided, and are both + // constants. + spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "iso8601" << true)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both date and iso8601 are provided, and are both + // expressions which evaluate to constants. + spec = BSON("$dateToParts" << BSON("date" << BSON("$add" << BSON_ARRAY(Date_t{} << 1000)) + << "iso8601" + << BSON("$not" << false))); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both date and timezone are provided, but + // date is not a constant. + spec = BSON("$dateToParts" << BSON("date" + << "$date" + << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both date and timezone are provided, but + // timezone is not a constant. + spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "timezone" + << "$tz")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both date and iso8601 are provided, but + // iso8601 is not a constant. + spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "iso8601" + << "$iso8601")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); +} + +} // namespace ExpressionDateToPartsTest + +namespace DateExpressionsTest { + +std::vector<StringData> dateExpressions = {"$year"_sd, + "$isoWeekYear"_sd, + "$month"_sd, + "$dayOfMonth"_sd, + "$hour"_sd, + "$minute"_sd, + "$second"_sd, + "$millisecond"_sd, + "$week"_sd, + "$isoWeek"_sd, + "$dayOfYear"_sd}; + +// This provides access to an ExpressionContext that has a valid ServiceContext with a +// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. +using DateExpressionTest = AggregationContextFixture; + +TEST_F(DateExpressionTest, ParsingAcceptsAllFormats) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + auto possibleSyntaxes = { + // Single argument. + BSON(expName << Date_t{}), + BSON(expName << "$date"), + BSON(expName << BSON("$add" << BSON_ARRAY(Date_t{} << 1000))), + // Single argument wrapped in an array. + BSON(expName << BSON_ARRAY("$date")), + BSON(expName << BSON_ARRAY(Date_t{})), + BSON(expName << BSON_ARRAY(BSON("$add" << BSON_ARRAY(Date_t{} << 1000)))), + // Object literal syntax. + BSON(expName << BSON("date" << Date_t{})), + BSON(expName << BSON("date" + << "$date")), + BSON(expName << BSON("date" << BSON("$add" << BSON_ARRAY("$date" << 1000)))), + BSON(expName << BSON("date" << Date_t{} << "timezone" + << "Europe/London")), + BSON(expName << BSON("date" << Date_t{} << "timezone" + << "$tz"))}; + for (auto&& syntax : possibleSyntaxes) { + Expression::parseExpression(expCtx, syntax, expCtx->variablesParseState); + } + } +} + +TEST_F(DateExpressionTest, ParsingRejectsUnrecognizedFieldsInObjectSpecification) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "Europe/London" + << "extra" + << 4)); + ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), + AssertionException, + 40535); + } +} + +TEST_F(DateExpressionTest, ParsingRejectsEmptyObjectSpecification) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSONObj()); + ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), + AssertionException, + 40539); + } +} + +TEST_F(DateExpressionTest, RejectsEmptyArray) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSONArray()); + // It will parse as an ExpressionArray, and fail at runtime. + ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), + AssertionException, + 40536); + } +} + +TEST_F(DateExpressionTest, RejectsArraysWithMoreThanOneElement) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON_ARRAY("$date" + << "$tz")); + // It will parse as an ExpressionArray, and fail at runtime. + ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), + AssertionException, + 40536); + } +} + +TEST_F(DateExpressionTest, RejectsArraysWithinObjectSpecification) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON("date" << BSON_ARRAY(Date_t{}) << "timezone" + << "Europe/London")); + // It will parse as an ExpressionArray, and fail at runtime. + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"_id", 0}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 16006); + + // Test that it rejects an array for the timezone option. + spec = + BSON(expName << BSON("date" << Date_t{} << "timezone" << BSON_ARRAY("Europe/London"))); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + contextDoc = Document{{"_id", 0}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 40533); + } +} + +TEST_F(DateExpressionTest, RejectsTypesThatCannotCoerceToDate) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << "$stringField"); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"stringField", "string"_sd}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 16006); + } +} + +TEST_F(DateExpressionTest, AcceptsObjectIds) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << "$oid"); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"oid", OID::gen()}}; + dateExp->evaluate(contextDoc); // Should not throw. + } +} + +TEST_F(DateExpressionTest, AcceptsTimestamps) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << "$ts"); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"ts", Timestamp{Date_t{}}}}; + dateExp->evaluate(contextDoc); // Should not throw. + } +} + +TEST_F(DateExpressionTest, RejectsNonStringTimezone) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "$intField")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"intField", 4}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 40533); + } +} + +TEST_F(DateExpressionTest, RejectsUnrecognizedTimeZoneSpecification) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "UNRECOGNIZED!")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"_id", 0}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 40485); + } +} + +TEST_F(DateExpressionTest, SerializesToObjectSyntax) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + // Test that it serializes to the full format if given an object specification. + BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "Europe/London")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto expectedSerialization = + Value(Document{{expName, + Document{{"date", Document{{"$const", Date_t{}}}}, + {"timezone", Document{{"$const", "Europe/London"_sd}}}}}}); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); + + // Test that it serializes to the full format if given a date. + spec = BSON(expName << Date_t{}); + expectedSerialization = + Value(Document{{expName, Document{{"date", Document{{"$const", Date_t{}}}}}}}); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); + + // Test that it serializes to the full format if given a date within an array. + spec = BSON(expName << BSON_ARRAY(Date_t{})); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); + } +} + +TEST_F(DateExpressionTest, OptimizesToConstantIfAllInputsAreConstant) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + // Test that it becomes a constant if only date is provided, and it is constant. + auto spec = BSON(expName << BSON("date" << Date_t{})); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both date and timezone are provided, and are both + // constants. + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both date and timezone are provided, and are both + // expressions which evaluate to constants. + spec = BSON(expName << BSON("date" << BSON("$add" << BSON_ARRAY(Date_t{} << 1000)) + << "timezone" + << BSON("$concat" << BSON_ARRAY("Europe" + << "/" + << "London")))); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both date and timezone are provided, but + // date is not a constant. + spec = BSON(expName << BSON("date" + << "$date" + << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both date and timezone are provided, but + // timezone is not a constant. + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "$tz")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + } +} + +TEST_F(DateExpressionTest, DoesRespectTimeZone) { + // Make sure they each successfully evaluate with a different TimeZone. + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + auto spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "America/New_York")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"_id", 0}}; + dateExp->evaluate(contextDoc); // Should not throw. + } + + // Make sure the time zone is used during evaluation. + auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); // 2017-06-06T19:38:43:234Z. + auto specWithoutTimezone = BSON("$hour" << BSON("date" << date)); + auto hourWithoutTimezone = + Expression::parseExpression(expCtx, specWithoutTimezone, expCtx->variablesParseState) + ->evaluate({}); + ASSERT_VALUE_EQ(hourWithoutTimezone, Value(19)); + + auto specWithTimezone = BSON("$hour" << BSON("date" << date << "timezone" + << "America/New_York")); + auto hourWithTimezone = + Expression::parseExpression(expCtx, specWithTimezone, expCtx->variablesParseState) + ->evaluate({}); + ASSERT_VALUE_EQ(hourWithTimezone, Value(15)); +} + +TEST_F(DateExpressionTest, DoesResultInNullIfGivenNullishInput) { + // Make sure they each successfully evaluate with a different TimeZone. + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + auto contextDoc = Document{{"_id", 0}}; + + // Test that the expression results in null if the date is nullish and the timezone is not + // specified. + auto spec = BSON(expName << BSON("date" + << "$missing")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + spec = BSON(expName << BSON("date" << BSONNULL)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + spec = BSON(expName << BSON("date" << BSONUndefined)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + // Test that the expression results in null if the date is present but the timezone is + // nullish. + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "$missing")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" << BSONNULL)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" << BSONUndefined)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + // Test that the expression results in null if the date and timezone both nullish. + spec = BSON(expName << BSON("date" + << "$missing" + << "timezone" + << BSONUndefined)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + // Test that the expression results in null if the date is nullish and timezone is present. + spec = BSON(expName << BSON("date" + << "$missing" + << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + } +} + +} // namespace DateExpressionsTest + +namespace ExpressionDateToStringTest { + +// This provides access to an ExpressionContext that has a valid ServiceContext with a +// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. +using ExpressionDateToStringTest = AggregationContextFixture; + +TEST_F(ExpressionDateToStringTest, SerializesToObjectSyntax) { + auto expCtx = getExpCtx(); + + // Test that it serializes to the full format if given an object specification. + BSONObj spec = BSON("$dateToString" << BSON("date" << Date_t{} << "timezone" + << "Europe/London" + << "format" + << "%Y-%m-%d" + << "onNull" + << "nullDefault")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto expectedSerialization = + Value(Document{{"$dateToString", + Document{{"format", "%Y-%m-%d"_sd}, + {"date", Document{{"$const", Date_t{}}}}, + {"timezone", Document{{"$const", "Europe/London"_sd}}}, + {"onNull", Document{{"$const", "nullDefault"_sd}}}}}}); + + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); +} + +TEST_F(ExpressionDateToStringTest, OptimizesToConstantIfAllInputsAreConstant) { + auto expCtx = getExpCtx(); + + // Test that it becomes a constant if both format and date are constant, and timezone is + // missing. + auto spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << Date_t{})); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if format, date and timezone are provided, and all are + // constants. + spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << Date_t{} + << "timezone" + << "Europe/Amsterdam")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if format, date and timezone are provided, and all + // expressions which evaluate to constants. + spec = BSON("$dateToString" << BSON("format" + << "%Y-%m%d" + << "date" + << BSON("$add" << BSON_ARRAY(Date_t{} << 1000)) + << "timezone" + << BSON("$concat" << BSON_ARRAY("Europe" + << "/" + << "London")))); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if all parameters are constant, including the optional + // 'onNull'. + spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << Date_t{} + << "timezone" + << "Europe/Amsterdam" + << "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 both format, date and timezone are provided, but + // date is not a constant. + spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << "$date" + << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both format, date and timezone are provided, but + // timezone is not a constant. + spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << Date_t{} + << "timezone" + << "$tz")); + 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' does not evaluate to a constant. + spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << Date_t{} + << "onNull" + << "$onNull")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); +} + +TEST_F(ExpressionDateToStringTest, ReturnsOnNullValueWhenInputIsNullish) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << BSONNULL + << "onNull" + << "null default")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value("null default"_sd), dateExp->evaluate(Document{})); + + spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << BSONNULL + << "onNull" + << BSONNULL)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); + + spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << "$missing" + << "onNull" + << "null default")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value("null default"_sd), dateExp->evaluate(Document{})); + + spec = BSON("$dateToString" << BSON("format" + << "%Y-%m-%d" + << "date" + << "$missing" + << "onNull" + << "$alsoMissing")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(), dateExp->evaluate(Document{})); +} +} // namespace ExpressionDateToStringTest + +namespace ExpressionDateFromStringTest { + +// This provides access to an ExpressionContext that has a valid ServiceContext with a +// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. +using ExpressionDateFromStringTest = AggregationContextFixture; + +TEST_F(ExpressionDateFromStringTest, SerializesToObjectSyntax) { + auto expCtx = getExpCtx(); + + // Test that it serializes to the full format if given an object specification. + BSONObj spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:06:44Z")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto expectedSerialization = Value( + Document{{"$dateFromString", + Document{{"dateString", Document{{"$const", "2017-07-04T13:06:44Z"_sd}}}}}}); + + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); + + // Test that it serializes to the full format if given an object specification. + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:06:44Z" + << "timezone" + << "Europe/London")); + 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}}}}}}); + + 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")); + 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}}}}}}); + + 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. + auto spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:09:57Z")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + 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" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant with the dateString, timezone, and format being a constant. + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:09:57" + << "timezone" + << "Europe/London" + << "format" + << "%Y-%m-%dT%H:%M:%S")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + dateVal = Date_t::fromMillisSinceEpoch(1499170197000); + ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); + + // Test that it does *not* become a constant if dateString is not a constant. + spec = BSON("$dateFromString" << BSON("dateString" + << "$date")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if timezone is not a constant. + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:09:57Z" + << "timezone" + << "$tz")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if format is not a constant. + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-04T13:09:57Z" + << "timezone" + << "Europe/London" + << "format" + << "$format")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); +} + +TEST_F(ExpressionDateFromStringTest, RejectsUnparsableString) { + auto expCtx = getExpCtx(); + + 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); +} + +TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInString) { + auto expCtx = getExpCtx(); + + 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); + + spec = BSON("$dateFromString" << BSON("dateString" + << "July 4, 2017 Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); +} + +TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInStringAndArgument) { + auto expCtx = getExpCtx(); + + // Test with "Z" and timezone + auto spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-14T15:24:38Z" + << "timezone" + << "Europe/London")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40551); + + // Test with timezone abbreviation and timezone + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-14T15:24:38 PDT" + << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40551); + + // Test with GMT offset and timezone + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-14T15:24:38+02:00" + << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40554); + + // Test with GMT offset and GMT timezone + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-14 -0400" + << "timezone" + << "GMT")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40554); +} + +TEST_F(ExpressionDateFromStringTest, RejectsNonStringFormat) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-13T10:02:57" + << "format" + << 2)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40684); + + spec = BSON("$dateFromString" << BSON("dateString" + << "July 4, 2017" + << "format" + << true)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40684); +} + +TEST_F(ExpressionDateFromStringTest, RejectsStringsThatDoNotMatchFormat) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07" + << "format" + << "%Y-%m-%d")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); + + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07" + << "format" + << "%m-%Y")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); +} + +TEST_F(ExpressionDateFromStringTest, EscapeCharacterAllowsPrefixUsage) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "2017 % 01 % 01" + << "format" + << "%Y %% %m %% %d")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2017-01-01T00:00:00.000Z", dateExp->evaluate(Document{}).toString()); +} + + +TEST_F(ExpressionDateFromStringTest, EvaluatesToNullIfFormatIsNullish) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "1/1/2017" + << "format" + << BSONNULL)); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromString" << BSON("dateString" + << "1/1/2017" + << "format" + << "$missing")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); + + spec = BSON("$dateFromString" << BSON("dateString" + << "1/1/2017" + << "format" + << BSONUndefined)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); +} + +TEST_F(ExpressionDateFromStringTest, ReadWithUTCOffset) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-28T10:47:52.912" + << "timezone" + << "-01:00")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2017-07-28T11:47:52.912Z", dateExp->evaluate(Document{}).toString()); + + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-28T10:47:52.912" + << "timezone" + << "+01:00")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2017-07-28T09:47:52.912Z", dateExp->evaluate(Document{}).toString()); + + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-28T10:47:52.912" + << "timezone" + << "+0445")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2017-07-28T06:02:52.912Z", dateExp->evaluate(Document{}).toString()); + + spec = BSON("$dateFromString" << BSON("dateString" + << "2017-07-28T10:47:52.912" + << "timezone" + << "+10:45")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2017-07-28T00:02:52.912Z", dateExp->evaluate(Document{}).toString()); + + spec = BSON("$dateFromString" << BSON("dateString" + << "1945-07-28T10:47:52.912" + << "timezone" + << "-08:00")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("1945-07-28T18:47:52.912Z", dateExp->evaluate(Document{}).toString()); +} + +TEST_F(ExpressionDateFromStringTest, ConvertStringWithUTCOffsetAndFormat) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "10:47:52.912 on 7/28/2017" + << "timezone" + << "-01:00" + << "format" + << "%H:%M:%S.%L on %m/%d/%Y")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2017-07-28T11:47:52.912Z", dateExp->evaluate(Document{}).toString()); + + spec = BSON("$dateFromString" << BSON("dateString" + << "10:47:52.912 on 7/28/2017" + << "timezone" + << "+01:00" + << "format" + << "%H:%M:%S.%L on %m/%d/%Y")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2017-07-28T09:47:52.912Z", dateExp->evaluate(Document{}).toString()); +} + +TEST_F(ExpressionDateFromStringTest, ConvertStringWithISODateFormat) { + auto expCtx = getExpCtx(); + + auto spec = BSON("$dateFromString" << BSON("dateString" + << "Day 7 Week 53 Year 2017" + << "format" + << "Day %u Week %V Year %G")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2018-01-07T00:00:00.000Z", dateExp->evaluate(Document{}).toString()); + + // Week and day of week default to '1' if not specified. + spec = BSON("$dateFromString" << BSON("dateString" + << "Week 53 Year 2017" + << "format" + << "Week %V Year %G")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2018-01-01T00:00:00.000Z", dateExp->evaluate(Document{}).toString()); + + spec = BSON("$dateFromString" << BSON("dateString" + << "Day 7 Year 2017" + << "format" + << "Day %u Year %G")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_EQ("2017-01-08T00:00:00.000Z", dateExp->evaluate(Document{}).toString()); +} + +} // namespace ExpressionDateFromStringTest + +} // namespace mongo diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp index 345c5042f09..a32473505cf 100644 --- a/src/mongo/db/pipeline/expression_test.cpp +++ b/src/mongo/db/pipeline/expression_test.cpp @@ -33,7 +33,6 @@ #include "mongo/db/jsobj.h" #include "mongo/db/json.h" #include "mongo/db/pipeline/accumulator.h" -#include "mongo/db/pipeline/aggregation_context_fixture.h" #include "mongo/db/pipeline/document.h" #include "mongo/db/pipeline/document_value_test_util.h" #include "mongo/db/pipeline/expression.h" @@ -5286,958 +5285,6 @@ TEST(GetComputedPathsTest, ExpressionMapNotConsideredRenameWithDottedInputPath) } // namespace GetComputedPathsTest -namespace ExpressionDateFromPartsTest { - -// This provides access to an ExpressionContext that has a valid ServiceContext with a -// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. -using ExpressionDateFromPartsTest = AggregationContextFixture; - -TEST_F(ExpressionDateFromPartsTest, SerializesToObjectSyntax) { - auto expCtx = getExpCtx(); - - // Test that it serializes to the full format if given an object specification. - BSONObj spec = - BSON("$dateFromParts" << BSON( - "year" << 2017 << "month" << 6 << "day" << 27 << "hour" << 14 << "minute" << 37 - << "second" - << 15 - << "millisecond" - << 414 - << "timezone" - << "America/Los_Angeles")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto expectedSerialization = - Value(Document{{"$dateFromParts", - Document{{"year", Document{{"$const", 2017}}}, - {"month", Document{{"$const", 6}}}, - {"day", Document{{"$const", 27}}}, - {"hour", Document{{"$const", 14}}}, - {"minute", Document{{"$const", 37}}}, - {"second", Document{{"$const", 15}}}, - {"millisecond", Document{{"$const", 414}}}, - {"timezone", Document{{"$const", "America/Los_Angeles"_sd}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); -} - -TEST_F(ExpressionDateFromPartsTest, OptimizesToConstantIfAllInputsAreConstant) { - auto expCtx = getExpCtx(); - auto spec = BSON("$dateFromParts" << BSON("year" << 2017)); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both year, month and day are provided, and are both - // constants. - spec = BSON("$dateFromParts" << BSON("year" << 2017 << "month" << 6 << "day" << 27)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both year, hour and minute are provided, and are both - // expressions which evaluate to constants. - spec = BSON("$dateFromParts" << BSON("year" << BSON("$add" << BSON_ARRAY(1900 << 107)) << "hour" - << BSON("$add" << BSON_ARRAY(13 << 1)) - << "minute" - << BSON("$add" << BSON_ARRAY(40 << 3)))); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both year and milliseconds are provided, and year is an - // expressions which evaluate to a constant, with milliseconds a constant - spec = BSON("$dateFromParts" << BSON( - "year" << BSON("$add" << BSON_ARRAY(1900 << 107)) << "millisecond" << 514)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both isoWeekYear, and isoWeek are provided, and are both - // constants. - spec = BSON("$dateFromParts" << BSON("isoWeekYear" << 2017 << "isoWeek" << 26)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both isoWeekYear, isoWeek and isoDayOfWeek are provided, - // and are both expressions which evaluate to constants. - spec = BSON("$dateFromParts" << BSON("isoWeekYear" << BSON("$add" << BSON_ARRAY(1017 << 1000)) - << "isoWeek" - << BSON("$add" << BSON_ARRAY(20 << 6)) - << "isoDayOfWeek" - << BSON("$add" << BSON_ARRAY(3 << 2)))); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both year and month are provided, but - // year is not a constant. - spec = BSON("$dateFromParts" << BSON("year" - << "$year" - << "month" - << 6)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both year and day are provided, but - // day is not a constant. - spec = BSON("$dateFromParts" << BSON("year" << 2017 << "day" - << "$day")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both isoWeekYear and isoDayOfWeek are provided, - // but isoDayOfWeek is not a constant. - spec = BSON("$dateFromParts" << BSON("isoWeekYear" << 2017 << "isoDayOfWeek" - << "$isoDayOfWeekday")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); -} - -TEST_F(ExpressionDateFromPartsTest, TestThatOutOfRangeValuesRollOver) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$dateFromParts" << BSON("year" << 2017 << "month" << -1)); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto dateVal = Date_t::fromMillisSinceEpoch(1477958400000); // 11/1/2016 in ms. - ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); - - spec = BSON("$dateFromParts" << BSON("year" << 2017 << "day" << -1)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - dateVal = Date_t::fromMillisSinceEpoch(1483056000000); // 12/30/2016 - ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); - - spec = BSON("$dateFromParts" << BSON("year" << 2017 << "hour" << 25)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - dateVal = Date_t::fromMillisSinceEpoch(1483318800000); // 1/2/2017 01:00:00 - ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); - - spec = BSON("$dateFromParts" << BSON("year" << 2017 << "minute" << 61)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - dateVal = Date_t::fromMillisSinceEpoch(1483232460000); // 1/1/2017 01:01:00 - ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); - - spec = BSON("$dateFromParts" << BSON("year" << 2017 << "second" << 61)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - dateVal = Date_t::fromMillisSinceEpoch(1483228861000); // 1/1/2017 00:01:01 - ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); -} - -} // namespace ExpressionDateFromPartsTest - -namespace ExpressionDateToPartsTest { - -// This provides access to an ExpressionContext that has a valid ServiceContext with a -// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. -using ExpressionDateToPartsTest = AggregationContextFixture; - -TEST_F(ExpressionDateToPartsTest, SerializesToObjectSyntax) { - auto expCtx = getExpCtx(); - - // Test that it serializes to the full format if given an object specification. - BSONObj spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "timezone" - << "Europe/London" - << "iso8601" - << false)); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto expectedSerialization = - Value(Document{{"$dateToParts", - Document{{"date", Document{{"$const", Date_t{}}}}, - {"timezone", Document{{"$const", "Europe/London"_sd}}}, - {"iso8601", Document{{"$const", false}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); -} - -TEST_F(ExpressionDateToPartsTest, OptimizesToConstantIfAllInputsAreConstant) { - auto expCtx = getExpCtx(); - auto spec = BSON("$dateToParts" << BSON("date" << Date_t{})); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both date and timezone are provided, and are both - // constants. - spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "timezone" - << "UTC")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both date and timezone are provided, and are both - // expressions which evaluate to constants. - spec = BSON("$dateToParts" << BSON("date" << BSON("$add" << BSON_ARRAY(Date_t{} << 1000)) - << "timezone" - << BSON("$concat" << BSON_ARRAY("Europe" - << "/" - << "London")))); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both date and iso8601 are provided, and are both - // constants. - spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "iso8601" << true)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both date and iso8601 are provided, and are both - // expressions which evaluate to constants. - spec = BSON("$dateToParts" << BSON("date" << BSON("$add" << BSON_ARRAY(Date_t{} << 1000)) - << "iso8601" - << BSON("$not" << false))); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both date and timezone are provided, but - // date is not a constant. - spec = BSON("$dateToParts" << BSON("date" - << "$date" - << "timezone" - << "Europe/London")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both date and timezone are provided, but - // timezone is not a constant. - spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "timezone" - << "$tz")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both date and iso8601 are provided, but - // iso8601 is not a constant. - spec = BSON("$dateToParts" << BSON("date" << Date_t{} << "iso8601" - << "$iso8601")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); -} - -} // namespace ExpressionDateToPartsTest - -namespace DateExpressionsTest { - -std::vector<StringData> dateExpressions = {"$year"_sd, - "$isoWeekYear"_sd, - "$month"_sd, - "$dayOfMonth"_sd, - "$hour"_sd, - "$minute"_sd, - "$second"_sd, - "$millisecond"_sd, - "$week"_sd, - "$isoWeek"_sd, - "$dayOfYear"_sd}; - -// This provides access to an ExpressionContext that has a valid ServiceContext with a -// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. -using DateExpressionTest = AggregationContextFixture; - -TEST_F(DateExpressionTest, ParsingAcceptsAllFormats) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - auto possibleSyntaxes = { - // Single argument. - BSON(expName << Date_t{}), - BSON(expName << "$date"), - BSON(expName << BSON("$add" << BSON_ARRAY(Date_t{} << 1000))), - // Single argument wrapped in an array. - BSON(expName << BSON_ARRAY("$date")), - BSON(expName << BSON_ARRAY(Date_t{})), - BSON(expName << BSON_ARRAY(BSON("$add" << BSON_ARRAY(Date_t{} << 1000)))), - // Object literal syntax. - BSON(expName << BSON("date" << Date_t{})), - BSON(expName << BSON("date" - << "$date")), - BSON(expName << BSON("date" << BSON("$add" << BSON_ARRAY("$date" << 1000)))), - BSON(expName << BSON("date" << Date_t{} << "timezone" - << "Europe/London")), - BSON(expName << BSON("date" << Date_t{} << "timezone" - << "$tz"))}; - for (auto&& syntax : possibleSyntaxes) { - Expression::parseExpression(expCtx, syntax, expCtx->variablesParseState); - } - } -} - -TEST_F(DateExpressionTest, ParsingRejectsUnrecognizedFieldsInObjectSpecification) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" - << "Europe/London" - << "extra" - << 4)); - ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), - AssertionException, - 40535); - } -} - -TEST_F(DateExpressionTest, ParsingRejectsEmptyObjectSpecification) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << BSONObj()); - ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), - AssertionException, - 40539); - } -} - -TEST_F(DateExpressionTest, RejectsEmptyArray) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << BSONArray()); - // It will parse as an ExpressionArray, and fail at runtime. - ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), - AssertionException, - 40536); - } -} - -TEST_F(DateExpressionTest, RejectsArraysWithMoreThanOneElement) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << BSON_ARRAY("$date" - << "$tz")); - // It will parse as an ExpressionArray, and fail at runtime. - ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), - AssertionException, - 40536); - } -} - -TEST_F(DateExpressionTest, RejectsArraysWithinObjectSpecification) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << BSON("date" << BSON_ARRAY(Date_t{}) << "timezone" - << "Europe/London")); - // It will parse as an ExpressionArray, and fail at runtime. - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto contextDoc = Document{{"_id", 0}}; - ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 16006); - - // Test that it rejects an array for the timezone option. - spec = - BSON(expName << BSON("date" << Date_t{} << "timezone" << BSON_ARRAY("Europe/London"))); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - contextDoc = Document{{"_id", 0}}; - ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 40533); - } -} - -TEST_F(DateExpressionTest, RejectsTypesThatCannotCoerceToDate) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << "$stringField"); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto contextDoc = Document{{"stringField", "string"_sd}}; - ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 16006); - } -} - -TEST_F(DateExpressionTest, AcceptsObjectIds) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << "$oid"); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto contextDoc = Document{{"oid", OID::gen()}}; - dateExp->evaluate(contextDoc); // Should not throw. - } -} - -TEST_F(DateExpressionTest, AcceptsTimestamps) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << "$ts"); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto contextDoc = Document{{"ts", Timestamp{Date_t{}}}}; - dateExp->evaluate(contextDoc); // Should not throw. - } -} - -TEST_F(DateExpressionTest, RejectsNonStringTimezone) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" - << "$intField")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto contextDoc = Document{{"intField", 4}}; - ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 40533); - } -} - -TEST_F(DateExpressionTest, RejectsUnrecognizedTimeZoneSpecification) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" - << "UNRECOGNIZED!")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto contextDoc = Document{{"_id", 0}}; - ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), AssertionException, 40485); - } -} - -TEST_F(DateExpressionTest, SerializesToObjectSyntax) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - // Test that it serializes to the full format if given an object specification. - BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" - << "Europe/London")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto expectedSerialization = - Value(Document{{expName, - Document{{"date", Document{{"$const", Date_t{}}}}, - {"timezone", Document{{"$const", "Europe/London"_sd}}}}}}); - ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); - - // Test that it serializes to the full format if given a date. - spec = BSON(expName << Date_t{}); - expectedSerialization = - Value(Document{{expName, Document{{"date", Document{{"$const", Date_t{}}}}}}}); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); - - // Test that it serializes to the full format if given a date within an array. - spec = BSON(expName << BSON_ARRAY(Date_t{})); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); - } -} - -TEST_F(DateExpressionTest, OptimizesToConstantIfAllInputsAreConstant) { - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - // Test that it becomes a constant if only date is provided, and it is constant. - auto spec = BSON(expName << BSON("date" << Date_t{})); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both date and timezone are provided, and are both - // constants. - spec = BSON(expName << BSON("date" << Date_t{} << "timezone" - << "Europe/London")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both date and timezone are provided, and are both - // expressions which evaluate to constants. - spec = BSON(expName << BSON("date" << BSON("$add" << BSON_ARRAY(Date_t{} << 1000)) - << "timezone" - << BSON("$concat" << BSON_ARRAY("Europe" - << "/" - << "London")))); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both date and timezone are provided, but - // date is not a constant. - spec = BSON(expName << BSON("date" - << "$date" - << "timezone" - << "Europe/London")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both date and timezone are provided, but - // timezone is not a constant. - spec = BSON(expName << BSON("date" << Date_t{} << "timezone" - << "$tz")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - } -} - -TEST_F(DateExpressionTest, DoesRespectTimeZone) { - // Make sure they each successfully evaluate with a different TimeZone. - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - auto spec = BSON(expName << BSON("date" << Date_t{} << "timezone" - << "America/New_York")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto contextDoc = Document{{"_id", 0}}; - dateExp->evaluate(contextDoc); // Should not throw. - } - - // Make sure the time zone is used during evaluation. - auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); // 2017-06-06T19:38:43:234Z. - auto specWithoutTimezone = BSON("$hour" << BSON("date" << date)); - auto hourWithoutTimezone = - Expression::parseExpression(expCtx, specWithoutTimezone, expCtx->variablesParseState) - ->evaluate({}); - ASSERT_VALUE_EQ(hourWithoutTimezone, Value(19)); - - auto specWithTimezone = BSON("$hour" << BSON("date" << date << "timezone" - << "America/New_York")); - auto hourWithTimezone = - Expression::parseExpression(expCtx, specWithTimezone, expCtx->variablesParseState) - ->evaluate({}); - ASSERT_VALUE_EQ(hourWithTimezone, Value(15)); -} - -TEST_F(DateExpressionTest, DoesResultInNullIfGivenNullishInput) { - // Make sure they each successfully evaluate with a different TimeZone. - auto expCtx = getExpCtx(); - for (auto&& expName : dateExpressions) { - auto contextDoc = Document{{"_id", 0}}; - - // Test that the expression results in null if the date is nullish and the timezone is not - // specified. - auto spec = BSON(expName << BSON("date" - << "$missing")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); - - spec = BSON(expName << BSON("date" << BSONNULL)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); - - spec = BSON(expName << BSON("date" << BSONUndefined)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); - - // Test that the expression results in null if the date is present but the timezone is - // nullish. - spec = BSON(expName << BSON("date" << Date_t{} << "timezone" - << "$missing")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); - - spec = BSON(expName << BSON("date" << Date_t{} << "timezone" << BSONNULL)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); - - spec = BSON(expName << BSON("date" << Date_t{} << "timezone" << BSONUndefined)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); - - // Test that the expression results in null if the date and timezone both nullish. - spec = BSON(expName << BSON("date" - << "$missing" - << "timezone" - << BSONUndefined)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); - - // Test that the expression results in null if the date is nullish and timezone is present. - spec = BSON(expName << BSON("date" - << "$missing" - << "timezone" - << "Europe/London")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); - } -} - -} // namespace DateExpressionsTest - -namespace ExpressionDateToStringTest { - -// This provides access to an ExpressionContext that has a valid ServiceContext with a -// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. -using ExpressionDateToStringTest = AggregationContextFixture; - -TEST_F(ExpressionDateToStringTest, SerializesToObjectSyntax) { - auto expCtx = getExpCtx(); - - // Test that it serializes to the full format if given an object specification. - BSONObj spec = BSON("$dateToString" << BSON("date" << Date_t{} << "timezone" - << "Europe/London" - << "format" - << "%Y-%m-%d")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto expectedSerialization = - Value(Document{{"$dateToString", - Document{{"format", "%Y-%m-%d"_sd}, - {"date", Document{{"$const", Date_t{}}}}, - {"timezone", Document{{"$const", "Europe/London"_sd}}}}}}); - - ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); -} - -TEST_F(ExpressionDateToStringTest, OptimizesToConstantIfAllInputsAreConstant) { - auto expCtx = getExpCtx(); - - // Test that it becomes a constant if both format and date are constant, and timezone is - // missing. - auto spec = BSON("$dateToString" << BSON("format" - << "%Y-%m-%d" - << "date" - << Date_t{})); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both format, date and timezone are provided, and are both - // constants. - spec = BSON("$dateToString" << BSON("format" - << "%Y-%m-%d" - << "date" - << Date_t{} - << "timezone" - << "Europe/Amsterdam")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant if both format, date and timezone are provided, and are both - // expressions which evaluate to constants. - spec = BSON("$dateToString" << BSON("format" - << "%Y-%m%d" - << "date" - << BSON("$add" << BSON_ARRAY(Date_t{} << 1000)) - << "timezone" - << BSON("$concat" << BSON_ARRAY("Europe" - << "/" - << "London")))); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both format, date and timezone are provided, but - // date is not a constant. - spec = BSON("$dateToString" << BSON("format" - << "%Y-%m-%d" - << "date" - << "$date" - << "timezone" - << "Europe/London")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if both format, date and timezone are provided, but - // timezone is not a constant. - spec = BSON("$dateToString" << BSON("format" - << "%Y-%m-%d" - << "date" - << Date_t{} - << "timezone" - << "$tz")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); -} -} // namespace ExpressionDateToStringTest - -namespace ExpressionDateFromStringTest { - -// This provides access to an ExpressionContext that has a valid ServiceContext with a -// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. -using ExpressionDateFromStringTest = AggregationContextFixture; - -TEST_F(ExpressionDateFromStringTest, SerializesToObjectSyntax) { - auto expCtx = getExpCtx(); - - // Test that it serializes to the full format if given an object specification. - BSONObj spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-04T13:06:44Z")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - auto expectedSerialization = Value( - Document{{"$dateFromString", - Document{{"dateString", Document{{"$const", "2017-07-04T13:06:44Z"_sd}}}}}}); - - ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); - ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); - - // Test that it serializes to the full format if given an object specification. - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-04T13:06:44Z" - << "timezone" - << "Europe/London")); - 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}}}}}}); - - 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")); - 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}}}}}}); - - 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. - auto spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-04T13:09:57Z")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - 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" - << "Europe/London")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it becomes a constant with the dateString, timezone, and format being a constant. - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-04T13:09:57" - << "timezone" - << "Europe/London" - << "format" - << "%Y-%m-%dT%H:%M:%S")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - dateVal = Date_t::fromMillisSinceEpoch(1499170197000); - ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{})); - - // Test that it does *not* become a constant if dateString is not a constant. - spec = BSON("$dateFromString" << BSON("dateString" - << "$date")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if timezone is not a constant. - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-04T13:09:57Z" - << "timezone" - << "$tz")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); - - // Test that it does *not* become a constant if format is not a constant. - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-04T13:09:57Z" - << "timezone" - << "Europe/London" - << "format" - << "$format")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); -} - -TEST_F(ExpressionDateFromStringTest, RejectsUnparsableString) { - auto expCtx = getExpCtx(); - - 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); -} - -TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInString) { - auto expCtx = getExpCtx(); - - 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); - - spec = BSON("$dateFromString" << BSON("dateString" - << "July 4, 2017 Europe/London")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); -} - -TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInStringAndArgument) { - auto expCtx = getExpCtx(); - - // Test with "Z" and timezone - auto spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-14T15:24:38Z" - << "timezone" - << "Europe/London")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40551); - - // Test with timezone abbreviation and timezone - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-14T15:24:38 PDT" - << "timezone" - << "Europe/London")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40551); - - // Test with GMT offset and timezone - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-14T15:24:38+02:00" - << "timezone" - << "Europe/London")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40554); - - // Test with GMT offset and GMT timezone - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-14 -0400" - << "timezone" - << "GMT")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40554); -} - -TEST_F(ExpressionDateFromStringTest, RejectsNonStringFormat) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-13T10:02:57" - << "format" - << 2)); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40684); - - spec = BSON("$dateFromString" << BSON("dateString" - << "July 4, 2017" - << "format" - << true)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40684); -} - -TEST_F(ExpressionDateFromStringTest, RejectsStringsThatDoNotMatchFormat) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07" - << "format" - << "%Y-%m-%d")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); - - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07" - << "format" - << "%m-%Y")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553); -} - -TEST_F(ExpressionDateFromStringTest, EscapeCharacterAllowsPrefixUsage) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$dateFromString" << BSON("dateString" - << "2017 % 01 % 01" - << "format" - << "%Y %% %m %% %d")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2017-01-01T00:00:00.000Z", dateExp->evaluate(Document{}).toString()); -} - - -TEST_F(ExpressionDateFromStringTest, EvaluatesToNullIfFormatIsNullish) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$dateFromString" << BSON("dateString" - << "1/1/2017" - << "format" - << BSONNULL)); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); - - spec = BSON("$dateFromString" << BSON("dateString" - << "1/1/2017" - << "format" - << "$missing")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); - - spec = BSON("$dateFromString" << BSON("dateString" - << "1/1/2017" - << "format" - << BSONUndefined)); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{})); -} - -TEST_F(ExpressionDateFromStringTest, ReadWithUTCOffset) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-28T10:47:52.912" - << "timezone" - << "-01:00")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2017-07-28T11:47:52.912Z", dateExp->evaluate(Document{}).toString()); - - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-28T10:47:52.912" - << "timezone" - << "+01:00")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2017-07-28T09:47:52.912Z", dateExp->evaluate(Document{}).toString()); - - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-28T10:47:52.912" - << "timezone" - << "+0445")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2017-07-28T06:02:52.912Z", dateExp->evaluate(Document{}).toString()); - - spec = BSON("$dateFromString" << BSON("dateString" - << "2017-07-28T10:47:52.912" - << "timezone" - << "+10:45")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2017-07-28T00:02:52.912Z", dateExp->evaluate(Document{}).toString()); - - spec = BSON("$dateFromString" << BSON("dateString" - << "1945-07-28T10:47:52.912" - << "timezone" - << "-08:00")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("1945-07-28T18:47:52.912Z", dateExp->evaluate(Document{}).toString()); -} - -TEST_F(ExpressionDateFromStringTest, ConvertStringWithUTCOffsetAndFormat) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$dateFromString" << BSON("dateString" - << "10:47:52.912 on 7/28/2017" - << "timezone" - << "-01:00" - << "format" - << "%H:%M:%S.%L on %m/%d/%Y")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2017-07-28T11:47:52.912Z", dateExp->evaluate(Document{}).toString()); - - spec = BSON("$dateFromString" << BSON("dateString" - << "10:47:52.912 on 7/28/2017" - << "timezone" - << "+01:00" - << "format" - << "%H:%M:%S.%L on %m/%d/%Y")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2017-07-28T09:47:52.912Z", dateExp->evaluate(Document{}).toString()); -} - -TEST_F(ExpressionDateFromStringTest, ConvertStringWithISODateFormat) { - auto expCtx = getExpCtx(); - - auto spec = BSON("$dateFromString" << BSON("dateString" - << "Day 7 Week 53 Year 2017" - << "format" - << "Day %u Week %V Year %G")); - auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2018-01-07T00:00:00.000Z", dateExp->evaluate(Document{}).toString()); - - // Week and day of week default to '1' if not specified. - spec = BSON("$dateFromString" << BSON("dateString" - << "Week 53 Year 2017" - << "format" - << "Week %V Year %G")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2018-01-01T00:00:00.000Z", dateExp->evaluate(Document{}).toString()); - - spec = BSON("$dateFromString" << BSON("dateString" - << "Day 7 Year 2017" - << "format" - << "Day %u Year %G")); - dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); - ASSERT_EQ("2017-01-08T00:00:00.000Z", dateExp->evaluate(Document{}).toString()); -} - -} // namespace ExpressionDateFromStringTest - class All : public Suite { public: All() : Suite("expression") {} |