Files
mongo/jstests/sharding/hashed_shard_key_index_optional_shard_collection.js
Cheahuychou Mao 5293e0a60f SERVER-99745 Make hashed shard key index optional upon sharding a collection (#47588)
GitOrigin-RevId: 8967eaf6bcd42371e027382666c86fd3e580d5a4
2026-02-06 17:30:31 +00:00

337 lines
14 KiB
JavaScript

/*
* Test running shardCollection commands against a collection without a shard key index with the
* "skipHashedShardKeyIndexCreation" option and check the following:
* 1. If the shard key is not hashed or featureFlagHashedShardKeyIndexOptionalUponShardingCollection
* is not enabled, the command fails with InvalidOptions.
* 2. If the shardCollection command is run with "skipHashedShardKeyIndexCreation" false:
* - If the collection is non-existent or empty, then the command succeeds and the shard key
* index is created.
* - Otherwise, the command fails with InvalidOptions.
* 3. If the shardCollection command is run with "skipHashedShardKeyIndexCreation" true:
* - If the shard key is hashed and featureFlagHashedShardKeyIndexOptionalUponShardingCollection
* is enabled, then the command succeeds and the shard key index doesn't get created.
* - Otherwise, the command fails with InvalidOptions.
*
* For each resulting sharded collection, check that:
* - find commands that filter by shard key equality uses IXSCAN if the collection has a shard key
* index (because the shardCollection command was run with "skipHashedShardKeyIndexCreation"
* false) or a non-shard key compatible index.
* - moveChunk commands against the given hashed sharded collection fails with IndexNotFound if
* the shardCollection command was run with "skipHashedShardKeyIndexCreation" true.
*
* Consider the shard key {x: "hashed"}, the shard key indexes for this shard key include
* {x: "hashed"}, {x: "hashed", y: 1} and the non-shard key but compatible indexes for this shard
* key include {x: 1}, {x: 1, y: 1} and {x: 1, y: "hashed"}.
*
* @tags: [
* requires_fcv_83,
* featureFlagHashedShardKeyIndexOptionalUponShardingCollection
* ]
*/
import {getWinningPlanFromExplain} from "jstests/libs/query/analyze_plan.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";
import {AnalyzeShardKeyUtil} from "jstests/sharding/analyze_shard_key/libs/analyze_shard_key_util.js";
const dbName = "testDb";
let shardNames = [];
function makeDocument(shardKey, indexKey, val) {
let doc = {};
for (let fieldName in shardKey) {
AnalyzeShardKeyUtil.setDottedField(doc, fieldName, val);
}
if (indexKey) {
for (let fieldName in indexKey) {
AnalyzeShardKeyUtil.setDottedField(doc, fieldName, val);
}
}
return doc;
}
function checkIfIndexExists(st, dbName, collName, indexKey) {
const indexes = st.s.getDB(dbName).getCollection(collName).getIndexes();
jsTest.log("Checking indexes " + tojson({dbName, collName, indexes}));
return indexes.some((index) => bsonWoCompare(index.key, indexKey) == 0);
}
/*
* Checks that find commands that filter by shard key equality against the given sharded
* collection uses IXSCAN if the collection has a shard key index (because the shardCollection
* command was run with "skipHashedShardKeyIndexCreation" false) or a non-shard key compatible
* index.
*/
function testFindCommand(st, dbName, collName, doc, testCase) {
const coll = st.s.getDB(dbName).getCollection(collName);
const shardKeyValue = AnalyzeShardKeyUtil.extractShardKeyValueFromDocument(doc, testCase.shardKey);
const explain = coll.find(shardKeyValue).explain();
const winningPlan = getWinningPlanFromExplain(explain);
const isIdHashedShardKey = bsonWoCompare(testCase.shardKey, {_id: "hashed"}) == 0;
if (isIdHashedShardKey) {
const isClusteredColl = AnalyzeShardKeyUtil.isClusterCollection(st.s, dbName, collName);
if (isClusteredColl) {
assert.eq(winningPlan.stage, "EXPRESS_CLUSTERED_IXSCAN", winningPlan);
assert(!winningPlan.keyPattern);
} else {
assert.eq(winningPlan.stage, "EXPRESS_IXSCAN", winningPlan);
assert.eq(winningPlan.keyPattern, "{ _id: 1 }", winningPlan);
}
} else if (!testCase.skipHashedShardKeyIndexCreation || testCase.containsCompatibleIndex) {
assert.eq(winningPlan.stage, "FETCH", winningPlan);
assert.eq(winningPlan.inputStage.stage, "IXSCAN", winningPlan);
if (testCase.skipHashedShardKeyIndexCreation) {
assert(bsonWoCompare(winningPlan.inputStage.keyPattern, testCase.indexKey) == 0, winningPlan);
} else {
assert(
bsonWoCompare(winningPlan.inputStage.keyPattern, testCase.shardKey) == 0 ||
bsonWoCompare(winningPlan.inputStage.keyPattern, testCase.indexKey) == 0,
winningPlan,
);
}
}
}
/*
* Checks that moveChunk commands against the given sharded collection fails with IndexNotFound if
* the shardCollection command was run with "skipHashedShardKeyIndexCreation" true.
*/
function testMoveChunkCommand(st, dbName, collName, doc, testCase) {
const ns = dbName + "." + collName;
const shardKeyValue = AnalyzeShardKeyUtil.extractShardKeyValueFromDocument(doc, testCase.shardKey);
let failed = false;
for (let shardName of shardNames) {
const res = st.s.adminCommand({moveChunk: ns, find: shardKeyValue, to: shardName});
if (testCase.skipHashedShardKeyIndexCreation) {
assert.commandWorkedOrFailedWithCode(res, ErrorCodes.IndexNotFound);
if (res.code == ErrorCodes.IndexNotFound) {
failed = true;
}
} else {
assert.commandWorked(res);
}
}
if (testCase.skipHashedShardKeyIndexCreation) {
assert(failed);
}
}
function testNonExistentCollection(st, testCase) {
jsTest.log("Test sharding a non-existent collection " + tojson(testCase));
const collName1 = "testColl1";
const ns1 = dbName + "." + collName1;
const coll = st.s.getCollection(ns1);
const res = st.s.adminCommand({
shardCollection: ns1,
key: testCase.shardKey,
skipHashedShardKeyIndexCreation: testCase.skipHashedShardKeyIndexCreation,
});
if (testCase.expectErrorCode) {
assert.commandFailedWithCode(res, testCase.expectErrorCode);
const expectedIndexExists = false;
assert.eq(checkIfIndexExists(st, dbName, collName1, testCase.shardKey), expectedIndexExists, {testCase});
} else {
assert.commandWorked(res);
const expectedIndexExists = !testCase.skipHashedShardKeyIndexCreation;
assert.eq(checkIfIndexExists(st, dbName, collName1, testCase.shardKey), expectedIndexExists, {testCase});
const doc = makeDocument(testCase.shardKey, testCase.indexKey, 1);
assert.commandWorked(coll.insert(doc));
testFindCommand(st, dbName, collName1, doc, testCase);
testMoveChunkCommand(st, dbName, collName1, doc, testCase, expectedIndexExists);
}
assert(coll.drop());
}
function testExistentCollection(st, testCase) {
jsTest.log("Test sharding an existent collection " + tojson(testCase));
const collName2 = "testColl2";
const ns2 = dbName + "." + collName2;
const db = st.s.getDB(dbName);
const coll = db.getCollection(collName2);
let doc;
if (testCase.indexKey) {
assert.commandWorked(coll.createIndex(testCase.indexKey));
} else {
assert.commandWorked(db.createCollection(collName2));
}
if (!testCase.isEmptyCollection) {
doc = makeDocument(testCase.shardKey, testCase.indexKey, 1);
assert.commandWorked(coll.insert(doc));
}
const res = st.s.adminCommand({
shardCollection: ns2,
key: testCase.shardKey,
skipHashedShardKeyIndexCreation: testCase.skipHashedShardKeyIndexCreation,
});
if (testCase.expectErrorCode) {
assert.commandFailedWithCode(res, testCase.expectErrorCode);
const expectedIndexExists = false;
assert.eq(checkIfIndexExists(st, dbName, collName2, testCase.shardKey), expectedIndexExists, {testCase});
} else {
assert.commandWorked(res);
const expectedIndexExists = !testCase.skipHashedShardKeyIndexCreation;
assert.eq(checkIfIndexExists(st, dbName, collName2, testCase.shardKey), expectedIndexExists, {testCase});
if (testCase.isEmptyCollection) {
doc = makeDocument(testCase.shardKey, testCase.indexKey, 2);
assert.commandWorked(coll.insert(doc));
}
testFindCommand(st, dbName, collName2, doc, testCase);
testMoveChunkCommand(st, dbName, collName2, doc, testCase, expectedIndexExists);
}
assert(coll.drop());
}
// Test cases of non-shard key indexes. shardCollection commands with
// 'skipHashedShardKeyIndexCreation' true should only succeed when the shard key is hashed and
// featureFlagHashedShardKeyIndexOptionalUponShardingCollection is true.
const shardKeyTestCases = [
{
shardKey: {_id: "hashed"},
indexKeyTestCases: [
{key: {a: "hashed"}, isCompatible: false},
{key: {a: 1}, isCompatible: false},
],
},
{
shardKey: {a: 1},
indexKeyTestCases: [{key: {a: "hashed"}, isCompatible: true}],
},
{
shardKey: {a: "hashed"},
indexKeyTestCases: [
{key: {a: 1}, isCompatible: true},
{key: {a: 1, b: 1}, isCompatible: true},
{key: {a: 1, b: "hashed"}, isCompatible: true},
{key: {b: 1, a: "hashed"}, isCompatible: false},
{key: {b: 1, a: 1}, isCompatible: false},
],
},
{
shardKey: {a: 1, b: 1},
indexKeyTestCases: [
{key: {a: "hashed", b: 1}, isCompatible: true},
{key: {a: 1, b: "hashed"}, isCompatible: true},
],
},
{
shardKey: {a: 1, b: "hashed"},
indexKeyTestCases: [
{key: {a: "hashed", b: 1}, isCompatible: true},
{key: {a: 1, b: 1, c: 1}, isCompatible: true},
{key: {a: 1}, isCompatible: false},
{key: {b: "hashed"}, isCompatible: false},
{key: {b: "hashed", a: 1}, isCompatible: false},
],
},
{
shardKey: {"a.x": 1, b: 1},
indexKeyTestCases: [
{key: {"a.x": 1, b: "hashed"}, isCompatible: true},
{key: {"a.x": "hashed", b: 1}, isCompatible: true},
],
},
{
shardKey: {"a.x": "hashed", b: 1},
indexKeyTestCases: [
{key: {"a.x": 1, b: "hashed"}, isCompatible: true},
{key: {"a.x": 1, b: 1, c: 1}, isCompatible: true},
{key: {"a.x": "hashed"}, isCompatible: false},
{key: {b: 1}, isCompatible: false},
{key: {b: 1, "a.x": "hashed"}, isCompatible: false},
],
},
];
function runTest(hashedShardKeyIndexOptionalUponShardingCollection) {
jsTest.log(
"Testing with " +
tojson({
hashedShardKeyIndexOptionalUponShardingCollection,
}),
);
const st = new ShardingTest({
shards: 2,
rs: {
nodes: 1,
setParameter: {
featureFlagHashedShardKeyIndexOptionalUponShardingCollection:
hashedShardKeyIndexOptionalUponShardingCollection,
},
},
});
shardNames = [];
shardNames.push(st.shard0.shardName);
shardNames.push(st.shard1.shardName);
assert.commandWorked(st.s.adminCommand({enableSharding: dbName, primaryShard: st.shard0.shardName}));
for (let shardKeyTestCase of shardKeyTestCases) {
const isHashedShardKey = AnalyzeShardKeyUtil.isHashedKeyPattern(shardKeyTestCase.shardKey);
jsTest.log("Testing " + tojson({shardKeyTestCase}));
for (let skipHashedShardKeyIndexCreation of [true, false]) {
const expectValidationError = !hashedShardKeyIndexOptionalUponShardingCollection || !isHashedShardKey;
testNonExistentCollection(st, {
shardKey: shardKeyTestCase.shardKey,
skipHashedShardKeyIndexCreation,
expectErrorCode: (() => {
if (expectValidationError) {
return ErrorCodes.InvalidOptions;
}
return null;
})(),
});
for (let isEmptyCollection of [true, false]) {
testExistentCollection(st, {
shardKey: shardKeyTestCase.shardKey,
isEmptyCollection,
skipHashedShardKeyIndexCreation,
expectErrorCode: (() => {
if (expectValidationError) {
return ErrorCodes.InvalidOptions;
}
if (skipHashedShardKeyIndexCreation) {
return null;
}
return isEmptyCollection ? null : ErrorCodes.InvalidOptions;
})(),
});
for (let indexTestCase of shardKeyTestCase.indexKeyTestCases) {
testExistentCollection(st, {
shardKey: shardKeyTestCase.shardKey,
indexKey: indexTestCase.key,
containsCompatibleIndex: indexTestCase.isCompatible,
isEmptyCollection,
skipHashedShardKeyIndexCreation,
expectErrorCode: (() => {
if (expectValidationError) {
return ErrorCodes.InvalidOptions;
}
if (skipHashedShardKeyIndexCreation) {
return null;
}
return isEmptyCollection ? null : ErrorCodes.InvalidOptions;
})(),
});
}
}
}
}
st.stop();
}
for (let hashedShardKeyIndexOptionalUponShardingCollection of [true, false]) {
runTest(hashedShardKeyIndexOptionalUponShardingCollection);
}