/** * 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] */ const testName = "view_definition_feature_compatibility_version_multiversion"; const dbpath = MongoRunner.dataPath + testName; // An array of feature flags that must be enabled to run feature flag tests. const featureFlagsToEnable = ["featureFlagExposeArrayIndexInMapFilterReduce"]; // These arrays should be populated with aggregation pipelines that use // aggregation features in new versions 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 testCasesLastContinuous = []; const testCasesLastContinuousWithFeatureFlags = [ // TODO(SERVER-115778): Move arrayIndexAs/as/valueAs queries to 'testCasesLastStable' when 8.3 becomes last continuous. // TODO(SERVER-90514): Remove arrayIndexAs/as/valueAs queries when feature flag is removed. [ { $match: { $expr: { $eq: [ { $map: { input: "$a", arrayIndexAs: "i", in: "$$i", }, }, [0, 1, 2], ], }, }, }, ], [ { $match: { $expr: { $eq: [ { $reduce: { input: "$a", arrayIndexAs: "i", initialValue: 0, in: {$add: ["$$value", "$$i"]}, }, }, 0, ], }, }, }, ], [ { $match: { $expr: { $eq: [ { $reduce: { input: "$a", initialValue: 0, in: {$add: ["$$value", "$$IDX"]}, }, }, 0, ], }, }, }, ], [ { $match: { $expr: { $eq: [ { $reduce: { input: "$a", as: "elem", valueAs: "acc", initialValue: 0, in: {$add: ["$$acc", "$$elem"]}, }, }, 0, ], }, }, }, ], [ { $match: { $expr: { $eq: [ { $filter: { input: "$a", arrayIndexAs: "i", cond: {$eq: ["$$i", 1]}, }, }, [1, 2, 3], ], }, }, }, ], ]; // Anything that's incompatible with the last continuous release is incompatible with the last // stable release. const testCasesLastStable = testCasesLastContinuous.concat([]); const testCasesLastStableWithFeatureFlags = testCasesLastContinuousWithFeatureFlags.concat([]); // Tests Feature Compatibility Version behavior of view creation while using aggregation pipelines // 'testCases' and using a previous stable version 'lastVersion' of mongod. // 'lastVersion' can have values "last-lts" and "last-continuous". function testViewDefinitionFCVBehavior(lastVersion, testCases, featureFlags = []) { if (testCases.length === 0) { jsTest.log.info("Skipping setup for tests against " + lastVersion + " since there are none"); return; } let conn = MongoRunner.runMongod({dbpath: dbpath, binVersion: "latest"}); assert.neq(null, conn, "mongod was unable to start up"); let testDB = conn.getDB(testName); for (let i = 0; i < featureFlags.length; i++) { const command = {"getParameter": 1}; command[featureFlags[i]] = 1; const featureEnabled = assert.commandWorked(testDB.adminCommand(command))[featureFlags[i]].value; if (!featureEnabled) { jsTest.log.info("Skipping test because the " + featureFlags[i] + " feature flag is disabled"); MongoRunner.stopMongod(conn); return; } } // Explicitly set feature compatibility version to the latest version. assert.commandWorked(testDB.adminCommand({setFeatureCompatibilityVersion: latestFCV, confirm: true})); // Test that we are able to create a new view with any of the new features. testCases.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. testCases.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), confirm: true}), ); // Read against an existing view using new query features should fail. testCases.forEach((pipe, i) => { assert.commandFailed( testDB.runCommand({find: "firstView" + i}), `Succeeded to query view with pipeline ${tojson(pipe)}`, ); }); // TODO SERVER-115604 enable when internalFeatureAsPrimary work is investigated. // 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.) testCases.forEach((pipe, i) => assert.commandFailed( testDB2.createView("view_fail" + i, "coll", pipe), `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. testCases.forEach((pipe, i) => assert.commandFailed( testDB.runCommand({collMod: "emptyView", viewOn: "coll", pipeline: pipe}), `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. testCases.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 fail on a lower FCV. testCases.forEach((pipe, i) => { assert.commandFailed( 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, confirm: true})); testCases.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), confirm: true}), ); MongoRunner.stopMongod(conn); // TODO SERVER-115604 investigate usages of internalValidateFeaturesAsPrimary /*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 (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. testCases.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); } testViewDefinitionFCVBehavior("last-lts", testCasesLastStable); testViewDefinitionFCVBehavior("last-lts", testCasesLastStableWithFeatureFlags, featureFlagsToEnable); testViewDefinitionFCVBehavior("last-continuous", testCasesLastContinuous); testViewDefinitionFCVBehavior("last-continuous", testCasesLastContinuousWithFeatureFlags, featureFlagsToEnable);