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:
parent
65bb179286
commit
a803a0599c
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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});
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user