diff options
author | Derick Rethans <github@derickrethans.nl> | 2017-06-16 16:22:47 +0100 |
---|---|---|
committer | Derick Rethans <github@derickrethans.nl> | 2017-07-11 11:47:53 +0100 |
commit | b8de37d364fcde0ed2c93cdf8f5ed0001663e372 (patch) | |
tree | 0ef21d51dd7931a7135f1f6180866beb0f6352d3 /src | |
parent | 6134f8fbafdb472fb4ca94631cf888218a51cd06 (diff) | |
download | mongo-b8de37d364fcde0ed2c93cdf8f5ed0001663e372.tar.gz |
SERVER-29208 Add the $dateFromString aggregation operator
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 73 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 19 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_test.cpp | 39 | ||||
-rw-r--r-- | src/mongo/db/query/datetime/date_time_support.cpp | 46 | ||||
-rw-r--r-- | src/mongo/db/query/datetime/date_time_support.h | 5 |
5 files changed, 182 insertions, 0 deletions
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 798d05ca285..8b3b15143b8 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -1314,6 +1314,79 @@ void ExpressionDateFromParts::addDependencies(DepsTracker* deps) const { } } +/* ---------------------- ExpressionDateFromString --------------------- */ + +REGISTER_EXPRESSION(dateFromString, ExpressionDateFromString::parse); +intrusive_ptr<Expression> ExpressionDateFromString::parse( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + BSONElement expr, + const VariablesParseState& vps) { + + uassert(40540, + str::stream() << "$dateFromString only supports an object as an argument, found: " + << typeName(expr.type()), + expr.type() == BSONType::Object); + + BSONElement dateStringElem; + const BSONObj args = expr.embeddedObject(); + for (auto&& arg : args) { + if (arg.fieldNameStringData() == "dateString"_sd) { + dateStringElem = arg; + } else { + uasserted(40541, + str::stream() << "Unrecognized argument to $dateFromString: " + << arg.fieldName()); + } + } + + uassert(40542, "Missing 'dateString' parameter to $dateFromString", dateStringElem); + + return new ExpressionDateFromString(expCtx, parseOperand(expCtx, dateStringElem, vps)); +} + +ExpressionDateFromString::ExpressionDateFromString( + const boost::intrusive_ptr<ExpressionContext>& expCtx, intrusive_ptr<Expression> dateString) + : Expression(expCtx), _dateString(dateString) {} + +intrusive_ptr<Expression> ExpressionDateFromString::optimize() { + _dateString = _dateString->optimize(); + + if (dynamic_cast<ExpressionConstant*>(_dateString.get())) { + // Everything is a constant, so we can turn into a constant. + return ExpressionConstant::create(getExpressionContext(), evaluate(Document{})); + } + return this; +} + +Value ExpressionDateFromString::serialize(bool explain) const { + return Value( + Document{{"$dateFromString", Document{{"dateString", _dateString->serialize(explain)}}}}); +} + +Value ExpressionDateFromString::evaluate(const Document& root) const { + const Value dateString = _dateString->evaluate(root); + + if (dateString.nullish()) { + return Value(BSONNULL); + } + + uassert(40543, + str::stream() << "$dateFromString requires that 'dateString' be a string, found: " + << typeName(dateString.getType()) + << " with value " + << dateString.toString(), + dateString.getType() == BSONType::String); + const std::string& dateTimeString = dateString.getString(); + + auto tzdb = TimeZoneDatabase::get(getExpressionContext()->opCtx->getServiceContext()); + + return Value(tzdb->fromString(dateTimeString)); +} + +void ExpressionDateFromString::addDependencies(DepsTracker* deps) const { + _dateString->addDependencies(deps); +} + /* ---------------------- ExpressionDateToParts ----------------------- */ REGISTER_EXPRESSION(dateToParts, ExpressionDateToParts::parse); diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index aad2ced9b55..c7889305aa0 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -828,6 +828,25 @@ private: typedef ExpressionFixedArity<ExpressionCond, 3> Base; }; +class ExpressionDateFromString final : public Expression { +public: + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; + Value evaluate(const Document&) const final; + void addDependencies(DepsTracker*) const final; + + static boost::intrusive_ptr<Expression> parse( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + BSONElement expr, + const VariablesParseState& vps); + +private: + ExpressionDateFromString(const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::intrusive_ptr<Expression> dateString); + + boost::intrusive_ptr<Expression> _dateString; +}; + class ExpressionDateFromParts final : public Expression { public: boost::intrusive_ptr<Expression> optimize() final; diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp index f89735aff56..3ae780066e0 100644 --- a/src/mongo/db/pipeline/expression_test.cpp +++ b/src/mongo/db/pipeline/expression_test.cpp @@ -4884,6 +4884,45 @@ TEST_F(DateExpressionTest, DoesResultInNullIfGivenNullishInput) { } // namespace DateExpressionsTest +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_F(ExpressionDateFromStringTest, OptimizesToConstantIfAllInputsAreConstant) { + auto expCtx = getExpCtx(); + 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 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())); +} + +} // namespace ExpressionDateFromStringTest + class All : public Suite { public: All() : Suite("expression") {} diff --git a/src/mongo/db/query/datetime/date_time_support.cpp b/src/mongo/db/query/datetime/date_time_support.cpp index 1356356665d..88ba04d5549 100644 --- a/src/mongo/db/query/datetime/date_time_support.cpp +++ b/src/mongo/db/query/datetime/date_time_support.cpp @@ -122,6 +122,52 @@ TimeZone TimeZoneDatabase::utcZone() { return TimeZone{nullptr}; } +extern "C" { +static timelib_tzinfo* timezonedatabase_gettzinfowrapper(char* tz_id, + const _timelib_tzdb* db, + int* error) { + uasserted( + 40544, + str::stream() + << "passing a time zone identifier as part of the string is not allowed, found: \"" + << tz_id + << "\""); + + return nullptr; +} + +} // extern "C" + +Date_t TimeZoneDatabase::fromString(StringData dateString) const { + std::unique_ptr<timelib_time, TimeZone::TimelibTimeDeleter> t( + timelib_strtotime(const_cast<char*>(dateString.toString().c_str()), + dateString.size(), + nullptr, + _timeZoneDatabase.get(), + timezonedatabase_gettzinfowrapper)); + + // If the time portion is fully missing, initialize to 0. This allows for the '%Y-%m-%d' format + // to be passed too, which is what the BI connector may request + if (t->h == TIMELIB_UNSET && t->i == TIMELIB_UNSET && t->s == TIMELIB_UNSET) { + t->h = t->i = t->s = t->f = 0; + } + + if (t->y == TIMELIB_UNSET || t->m == TIMELIB_UNSET || t->d == TIMELIB_UNSET || + t->h == TIMELIB_UNSET || t->i == TIMELIB_UNSET || t->s == TIMELIB_UNSET) { + uasserted(40545, + str::stream() + << "an incomplete date/time string has been found, with elements missing: \"" + << dateString + << "\""); + } + + timelib_update_ts(t.get(), nullptr); + timelib_unixtime2local(t.get(), t->sse); + + return Date_t::fromMillisSinceEpoch((static_cast<double>(t->sse) + static_cast<double>(t->f)) * + 1000); +} + TimeZone TimeZoneDatabase::getTimeZone(StringData timeZoneId) const { auto tz = _timeZones.find(timeZoneId); if (tz != _timeZones.end()) { diff --git a/src/mongo/db/query/datetime/date_time_support.h b/src/mongo/db/query/datetime/date_time_support.h index 6f1e43aafad..0482cb70827 100644 --- a/src/mongo/db/query/datetime/date_time_support.h +++ b/src/mongo/db/query/datetime/date_time_support.h @@ -303,6 +303,11 @@ public: std::unique_ptr<TimeZoneDatabase> timeZoneDatabase); /** + * Use the timezone database to create a Date_t from a string. + */ + Date_t fromString(StringData dateString) const; + + /** * Returns a TimeZone object representing the UTC time zone. */ static TimeZone utcZone(); |