summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharlie Swanson <charlie.swanson@mongodb.com>2019-05-21 17:22:25 -0400
committerCharlie Swanson <charlie.swanson@mongodb.com>2019-05-22 16:29:09 -0400
commit5594eeb9c42f5a28ebb20c8fcce87a2a1a01f6a5 (patch)
tree8453d01043b1bc67161a4ee684c69e637e989645
parent76cf536d476b50994c75dd16ec5c7caca23759a5 (diff)
downloadmongo-5594eeb9c42f5a28ebb20c8fcce87a2a1a01f6a5.tar.gz
SERVER-41257 Disallow persisting new 4.2 expressions in FCV 4.0
-rw-r--r--jstests/multiVersion/collection_validator_feature_compatibility_version.js205
-rw-r--r--jstests/multiVersion/view_definition_feature_compatibility_version.js196
-rw-r--r--jstests/replsets/collection_validator_initial_sync_with_feature_compatibility_version.js91
-rw-r--r--jstests/replsets/view_definition_initial_sync_with_feature_compatibility_version.js116
-rw-r--r--src/mongo/db/pipeline/expression.cpp44
-rw-r--r--src/mongo/db/pipeline/expression.h4
-rw-r--r--src/mongo/db/pipeline/expression_context.cpp1
7 files changed, 653 insertions, 4 deletions
diff --git a/jstests/multiVersion/collection_validator_feature_compatibility_version.js b/jstests/multiVersion/collection_validator_feature_compatibility_version.js
new file mode 100644
index 00000000000..dbb25b1127a
--- /dev/null
+++ b/jstests/multiVersion/collection_validator_feature_compatibility_version.js
@@ -0,0 +1,205 @@
+/**
+ * Test that mongod will not allow creation of collection validators using 4.2 query features when
+ * the feature compatibility version is older than 4.2.
+ *
+ * TODO SERVER-41273: Remove FCV 4.0 validation during the 4.3 development cycle.
+ *
+ * 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;
+
+ // In order to avoid restarting the server for each test case, we declare all the test cases up
+ // front, and test them all at once.
+ const testCases = [
+ {validator: {$expr: {$eq: [{$round: "$a"}, 4]}}, nonMatchingDocument: {a: 5.2}},
+ {validator: {$expr: {$eq: [{$trunc: ["$a", 2]}, 4.1]}}, nonMatchingDocument: {a: 4.23}},
+ {
+ validator: {$expr: {$regexMatch: {input: "$a", regex: /sentinel/}}},
+ nonMatchingDocument: {a: "no dice"}
+ },
+ ];
+
+ 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 feature compatibility version 4.2.
+ assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: "4.2"}));
+
+ testCases.forEach(function(test, i) {
+ // Create a collection with a validator using 4.2 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 4.2 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 4.0.
+ assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: "4.0"}));
+
+ 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 4.2 query features should fail
+ // while feature compatibility version is 4.0.
+ 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 4.2 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 we try to start up a 4.0 mongod, it will fail, because it will not be able to parse
+ // the validator using 4.2 query features.
+ conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "4.0", noCleanData: true});
+ assert.eq(
+ null, conn, "mongod 4.0 started, even with a validator using 4.2 query features in place.");
+
+ // Starting up a 4.2 mongod, however, should succeed, even though the feature compatibility
+ // version is still set to 4.0.
+ 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 a 4.0 mongod.
+ conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "4.0", noCleanData: true});
+ assert.neq(
+ null,
+ conn,
+ "mongod 4.0 failed to start, even after we removed the validator using 4.2 query features");
+
+ MongoRunner.stopMongod(conn);
+
+ // The rest of the test uses mongod 4.2.
+ 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 4.2.
+ assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: "4.2"}));
+
+ testCases.forEach(function(test, i) {
+ const coll = testDB["coll2" + i];
+
+ // Now we should be able to create a collection with a validator using 4.2 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 4.2 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 4.0 and then restart with
+ // internalValidateFeaturesAsMaster=false.
+ assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: "4.0"}));
+ MongoRunner.stopMongod(conn);
+ conn = MongoRunner.runMongod({
+ dbpath: dbpath,
+ binVersion: "latest",
+ noCleanData: true,
+ setParameter: "internalValidateFeaturesAsMaster=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 4.0, we should still be able to add a
+ // validator using 4.2 query features, because internalValidateFeaturesAsMaster 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 4.2 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);
+
+}());
diff --git a/jstests/multiVersion/view_definition_feature_compatibility_version.js b/jstests/multiVersion/view_definition_feature_compatibility_version.js
new file mode 100644
index 00000000000..c8d9eb54d12
--- /dev/null
+++ b/jstests/multiVersion/view_definition_feature_compatibility_version.js
@@ -0,0 +1,196 @@
+/**
+ * Test that mongod will not allow creation of a view using 4.2 aggregation features when the
+ * feature compatibility version is older than 4.2.
+ *
+ * TODO SERVER-41273: Remove FCV 4.0 validation during the 4.3 development cycle.
+ *
+ * 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;
+
+ // In order to avoid restarting the server for each test case, we declare all the test cases up
+ // front, and test them all at once.
+ const pipelinesWithNewFeatures = [
+ [{$addFields: {x: {$round: 4.57}}}],
+ [{$addFields: {x: {$trunc: [4.57, 1]}}}],
+ [{$addFields: {x: {$regexFind: {input: "string", regex: /st/}}}}],
+ [{$addFields: {x: {$regexFindAll: {input: "string", regex: /st/}}}}],
+ [{$addFields: {x: {$regexMatch: {input: "string", regex: /st/}}}}],
+ [{$facet: {pipe1: [{$addFields: {x: {$round: 4.57}}}]}}],
+ [{
+ $facet: {
+ pipe1: [{$addFields: {x: {$round: 4.57}}}],
+ pipe2: [{$addFields: {newThing: {$regexMatch: {input: "string", regex: /st/}}}}]
+ }
+ }],
+ [{$lookup: {from: 'x', pipeline: [{$addFields: {x: {$round: 4.57}}}], as: 'results'}}],
+ [{
+ $graphLookup: {
+ from: 'x',
+ startWith: ["$_id"],
+ connectFromField: "target_id",
+ connectToField: "_id",
+ restrictSearchWithMatch: {$expr: {$eq: [4, {$round: "$x"}]}},
+ as: 'results'
+ }
+ }],
+ [{
+ $lookup: {
+ from: 'x',
+ pipeline: [{$facet: {pipe1: [{$addFields: {x: {$round: 4.57}}}]}}],
+ as: 'results'
+ }
+ }],
+ ];
+
+ 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 4.2.
+ assert.commandWorked(testDB.adminCommand({setFeatureCompatibilityVersion: "4.2"}));
+
+ // 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 4.2`));
+
+ // Test that we are able to create a new 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 4.2`);
+ });
+
+ // Create an empty view which we will attempt to update to use 4.0 query features under
+ // feature compatibility mode 4.0.
+ assert.commandWorked(testDB.createView("emptyView", "coll", []));
+
+ // Set the feature compatibility version to 4.0.
+ assert.commandWorked(testDB.adminCommand({setFeatureCompatibilityVersion: "4.0"}));
+
+ // Read against an existing view using 4.2 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 using 4.2 query features should fail.
+ pipelinesWithNewFeatures.forEach(
+ (pipe, i) => assert.commandFailedWithCode(
+ testDB.createView("view_fail" + i, "coll", pipe),
+ ErrorCodes.QueryFeatureNotAllowed,
+ `Expected *not* to be able to create view with pipeline ${tojson(pipe)} while in FCV 4.0`));
+
+ // Trying to update existing view to use 4.2 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
+ 4.0`));
+
+ MongoRunner.stopMongod(conn);
+
+ // Starting up a 4.0 mongod with 4.2 query features will succeed.
+ conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "4.0", noCleanData: true});
+ assert.neq(null, conn, "mongod 4.0 was unable to start up");
+ testDB = conn.getDB(testName);
+
+ // Reads will fail against views with 4.2 query features when running a 4.0 binary.
+ // 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 4.0 binary`));
+
+ // Test that a read against a view that does not contain 4.2 query features succeeds.
+ assert.commandWorked(testDB.runCommand({find: "emptyView"}));
+
+ MongoRunner.stopMongod(conn);
+
+ // Starting up a 4.2 mongod should succeed, even though the feature compatibility version is
+ // still set to 4.0.
+ 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 4.2 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 4.2.
+ assert.commandWorked(testDB.adminCommand({setFeatureCompatibilityVersion: "4.2"}));
+
+ 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 4.2`);
+
+ // 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 4.2`);
+ });
+
+ // Set the feature compatibility version to 4.0 and then restart with
+ // internalValidateFeaturesAsMaster=false.
+ assert.commandWorked(testDB.adminCommand({setFeatureCompatibilityVersion: "4.0"}));
+ MongoRunner.stopMongod(conn);
+ conn = MongoRunner.runMongod({
+ dbpath: dbpath,
+ binVersion: "latest",
+ noCleanData: true,
+ setParameter: "internalValidateFeaturesAsMaster=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 4.0, we should still be able to create a
+ // view using 4.2 query features, because internalValidateFeaturesAsMaster is false.
+ assert.commandWorked(
+ testDB.createView("thirdView" + i, "coll", pipe),
+ `Expected to be able to create view with pipeline ${tojson(pipe)} while in FCV 4.0 ` +
+ `with internalValidateFeaturesAsMaster=false`);
+
+ // We should also be able to modify a view to use 4.2 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 4.0 ` +
+ `with internalValidateFeaturesAsMaster=false`);
+ });
+
+ MongoRunner.stopMongod(conn);
+
+ // Starting up a 4.0 mongod with 4.2 query features.
+ conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "4.0", noCleanData: true});
+ assert.neq(null, conn, "mongod 4.0 was unable to start up");
+ testDB = conn.getDB(testName);
+
+ // Existing views with 4.2 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);
+}());
diff --git a/jstests/replsets/collection_validator_initial_sync_with_feature_compatibility_version.js b/jstests/replsets/collection_validator_initial_sync_with_feature_compatibility_version.js
new file mode 100644
index 00000000000..6a9101c3586
--- /dev/null
+++ b/jstests/replsets/collection_validator_initial_sync_with_feature_compatibility_version.js
@@ -0,0 +1,91 @@
+/**
+ * Test that a new replica set member can successfully sync a collection with a validator using 4.2
+ * aggregation features, even when the replica set was downgraded to feature compatibility version
+ * 4.0.
+ *
+ * TODO SERVER-41273: Remove FCV 4.0 validation during the 4.3 development cycle.
+ *
+ * We restart the secondary as part of this test with the expectation that it still has the same
+ * data after the restart.
+ * @tags: [requires_persistence]
+ */
+
+load("jstests/replsets/rslib.js");
+
+(function() {
+ "use strict";
+ const testName = "collection_validator_initial_sync_with_feature_compatibility";
+
+ function testValidator(validator, nonMatchingDocument) {
+ //
+ // Create a single-node replica set.
+ //
+ let replTest = new ReplSetTest({name: testName, nodes: 1});
+
+ replTest.startSet();
+ replTest.initiate();
+
+ let primary = replTest.getPrimary();
+ let testDB = primary.getDB("test");
+
+ //
+ // Explicitly set the replica set to feature compatibility version 4.2.
+ //
+ assert.commandWorked(primary.adminCommand({setFeatureCompatibilityVersion: "4.2"}));
+
+ //
+ // Create a collection with a validator using 4.2 query features.
+ //
+ assert.commandWorked(testDB.createCollection("coll", {validator: validator}));
+
+ //
+ // Downgrade the replica set to feature compatibility version 4.0.
+ //
+ assert.commandWorked(primary.adminCommand({setFeatureCompatibilityVersion: "4.0"}));
+
+ //
+ // Add a new member to the replica set.
+ //
+ let secondaryDBPath = MongoRunner.dataPath + testName + "_secondary";
+ resetDbpath(secondaryDBPath);
+ let secondary = replTest.add({dbpath: secondaryDBPath});
+ replTest.reInitiate(secondary);
+ reconnect(primary);
+ reconnect(secondary);
+
+ //
+ // Once the new member completes its initial sync, stop it, remove it from the replica set,
+ // and start it back up as an individual instance.
+ //
+ replTest.waitForState(secondary, [ReplSetTest.State.PRIMARY, ReplSetTest.State.SECONDARY]);
+
+ replTest.stopSet(undefined /* send default signal */,
+ true /* don't clear data directory */);
+
+ secondary = MongoRunner.runMongod({dbpath: secondaryDBPath, noCleanData: true});
+ assert.neq(null, secondary, "mongod was unable to start up");
+
+ //
+ // Verify that the validator synced to the new member by attempting to insert a document
+ // that does not validate and checking that the insert fails.
+ //
+ let secondaryDB = secondary.getDB("test");
+ assert.writeError(secondaryDB.coll.insert(nonMatchingDocument),
+ ErrorCodes.DocumentValidationFailure);
+
+ //
+ // Verify that, even though the existing validator still works, it is not possible to create
+ // a new validator using 4.2 query features because of feature compatibility version 4.0.
+ //
+ assert.commandFailedWithCode(
+ secondaryDB.runCommand({collMod: "coll", validator: validator}),
+ ErrorCodes.QueryFeatureNotAllowed);
+
+ MongoRunner.stopMongod(secondary);
+ }
+
+ // Ban the use of expressions that were introduced or had their parsing modified in 4.2.
+ testValidator({$expr: {$eq: [{$round: "$a"}, 4]}}, {a: 5.2});
+ testValidator({$expr: {$eq: [{$trunc: ["$a", 2]}, 4.1]}}, {a: 4.23});
+ testValidator({$expr: {$regexMatch: {input: "$a", regex: /sentinel/}}}, {a: "no dice"});
+}());
diff --git a/jstests/replsets/view_definition_initial_sync_with_feature_compatibility_version.js b/jstests/replsets/view_definition_initial_sync_with_feature_compatibility_version.js
new file mode 100644
index 00000000000..ad5ef00391d
--- /dev/null
+++ b/jstests/replsets/view_definition_initial_sync_with_feature_compatibility_version.js
@@ -0,0 +1,116 @@
+/**
+ * Test that a new replica set member can successfully sync a collection with a view using 4.2
+ * aggregation features, even when the replica set was downgraded to feature compatibility version
+ * 4.0.
+ *
+ * TODO SERVER-41273: Remove FCV 4.0 validation during the 4.3 development cycle.
+ *
+ * We restart the secondary as part of this test with the expectation that it still has the same
+ * data after the restart.
+ * @tags: [requires_persistence]
+ */
+
+load("jstests/replsets/rslib.js");
+
+(function() {
+ "use strict";
+ const testName = "view_definition_initial_sync_with_feature_compatibility";
+
+ function testView(pipeline) {
+ //
+ // Create a single-node replica set.
+ //
+ let replTest = new ReplSetTest({name: testName, nodes: 1});
+
+ replTest.startSet();
+ replTest.initiate();
+
+ let primary = replTest.getPrimary();
+ let testDB = primary.getDB("test");
+
+ //
+ // Explicitly set the replica set to feature compatibility version 4.2.
+ //
+ assert.commandWorked(primary.adminCommand({setFeatureCompatibilityVersion: "4.2"}));
+
+ //
+ // Create a view using 4.2 query features.
+ //
+ assert.commandWorked(testDB.createView("view1", "coll", pipeline));
+
+ //
+ // Downgrade the replica set to feature compatibility version 4.0.
+ //
+ assert.commandWorked(primary.adminCommand({setFeatureCompatibilityVersion: "4.0"}));
+
+ //
+ // Add a new member to the replica set.
+ //
+ let secondaryDBPath = MongoRunner.dataPath + testName + "_secondary";
+ resetDbpath(secondaryDBPath);
+ let secondary = replTest.add({dbpath: secondaryDBPath});
+ replTest.reInitiate(secondary);
+ reconnect(primary);
+ reconnect(secondary);
+
+ //
+ // Once the new member completes its initial sync, stop it, remove it from the replica
+ // set, and start it back up as an individual instance.
+ //
+ replTest.waitForState(secondary, [ReplSetTest.State.PRIMARY, ReplSetTest.State.SECONDARY]);
+
+ replTest.stopSet(undefined /* send default signal */,
+ true /* don't clear data directory */);
+
+ secondary = MongoRunner.runMongod({dbpath: secondaryDBPath, noCleanData: true});
+ assert.neq(null, secondary, "mongod was unable to start up");
+
+ //
+ // Verify that the view synced to the new member.
+ //
+ let secondaryDB = secondary.getDB("test");
+ assert.eq(secondaryDB.system.views.findOne({_id: "test.view1"}, {_id: 1}),
+ {_id: "test.view1"});
+
+ //
+ // Verify that, even though a view using 4.2 query features exists, it is not possible to
+ // create a new view using 4.2 query features because of feature compatibility version 4.0.
+ //
+ assert.commandFailedWithCode(secondaryDB.createView("view2", "coll", pipeline),
+ ErrorCodes.QueryFeatureNotAllowed);
+
+ MongoRunner.stopMongod(secondary);
+ }
+
+ testView([{$addFields: {x: {$round: 4.57}}}]);
+ testView([{$addFields: {x: {$trunc: [4.57, 1]}}}]);
+ testView([{$addFields: {x: {$regexFind: {input: "string", regex: /st/}}}}]);
+ testView([{$addFields: {x: {$regexFindAll: {input: "string", regex: /st/}}}}]);
+ testView([{$addFields: {x: {$regexMatch: {input: "string", regex: /st/}}}}]);
+ testView([{$facet: {pipe1: [{$addFields: {x: {$round: 4.57}}}]}}]);
+ testView([{
+ $facet: {
+ pipe1: [{$addFields: {x: {$round: 4.57}}}],
+ pipe2: [{$addFields: {newThing: {$regexMatch: {input: "string", regex: /st/}}}}]
+ }
+ }]);
+ testView(
+ [{$lookup: {from: 'x', pipeline: [{$addFields: {x: {$round: 4.57}}}], as: 'results'}}]);
+ testView([{
+ $graphLookup: {
+ from: 'x',
+ startWith: ["$_id"],
+ connectFromField: "target_id",
+ connectToField: "_id",
+ restrictSearchWithMatch: {$expr: {$eq: [4, {$round: "$x"}]}},
+ as: 'results'
+ }
+ }]);
+ testView([{
+ $lookup: {
+ from: 'x',
+ pipeline: [{$facet: {pipe1: [{$addFields: {x: {$round: 4.57}}}]}}],
+ as: 'results'
+ }
+ }]);
+}());
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp
index 0e8984a81fa..d6068398342 100644
--- a/src/mongo/db/pipeline/expression.cpp
+++ b/src/mongo/db/pipeline/expression.cpp
@@ -4924,7 +4924,10 @@ Value ExpressionRound::evaluate(const Document& root) const {
root, _children, getOpName(), Decimal128::kRoundTiesToEven, &std::round);
}
-REGISTER_EXPRESSION(round, ExpressionRound::parse);
+REGISTER_EXPRESSION_WITH_MIN_VERSION(
+ round,
+ ExpressionRound::parse,
+ ServerGlobalParams::FeatureCompatibility::Version::kFullyUpgradedTo42);
const char* ExpressionRound::getOpName() const {
return "$round";
}
@@ -4934,6 +4937,30 @@ Value ExpressionTrunc::evaluate(const Document& root) const {
root, _children, getOpName(), Decimal128::kRoundTowardZero, &std::trunc);
}
+intrusive_ptr<Expression> ExpressionTrunc::parse(const intrusive_ptr<ExpressionContext>& expCtx,
+ BSONElement elem,
+ const VariablesParseState& vps) {
+ // In version 4.2 we added new arguments. In all previous versions the expression existed but
+ // only supported a single argument.
+ const bool newArgumentsAllowed =
+ (!expCtx->maxFeatureCompatibilityVersion ||
+ (*expCtx->maxFeatureCompatibilityVersion >=
+ ServerGlobalParams::FeatureCompatibility::Version::kFullyUpgradedTo42));
+ uassert(
+ ErrorCodes::QueryFeatureNotAllowed,
+ // TODO SERVER-31968 we would like to include the current version and the required minimum
+ // version in this error message, but using FeatureCompatibilityVersion::toString() would
+ // introduce a dependency cycle.
+ str::stream()
+ << elem.fieldNameStringData()
+ << " with >1 argument is not allowed in the current feature compatibility version. See "
+ << feature_compatibility_version_documentation::kCompatibilityLink
+ << " for more information.",
+ // Allow non-arrays since they will be rejected anyway by the parser below.
+ elem.type() != BSONType::Array || elem.Array().size() <= 1 || newArgumentsAllowed);
+ return ExpressionRangedArity<ExpressionTrunc, 1, 2>::parse(expCtx, elem, vps);
+}
+
REGISTER_EXPRESSION(trunc, ExpressionTrunc::parse);
const char* ExpressionTrunc::getOpName() const {
return "$trunc";
@@ -6015,7 +6042,10 @@ void ExpressionRegex::_doAddDependencies(DepsTracker* deps) const {
/* -------------------------- ExpressionRegexFind ------------------------------ */
-REGISTER_EXPRESSION(regexFind, ExpressionRegexFind::parse);
+REGISTER_EXPRESSION_WITH_MIN_VERSION(
+ regexFind,
+ ExpressionRegexFind::parse,
+ ServerGlobalParams::FeatureCompatibility::Version::kFullyUpgradedTo42);
boost::intrusive_ptr<Expression> ExpressionRegexFind::parse(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
BSONElement expr,
@@ -6036,7 +6066,10 @@ Value ExpressionRegexFind::evaluate(const Document& root) const {
/* -------------------------- ExpressionRegexFindAll ------------------------------ */
-REGISTER_EXPRESSION(regexFindAll, ExpressionRegexFindAll::parse);
+REGISTER_EXPRESSION_WITH_MIN_VERSION(
+ regexFindAll,
+ ExpressionRegexFindAll::parse,
+ ServerGlobalParams::FeatureCompatibility::Version::kFullyUpgradedTo42);
boost::intrusive_ptr<Expression> ExpressionRegexFindAll::parse(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
BSONElement expr,
@@ -6097,7 +6130,10 @@ Value ExpressionRegexFindAll::evaluate(const Document& root) const {
/* -------------------------- ExpressionRegexMatch ------------------------------ */
-REGISTER_EXPRESSION(regexMatch, ExpressionRegexMatch::parse);
+REGISTER_EXPRESSION_WITH_MIN_VERSION(
+ regexMatch,
+ ExpressionRegexMatch::parse,
+ ServerGlobalParams::FeatureCompatibility::Version::kFullyUpgradedTo42);
boost::intrusive_ptr<Expression> ExpressionRegexMatch::parse(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
BSONElement expr,
diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h
index 7033ee717d1..7ec1700afb3 100644
--- a/src/mongo/db/pipeline/expression.h
+++ b/src/mongo/db/pipeline/expression.h
@@ -2325,6 +2325,10 @@ public:
explicit ExpressionTrunc(const boost::intrusive_ptr<ExpressionContext>& expCtx)
: ExpressionRangedArity<ExpressionTrunc, 1, 2>(expCtx) {}
+ static boost::intrusive_ptr<Expression> parse(
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ BSONElement elem,
+ const VariablesParseState& vps);
Value evaluate(const Document& root) const final;
const char* getOpName() const final;
diff --git a/src/mongo/db/pipeline/expression_context.cpp b/src/mongo/db/pipeline/expression_context.cpp
index 0054e0de98c..9f65c12669f 100644
--- a/src/mongo/db/pipeline/expression_context.cpp
+++ b/src/mongo/db/pipeline/expression_context.cpp
@@ -160,6 +160,7 @@ intrusive_ptr<ExpressionContext> ExpressionContext::copyWith(
expCtx->inMongos = inMongos;
expCtx->allowDiskUse = allowDiskUse;
expCtx->bypassDocumentValidation = bypassDocumentValidation;
+ expCtx->maxFeatureCompatibilityVersion = maxFeatureCompatibilityVersion;
expCtx->subPipelineDepth = subPipelineDepth;
expCtx->tempDir = tempDir;