diff options
-rw-r--r-- | jstests/aggregation/expressions/date_diff.js | 103 | ||||
-rw-r--r-- | jstests/multiVersion/genericSetFCVUsage/collection_validator_feature_compatibility_version.js | 246 | ||||
-rw-r--r-- | jstests/multiVersion/genericSetFCVUsage/view_definition_feature_compatibility_version.js | 217 | ||||
-rw-r--r-- | src/mongo/db/exec/document_value/value.h | 7 | ||||
-rw-r--r-- | src/mongo/db/pipeline/SConscript | 12 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 145 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 62 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_bm.cpp | 120 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_date_test.cpp | 283 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_visitor.h | 2 | ||||
-rw-r--r-- | src/mongo/db/query/datetime/date_time_support.cpp | 145 | ||||
-rw-r--r-- | src/mongo/db/query/datetime/date_time_support.h | 52 | ||||
-rw-r--r-- | src/mongo/db/query/datetime/date_time_support_test.cpp | 315 | ||||
-rw-r--r-- | src/mongo/db/query/sbe_stage_builder_expression.cpp | 5 |
14 files changed, 1709 insertions, 5 deletions
diff --git a/jstests/aggregation/expressions/date_diff.js b/jstests/aggregation/expressions/date_diff.js new file mode 100644 index 00000000000..2c33451dd06 --- /dev/null +++ b/jstests/aggregation/expressions/date_diff.js @@ -0,0 +1,103 @@ +/** + * Tests $dateDiff expression. + * @tags: [ + * sbe_incompatible, + * requires_fcv_49 + * ] + */ +(function() { +"use strict"; + +const testDB = db.getSiblingDB(jsTestName()); +const coll = testDB.collection; + +// Drop the test database. +assert.commandWorked(testDB.dropDatabase()); + +// Executes a test case that inserts documents, issues an aggregate command on a collection and +// compares the results with the expected. +function executeTestCase(testCase) { + jsTestLog(tojson(testCase)); + coll.remove({}); + + // Insert some documents into the collection. + assert.commandWorked(coll.insert(testCase.inputDocuments)); + + // Issue an aggregate command and verify the result. + try { + const actualResults = coll.aggregate(testCase.pipeline).toArray(); + assert(testCase.expectedErrorCode === undefined, + `Expected an exception with code ${testCase.expectedErrorCode}`); + assert.eq(actualResults, testCase.expectedResults); + } catch (error) { + if (testCase.expectedErrorCode === undefined) { + throw error; + } + assert.eq(testCase.expectedErrorCode, error.code, tojson(error)); + } +} +const someDate = new Date("2020-11-01T18:23:36Z"); +const testCases = [ + { + // Parameters are constants, timezone is not specified. + pipeline: [{ + $project: { + _id: true, + date_diff: { + $dateDiff: { + startDate: new Date("2020-11-01T18:23:36Z"), + endDate: new Date("2020-11-02T00:00:00Z"), + unit: "hour" + } + } + } + }], + inputDocuments: [{_id: 1}], + expectedResults: [{_id: 1, date_diff: NumberLong("6")}] + }, + { + // Parameters are field paths. + pipeline: [{ + $project: { + _id: true, + date_diff: { + $dateDiff: { + startDate: "$startDate", + endDate: "$endDate", + unit: "$units", + timezone: "$timeZone" + } + } + } + }], + inputDocuments: [{ + _id: 1, + startDate: new Date("2020-11-01T18:23:36Z"), + endDate: new Date("2020-11-02T00:00:00Z"), + units: "hour", + timeZone: "America/New_York" + }], + expectedResults: [{_id: 1, date_diff: NumberLong("6")}] + }, + { + // Invalid inputs. + pipeline: [{ + $project: { + _id: true, + date_diff: { + $dateDiff: { + startDate: "$startDate", + endDate: "$endDate", + unit: "$units", + timezone: "$timeZone" + } + } + } + }], + inputDocuments: + [{_id: 1, startDate: "string", endDate: someDate, units: "decade", timeZone: "UTC"}], + expectedErrorCode: 5166307, + } +]; +testCases.forEach(executeTestCase); +}());
\ No newline at end of file diff --git a/jstests/multiVersion/genericSetFCVUsage/collection_validator_feature_compatibility_version.js b/jstests/multiVersion/genericSetFCVUsage/collection_validator_feature_compatibility_version.js new file mode 100644 index 00000000000..5f75bf8e0d0 --- /dev/null +++ b/jstests/multiVersion/genericSetFCVUsage/collection_validator_feature_compatibility_version.js @@ -0,0 +1,246 @@ +/** + * Test that mongod will not allow creation of collection validators using new query features when + * the feature compatibility version is older than the latest version. + * + * We restart mongod during the test and expect it to have the same data after restarting. + * @tags: [requires_persistence] + */ + +(function() { +"use strict"; + +const testName = "collection_validator_feature_compatibility_version"; +const dbpath = MongoRunner.dataPath + testName; + +// The 'testCases' array should be populated with +// +// { validator: { ... }, nonMatchingDocument: { ... }, lastStableErrCode } +// +// objects that use query features new in the latest version of mongod. Note that this also +// includes new aggregation expressions able to be used with the $expr match expression. This +// test ensures that a collection validator accepts the new query feature when the feature +// compatibility version is the latest version, and rejects it when the feature compatibility +// version is the last version. +// The 'lastStableErrCode' field indicates what error the last version would throw when +// parsing the validator. +const testCases = [ + { + validator: { + $expr: { + $eq: [ + { + $dateDiff: { + startDate: new Date("2020-02-02T02:02:02"), + endDate: new Date("2020-02-02T03:02:02"), + unit: "hour" + } + }, + 0 + ] + } + }, + nonMatchingDocument: {a: 1}, + lastStableErrCode: 168 + }, +]; + +// Tests Feature Compatibility Version behavior of the validator of a collection by executing test +// cases 'testCases' and using a previous stable version 'lastVersion' of mongod. 'lastVersion' can +// have values "last-lts" and "last-continuous". +function testCollectionValidatorFCVBehavior(lastVersion) { + let conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "latest"}); + assert.neq(null, conn, "mongod was unable to start up"); + + let testDB = conn.getDB(testName); + + let adminDB = conn.getDB("admin"); + + // Explicitly set the feature compatibility version to the latest version. + assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: latestFCV})); + + testCases.forEach(function(test, i) { + // Create a collection with a validator using new query features. + const coll = testDB["coll" + i]; + assert.commandWorked( + testDB.createCollection(coll.getName(), {validator: test.validator}), + `Expected to be able to create collection with validator ${tojson(test.validator)}`); + + // The validator should cause this insert to fail. + assert.writeErrorWithCode( + coll.insert(test.nonMatchingDocument), + ErrorCodes.DocumentValidationFailure, + `Expected document ${tojson(test.nonMatchingDocument)} to fail validation for ` + + `collection with validator ${tojson(test.validator)}`); + + // Set a validator using new query features on an existing collection. + coll.drop(); + assert.commandWorked(testDB.createCollection(coll.getName())); + assert.commandWorked( + testDB.runCommand({collMod: coll.getName(), validator: test.validator}), + `Expected to be able to modify collection validator to be ${tojson(test.validator)}`); + + // Another failing update. + assert.writeErrorWithCode( + coll.insert(test.nonMatchingDocument), + ErrorCodes.DocumentValidationFailure, + `Expected document ${tojson(test.nonMatchingDocument)} to fail validation for ` + + `collection with validator ${tojson(test.validator)}`); + }); + + // Set the feature compatibility version to the last version. + assert.commandWorked( + adminDB.runCommand({setFeatureCompatibilityVersion: binVersionToFCV(lastVersion)})); + + testCases.forEach( + function(test, i) { + // The validator is already in place, so it should still cause this insert to fail. + const coll = testDB["coll" + i]; + assert.writeErrorWithCode( + coll.insert(test.nonMatchingDocument), + ErrorCodes.DocumentValidationFailure, + `Expected document ${tojson(test.nonMatchingDocument)} to fail validation for ` + + `collection with validator ${tojson(test.validator)}`); + + // Trying to create a new collection with a validator using new query features should + // fail while feature compatibility version is the last version. + let res = testDB.createCollection("other", {validator: test.validator}); + assert.commandFailedWithCode( + res, + ErrorCodes.QueryFeatureNotAllowed, + 'Expected *not* to be able to create collection with validator ' + + tojson(test.validator)); + assert(res.errmsg.match(/feature compatibility version/), + `Expected error message from createCollection with validator ` + + `${tojson(test.validator)} to reference 'feature compatibility version' but got: ` + + res.errmsg); + + // Trying to update a collection with a validator using new query features should also + // fail. + res = testDB.runCommand({collMod: coll.getName(), validator: test.validator}); + assert.commandFailedWithCode(res, + ErrorCodes.QueryFeatureNotAllowed, + `Expected to be able to create collection with validator ${ + tojson(test.validator)}`); + assert(res.errmsg.match(/feature compatibility version/), + `Expected error message from createCollection with validator ` + + `${tojson(test.validator)} to reference 'feature compatibility version' but got: ` + + res.errmsg); + }); + + MongoRunner.stopMongod(conn); + + if (testCases.length > 0) { + // Versions of mongod 4.2 and later are able to start up with a collection validator that's + // considered invalid. However, any writes to the collection will fail. + conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: lastVersion, noCleanData: true}); + assert.neq( + null, conn, lastVersion + " mongod was unable to start up with invalid validator"); + const testDB = conn.getDB(testName); + + // Check that writes fail to all collections with validators using new query features. + testCases.forEach(function(test, i) { + const coll = testDB["coll" + i]; + assert.commandFailedWithCode(coll.insert({foo: 1}), test.lastStableErrCode); + }); + + MongoRunner.stopMongod(conn); + } + + // Starting up the latest version of mongod, however, should succeed, even though the feature + // compatibility version is still set to the last version. + conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "latest", noCleanData: true}); + assert.neq(null, conn, "mongod was unable to start up"); + + adminDB = conn.getDB("admin"); + testDB = conn.getDB(testName); + + // And the validator should still work. + testCases.forEach(function(test, i) { + const coll = testDB["coll" + i]; + assert.writeErrorWithCode( + coll.insert(test.nonMatchingDocument), + ErrorCodes.DocumentValidationFailure, + `Expected document ${tojson(test.nonMatchingDocument)} to fail validation for ` + + `collection with validator ${tojson(test.validator)}`); + + // Remove the validator. + assert.commandWorked(testDB.runCommand({collMod: coll.getName(), validator: {}})); + }); + + MongoRunner.stopMongod(conn); + + // Now, we should be able to start up the last version of mongod. + conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: lastVersion, noCleanData: true}); + assert.neq( + null, + conn, + `version ${MongoRunner.getBinVersionFor(lastVersion)} of mongod failed to start, even` + + " after we removed the validator using new query features"); + + MongoRunner.stopMongod(conn); + + // The rest of the test uses the latest version of mongod. + conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "latest", noCleanData: true}); + assert.neq(null, conn, "mongod was unable to start up"); + + adminDB = conn.getDB("admin"); + testDB = conn.getDB(testName); + + // Set the feature compatibility version back to the latest version. + assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: latestFCV})); + + testCases.forEach(function(test, i) { + const coll = testDB["coll2" + i]; + + // Now we should be able to create a collection with a validator using new query features + // again. + assert.commandWorked( + testDB.createCollection(coll.getName(), {validator: test.validator}), + `Expected to be able to create collection with validator ${tojson(test.validator)}`); + + // And we should be able to modify a collection to have a validator using new query + // features. + assert.commandWorked( + testDB.runCommand({collMod: coll.getName(), validator: test.validator}), + `Expected to be able to modify collection validator to be ${tojson(test.validator)}`); + }); + + // Set the feature compatibility version to the last version and then restart with + // internalValidateFeaturesAsPrimary=false. + assert.commandWorked( + adminDB.runCommand({setFeatureCompatibilityVersion: binVersionToFCV(lastVersion)})); + MongoRunner.stopMongod(conn); + conn = MongoRunner.runMongod({ + dbpath: dbpath, + binVersion: "latest", + noCleanData: true, + setParameter: "internalValidateFeaturesAsPrimary=false" + }); + assert.neq(null, conn, "mongod was unable to start up"); + + testDB = conn.getDB(testName); + + testCases.forEach(function(test, i) { + const coll = testDB["coll3" + i]; + // Even though the feature compatibility version is the last version, we should still + // be able to add a validator using new query features, because + // internalValidateFeaturesAsPrimary is false. + assert.commandWorked( + testDB.createCollection(coll.getName(), {validator: test.validator}), + `Expected to be able to create collection with validator ${tojson(test.validator)}`); + + // We should also be able to modify a collection to have a validator using new query + // features. + coll.drop(); + assert.commandWorked(testDB.createCollection(coll.getName())); + assert.commandWorked( + testDB.runCommand({collMod: coll.getName(), validator: test.validator}), + `Expected to be able to modify collection validator to be ${tojson(test.validator)}`); + }); + + MongoRunner.stopMongod(conn); +} +for (const version of ["last-lts", "last-continuous"]) { + testCollectionValidatorFCVBehavior(version); +} +}()); diff --git a/jstests/multiVersion/genericSetFCVUsage/view_definition_feature_compatibility_version.js b/jstests/multiVersion/genericSetFCVUsage/view_definition_feature_compatibility_version.js new file mode 100644 index 00000000000..5c866ed0d32 --- /dev/null +++ b/jstests/multiVersion/genericSetFCVUsage/view_definition_feature_compatibility_version.js @@ -0,0 +1,217 @@ +/** + * Test that mongod will not allow creation of a view using new aggregation features when the + * feature compatibility version is older than the latest version. + * + * We restart mongod during the test and expect it to have the same data after restarting. + * @tags: [requires_persistence] + */ + +(function() { +"use strict"; + +const testName = "view_definition_feature_compatibility_version_multiversion"; +const dbpath = MongoRunner.dataPath + testName; + +// The 'pipelinesWithNewFeatures' array should be populated with aggregation pipelines that use +// aggregation features new in the latest version of mongod. This test ensures that a view +// definition accepts the new aggregation feature when the feature compatibility version is the +// latest version, and rejects it when the feature compatibility version is the last +// version. +const pipelinesWithNewFeatures = [ + [{ + $project: { + x: { + $dateDiff: { + startDate: new Date("2020-02-02T02:02:02"), + endDate: new Date("2020-02-02T03:02:02"), + unit: "hour" + } + } + } + }], +]; + +// Tests Feature Compatibility Version behavior of view creation while using aggregation pipelines +// 'pipelinesWithNewFeatures' and using a previous stable version 'lastVersion' of mongod. +// 'lastVersion' can have values "last-lts" and "last-continuous". +function testViewDefinitionFCVBehavior(lastVersion) { + let conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "latest"}); + assert.neq(null, conn, "mongod was unable to start up"); + let testDB = conn.getDB(testName); + + // Explicitly set feature compatibility version to the latest version. + assert.commandWorked(testDB.adminCommand({setFeatureCompatibilityVersion: latestFCV})); + + // Test that we are able to create a new view with any of the new features. + pipelinesWithNewFeatures.forEach( + (pipe, i) => assert.commandWorked( + testDB.createView("firstView" + i, "coll", pipe), + `Expected to be able to create view with pipeline ${tojson(pipe)} while in FCV` + + ` ${latestFCV}`)); + + // Test that we are able to update an existing view with any of the new features. + pipelinesWithNewFeatures.forEach(function(pipe, i) { + assert(testDB["firstView" + i].drop(), `Drop of view with pipeline ${tojson(pipe)} failed`); + assert.commandWorked(testDB.createView("firstView" + i, "coll", [])); + assert.commandWorked( + testDB.runCommand({collMod: "firstView" + i, viewOn: "coll", pipeline: pipe}), + `Expected to be able to modify view to use pipeline ${tojson(pipe)} while in FCV` + + ` ${latestFCV}`); + }); + + // Create an empty view which we will attempt to update to use new query features while the + // feature compatibility version is the last version. + assert.commandWorked(testDB.createView("emptyView", "coll", [])); + + // Set the feature compatibility version to the last version. + assert.commandWorked( + testDB.adminCommand({setFeatureCompatibilityVersion: binVersionToFCV(lastVersion)})); + + // Read against an existing view using new query features should not fail. + pipelinesWithNewFeatures.forEach( + (pipe, i) => assert.commandWorked(testDB.runCommand({find: "firstView" + i}), + `Failed to query view with pipeline ${tojson(pipe)}`)); + + // Trying to create a new view in the same database as existing invalid view should fail, + // even if the new view doesn't use any new query features. + assert.commandFailedWithCode( + testDB.createView("newViewOldFeatures", "coll", [{$project: {_id: 1}}]), + ErrorCodes.QueryFeatureNotAllowed, + `Expected *not* to be able to create view on database ${testDB} while in FCV ${ + binVersionToFCV(lastVersion)}`); + + // Trying to create a new view succeeds if it's on a separate database. + const testDB2 = conn.getDB(testName + '2'); + assert.commandWorked(testDB2.dropDatabase()); + assert.commandWorked(testDB2.createView("newViewOldFeatures", "coll", [{$project: {_id: 1}}])); + + // Trying to create a new view using new query features should fail. + // (We use a separate DB to ensure this can only fail because of the view we're trying to + // create, as opposed to an existing view.) + pipelinesWithNewFeatures.forEach( + (pipe, i) => assert.commandFailedWithCode( + testDB2.createView("view_fail" + i, "coll", pipe), + ErrorCodes.QueryFeatureNotAllowed, + `Expected *not* to be able to create view with pipeline ${tojson(pipe)} while in FCV` + + ` ${binVersionToFCV(lastVersion)}`)); + + // Trying to update existing view to use new query features should also fail. + pipelinesWithNewFeatures.forEach( + (pipe, i) => assert.commandFailedWithCode( + testDB.runCommand({collMod: "emptyView", viewOn: "coll", pipeline: pipe}), + ErrorCodes.QueryFeatureNotAllowed, + `Expected *not* to be able to modify view to use pipeline ${tojson(pipe)} while in` + + `FCV ${binVersionToFCV(lastVersion)}`)); + + MongoRunner.stopMongod(conn); + + // Starting up the last version of mongod with new query features will succeed. + conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: lastVersion, noCleanData: true}); + assert.neq(null, + conn, + `version ${MongoRunner.getBinVersionFor(lastVersion)} of mongod was` + + " unable to start up"); + testDB = conn.getDB(testName); + + // Reads will fail against views with new query features when running the last version. + // Not checking the code returned on failure as it is not uniform across the various + // 'pipeline' arguments tested. + pipelinesWithNewFeatures.forEach( + (pipe, i) => assert.commandFailed( + testDB.runCommand({find: "firstView" + i}), + `Expected read against view with pipeline ${tojson(pipe)} to fail on version` + + ` ${MongoRunner.getBinVersionFor(lastVersion)}`)); + + // Test that a read against a view that does not contain new query features succeeds. + assert.commandWorked(testDB.runCommand({find: "emptyView"})); + + MongoRunner.stopMongod(conn); + + // Starting up the latest version of mongod should succeed, even though the feature + // compatibility version is still set to the last version. + conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "latest", noCleanData: true}); + assert.neq(null, conn, "mongod was unable to start up"); + testDB = conn.getDB(testName); + + // Read against an existing view using new query features should not fail. + pipelinesWithNewFeatures.forEach( + (pipe, i) => assert.commandWorked(testDB.runCommand({find: "firstView" + i}), + `Failed to query view with pipeline ${tojson(pipe)}`)); + + // Set the feature compatibility version back to the latest version. + assert.commandWorked(testDB.adminCommand({setFeatureCompatibilityVersion: latestFCV})); + + pipelinesWithNewFeatures.forEach(function(pipe, i) { + assert.commandWorked(testDB.runCommand({find: "firstView" + i}), + `Failed to query view with pipeline ${tojson(pipe)}`); + // Test that we are able to create a new view with any of the new features. + assert.commandWorked( + testDB.createView("secondView" + i, "coll", pipe), + `Expected to be able to create view with pipeline ${tojson(pipe)} while in FCV` + + ` ${latestFCV}`); + + // Test that we are able to update an existing view to use any of the new features. + assert(testDB["secondView" + i].drop(), + `Drop of view with pipeline ${tojson(pipe)} failed`); + assert.commandWorked(testDB.createView("secondView" + i, "coll", [])); + assert.commandWorked( + testDB.runCommand({collMod: "secondView" + i, viewOn: "coll", pipeline: pipe}), + `Expected to be able to modify view to use pipeline ${tojson(pipe)} while in FCV` + + ` ${latestFCV}`); + }); + + // Set the feature compatibility version to the last version and then restart with + // internalValidateFeaturesAsPrimary=false. + assert.commandWorked( + testDB.adminCommand({setFeatureCompatibilityVersion: binVersionToFCV(lastVersion)})); + MongoRunner.stopMongod(conn); + conn = MongoRunner.runMongod({ + dbpath: dbpath, + binVersion: "latest", + noCleanData: true, + setParameter: "internalValidateFeaturesAsPrimary=false" + }); + assert.neq(null, conn, "mongod was unable to start up"); + testDB = conn.getDB(testName); + + pipelinesWithNewFeatures.forEach(function(pipe, i) { + // Even though the feature compatibility version is the last version, we should still be + // able to create a view using new query features, because internalValidateFeaturesAsPrimary + // is false. + assert.commandWorked( + testDB.createView("thirdView" + i, "coll", pipe), + `Expected to be able to create view with pipeline ${tojson(pipe)} while in FCV` + + ` ${binVersionToFCV(lastVersion)} with internalValidateFeaturesAsPrimary=false`); + + // We should also be able to modify a view to use new query features. + assert(testDB["thirdView" + i].drop(), `Drop of view with pipeline ${tojson(pipe)} failed`); + assert.commandWorked(testDB.createView("thirdView" + i, "coll", [])); + assert.commandWorked( + testDB.runCommand({collMod: "thirdView" + i, viewOn: "coll", pipeline: pipe}), + `Expected to be able to modify view to use pipeline ${tojson(pipe)} while in FCV` + + ` ${binVersionToFCV(lastVersion)} with internalValidateFeaturesAsPrimary=false`); + }); + + MongoRunner.stopMongod(conn); + + // Starting up the last version of mongod with new query features should succeed. + conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: lastVersion, noCleanData: true}); + assert.neq(null, + conn, + `version ${MongoRunner.getBinVersionFor(lastVersion)} of mongod was` + + " unable to start up"); + testDB = conn.getDB(testName); + + // Existing views with new query features can be dropped. + pipelinesWithNewFeatures.forEach( + (pipe, i) => assert(testDB["firstView" + i].drop(), + `Drop of view with pipeline ${tojson(pipe)} failed`)); + assert(testDB.system.views.drop(), "Drop of system.views collection failed"); + + MongoRunner.stopMongod(conn); +} + +for (const version of ["last-lts", "last-continuous"]) { + testViewDefinitionFCVBehavior(version); +} +}()); diff --git a/src/mongo/db/exec/document_value/value.h b/src/mongo/db/exec/document_value/value.h index 7b593780cbf..4e99aeab9e9 100644 --- a/src/mongo/db/exec/document_value/value.h +++ b/src/mongo/db/exec/document_value/value.h @@ -186,6 +186,13 @@ public: */ bool integral64Bit() const; + /** + * Returns true if this value can be coerced to a Date, and false otherwise. + */ + bool coercibleToDate() const { + return Date == getType() || bsonTimestamp == getType() || jstOID == getType(); + } + /// Get the BSON type of the field. BSONType getType() const { return _storage.bsonType(); diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index 51a4bd7b25a..45b8e73144d 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -441,3 +441,15 @@ env.CppUnitTest( 'sharded_agg_helpers', ] ) + +env.Benchmark( + target='expression_bm', + source=[ + 'expression_bm.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/db/query/query_test_service_context', + '$BUILD_DIR/mongo/db/service_context_test_fixture', + 'expression_context', + ], +) diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 8ccff46430e..6d95102b5e4 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -1849,6 +1849,151 @@ void ExpressionDateToString::_doAddDependencies(DepsTracker* deps) const { } } +/* ----------------------- ExpressionDateDiff ---------------------------- */ + +// TODO SERVER-53028: make the expression to be available for any FCV when 5.0 becomes last-lts. +REGISTER_EXPRESSION_WITH_MIN_VERSION(dateDiff, + ExpressionDateDiff::parse, + ServerGlobalParams::FeatureCompatibility::Version::kVersion49); + +ExpressionDateDiff::ExpressionDateDiff(ExpressionContext* const expCtx, + boost::intrusive_ptr<Expression> startDate, + boost::intrusive_ptr<Expression> endDate, + boost::intrusive_ptr<Expression> unit, + boost::intrusive_ptr<Expression> timezone) + : Expression{expCtx, + {std::move(startDate), std::move(endDate), std::move(unit), std::move(timezone)}}, + _startDate{_children[0]}, + _endDate{_children[1]}, + _unit{_children[2]}, + _timeZone{_children[3]} {} + +boost::intrusive_ptr<Expression> ExpressionDateDiff::parse(ExpressionContext* const expCtx, + BSONElement expr, + const VariablesParseState& vps) { + invariant(expr.fieldNameStringData() == "$dateDiff"); + uassert(5166301, + "$dateDiff only supports an object as its argument", + expr.type() == BSONType::Object); + BSONElement startDateElement, endDateElement, unitElement, timezoneElem; + for (auto&& element : expr.embeddedObject()) { + auto field = element.fieldNameStringData(); + if ("startDate"_sd == field) { + startDateElement = element; + } else if ("endDate"_sd == field) { + endDateElement = element; + } else if ("unit"_sd == field) { + unitElement = element; + } else if ("timezone"_sd == field) { + timezoneElem = element; + } else { + uasserted(5166302, + str::stream() + << "Unrecognized argument to $dateDiff: " << element.fieldName()); + } + } + uassert(5166303, "Missing 'startDate' parameter to $dateDiff", startDateElement); + uassert(5166304, "Missing 'endDate' parameter to $dateDiff", endDateElement); + uassert(5166305, "Missing 'unit' parameter to $dateDiff", unitElement); + return new ExpressionDateDiff(expCtx, + parseOperand(expCtx, startDateElement, vps), + parseOperand(expCtx, endDateElement, vps), + parseOperand(expCtx, unitElement, vps), + timezoneElem ? parseOperand(expCtx, timezoneElem, vps) : nullptr); +} + +boost::intrusive_ptr<Expression> ExpressionDateDiff::optimize() { + _startDate = _startDate->optimize(); + _endDate = _endDate->optimize(); + _unit = _unit->optimize(); + if (_timeZone) { + _timeZone = _timeZone->optimize(); + } + if (ExpressionConstant::allNullOrConstant({_startDate, _endDate, _unit, _timeZone})) { + // Everything is a constant, so we can turn into a constant. + return ExpressionConstant::create( + getExpressionContext(), evaluate(Document{}, &(getExpressionContext()->variables))); + } + return this; +}; + +Value ExpressionDateDiff::serialize(bool explain) const { + return Value{ + Document{{"$dateDiff"_sd, + Document{{"startDate"_sd, _startDate->serialize(explain)}, + {"endDate"_sd, _endDate->serialize(explain)}, + {"unit"_sd, _unit->serialize(explain)}, + {"timezone"_sd, _timeZone ? _timeZone->serialize(explain) : Value{}}}}}}; +}; + +Date_t ExpressionDateDiff::convertToDate(const Value& value, StringData parameterName) { + uassert(5166307, + str::stream() << "$dateDiff requires '" << parameterName << "' to be a date, but got " + << typeName(value.getType()), + value.coercibleToDate()); + return value.coerceToDate(); +} + +/** + * Calls function 'function' with zero parameters and returns the result. If AssertionException is + * raised during the call of 'function', adds a context 'errorContext' to the exception. + */ +template <typename F> +auto addContextToAssertionException(F&& function, StringData errorContext) { + try { + return function(); + } catch (AssertionException& exception) { + exception.addContext(str::stream() << errorContext); + throw; + } +} + +TimeUnit ExpressionDateDiff::convertToTimeUnit(const Value& value) { + uassert(5166306, + str::stream() << "$dateDiff requires 'unit' to be a string, but got " + << typeName(value.getType()), + BSONType::String == value.getType()); + return addContextToAssertionException([&]() { return parseTimeUnit(value.getString()); }, + "$dateDiff parameter 'unit' value parsing failed"_sd); +} + +Value ExpressionDateDiff::evaluate(const Document& root, Variables* variables) const { + const Value startDateValue = _startDate->evaluate(root, variables); + if (startDateValue.nullish()) { + return Value(BSONNULL); + } + const Value endDateValue = _endDate->evaluate(root, variables); + if (endDateValue.nullish()) { + return Value(BSONNULL); + } + const Value unitValue = _unit->evaluate(root, variables); + if (unitValue.nullish()) { + return Value(BSONNULL); + } + const auto timezone = addContextToAssertionException( + [&]() { + return makeTimeZone( + getExpressionContext()->timeZoneDatabase, root, _timeZone.get(), variables); + }, + "$dateDiff parameter 'timezone' value parsing failed"_sd); + if (!timezone) { + return Value(BSONNULL); + } + const Date_t startDate = convertToDate(startDateValue, "startDate"_sd); + const Date_t endDate = convertToDate(endDateValue, "endDate"_sd); + const TimeUnit unit = convertToTimeUnit(unitValue); + return Value{dateDiff(startDate, endDate, unit, *timezone)}; +} + +void ExpressionDateDiff::_doAddDependencies(DepsTracker* deps) const { + _startDate->addDependencies(deps); + _endDate->addDependencies(deps); + _unit->addDependencies(deps); + if (_timeZone) { + _timeZone->addDependencies(deps); + } +} + /* ----------------------- ExpressionDivide ---------------------------- */ Value ExpressionDivide::evaluate(const Document& root, Variables* variables) const { diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index 0e614dfb8be..8c326db6360 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -286,10 +286,10 @@ protected: /** * Owning container for all sub-Expressions. * - * Some derived classes contain named fields since they orginate from user syntax containing + * Some derived classes contain named fields since they originate from user syntax containing * field names. These classes contain alternate data structures or object members for accessing - * children. These structures or object memebers are expected to reference this data structure. - * In addition this structure should not be modified by named-field derivied classes to avoid + * children. These structures or object members are expected to reference this data structure. + * In addition this structure should not be modified by named-field derived classes to avoid * invalidating references. */ ExpressionVector _children; @@ -1346,6 +1346,62 @@ public: } }; +/** + * $dateDiff expression that determines a difference between two time instants. + */ +class ExpressionDateDiff final : public Expression { +public: + /** + * startDate - an expression that resolves to a Value that is coercible to date. + * endDate - an expression that resolves to a Value that is coercible to date. + * unit - expression defining a length of time interval to measure the difference in that + * resolves to a string Value. + * timezone - expression defining a timezone to perform the operation in that resolves to a + * string Value. Can be nullptr. + */ + ExpressionDateDiff(ExpressionContext* const expCtx, + boost::intrusive_ptr<Expression> startDate, + boost::intrusive_ptr<Expression> endDate, + boost::intrusive_ptr<Expression> unit, + boost::intrusive_ptr<Expression> timezone); + boost::intrusive_ptr<Expression> optimize() final; + Value serialize(bool explain) const final; + Value evaluate(const Document& root, Variables* variables) const final; + static boost::intrusive_ptr<Expression> parse(ExpressionContext* const expCtx, + BSONElement expr, + const VariablesParseState& vps); + void acceptVisitor(ExpressionVisitor* visitor) final { + return visitor->visit(this); + } + +protected: + void _doAddDependencies(DepsTracker* deps) const final; + +private: + /** + * Converts 'value' to Date_t type for $dateDiff expression for parameter 'parameterName'. + */ + static Date_t convertToDate(const Value& value, StringData parameterName); + + /** + * Converts 'value' to TimeUnit for $dateDiff expression parameter 'unit'. + */ + static TimeUnit convertToTimeUnit(const Value& value); + + // Starting time instant expression. Accepted types: Date_t, Timestamp, OID. + boost::intrusive_ptr<Expression>& _startDate; + + // Ending time instant expression. Accepted types the same as for '_startDate'. + boost::intrusive_ptr<Expression>& _endDate; + + // Length of time interval to measure the difference. Accepted type: std::string. Accepted + // values: enumerators from TimeUnit enumeration. + boost::intrusive_ptr<Expression>& _unit; + + // Timezone to use for the difference calculation. Accepted type: std::string. If not specified, + // UTC is used. + boost::intrusive_ptr<Expression>& _timeZone; +}; class ExpressionDivide final : public ExpressionFixedArity<ExpressionDivide, 2> { public: diff --git a/src/mongo/db/pipeline/expression_bm.cpp b/src/mongo/db/pipeline/expression_bm.cpp new file mode 100644 index 00000000000..2b1d642954e --- /dev/null +++ b/src/mongo/db/pipeline/expression_bm.cpp @@ -0,0 +1,120 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * 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 Server Side 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 <benchmark/benchmark.h> + +#include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/pipeline/expression.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/db/query/query_test_service_context.h" + +namespace mongo { +namespace { +/** + * Tests performance of 'evaluate()' of $dateDiff expression. + * + * startDate - start date in milliseconds from the UNIX epoch. + * endDate - end date in milliseconds from the UNIX epoch. + * unit - a string expression of units to use for date difference calculation. + * timezone - a string representation of timezone to use for date difference calculation. + * state - benchmarking state. + */ +void testDateDiffExpression(long long startDate, + long long endDate, + std::string unit, + boost::optional<std::string> timezone, + benchmark::State& state) { + QueryTestServiceContext testServiceContext; + auto opContext = testServiceContext.makeOperationContext(); + NamespaceString nss("test.bm"); + boost::intrusive_ptr<ExpressionContextForTest> exprContext = + new ExpressionContextForTest(opContext.get(), nss); + + // Build a $dateDiff expression. + BSONObjBuilder objBuilder; + objBuilder << "startDate" << Date_t::fromMillisSinceEpoch(startDate) << "endDate" + << Date_t::fromMillisSinceEpoch(endDate) << "unit" << unit; + if (timezone) { + objBuilder << "timezone" << *timezone; + } + auto expression = BSON("$dateDiff" << objBuilder.obj()); + auto dateDiffExpression = Expression::parseExpression( + exprContext.get(), expression, exprContext->variablesParseState); + + // Prepare parameters for the 'evaluate()' call. + auto variables = &(exprContext->variables); + Document document; + + // Run the test. + for (auto keepRunning : state) { + benchmark::DoNotOptimize(dateDiffExpression->evaluate(document, variables)); + benchmark::ClobberMemory(); + } +} + +void BM_DateDiffEvaluateMinute300Years(benchmark::State& state) { + testDateDiffExpression(-1640989478000LL /* 1918-01-01*/, + 7826117722000LL /* 2218-01-01*/, + "minute", + boost::none /*timezone*/, + state); +} + +void BM_DateDiffEvaluateMinute2Years(benchmark::State& state) { + testDateDiffExpression(1542448721000LL /* 2018-11-17*/, + 1605607121000LL /* 2020-11-17*/, + "minute", + boost::none /*timezone*/, + state); +} + +void BM_DateDiffEvaluateMinute2YearsWithTimezone(benchmark::State& state) { + testDateDiffExpression(1542448721000LL /* 2018-11-17*/, + 1605607121000LL /* 2020-11-17*/, + "minute", + std::string{"America/New_York"}, + state); +} + +void BM_DateDiffEvaluateWeek(benchmark::State& state) { + testDateDiffExpression(7826117722000LL /* 2218-01-01*/, + 4761280721000LL /*2120-11-17*/, + "week", + boost::none /*timezone*/, + state); +} + +BENCHMARK(BM_DateDiffEvaluateMinute300Years); +BENCHMARK(BM_DateDiffEvaluateMinute2Years); +BENCHMARK(BM_DateDiffEvaluateMinute2YearsWithTimezone); +BENCHMARK(BM_DateDiffEvaluateWeek); +} // namespace +} // namespace mongo
\ No newline at end of file diff --git a/src/mongo/db/pipeline/expression_date_test.cpp b/src/mongo/db/pipeline/expression_date_test.cpp index 3098eec3f5a..ba0b3552188 100644 --- a/src/mongo/db/pipeline/expression_date_test.cpp +++ b/src/mongo/db/pipeline/expression_date_test.cpp @@ -29,6 +29,9 @@ #include "mongo/platform/basic.h" +#include <set> +#include <string> + #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/pipeline/aggregation_context_fixture.h" #include "mongo/unittest/unittest.h" @@ -1398,4 +1401,284 @@ TEST_F(ExpressionDateFromStringTest, OnErrorEvaluatedLazily) { } } // namespace ExpressionDateFromStringTest + +namespace ExpressionDateDiffTest { +class ExpressionDateDiffTest : public AggregationContextFixture { +public: + /** + * Parses expression 'expression' and asserts that the expression fails to parse with error + * 'expectedErrorCode' and exception message 'expectedErrorMessage'. + */ + void assertFailsToParseExpression(BSONObj expression, + int expectedErrorCode, + std::string expectedErrorMessage) { + auto expCtx = getExpCtx(); + ASSERT_THROWS_CODE_AND_WHAT( + Expression::parseExpression(expCtx.get(), expression, expCtx->variablesParseState), + AssertionException, + expectedErrorCode, + expectedErrorMessage); + } + + /** + * Parses expression 'expression' which is expected to parse successfully and then serializes + * expression instance to compare with 'expectedSerializedExpression'. + */ + void assertParsesAndSerializesExpression(BSONObj expression, + BSONObj expectedSerializedExpression) { + auto expCtx = getExpCtx(); + auto dateDiffExpr = + Expression::parseExpression(expCtx.get(), expression, expCtx->variablesParseState); + auto expectedSerialization = Value(expectedSerializedExpression); + ASSERT_VALUE_EQ(dateDiffExpr->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateDiffExpr->serialize(false), expectedSerialization); + + // Verify that parsed and then serialized expression is the same. + ASSERT_VALUE_EQ(Expression::parseExpression( + expCtx.get(), expectedSerializedExpression, expCtx->variablesParseState) + ->serialize(false), + expectedSerialization); + } + + /** + * Builds a $dateDiff expression with given values of parameters. + */ + auto buildExpressionWithParameters(Value startDate, Value endDate, Value unit, Value timezone) { + auto expCtx = getExpCtx(); + auto expression = + BSON("$dateDiff" << BSON("startDate" << startDate << "endDate" << endDate << "unit" + << unit << "timezone" << timezone)); + return Expression::parseExpression(expCtx.get(), expression, expCtx->variablesParseState); + } +}; + +TEST_F(ExpressionDateDiffTest, ParsesAndSerializesValidExpression) { + assertParsesAndSerializesExpression(BSON("$dateDiff" << BSON("startDate" + << "$startDateField" + << "endDate" + << "$endDateField" + << "unit" + << "day" + << "timezone" + << "America/New_York")), + BSON("$dateDiff" << BSON("startDate" + << "$startDateField" + << "endDate" + << "$endDateField" + << "unit" + << BSON("$const" + << "day") + << "timezone" + << BSON("$const" + << "America/New_York")))); + assertParsesAndSerializesExpression(BSON("$dateDiff" << BSON("startDate" + << "$startDateField" + << "endDate" + << "$endDateField" + << "unit" + << "$unit")), + BSON("$dateDiff" << BSON("startDate" + << "$startDateField" + << "endDate" + << "$endDateField" + << "unit" + << "$unit"))); +} + +TEST_F(ExpressionDateDiffTest, ParsesInvalidExpression) { + // Verify that invalid fields are rejected. + assertFailsToParseExpression(BSON("$dateDiff" << BSON("startDate" + << "$startDateField" + << "endDate" + << "$endDateField" + << "unit" + << "day" + << "timeGone" + << "yes")), + 5166302, + "Unrecognized argument to $dateDiff: timeGone"); + + // Verify that field 'startDate' is required. + assertFailsToParseExpression(BSON("$dateDiff" << BSON("endDate" + << "$endDateField" + << "unit" + << "day")), + 5166303, + "Missing 'startDate' parameter to $dateDiff"); + + // Verify that field 'endDate' is required. + assertFailsToParseExpression(BSON("$dateDiff" << BSON("startDate" + << "$startDateField" + << "unit" + << "day")), + 5166304, + "Missing 'endDate' parameter to $dateDiff"); + + // Verify that field 'unit' is required. + assertFailsToParseExpression(BSON("$dateDiff" << BSON("startDate" + << "$startDateField" + << "endDate" + << "$endDateField")), + 5166305, + "Missing 'unit' parameter to $dateDiff"); + + // Verify that only $dateDiff: {..} is accepted. + assertFailsToParseExpression(BSON("$dateDiff" + << "startDate"), + 5166301, + "$dateDiff only supports an object as its argument"); +} + +TEST_F(ExpressionDateDiffTest, EvaluatesExpression) { + struct TestCase { + Value startDate; + Value endDate; + Value unit; + Value timezone; + Value expectedResult; + int expectedErrorCode{0}; + std::string expectedErrorMessage; + }; + auto expCtx = getExpCtx(); + const auto anyDate = Value{Date_t{}}; + const auto null = Value{BSONNULL}; + const auto hour = Value{"hour"_sd}; + const auto utc = Value{"GMT"_sd}; + const auto objectId = Value{OID::gen()}; + const std::vector<TestCase> testCases{ + {// Sunny day case. + Value{Date_t::fromMillisSinceEpoch(1604255016000) /* 2020-11-01T18:23:36 UTC+00:00 */}, + Value{Date_t::fromMillisSinceEpoch(1604260800000) /* 2020-11-01T20:00:00 UTC+00:00 */}, + hour, + utc, + Value{2}}, + {// 'startDate' is null. + null, + anyDate, + hour, + utc, + null}, + {// 'endDate' is null. + anyDate, + null, + hour, + utc, + null}, + {// 'unit' is null. + anyDate, + anyDate, + null, + utc, + null}, + {// Invalid 'startDate' type. + Value{"date"_sd}, + anyDate, + hour, + utc, + null, + 5166307, // Error code. + "$dateDiff requires 'startDate' to be a date, but got string"}, + {// Invalid 'endDate' type. + anyDate, + Value{"date"_sd}, + hour, + utc, + null, + 5166307, // Error code. + "$dateDiff requires 'endDate' to be a date, but got string"}, + {// Invalid 'unit' type. + anyDate, + anyDate, + Value{2}, + utc, + null, + 5166306, // Error code. + "$dateDiff requires 'unit' to be a string, but got int"}, + {// Invalid 'unit' value. + anyDate, + anyDate, + Value{"century"_sd}, + utc, + null, + ErrorCodes::FailedToParse, // Error code. + "$dateDiff parameter 'unit' value parsing failed :: caused by :: unknown time unit value: " + "century"}, + {// Invalid 'timezone' value. + anyDate, + anyDate, + hour, + Value{"INVALID"_sd}, + null, + 40485, // Error code. + "$dateDiff parameter 'timezone' value parsing failed :: caused by :: unrecognized time " + "zone identifier: \"INVALID\""}, + {// Accepts OID. + objectId, + objectId, + hour, + utc, + Value{0}}, + {// Accepts timestamp. + Value{Timestamp{Seconds(1604255016), 0} /* 2020-11-01T18:23:36 UTC+00:00 */}, + Value{Timestamp{Seconds(1604260800), 0} /* 2020-11-01T20:00:00 UTC+00:00 */}, + Value{"minute"_sd}, + Value{} /* 'timezone' not specified*/, + Value{97}}}; + + for (auto&& testCase : testCases) { + auto dateDiffExpression = buildExpressionWithParameters( + testCase.startDate, testCase.endDate, testCase.unit, testCase.timezone); + if (testCase.expectedErrorCode) { + ASSERT_THROWS_CODE_AND_WHAT(dateDiffExpression->evaluate({}, &(expCtx->variables)), + AssertionException, + testCase.expectedErrorCode, + testCase.expectedErrorMessage); + } else { + ASSERT_VALUE_EQ(Value{testCase.expectedResult}, + dateDiffExpression->evaluate({}, &(expCtx->variables))); + } + } +} + +TEST_F(ExpressionDateDiffTest, OptimizesToConstantIfAllInputsAreConstant) { + auto dateDiffExpression = buildExpressionWithParameters( + Value{Date_t::fromMillisSinceEpoch(0)}, + Value{Date_t::fromMillisSinceEpoch(31571873000) /*1971-mm-dd*/}, + Value{"year"_sd}, + Value{"GMT"_sd}); + + // Verify that 'optimize()' returns a constant expression when all parameters evaluate to + // constants. + auto optimizedDateDiffExpression1 = dateDiffExpression->optimize(); + auto constantExpression = dynamic_cast<ExpressionConstant*>(optimizedDateDiffExpression1.get()); + ASSERT(constantExpression); + ASSERT_VALUE_EQ(Value{1LL}, constantExpression->getValue()); +} + +TEST_F(ExpressionDateDiffTest, DoesNotOptimizeToConstantIfNotAllInputsAreConstant) { + auto dateDiffExpression = buildExpressionWithParameters(Value{Date_t::fromMillisSinceEpoch(0)}, + Value{Date_t::fromMillisSinceEpoch(0)}, + Value{"$year"_sd}, + Value{} /* Time zone not specified*/); + + // Verify that 'optimize()' returns a $dateDiff expression when not all parameters evaluate to + // constants. + auto optimizedDateDiffExpression = dateDiffExpression->optimize(); + ASSERT(dynamic_cast<ExpressionDateDiff*>(optimizedDateDiffExpression.get())); + ASSERT_EQUALS(dateDiffExpression.get(), optimizedDateDiffExpression.get()); +} + +TEST_F(ExpressionDateDiffTest, AddsDependencies) { + auto dateDiffExpression = buildExpressionWithParameters(Value{"$startDateField"_sd}, + Value{"$endDateField"_sd}, + Value{"$unitField"_sd}, + Value{"$timezoneField"_sd}); + + // Verify that dependencies for $dateDiff expression are determined correctly. + auto depsTracker = dateDiffExpression->getDependencies(); + ASSERT_TRUE( + (depsTracker.fields == + std::set<std::string>{"startDateField", "endDateField", "unitField", "timezoneField"})); +} +} // namespace ExpressionDateDiffTest } // namespace mongo diff --git a/src/mongo/db/pipeline/expression_visitor.h b/src/mongo/db/pipeline/expression_visitor.h index a8b3d08e6c7..de4b32908e3 100644 --- a/src/mongo/db/pipeline/expression_visitor.h +++ b/src/mongo/db/pipeline/expression_visitor.h @@ -152,6 +152,7 @@ class ExpressionInternalJsEmit; class ExpressionFunction; class ExpressionDegreesToRadians; class ExpressionRadiansToDegrees; +class ExpressionDateDiff; class AccumulatorAvg; class AccumulatorMax; @@ -199,6 +200,7 @@ public: virtual void visit(ExpressionCond*) = 0; virtual void visit(ExpressionDateFromString*) = 0; virtual void visit(ExpressionDateFromParts*) = 0; + virtual void visit(ExpressionDateDiff*) = 0; virtual void visit(ExpressionDateToParts*) = 0; virtual void visit(ExpressionDateToString*) = 0; virtual void visit(ExpressionDivide*) = 0; diff --git a/src/mongo/db/query/datetime/date_time_support.cpp b/src/mongo/db/query/datetime/date_time_support.cpp index 85a0fde53ac..498a44880be 100644 --- a/src/mongo/db/query/datetime/date_time_support.cpp +++ b/src/mongo/db/query/datetime/date_time_support.cpp @@ -40,6 +40,7 @@ #include "mongo/base/init.h" #include "mongo/bson/util/builder.h" #include "mongo/db/service_context.h" +#include "mongo/platform/overflow_arithmetic.h" #include "mongo/util/assert_util.h" #include "mongo/util/ctype.h" #include "mongo/util/duration.h" @@ -582,4 +583,148 @@ StatusWith<std::string> TimeZone::formatDate(StringData format, Date_t date) con else return formatted.str(); } + +namespace { +auto const kMonthsInOneYear = 12LL; +auto const kDaysInNonLeapYear = 365LL; +auto const kHoursPerDay = 24LL; +auto const kMinutesPerHour = 60LL; +auto const kSecondsPerMinute = 60LL; +auto const kDaysPerWeek = 7LL; +auto const kQuartersPerYear = 4LL; +auto const kQuarterLengthInMonths = 3LL; +auto const kLeapYearReferencePoint = -1000000000L; + +/** + * Determines a number of leap years in a year range (leap year reference point; 'year']. + */ +inline long leapYearsSinceReferencePoint(long year) { + // Count a number of leap years that happened since the reference point, where a leap year is + // when year%4==0, excluding years when year%100==0, except when year%400==0. + auto yearsSinceReferencePoint = year - kLeapYearReferencePoint; + return yearsSinceReferencePoint / 4 - yearsSinceReferencePoint / 100 + + yearsSinceReferencePoint / 400; +} + +/** + * Sums the number of days in the Gregorian calendar in years: 'startYear', + * 'startYear'+1, .., 'endYear'-1. + */ +inline long long daysBetweenYears(long startYear, long endYear) { + return leapYearsSinceReferencePoint(endYear - 1) - leapYearsSinceReferencePoint(startYear - 1) + + (endYear - startYear) * kDaysInNonLeapYear; +} + +/** + * Determines a correction needed in number of hours when calculating passed hours between two time + * instants 'startInstant' and 'endInstant' due to the Daylight Savings Time. Returns 0, if both + * time instants 'startInstant' and 'endInstant' are either in Standard Time (ST) or in Daylight + * Saving Time (DST); returns 1, if 'endInstant' is in ST and 'startInstant' is in DST and + * 'endInstant' > 'startInstant' or 'endInstant' is in DST and 'startInstant' is in ST and + * 'endInstant' < 'startInstant'; otherwise returns -1. + */ +inline long long dstCorrection(timelib_time* startInstant, timelib_time* endInstant) { + return (startInstant->z - endInstant->z) / (kMinutesPerHour * kSecondsPerMinute); +} + +inline long long dateDiffYear(timelib_time* startInstant, timelib_time* endInstant) { + return endInstant->y - startInstant->y; +} + +/** + * Determines which quarter month 'month' belongs to. 'month' value range is 1..12. Returns a number + * of a quarter, where 0 corresponds to the first quarter. + */ +inline int quarter(int month) { + return (month - 1) / kQuarterLengthInMonths; +} +inline long long dateDiffQuarter(timelib_time* startInstant, timelib_time* endInstant) { + return quarter(endInstant->m) - quarter(startInstant->m) + + dateDiffYear(startInstant, endInstant) * kQuartersPerYear; +} +inline long long dateDiffMonth(timelib_time* startInstant, timelib_time* endInstant) { + return endInstant->m - startInstant->m + + dateDiffYear(startInstant, endInstant) * kMonthsInOneYear; +} +inline long long dateDiffDay(timelib_time* startInstant, timelib_time* endInstant) { + return timelib_day_of_year(endInstant->y, endInstant->m, endInstant->d) - + timelib_day_of_year(startInstant->y, startInstant->m, startInstant->d) + + daysBetweenYears(startInstant->y, endInstant->y); +} +inline long long dateDiffWeek(timelib_time* startInstant, timelib_time* endInstant) { + // We use 'timelib_iso_day_of_week()' since it considers Monday as the first day of the week. + return (dateDiffDay(startInstant, endInstant) + + timelib_iso_day_of_week(startInstant->y, startInstant->m, startInstant->d) - + timelib_iso_day_of_week(endInstant->y, endInstant->m, endInstant->d)) / + kDaysPerWeek; +} +inline long long dateDiffHour(timelib_time* startInstant, timelib_time* endInstant) { + return endInstant->h - startInstant->h + dateDiffDay(startInstant, endInstant) * kHoursPerDay + + dstCorrection(startInstant, endInstant); +} +inline long long dateDiffMinute(timelib_time* startInstant, timelib_time* endInstant) { + return endInstant->i - startInstant->i + + dateDiffHour(startInstant, endInstant) * kMinutesPerHour; +} +inline long long dateDiffSecond(timelib_time* startInstant, timelib_time* endInstant) { + return endInstant->s - startInstant->s + + dateDiffMinute(startInstant, endInstant) * kSecondsPerMinute; +} +inline long long dateDiffMillisecond(Date_t startDate, Date_t endDate) { + long long result; + uassert(5166308, + "dateDiff overflowed", + !overflow::sub(endDate.toMillisSinceEpoch(), startDate.toMillisSinceEpoch(), &result)); + return result; +} +} // namespace + +long long dateDiff(Date_t startDate, Date_t endDate, TimeUnit unit, const TimeZone& timezone) { + if (TimeUnit::millisecond == unit) { + return dateDiffMillisecond(startDate, endDate); + } + + // Translate the time instants to the given timezone. + auto startDateInTimeZone = timezone.getTimelibTime(startDate); + auto endDateInTimeZone = timezone.getTimelibTime(endDate); + switch (unit) { + case TimeUnit::year: + return dateDiffYear(startDateInTimeZone.get(), endDateInTimeZone.get()); + case TimeUnit::quarter: + return dateDiffQuarter(startDateInTimeZone.get(), endDateInTimeZone.get()); + case TimeUnit::month: + return dateDiffMonth(startDateInTimeZone.get(), endDateInTimeZone.get()); + case TimeUnit::week: + return dateDiffWeek(startDateInTimeZone.get(), endDateInTimeZone.get()); + case TimeUnit::day: + return dateDiffDay(startDateInTimeZone.get(), endDateInTimeZone.get()); + case TimeUnit::hour: + return dateDiffHour(startDateInTimeZone.get(), endDateInTimeZone.get()); + case TimeUnit::minute: + return dateDiffMinute(startDateInTimeZone.get(), endDateInTimeZone.get()); + case TimeUnit::second: + return dateDiffSecond(startDateInTimeZone.get(), endDateInTimeZone.get()); + default: + MONGO_UNREACHABLE; + } +} + +TimeUnit parseTimeUnit(const std::string& unitName) { + static const StringMap<TimeUnit> timeUnitNameToTimeUnitMap{ + {"year", TimeUnit::year}, + {"quarter", TimeUnit::quarter}, + {"month", TimeUnit::month}, + {"week", TimeUnit::week}, + {"day", TimeUnit::day}, + {"hour", TimeUnit::hour}, + {"minute", TimeUnit::minute}, + {"second", TimeUnit::second}, + {"millisecond", TimeUnit::millisecond}, + }; + auto iterator = timeUnitNameToTimeUnitMap.find(unitName); + uassert(ErrorCodes::FailedToParse, + str::stream() << "unknown time unit value: " << unitName, + iterator != timeUnitNameToTimeUnitMap.end()); + return iterator->second; +} } // namespace mongo diff --git a/src/mongo/db/query/datetime/date_time_support.h b/src/mongo/db/query/datetime/date_time_support.h index 0bc4a779180..0394b661ed2 100644 --- a/src/mongo/db/query/datetime/date_time_support.h +++ b/src/mongo/db/query/datetime/date_time_support.h @@ -311,10 +311,9 @@ public: */ static void validateToStringFormat(StringData format); static void validateFromStringFormat(StringData format); - -private: std::unique_ptr<_timelib_time, TimelibTimeDeleter> getTimelibTime(Date_t) const; +private: /** * Only works with 1 <= spaces <= 4 and 0 <= number <= 9999. If spaces is less than the digit * count of number we simply insert the number without padding. @@ -472,4 +471,53 @@ private: std::unique_ptr<_timelib_tzdb, TimeZoneDBDeleter> _timeZoneDatabase; }; +/** + * A set of standard measures of time used to express a length of time interval. + */ +enum class TimeUnit { + year, + quarter, // A quarter of a year. + month, + week, + day, + hour, + minute, + second, + millisecond +}; + +/** + * Parses a string representation of an enumerator of TimeUnit type 'unitName' into a value of type + * TimeUnit. Throws an exception with error code ErrorCodes::FailedToParse when passed an invalid + * name. + */ +TimeUnit parseTimeUnit(const std::string& unitName); + +/** + * Determines the number of upper boundaries of time intervals crossed when moving from time instant + * 'startDate' to time instant 'endDate' in time zone 'timezone'. The time intervals are of length + * equal to one 'unit' and aligned so that the lower/upper bound is located in time axis at instant + * n*'unit', where n is an integer. + * + * If 'endDate' < 'startDate', then the returned number of crossed boundaries is negative. + * + * For 'unit' values 'hour' and smaller, when there is a transition from Daylight Saving Time to + * standard time the function behaves as if standard time intervals overlap Daylight Saving Time + * intervals. When there is a transition from standard time to Daylight Saving Time the function + * behaves as if the last interval in standard time is longer by one hour. + * + * An example: if startDate=2011-01-31T00:00:00 (in 'timezone'), endDate=2011-02-01T00:00:00 (in + * 'timezone'), unit='month', then the function returns 1, since a month boundary at + * 2011-02-01T00:00:00 was crossed. + * + * The function operates in the Gregorian calendar. The function does not account for leap seconds. + * For time instants before year 1583 the proleptic Gregorian calendar is used. + * + * startDate - starting time instant in UTC time zone. + * endDate - ending time instant in UTC time zone. + * unit - length of time intervals. + * timezone - determines the timezone used for counting the boundaries as well as Daylight Saving + * Time rules. + */ +long long dateDiff(Date_t startDate, Date_t endDate, TimeUnit unit, const TimeZone& timezone); } // namespace mongo diff --git a/src/mongo/db/query/datetime/date_time_support_test.cpp b/src/mongo/db/query/datetime/date_time_support_test.cpp index fba52b3e029..247ed2b8e2b 100644 --- a/src/mongo/db/query/datetime/date_time_support_test.cpp +++ b/src/mongo/db/query/datetime/date_time_support_test.cpp @@ -29,7 +29,9 @@ #include "mongo/platform/basic.h" +#include <limits> #include <sstream> +#include <timelib.h> #include "mongo/db/query/datetime/date_time_support.h" #include "mongo/unittest/unittest.h" @@ -1191,5 +1193,318 @@ TEST(DayOfWeek, DayNumber) { } } +// Time zones for testing 'dateDiff()'. +const TimeZone kNewYorkTimeZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); +const TimeZone kAustraliaEuclaTimeZone = + kDefaultTimeZoneDatabase.getTimeZone("Australia/Eucla"); // UTC offset +08:45 + +// Verifies 'dateDiff()' with TimeUnit::year. +TEST(DateDiff, Year) { + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 500), + kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 999), + TimeUnit::year, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 500), + kNewYorkTimeZone.createFromDateParts(2001, 1, 1, 0, 0, 0, 0), + TimeUnit::year, + kNewYorkTimeZone)); + ASSERT_EQ(-1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2001, 1, 1, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 500), + TimeUnit::year, + kNewYorkTimeZone)); + ASSERT_EQ(999, + dateDiff(kNewYorkTimeZone.createFromDateParts(1002, 1, 1, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2001, 1, 1, 0, 0, 0, 0), + TimeUnit::year, + kNewYorkTimeZone)); +} + +// Verifies 'dateDiff()' with TimeUnit::month. +TEST(DateDiff, Month) { + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 2, 28, 23, 59, 59, 500), + kNewYorkTimeZone.createFromDateParts(2020, 2, 29, 23, 59, 59, 999), + TimeUnit::month, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 2, 29, 23, 59, 59, 999), + kNewYorkTimeZone.createFromDateParts(2020, 3, 1, 0, 0, 0, 0), + TimeUnit::month, + kNewYorkTimeZone)); + ASSERT_EQ(-14, + dateDiff(kNewYorkTimeZone.createFromDateParts(2010, 2, 28, 23, 59, 59, 999), + kNewYorkTimeZone.createFromDateParts(2008, 12, 31, 23, 59, 59, 500), + TimeUnit::month, + kNewYorkTimeZone)); + ASSERT_EQ(1500 * 12, + dateDiff(kNewYorkTimeZone.createFromDateParts(520, 3, 1, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 3, 1, 0, 0, 0, 0), + TimeUnit::month, + kNewYorkTimeZone)); +} + +// Verifies 'dateDiff()' with TimeUnit::quarter. +TEST(DateDiff, Quarter) { + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 1, 1, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 3, 31, 23, 59, 59, 999), + TimeUnit::quarter, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 1, 1, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 4, 1, 0, 0, 0, 0), + TimeUnit::quarter, + kNewYorkTimeZone)); + ASSERT_EQ(-2001, + dateDiff(kNewYorkTimeZone.createFromDateParts(2000, 12, 31, 23, 59, 59, 500), + kNewYorkTimeZone.createFromDateParts(1500, 9, 30, 23, 59, 59, 999), + TimeUnit::quarter, + kNewYorkTimeZone)); +} + +// Verifies 'dateDiff()' with TimeUnit::week. +TEST(DateDiff, Week) { + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 9, 0, 0, 0, 0), + TimeUnit::week, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 15, 0, 0, 0, 0), + TimeUnit::week, + kNewYorkTimeZone)); + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0), + TimeUnit::week, + kNewYorkTimeZone)); + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0), + TimeUnit::week, + kNewYorkTimeZone)); + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 2, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 3, 0, 0, 0, 0), + TimeUnit::week, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 9, 0, 0, 0, 0), + TimeUnit::week, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 15, 0, 0, 0, 0), + TimeUnit::week, + kNewYorkTimeZone)); + ASSERT_EQ(-5, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 10, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 10, 8, 0, 0, 0, 0), + TimeUnit::week, + kNewYorkTimeZone)); +} + +// Verifies 'dateDiff()' with TimeUnit::day. +TEST(DateDiff, Day) { + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 23, 59, 59, 999), + TimeUnit::day, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 9, 0, 0, 0, 0), + TimeUnit::day, + kNewYorkTimeZone)); + ASSERT_EQ(-1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 9, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 23, 59, 59, 999), + TimeUnit::day, + kNewYorkTimeZone)); + + // Verifies number of days in a year calculation. + ASSERT_EQ(369, + dateDiff(kNewYorkTimeZone.createFromDateParts(1999, 12, 30, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2001, 1, 2, 0, 0, 0, 0), + TimeUnit::day, + kNewYorkTimeZone)); + ASSERT_EQ(6575, + dateDiff(kNewYorkTimeZone.createFromDateParts(1583, 1, 1, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(1601, 1, 1, 0, 0, 0, 0), + TimeUnit::day, + kNewYorkTimeZone)); + ASSERT_EQ(-6575, + dateDiff(kNewYorkTimeZone.createFromDateParts(1601, 1, 1, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(1583, 1, 1, 0, 0, 0, 0), + TimeUnit::day, + kNewYorkTimeZone)); + ASSERT_EQ(29, + dateDiff(kNewYorkTimeZone.createFromDateParts(2004, 2, 10, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2004, 3, 10, 0, 0, 0, 0), + TimeUnit::day, + kNewYorkTimeZone)); + ASSERT_EQ(28, + dateDiff(kAustraliaEuclaTimeZone.createFromDateParts(2005, 2, 10, 0, 0, 0, 0), + kAustraliaEuclaTimeZone.createFromDateParts(2005, 3, 10, 0, 0, 0, 0), + TimeUnit::day, + kAustraliaEuclaTimeZone)); + + // Use timelib_day_of_year as an oracle to verify day calculations. + for (int year = -1000; year < 3000; ++year) { + int expectedNumberOfDays = timelib_day_of_year(year, 12, 31) + 1; + ASSERT_EQ(expectedNumberOfDays, + dateDiff(kNewYorkTimeZone.createFromDateParts(year, 2, 3, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(year + 1, 2, 3, 0, 0, 0, 0), + TimeUnit::day, + kNewYorkTimeZone)); + } +} + +// Verifies 'dateDiff()' with TimeUnit::hour. +TEST(DateDiff, Hour) { + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 59, 59, 999), + TimeUnit::hour, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 59, 59, 999), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 2, 0, 0, 0), + TimeUnit::hour, + kNewYorkTimeZone)); + ASSERT_EQ(-25, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 10, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 23, 59, 59, 999), + TimeUnit::hour, + kNewYorkTimeZone)); + + // Hour difference calculation in UTC offset +08:45 time zone. + ASSERT_EQ( + 1, + dateDiff( + kAustraliaEuclaTimeZone.createFromDateParts(2020, 11, 10, 20, 55, 0, 0) /*UTC 12:10*/, + kAustraliaEuclaTimeZone.createFromDateParts(2020, 11, 10, 21, 5, 0, 0) /*UTC 12:20*/, + TimeUnit::hour, + kAustraliaEuclaTimeZone)); + + // Test of transition from DST to standard time. + ASSERT_EQ(1, + dateDiff(kDefaultTimeZone.createFromDateParts( + 2020, 11, 1, 5, 0, 0, 0) /* America/New_York 1:00AM EDT (UTC-4)*/, + kDefaultTimeZone.createFromDateParts( + 2020, 11, 1, 6, 0, 0, 0) /* America/New_York 1:00AM EST (UTC-5)*/, + TimeUnit::hour, + kNewYorkTimeZone)); + ASSERT_EQ(-1, + dateDiff(kDefaultTimeZone.createFromDateParts( + 2020, 11, 1, 6, 0, 0, 0) /* America/New_York 1:00AM EST (UTC-5)*/, + kDefaultTimeZone.createFromDateParts( + 2020, 11, 1, 5, 0, 0, 0) /* America/New_York 1:00AM EDT (UTC-4)*/, + TimeUnit::hour, + kNewYorkTimeZone)); + + // Test of transition from standard time to DST. + ASSERT_EQ(1, + dateDiff(kDefaultTimeZone.createFromDateParts( + 2020, 3, 8, 6, 45, 0, 0) /* America/New_York 1:45AM EST (UTC-5)*/, + kDefaultTimeZone.createFromDateParts( + 2020, 3, 8, 7, 0, 0, 0) /* America/New_York 3:00AM EDT (UTC-4)*/, + TimeUnit::hour, + kNewYorkTimeZone)); + ASSERT_EQ(-1, + dateDiff(kDefaultTimeZone.createFromDateParts( + 2020, 3, 8, 7, 0, 0, 0) /* America/New_York 3:00AM EDT (UTC-4)*/, + kDefaultTimeZone.createFromDateParts( + 2020, 3, 8, 6, 45, 0, 0) /* America/New_York 1:45AM EST (UTC-5)*/, + TimeUnit::hour, + kNewYorkTimeZone)); + + // Longer period test. + ASSERT_EQ(17545, + dateDiff(kNewYorkTimeZone.createFromDateParts(1999, 1, 1, 0, 0, 0, 0), + kNewYorkTimeZone.createFromDateParts(2001, 1, 1, 1, 0, 0, 0), + TimeUnit::hour, + kNewYorkTimeZone)); +} + +// Verifies 'dateDiff()' with TimeUnit::minute. +TEST(DateDiff, Minute) { + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 59, 999), + TimeUnit::minute, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 59, 999), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 31, 0, 0), + TimeUnit::minute, + kNewYorkTimeZone)); + ASSERT_EQ(-25, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 55, 0, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 59, 999), + TimeUnit::minute, + kNewYorkTimeZone)); + ASSERT_EQ(234047495, + dateDiff(kNewYorkTimeZone.createFromDateParts(1585, 11, 8, 1, 55, 0, 0), + kNewYorkTimeZone.createFromDateParts(2030, 11, 8, 1, 30, 59, 999), + TimeUnit::minute, + kNewYorkTimeZone)); +} + +// Verifies 'dateDiff()' with TimeUnit::second. +TEST(DateDiff, Second) { + ASSERT_EQ(0, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 999), + TimeUnit::second, + kNewYorkTimeZone)); + ASSERT_EQ(1, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 999), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 16, 0), + TimeUnit::second, + kNewYorkTimeZone)); + ASSERT_EQ(-2401, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 2, 10, 16, 999), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 999), + TimeUnit::second, + kNewYorkTimeZone)); + ASSERT_EQ(1604971816, + dateDiff(kDefaultTimeZone.createFromDateParts(1970, 1, 1, 0, 0, 0, 0), + kDefaultTimeZone.createFromDateParts(2020, 11, 10, 1, 30, 16, 0), + TimeUnit::second, + kNewYorkTimeZone)); +} + +// Verifies 'dateDiff()' with TimeUnit::millisecond. +TEST(DateDiff, Millisecond) { + ASSERT_EQ(100, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 0), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 100), + TimeUnit::millisecond, + kNewYorkTimeZone)); + ASSERT_EQ(-1500, + dateDiff(kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 16, 500), + kNewYorkTimeZone.createFromDateParts(2020, 11, 8, 1, 30, 15, 0), + TimeUnit::millisecond, + kNewYorkTimeZone)); + ASSERT_EQ(1604971816000, + dateDiff(kDefaultTimeZone.createFromDateParts(1970, 1, 1, 0, 0, 0, 0), + kDefaultTimeZone.createFromDateParts(2020, 11, 10, 1, 30, 16, 0), + TimeUnit::millisecond, + kNewYorkTimeZone)); + + // Verifies numeric overflow handling. + ASSERT_THROWS_CODE(dateDiff(Date_t::fromMillisSinceEpoch(std::numeric_limits<long long>::min()), + Date_t::fromMillisSinceEpoch(std::numeric_limits<long long>::max()), + TimeUnit::millisecond, + kNewYorkTimeZone), + AssertionException, + 5166308); +} } // namespace } // namespace mongo diff --git a/src/mongo/db/query/sbe_stage_builder_expression.cpp b/src/mongo/db/query/sbe_stage_builder_expression.cpp index 12589567e1e..938982b2fd2 100644 --- a/src/mongo/db/query/sbe_stage_builder_expression.cpp +++ b/src/mongo/db/query/sbe_stage_builder_expression.cpp @@ -366,6 +366,7 @@ public: void visit(ExpressionCond* expr) final { _context->evalStack.emplaceFrame(EvalStage{}); } + void visit(ExpressionDateDiff* expr) final {} void visit(ExpressionDateFromString* expr) final {} void visit(ExpressionDateFromParts* expr) final {} void visit(ExpressionDateToParts* expr) final {} @@ -523,6 +524,7 @@ public: void visit(ExpressionCond* expr) final { _context->evalStack.emplaceFrame(EvalStage{}); } + void visit(ExpressionDateDiff* expr) final {} void visit(ExpressionDateFromString* expr) final {} void visit(ExpressionDateFromParts* expr) final {} void visit(ExpressionDateToParts* expr) final {} @@ -1184,6 +1186,9 @@ public: void visit(ExpressionCond* expr) final { visitConditionalExpression(expr); } + void visit(ExpressionDateDiff* expr) final { + unsupportedExpression("$dateDiff"); + } void visit(ExpressionDateFromString* expr) final { unsupportedExpression("$dateFromString"); } |