SERVER-103774 Disallow compound wildcard indexes from being used as a shard key index (#40518) (#42275)

GitOrigin-RevId: 724234dd26a77de8da08b8e0cd24829bca6ddda4
This commit is contained in:
Silvia Surroca 2025-10-15 16:22:47 +02:00 committed by MongoDB Bot
parent 7d97af9679
commit a5eeb8939e
4 changed files with 280 additions and 2 deletions

View File

@ -0,0 +1,241 @@
/**
* Tests that an incompatible shard key index can't be the only shard key index on a sharded
* collection.
*
* @tags: [
* requires_fcv_82,
* ]
*/
import {after, before, beforeEach, describe, it} from "jstests/libs/mochalite.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";
const shardKeyPattern = {
skey: 1,
};
const incompatibleShardKeyIndexes = [
{
key: {a: 1},
options: {},
},
{
key: {a: 1, skey: 1},
options: {},
},
{
key: {skey: 1, multiKeyField: 1},
options: {},
isMultiKey: true,
},
{
key: {skey: 1, "a.$**": 1},
options: {},
},
{
key: {skey: 1},
options: {hidden: true},
},
{
key: {skey: 1, a: 1},
options: {hidden: true},
},
{
key: {skey: 1},
options: {sparse: true},
},
{
key: {skey: 1, a: 1},
options: {sparse: true},
},
{
key: {skey: 1},
options: {partialFilterExpression: {dummyField: {$gt: 0}}},
},
{
key: {skey: 1, a: 1},
options: {partialFilterExpression: {dummyField: {$gt: 0}}},
},
{
key: {skey: 1},
options: {collation: {locale: "fr_CA"}},
},
{
key: {skey: 1, a: 1},
options: {collation: {locale: "fr_CA"}},
},
];
describe("testing incompatible shard key indexes", function() {
before(() => {
this.st = new ShardingTest({shards: 1});
this.coll = this.st.s.getDB("test").coll;
});
after(() => {
this.st.stop();
});
for (let incompatibleShardKeyIndex of incompatibleShardKeyIndexes) {
incompatibleShardKeyIndex.canCoexistWithShardKeyIndex =
bsonWoCompare(incompatibleShardKeyIndex.key, shardKeyPattern) !== 0;
describe("test specific incompatible shard key index", () => {
before(() => {
jsTest.log.info("Running tests for", incompatibleShardKeyIndex);
});
beforeEach(() => {
this.coll.drop();
// Insert a document that will make the index on 'multiKeyField' multikey.
this.coll.insert({skey: 22, multiKeyField: [1, 2, 3]});
// Create the incompatible index.
assert.commandWorked(
this.coll.createIndex(incompatibleShardKeyIndex.key,
incompatibleShardKeyIndex.options),
);
});
it("can't shard a non-empty collection with only an incompatible shard key index",
() => {
const res = this.st.s.adminCommand(
{shardCollection: this.coll.getFullName(), key: shardKeyPattern});
this.coll.insert({skey: 33});
assert.commandFailedWithCode(
this.st.s.adminCommand(
{shardCollection: this.coll.getFullName(), key: shardKeyPattern}),
ErrorCodes.InvalidOptions,
);
});
it("shardColleciont on an empty collection will attempt to create a shard key index",
() => {
this.coll.deleteMany({});
if (incompatibleShardKeyIndex.isMultiKey) {
// We can't test a multiKey index on an empty collection, since it would not
// be multiKey.
return;
}
const res = this.st.s.adminCommand(
{shardCollection: this.coll.getFullName(), key: shardKeyPattern});
assert.commandWorkedOrFailedWithCode(res, [
ErrorCodes.InvalidOptions,
ErrorCodes.IndexKeySpecsConflict,
]);
if (res.ok) {
// The shardCollection command succeeded, check that the shard key index was
// created and it's not the incompatible index.
assert(
incompatibleShardKeyIndex.canCoexistWithShardKeyIndex,
"reshardCollection succeeded when it should have failed",
);
const shardKeyIndex = this.coll.getIndexByKey(shardKeyPattern);
const incompatibleIndex =
this.coll.getIndexByKey(incompatibleShardKeyIndex.key);
assert.neq(shardKeyIndex, null, "shard key index was not created");
assert.neq(incompatibleIndex, null, "incompatible index is missing");
assert.neq(incompatibleIndex,
shardKeyIndex,
"shard key index mustn't be the incompatible index");
}
});
it("can't drop or hide last compatible shard key index", () => {
// Shard the collection with a compatible shard key index.
let compatibleShardKeyIndex = shardKeyPattern;
if (!incompatibleShardKeyIndex.canCoexistWithShardKeyIndex) {
compatibleShardKeyIndex = {skey: 1, a: 1};
}
assert.commandWorked(this.coll.createIndex(compatibleShardKeyIndex));
assert.commandWorked(
this.st.s.adminCommand(
{shardCollection: this.coll.getFullName(), key: compatibleShardKeyIndex}),
);
// Attemt to drop or hide the last compatible shard key index.
assert.commandFailedWithCode(
this.coll.dropIndex(compatibleShardKeyIndex),
ErrorCodes.CannotDropShardKeyIndex,
);
assert.commandFailedWithCode(this.coll.hideIndex(compatibleShardKeyIndex),
ErrorCodes.InvalidOptions);
});
it("reshardCollection will attempt to create a shard key index", () => {
// Shard the collection first with shard key {_id: 1}.
assert.commandWorked(
this.st.s.adminCommand(
{shardCollection: this.coll.getFullName(), key: {"_id": 1}}),
);
// Attempt to reshard the collection to shard key 'shardKeyPattern'.
const res = this.st.s.adminCommand({
reshardCollection: this.coll.getFullName(),
key: shardKeyPattern,
numInitialChunks: 1,
});
assert.commandWorkedOrFailedWithCode(res, [
ErrorCodes.InvalidOptions,
ErrorCodes.IndexKeySpecsConflict,
]);
if (res.ok) {
// The reshardCollection command succeeded, check that the shard key index was
// created and it's not the incompatible index.
assert(
incompatibleShardKeyIndex.canCoexistWithShardKeyIndex,
"reshardCollection succeeded when it should have failed",
);
const shardKeyIndex = this.coll.getIndexByKey(shardKeyPattern);
const incompatibleIndex =
this.coll.getIndexByKey(incompatibleShardKeyIndex.key);
assert.neq(shardKeyIndex, null, "shard key index was not created");
assert.neq(incompatibleIndex, null, "incompatible index is missing");
assert.neq(incompatibleIndex,
shardKeyIndex,
"shard key index mustn't be the incompatible index");
}
});
it("can't call refineCollectionShardKey with only an incompatible shard key index",
() => {
// This test only makes sense if the incompatible shard key index has
// multiple fields and it's prefixed with the shard key.
if (!incompatibleShardKeyIndex.canCoexistWithShardKeyIndex ||
Object.keys(incompatibleShardKeyIndex.key)[0] !== "skey") {
return;
}
// Make sure the collection is sharded on 'shardKeyPattern'.
this.coll.createIndex(shardKeyPattern);
assert.commandWorked(
this.st.s.adminCommand(
{shardCollection: this.coll.getFullName(), key: shardKeyPattern}),
);
// Attempt to refine the shard key to the incompatible shard key index.
const res = this.st.s.adminCommand({
refineCollectionShardKey: this.coll.getFullName(),
key: incompatibleShardKeyIndex.key,
});
assert.commandFailedWithCode(res,
[ErrorCodes.InvalidOptions, ErrorCodes.BadValue]);
});
});
}
});

View File

@ -129,6 +129,7 @@ bool isCompatibleWithShardKey(OperationContext* opCtx,
const int kErrorMultikey = 0x04;
const int kErrorCollation = 0x08;
const int kErrorNotPrefix = 0x10;
const int kErrorWildcard = 0x20;
int reasons = 0;
auto desc = indexEntry->descriptor();
@ -146,6 +147,10 @@ bool isCompatibleWithShardKey(OperationContext* opCtx,
reasons |= kErrorNotPrefix;
}
if (desc->getIndexType() == IndexType::INDEX_WILDCARD) {
reasons |= kErrorWildcard;
}
if (reasons == 0) { // that is, not partial index, not sparse, and not prefix, then:
if (!indexEntry->isMultikey(opCtx, collection)) {
if (hasSimpleCollation) {
@ -181,6 +186,9 @@ bool isCompatibleWithShardKey(OperationContext* opCtx,
if (reasons & kErrorNotPrefix) {
errors += " Shard key is not a prefix of index key.";
}
if (reasons & kErrorWildcard) {
errors += " Index key is a wildcard index.";
}
if (!errMsg->empty()) {
*errMsg += "\n";
}

View File

@ -159,16 +159,44 @@ TEST_F(ShardKeyIndexUtilTest, ExcludesIncompatibleIndexes) {
createIndex(BSON("key" << BSON("x" << 1) << "name"
<< "collation"
<< "collation" << BSON("locale" << "fr") << "v" << kIndexVersion));
createIndex(BSON("key" << BSON("x" << 1 << "a.$**" << 1) << "name"
<< "wildcard" << "v" << kIndexVersion));
createIndex(BSON("key" << BSON("y" << 1) << "name" << "y" << "v" << kIndexVersion));
createIndex(
BSON("key" << BSON("y" << 1 << "x" << 1) << "name" << "y_x" << "v" << kIndexVersion));
{
const auto index =
findShardKeyPrefixedIndex(opCtx(), coll(), BSON("x" << 1), true /* requireSingleKey */);
ASSERT_FALSE(index);
}
createIndex(BSON("key" << BSON("x" << 1) << "name"
<< "x"
<< "v" << kIndexVersion));
{
const auto index =
findShardKeyPrefixedIndex(opCtx(), coll(), BSON("x" << 1), true /* requireSingleKey */);
ASSERT_TRUE(index);
ASSERT_EQ("x", index->descriptor()->indexName());
}
}
TEST_F(ShardKeyIndexUtilTest, MultiKeyIndexIsAnIncompatibleShardKey) {
createIndex(BSON("key" << BSON("x" << 1) << "name"
<< "x"
<< "v" << kIndexVersion));
DBDirectClient client(opCtx());
client.insert(nss(), BSON("x" << BSON_ARRAY(1 << 2)));
const auto index =
findShardKeyPrefixedIndex(opCtx(), coll(), BSON("x" << 1), true /* requireSingleKey */);
ASSERT_TRUE(index);
ASSERT_EQ("x", index->descriptor()->indexName());
ASSERT_FALSE(index);
}
TEST_F(ShardKeyIndexUtilTest, ExcludesMultiKeyIfRequiresSingleKey) {

View File

@ -415,6 +415,7 @@ void ValidationBehaviorsShardCollection::verifyUsefulNonMultiKeyIndex(
void ValidationBehaviorsShardCollection::verifyCanCreateShardKeyIndex(const NamespaceString& nss,
std::string* errMsg) const {
// shardCollection can only attempt to create a shard key index if the collection is empty.
repl::ReadConcernArgs readConcern =
repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern);
FindCommandRequest findCommand(nss);