SERVER-85346 Prevent unique indexes with non simple collation in sharded collections (#48769) (#49245)

Co-authored-by: Meryama <meryama.nadim@mongodb.com>
GitOrigin-RevId: 1de747462945a4f52ba76addc8526e8cdaf240e9
This commit is contained in:
Marcos Grillo 2026-03-11 09:38:39 +01:00 committed by MongoDB Bot
parent 65bb179286
commit a803a0599c
8 changed files with 170 additions and 58 deletions

View File

@ -0,0 +1,128 @@
/**
* Tests the interaction between shard keys and indexes with simple vs non-simple collation.
*
* @tags: [
* multiversion_incompatible,
* requires_sharding,
* ]
*/
import {after, before, beforeEach, describe, it} from "jstests/libs/mochalite.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";
describe("shard collection and non-simple collation tests", function() {
before(() => {
this.st = new ShardingTest({shards: 1});
this.db = this.st.s.getDB("test");
this.collName = "coll";
this.collName2 = "coll2";
});
after(() => {
this.st.stop();
});
beforeEach(() => {
this.db[this.collName].drop();
});
it("creates a collection implicitly when creating a unique index with simple collation and shards the collection successfully",
() => {
const coll = this.db[this.collName];
const coll2 = this.db[this.collName2];
const shardKey = {a: 1};
// Create unique index with simple collation (default).
assert.commandWorked(
coll.createIndex(shardKey, {unique: true, collation: {locale: "simple"}}));
// Shard the collection - should succeed because the index has simple collation.
assert.commandWorked(
this.st.s.adminCommand({shardCollection: coll.getFullName(), key: shardKey}));
// Shard another collection with explicit simple collation in the shardCollection
// command.
assert.commandWorked(
this.st.s.adminCommand({
shardCollection: coll2.getFullName(),
key: shardKey,
unique: true,
collation: {locale: "simple"},
}),
);
});
it("creates a collection implicitly when creating a unique index with non-simple collation and fails to shard the collection",
() => {
const coll = this.db[this.collName];
const shardKey = {a: 1};
// Create unique index with non-simple collation.
assert.commandWorked(
coll.createIndex(
shardKey,
{unique: true, collation: {locale: "en_US", strength: 2}, name: "a_1_enUS"}),
);
// Attempt to shard the collection - should fail because the index has non-simple
// collation.
assert.commandFailedWithCode(
this.st.s.adminCommand({shardCollection: coll.getFullName(), key: shardKey}),
ErrorCodes.InvalidOptions,
);
});
it("shards a collection and successfully creates a non-simple collation index with the same key format",
() => {
const coll = this.db[this.collName];
const shardKey = {a: 1};
// Shard the collection first.
assert.commandWorked(
this.st.s.adminCommand({shardCollection: coll.getFullName(), key: shardKey}));
// Create a non-unique index with non-simple collation - should succeed.
assert.commandWorked(coll.createIndex(
shardKey, {collation: {locale: "en_US", strength: 2}, name: "a_1_enUS"}));
});
it("shards a collection and fails to create a unique non-simple collation index with the same key format",
() => {
const coll = this.db[this.collName];
const shardKey = {a: 1};
// Shard the collection first.
assert.commandWorked(
this.st.s.adminCommand({shardCollection: coll.getFullName(), key: shardKey}));
// Attempt to create a unique index with non-simple collation - should fail.
assert.commandFailedWithCode(
coll.createIndex(
shardKey,
{unique: true, collation: {locale: "en_US", strength: 2}, name: "a_1_enUS"}),
ErrorCodes.CannotCreateIndex,
);
});
it("shards a collection and fails to use collMod to change prepareUnique with non-simple collation",
() => {
const coll = this.db[this.collName];
const shardKey = {a: 1};
// Shard the collection.
assert.commandWorked(
this.st.s.adminCommand({shardCollection: coll.getFullName(), key: shardKey}));
assert.commandWorked(coll.createIndex(
shardKey, {collation: {locale: "en_US", strength: 2}, name: "a_1_enUS"}));
// Attempt to use collMod to change prepareUnique and collation to non-simple - should
// fail.
assert.commandFailedWithCode(
this.db.runCommand({
collMod: this.collName,
index: {keyPattern: shardKey, name: "a_1_enUS", prepareUnique: true},
}),
ErrorCodes.InvalidOptions,
);
});
});

View File

@ -98,46 +98,6 @@ function runOnFieldsTests(targetShardKey, targetSplit) {
prevStages: prefixPipeline
});
// Test that a unique index on the "on" fields cannot be used to satisfy the requirement if
// it has a different collation.
resetTargetColl(targetShardKey, targetSplit);
assert.commandWorked(
targetColl.createIndex(indexSpec, {unique: true, collation: {locale: "en_US"}}));
assertMergeFailsWithoutUniqueIndex({
source: sourceColl,
onFields: Object.keys(indexSpec),
target: targetColl,
prevStages: prefixPipeline
});
assertMergeFailsWithoutUniqueIndex({
source: sourceColl,
onFields: Object.keys(indexSpec),
target: targetColl,
options: {collation: {locale: "en"}},
prevStages: prefixPipeline
});
assertMergeFailsWithoutUniqueIndex({
source: sourceColl,
onFields: Object.keys(indexSpec),
target: targetColl,
options: {collation: {locale: "simple"}},
prevStages: prefixPipeline
});
assertMergeFailsWithoutUniqueIndex({
source: sourceColl,
onFields: Object.keys(indexSpec),
target: targetColl,
options: {collation: {locale: "en_US", strength: 1}},
prevStages: prefixPipeline
});
assertMergeSucceedsWithExpectedUniqueIndex({
source: sourceColl,
target: targetColl,
onFields: Object.keys(indexSpec),
options: {collation: {locale: "en_US"}},
prevStages: prefixPipeline
});
// Test that a unique index with dotted field names can be used.
resetTargetColl(targetShardKey, targetSplit);
const dottedPathIndexSpec = Object.merge(targetShardKey, {"newField.subField": 1});

View File

@ -429,16 +429,18 @@ StatusWith<std::pair<ParsedCollModRequest, BSONObj>> parseCollModRequest(
cmrIndex->idx->unique()) {
indexForOplog->setPrepareUnique(boost::none);
} else {
// Checks if the index key pattern conflicts with the shard key pattern.
if (shardKeyPattern) {
if (!shardKeyPattern->isIndexUniquenessCompatible(
cmrIndex->idx->keyPattern())) {
// Checks if the index key pattern conflicts with the shard key pattern only if the
// prepareUnique will be set to true.
if (shardKeyPattern && cmdIndex.getPrepareUnique().value()) {
if (!shardKeyPattern->isIndexUniquenessAndCollationCompatible(
cmrIndex->idx->keyPattern(), cmrIndex->idx->collation())) {
return {
ErrorCodes::InvalidOptions,
fmt::format("cannot set 'prepareUnique' for index {} with shard key "
"pattern {}",
"pattern {} and collation {}",
cmrIndex->idx->keyPattern().toString(),
shardKeyPattern->toBSON().toString())};
shardKeyPattern->toBSON().toString(),
cmrIndex->idx->collation().toString())};
}
}
cmrIndex->indexPrepareUnique = cmdIndex.getPrepareUnique();

View File

@ -3674,12 +3674,14 @@ IndexBuildsCoordinator::prepareSpecListForCreate(OperationContext* opCtx,
const ShardKeyPattern shardKeyPattern(collDesc.getKeyPattern());
for (const BSONObj& spec : filteredSpecs) {
if (spec[kUniqueFieldName].trueValue() || spec[kPrepareUniqueFieldName].trueValue()) {
auto collation = spec["collation"].ok() ? spec["collation"].Obj() : BSONObj();
uassert(
ErrorCodes::CannotCreateIndex,
str::stream() << "cannot create index with 'unique' or 'prepareUnique' option over "
<< spec[kKeyFieldName].Obj() << " with shard key pattern "
<< shardKeyPattern.toBSON(),
shardKeyPattern.isIndexUniquenessCompatible(spec[kKeyFieldName].Obj()));
<< shardKeyPattern.toBSON() << " and collation " << collation,
shardKeyPattern.isIndexUniquenessAndCollationCompatible(spec[kKeyFieldName].Obj(),
collation));
}
}

View File

@ -178,13 +178,15 @@ bool validShardKeyIndexExists(OperationContext* opCtx,
BSONObj currentKey = idx["key"].embeddedObject();
bool isUnique = idx["unique"].trueValue();
bool isPrepareUnique = idx["prepareUnique"].trueValue();
auto collation = idx["collation"].ok() ? idx["collation"].Obj() : BSONObj();
uassert(ErrorCodes::InvalidOptions,
str::stream() << "can't shard collection '" << nss.toStringForErrorMsg()
<< "' with unique index on " << currentKey
<< " and proposed shard key " << shardKeyPattern.toBSON()
<< ". Uniqueness can't be maintained unless shard key is a prefix",
<< "' with unique index on " << currentKey << ", proposed shard key "
<< shardKeyPattern.toBSON() << " and collation " << collation
<< ". Uniqueness can't be maintained unless shard key is a prefix "
"and has simple collation",
(!isUnique && !isPrepareUnique) ||
shardKeyPattern.isIndexUniquenessCompatible(currentKey));
shardKeyPattern.isIndexUniquenessAndCollationCompatible(currentKey, collation));
}
// 2. Check for a useful index

View File

@ -39,6 +39,7 @@
#include "mongo/db/hasher.h"
#include "mongo/db/index_names.h"
#include "mongo/db/matcher/path_internal.h"
#include "mongo/db/query/collation/collation_spec.h"
#include "mongo/db/storage/key_string/key_string.h"
#include "mongo/util/assert_util.h"
#include "mongo/util/str.h"
@ -421,12 +422,14 @@ BSONObj ShardKeyPattern::emplaceMissingShardKeyValuesForDocument(const BSONObj d
return fullDocBuilder.obj();
}
bool ShardKeyPattern::isIndexUniquenessCompatible(const BSONObj& indexPattern) const {
bool ShardKeyPattern::isIndexUniquenessAndCollationCompatible(const BSONObj& indexPattern,
const BSONObj& collation) const {
if (!indexPattern.isEmpty() && indexPattern.firstElementFieldName() == kIdField) {
return true;
}
return _keyPattern.toBSON().isFieldNamePrefixOf(indexPattern);
const bool hasSimpleCollation = collation.isEmpty() ||
SimpleBSONObjComparator::kInstance.evaluate(collation == CollationSpec::kSimpleSpec);
return _keyPattern.toBSON().isFieldNamePrefixOf(indexPattern) && hasSimpleCollation;
}
size_t ShardKeyPattern::getApproximateSize() const {

View File

@ -226,7 +226,7 @@ public:
/**
* Returns true if the shard key pattern can ensure that the index uniqueness is respected
* across all shards.
* across all shards and has a simple collation.
*
* Primarily this just checks whether the shard key pattern field names are equal to or a
* prefix of the 'unique' or 'prepareUnique' index pattern field names. Since documents with the
@ -246,6 +246,8 @@ public:
* shard key {a : 1} is not compatible with a unique/prepareUnique index on {b : 1}
* shard key {a : "hashed", b : 1} is not compatible with unique/prepareUnique index on
* {b : 1}
* shard key {a : 1} is not compatible with unique/prepareUnique index on {a : 1},
* {collation: "en_US"}
*
* All unique index patterns starting with _id are assumed to be enforceable by the fact
* that _ids must be unique, and so all unique _id prefixed indexes are compatible with
@ -255,7 +257,8 @@ public:
* { k : "hashed" } is not capable of being a unique/prepareUnique index and is an invalid
* argument to this method.
*/
bool isIndexUniquenessCompatible(const BSONObj& indexPattern) const;
bool isIndexUniquenessAndCollationCompatible(const BSONObj& indexPattern,
const BSONObj& collation = BSONObj()) const;
/**
* Returns true if the key pattern has an "_id" field of any flavor.

View File

@ -525,7 +525,7 @@ TEST_F(ShardKeyPatternTest, ExtractQueryShardKeyHashed) {
}
static bool indexComp(const ShardKeyPattern& pattern, const BSONObj& indexPattern) {
return pattern.isIndexUniquenessCompatible(indexPattern);
return pattern.isIndexUniquenessAndCollationCompatible(indexPattern, BSONObj());
}
TEST_F(ShardKeyPatternTest, UniqueIndexCompatibleSingle) {
@ -970,5 +970,17 @@ TEST_F(ShardKeyPatternTest, IsExtendedBy) {
ASSERT_FALSE(shardKeyPatternHashed3_1.isExtendedBy(shardKeyPatternHashed2_1));
}
TEST_F(ShardKeyPatternTest, NonSimpleCollationUniqueIndexesForbidden) {
auto shardKeyBSON = BSON("x" << 1);
auto collationBSON = BSON("locale" << "en_US");
auto simpleCollationBSON = BSON("locale" << "simple");
ShardKeyPattern shardKeyPattern(shardKeyBSON);
ASSERT_TRUE(shardKeyPattern.isIndexUniquenessAndCollationCompatible(shardKeyBSON, BSONObj()));
ASSERT_TRUE(
shardKeyPattern.isIndexUniquenessAndCollationCompatible(shardKeyBSON, simpleCollationBSON));
ASSERT_FALSE(
shardKeyPattern.isIndexUniquenessAndCollationCompatible(shardKeyBSON, collationBSON));
}
} // namespace
} // namespace mongo