SERVER-123712 scan collection for non-complaint docs on constraint upgrade replset (#53218)

Co-authored-by: Mickey J. Winters <mickey.winters@mongodb.com>
GitOrigin-RevId: c93452067d3e9af49cd86c355720dbd65ff6e63b
This commit is contained in:
kmznam 2026-05-19 11:09:35 -04:00 committed by MongoDB Bot
parent d78e44e975
commit aae9723b66
5 changed files with 122 additions and 20 deletions

View File

@ -0,0 +1,109 @@
/**
* Tests that collMod correctly blocks upgrading validationLevel to 'constraint' when the
* collection contains documents that violate the validator, and allows the upgrade when all
* documents conform.
*
* @tags: [
* requires_fcv_90,
* featureFlagConstraintValidationLevel,
* assumes_unsharded_collection,
* assumes_against_mongod_not_mongos,
* ]
*/
import {beforeEach, describe, it} from "jstests/libs/mochalite.js";
const collName = jsTestName();
const validator = {a: {$exists: true}};
describe("collMod upgrade to constraint validationLevel", function () {
beforeEach(() => {
db[collName].drop();
});
it("blocks upgrade when a document violates the validator", () => {
assert.commandWorked(
db.createCollection(collName, {validator: validator, validationLevel: "strict", validationAction: "warn"}),
);
// Insert a non-compliant document. Succeeds because validationAction is "warn".
assert.commandWorked(db[collName].insert({b: 1}));
// 'constraint' requires action='error', so transition both fields together.
const res = assert.commandFailedWithCode(
db.runCommand({
collMod: collName,
validationLevel: "constraint",
validationAction: "error",
}),
12370902,
);
// Verify the scan-based error fired and surfaces a find query for the user.
assert(
res.errmsg.includes("Cannot upgrade validationLevel to 'constraint'"),
"expected scan-based error message: " + res.errmsg,
);
assert(
res.errmsg.includes("db." + collName + ".find("),
"expected find-query suggestion in errmsg: " + res.errmsg,
);
assert(res.errmsg.includes("$nor"), "expected $nor in suggested query: " + res.errmsg);
});
it("allows upgrade when all documents conform to the validator", () => {
assert.commandWorked(
db.createCollection(collName, {validator: validator, validationLevel: "strict", validationAction: "warn"}),
);
assert.commandWorked(db[collName].insert({a: 1}));
assert.commandWorked(
db.runCommand({
collMod: collName,
validationLevel: "constraint",
validationAction: "error",
}),
);
// After upgrade, compliant inserts still work.
assert.commandWorked(db[collName].insert({a: 2}));
// After upgrade, non-compliant inserts are rejected.
assert.commandFailedWithCode(db[collName].insert({b: 1}), ErrorCodes.DocumentValidationFailure);
});
it("allows upgrade on an empty collection with a validator", () => {
assert.commandWorked(db.createCollection(collName, {validator: validator, validationLevel: "strict"}));
assert.commandWorked(db.runCommand({collMod: collName, validationLevel: "constraint"}));
});
it("allows re-upgrade after downgrade and validator change", () => {
assert.commandWorked(
db.createCollection(collName, {validator: validator, validationLevel: "strict", validationAction: "warn"}),
);
// Insert a document with both 'a' and 'b' so it satisfies the updated validator later.
assert.commandWorked(db[collName].insert({a: 1, b: 1}));
assert.commandWorked(
db.runCommand({
collMod: collName,
validationLevel: "constraint",
validationAction: "error",
}),
);
// Downgrade to strict so the validator can be changed.
assert.commandWorked(db.runCommand({collMod: collName, validationLevel: "strict"}));
// Tighten the validator to require both 'a' and 'b'. The existing document satisfies this.
assert.commandWorked(
db.runCommand({
collMod: collName,
validator: {$and: [{a: {$exists: true}}, {b: {$exists: true}}]},
}),
);
// Re-upgrade to constraint -- the existing document satisfies the new validator.
assert.commandWorked(db.runCommand({collMod: collName, validationLevel: "constraint"}));
// A document with only 'a' (missing 'b') is now rejected.
assert.commandFailedWithCode(db[collName].insert({a: 1}), ErrorCodes.DocumentValidationFailure);
});
});

View File

@ -840,6 +840,7 @@ mongo_cc_library(
"//src/mongo/db/commands/query_cmd:run_aggregate.cpp",
"//src/mongo/db/commands/query_cmd:write_commands.cpp",
"//src/mongo/db/global_catalog/ddl:shuffle_list_command_results.cpp",
"//src/mongo/db/matcher/doc_validation:constraint_validation_level_upgrade.cpp",
"//src/mongo/db/shard_role/ddl:collmod_cmd.cpp",
"//src/mongo/db/shard_role/ddl:create_command.cpp",
"//src/mongo/db/shard_role/ddl:create_indexes_cmd.cpp",
@ -913,7 +914,6 @@ mongo_cc_library(
"//src/mongo/db/query/write_ops",
"//src/mongo/db/query/write_ops:write_ops_exec",
"//src/mongo/db/repl:replica_set_messages",
"//src/mongo/db/repl:storage_interface",
"//src/mongo/db/s:query_analysis_writer",
"//src/mongo/db/session:session_catalog_mongod",
"//src/mongo/db/shard_role:resource_yielders",

View File

@ -21,24 +21,6 @@ mongo_cc_library(
],
)
mongo_cc_library(
name = "constraint_validation_level_upgrade",
srcs = [
"constraint_validation_level_upgrade.cpp",
],
hdrs = [
"constraint_validation_level_upgrade.h",
],
deps = [
"//src/mongo/db/auth:authprivilege",
"//src/mongo/db/commands:standalone",
"//src/mongo/db/pipeline:lite_parsed_document_source",
"//src/mongo/db/query/client_cursor:cursor_response_idl",
"//src/mongo/db/shard_role",
"//src/mongo/rpc",
],
)
mongo_cc_unit_test(
name = "constraint_validation_level_upgrade_test",
srcs = [
@ -46,8 +28,8 @@ mongo_cc_unit_test(
],
tags = ["mongo_unittest_sixth_group"],
deps = [
":constraint_validation_level_upgrade",
"//src/mongo/db:dbhelpers",
"//src/mongo/db/commands:standalone",
"//src/mongo/db/shard_role/shard_catalog:catalog_test_fixture",
],
)

View File

@ -40,6 +40,7 @@
#include "mongo/db/commands/test_commands_enabled.h"
#include "mongo/db/dbhelpers.h"
#include "mongo/db/global_catalog/type_collection.h"
#include "mongo/db/matcher/doc_validation/constraint_validation_level_upgrade.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/profile_settings.h"
@ -51,6 +52,7 @@
#include "mongo/db/shard_role/ddl/coll_mod_reply_validation.h"
#include "mongo/db/shard_role/ddl/replica_set_ddl_tracker.h"
#include "mongo/db/shard_role/shard_catalog/coll_mod.h"
#include "mongo/db/shard_role/shard_catalog/operation_sharding_state.h"
#include "mongo/db/sharding_environment/grid.h"
#include "mongo/db/storage/record_store.h"
#include "mongo/db/storage/storage_engine.h"
@ -183,6 +185,13 @@ public:
gFeatureFlagConstraintValidationLevel
.isEnabledUseLastLTSFCVWhenUninitialized(
VersionContext::getDecoration(opCtx), fcvSnapshot));
auto& oss = OperationShardingState::get(opCtx);
uassertStatusOK(noDocumentsViolatingValidator(
opCtx,
nss,
PlacementConcern{oss.getDbVersion(nss.dbName()),
oss.getShardVersion(nss)}));
}
}

View File

@ -59,6 +59,8 @@ Status checkValidationOptionsCanBeUsed(const CollectionOptions& opts,
ErrorCodes::BadValue,
"Validation action of 'warn' is not allowed when Validation level is 'constraint'");
}
// TODO SERVER-123713: enforce that validationAction is set to 'error' when upgrading
// validationLevel to 'constraint'.
if (opts.uuid) { // existing collection
if (opts.validationLevel == ValidationLevelEnum::constraint && newValidator) {
return Status(ErrorCodes::BadValue,