summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/aggregation/expressions/date_diff.js103
-rw-r--r--jstests/multiVersion/genericSetFCVUsage/collection_validator_feature_compatibility_version.js246
-rw-r--r--jstests/multiVersion/genericSetFCVUsage/view_definition_feature_compatibility_version.js217
-rw-r--r--src/mongo/db/exec/document_value/value.h7
-rw-r--r--src/mongo/db/pipeline/SConscript12
-rw-r--r--src/mongo/db/pipeline/expression.cpp145
-rw-r--r--src/mongo/db/pipeline/expression.h62
-rw-r--r--src/mongo/db/pipeline/expression_bm.cpp120
-rw-r--r--src/mongo/db/pipeline/expression_date_test.cpp283
-rw-r--r--src/mongo/db/pipeline/expression_visitor.h2
-rw-r--r--src/mongo/db/query/datetime/date_time_support.cpp145
-rw-r--r--src/mongo/db/query/datetime/date_time_support.h52
-rw-r--r--src/mongo/db/query/datetime/date_time_support_test.cpp315
-rw-r--r--src/mongo/db/query/sbe_stage_builder_expression.cpp5
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");
}