/** * 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] */ const testName = "collection_validator_feature_compatibility_version"; 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 // // { validator: { ... }, nonMatchingDocument: { ... } } // // objects that use query features in new versions 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. const testCasesLastContinuous = [ // // Populate with any new expressions. // ]; 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. { validator: { $expr: { $eq: [ { $map: { input: "$a", arrayIndexAs: "i", in: "$$i", }, }, [0, 1, 2], ], }, }, nonMatchingDocument: {a: [0, 0]}, }, { validator: { $expr: { $eq: [ { $reduce: { input: "$a", arrayIndexAs: "i", initialValue: 0, in: {$add: ["$$value", "$$i"]}, }, }, 0, ], }, }, nonMatchingDocument: {a: [0, 1]}, }, { validator: { $expr: { $eq: [ { $reduce: { input: "$a", initialValue: 0, in: {$add: ["$$value", "$$IDX"]}, }, }, 0, ], }, }, nonMatchingDocument: {a: [0, 1]}, }, { validator: { $expr: { $eq: [ { $reduce: { input: "$a", as: "elem", valueAs: "acc", initialValue: 0, in: {$add: ["$$acc", "$$elem"]}, }, }, 0, ], }, }, nonMatchingDocument: {a: [0, 1]}, }, { validator: { $expr: { $eq: [ { $filter: { input: "$a", arrayIndexAs: "i", cond: {$eq: ["$$i", 1]}, }, }, [1, 2, 3], ], }, }, nonMatchingDocument: {a: [0, 0]}, }, ]; const testCasesLastStable = testCasesLastContinuous.concat([]); const testCasesLastStableWithFeatureFlags = testCasesLastContinuousWithFeatureFlags.concat([]); // 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, 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; } } let adminDB = conn.getDB("admin"); // Explicitly set the feature compatibility version to the latest version. assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: latestFCV, confirm: true})); 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), confirm: true}), ); 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.commandFailed( res, "Expected *not* to be able to create collection with validator " + tojson(test.validator), ); // 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.commandFailed(res, `Expected to be able to create collection with validator ${tojson(test.validator)}`); }); MongoRunner.stopMongod(conn); // 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"); 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]; const res = assert.writeError(coll.insert(test.nonMatchingDocument)); assert.neq( res.getWriteError(), ErrorCodes.DocumentValidationFailure, `Expected validator ${tojson(test.validator)} to not be able to execute new query features`, ); }); 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"); adminDB = conn.getDB("admin"); testDB = conn.getDB(testName); // And the validator shouldn't be able to execute the query with new features. testCases.forEach(function (test, i) { const coll = testDB["coll" + i]; const res = assert.writeError(coll.insert(test.nonMatchingDocument)); assert.neq( res.getWriteError(), ErrorCodes.DocumentValidationFailure, `Expected validator ${tojson(test.validator)} to not be able to execute new query features`, ); // 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, confirm: true})); 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), confirm: true}), ); MongoRunner.stopMongod(conn); // TODO SERVER-115604 investigate usage of internalValidateFeaturesAsPrimary, and confirm if we // should remove this or fix query validation. /*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);*/ } testCollectionValidatorFCVBehavior("last-lts", testCasesLastStable); testCollectionValidatorFCVBehavior("last-lts", testCasesLastStableWithFeatureFlags, featureFlagsToEnable); testCollectionValidatorFCVBehavior("last-continuous", testCasesLastContinuous); testCollectionValidatorFCVBehavior("last-continuous", testCasesLastContinuousWithFeatureFlags, featureFlagsToEnable);