mirror of https://github.com/mongodb/mongo
227 lines
8.1 KiB
JavaScript
227 lines
8.1 KiB
JavaScript
/**
|
|
* Test to verify the covering behaviour of compound hashed index on a cluster sharded with compound
|
|
* hashed shard key.
|
|
*/
|
|
import {assertStagesForExplainOfCommand} from "jstests/libs/query/analyze_plan.js";
|
|
import {ShardingTest} from "jstests/libs/shardingtest.js";
|
|
|
|
const st = new ShardingTest({shards: 2});
|
|
const kDbName = jsTestName();
|
|
assert.commandWorked(st.s.adminCommand({enableSharding: kDbName, primaryShard: st.shard0.shardName}));
|
|
const coll = st.s.getDB(kDbName)["coll"];
|
|
|
|
/**
|
|
* Runs find command with the 'filter', 'projection' and 'hint' parameters. Then validates that the
|
|
* output returned matches 'expectedOutput'. Also runs explain() command on the same find command
|
|
* and validates that all the 'expectedStages' are present and all the 'stagesNotExpected' are not
|
|
* present in the plan returned.
|
|
*/
|
|
function validateFindCmdOutputAndPlan({
|
|
filter,
|
|
projection = {
|
|
_id: 0,
|
|
},
|
|
hint,
|
|
expectedStages,
|
|
stagesNotExpected,
|
|
expectedOutput,
|
|
}) {
|
|
const cmdObj = Object.assign(
|
|
{find: coll.getName(), filter: filter, projection: projection},
|
|
hint ? {hint: hint} : null,
|
|
);
|
|
const res = assert.commandWorked(coll.runCommand(cmdObj));
|
|
const ouputArray = new DBCommandCursor(coll.getDB(), res).toArray();
|
|
|
|
// We ignore the order since hashed index order is not predictable.
|
|
assert.sameMembers(expectedOutput, ouputArray);
|
|
assertStagesForExplainOfCommand({coll, cmdObj, expectedStages, stagesNotExpected});
|
|
}
|
|
|
|
//
|
|
// Test to validate that the orphan documents are correctly rejected during shard filter analysis.
|
|
//
|
|
assert.commandWorked(st.s.adminCommand({shardCollection: coll.getFullName(), key: {a: 1, b: "hashed", c: 1}}));
|
|
assert.commandWorked(st.s.adminCommand({split: coll.getFullName(), middle: {a: 0, b: NumberLong(0), c: MinKey}}));
|
|
|
|
// Postive hashed values of 'b' should go to 'shard1DB' and negative value should go to 'shard0DB'
|
|
assert.commandWorked(
|
|
st.s.adminCommand({
|
|
moveChunk: coll.getFullName(),
|
|
bounds: [
|
|
{a: 0, b: NumberLong(0), c: MinKey},
|
|
{a: MaxKey, b: MaxKey, c: MaxKey},
|
|
],
|
|
to: st.shard1.shardName,
|
|
}),
|
|
);
|
|
|
|
// Make sure that we have at least one valid document and one orphan document. The hashed value of 0
|
|
// is postive and hence this document should belong to shard1.
|
|
let validDocs = [{a: 0, b: 0, c: "valid"}];
|
|
assert.gt(convertShardKeyToHashed(validDocs[0]["b"]), 0);
|
|
|
|
// The hashed value of 3 is negative and hence should be an orphan on shard1.
|
|
let orphanDocs = [{a: 0, b: 3, c: "orphan"}];
|
|
assert.lt(convertShardKeyToHashed(orphanDocs[0]["b"]), 0);
|
|
|
|
// Generate 100 more documents and distribute them between 'validDocs' and 'orphanDocs' based on the
|
|
// hashed value of 'b'.
|
|
for (let i = 0; i < 100; i++) {
|
|
// Generate a random number between 0 and 1000000.
|
|
const insertObj = {a: 0, b: Math.floor(Math.random() * 1000000), c: "value"};
|
|
if (convertShardKeyToHashed(insertObj["b"]) < 0) {
|
|
orphanDocs.push(insertObj);
|
|
} else {
|
|
validDocs.push(insertObj);
|
|
}
|
|
}
|
|
|
|
// Insert documents by directly connecting to the shard1 so that we can explicity create orphan
|
|
// documents. We then run a 'find' command by connecting to mongos and validate that the orphan
|
|
// documents are correctly rejected.
|
|
const shard1DB = st.rs1.getPrimary().getDB(kDbName);
|
|
assert.commandWorked(shard1DB.coll.insertMany(validDocs, {ordered: false}));
|
|
assert.commandWorked(shard1DB.coll.insertMany(orphanDocs, {ordered: false}));
|
|
// We do not project 'b' so that the query can be covered.
|
|
for (let validDoc of validDocs) {
|
|
delete validDoc.b;
|
|
}
|
|
validateFindCmdOutputAndPlan({
|
|
filter: {a: 0},
|
|
projection: {a: 1, c: 1, _id: 0},
|
|
expectedOutput: validDocs, // Ophan documents are not returned.
|
|
expectedStages: ["IXSCAN", "SHARD_MERGE", "SHARDING_FILTER"],
|
|
stagesNotExpected: ["FETCH"],
|
|
});
|
|
coll.drop();
|
|
|
|
//
|
|
// Tests to validate covering behaviour in the presense of various indexes.
|
|
//
|
|
assert.commandWorked(
|
|
st.s.getDB("config").adminCommand({shardCollection: coll.getFullName(), key: {a: 1, b: "hashed", c: 1}}),
|
|
);
|
|
assert.commandWorked(st.s.adminCommand({split: coll.getFullName(), middle: {a: 0, b: MinKey, c: MinKey}}));
|
|
|
|
// Postive numbers of 'a' should go to 'shard1DB' and negative numbers should go to 'shard0DB'
|
|
assert.commandWorked(
|
|
st.s.adminCommand({
|
|
moveChunk: coll.getFullName(),
|
|
bounds: [
|
|
{a: 0, b: MinKey, c: MinKey},
|
|
{a: MaxKey, b: MaxKey, c: MaxKey},
|
|
],
|
|
to: st.shard1.shardName,
|
|
}),
|
|
);
|
|
|
|
for (let i = 20; i < 40; i++) {
|
|
assert.commandWorked(coll.insert({a: i, b: {subObj: "str_" + (i % 13)}, c: NumberInt(i % 10)}));
|
|
assert.commandWorked(coll.insert({a: -i, b: {subObj: "str_" + (i % 13)}, c: NumberInt(i % 10)}));
|
|
}
|
|
|
|
// Verify that the query can be covered if neither the query nor the project uses hashed field.
|
|
validateFindCmdOutputAndPlan({
|
|
filter: {a: {$gt: 25, $lt: 29}},
|
|
projection: {a: 1, c: 1, _id: 0},
|
|
expectedOutput: [
|
|
{a: 26, c: 6},
|
|
{a: 27, c: 7},
|
|
{a: 28, c: 8},
|
|
],
|
|
expectedStages: ["IXSCAN", "SINGLE_SHARD", "SHARDING_FILTER"],
|
|
stagesNotExpected: ["FETCH"],
|
|
});
|
|
validateFindCmdOutputAndPlan({
|
|
filter: {a: {$gte: -20, $lt: 21}},
|
|
projection: {a: 1, c: 1, _id: 0},
|
|
expectedOutput: [
|
|
{a: -20, c: 0},
|
|
{a: 20, c: 0},
|
|
],
|
|
expectedStages: ["IXSCAN", "SHARD_MERGE", "SHARDING_FILTER"],
|
|
stagesNotExpected: ["FETCH"],
|
|
});
|
|
|
|
// Verify that a query on hashed field is always fetched, even if the projection does not include
|
|
// the hashed field.
|
|
validateFindCmdOutputAndPlan({
|
|
filter: {a: {$gt: 25, $lt: 29}, b: {subObj: "str_0"}},
|
|
projection: {a: 1, c: 1, _id: 0},
|
|
expectedOutput: [{a: 26, c: 6}],
|
|
expectedStages: ["IXSCAN", "FETCH", "SINGLE_SHARD", "SHARDING_FILTER"],
|
|
});
|
|
|
|
// Verify that the query cannot be covered if the project includes hashed field.
|
|
validateFindCmdOutputAndPlan({
|
|
filter: {a: {$gt: 25, $lt: 29}},
|
|
projection: {a: 1, b: 1, c: 1, _id: 0},
|
|
expectedOutput: [
|
|
{a: 26, b: {subObj: "str_0"}, c: 6},
|
|
{a: 27, b: {subObj: "str_1"}, c: 7},
|
|
{a: 28, b: {subObj: "str_2"}, c: 8},
|
|
],
|
|
expectedStages: ["IXSCAN", "SINGLE_SHARD", "FETCH", "SHARDING_FILTER"],
|
|
});
|
|
|
|
// Create an index which doesn't include one of the shard key fields and verify that the query is
|
|
// fetched.
|
|
coll.createIndex({a: 1, c: 1});
|
|
validateFindCmdOutputAndPlan({
|
|
filter: {a: {$gt: 25, $lt: 29}},
|
|
projection: {a: 1, c: 1, _id: 0},
|
|
hint: {a: 1, c: 1},
|
|
expectedOutput: [
|
|
{a: 26, c: 6},
|
|
{a: 27, c: 7},
|
|
{a: 28, c: 8},
|
|
],
|
|
expectedStages: ["IXSCAN", "SINGLE_SHARD", "FETCH", "SHARDING_FILTER"],
|
|
});
|
|
|
|
// Verify that the query cannot be covered if index provides hashed value for a field ('c'), but the
|
|
// corresponding shard key field is a range field.
|
|
coll.createIndex({a: 1, b: 1, c: "hashed"});
|
|
validateFindCmdOutputAndPlan({
|
|
filter: {a: {$gt: 25, $lt: 29}},
|
|
projection: {a: 1, _id: 0},
|
|
hint: {a: 1, b: 1, c: "hashed"},
|
|
expectedOutput: [{a: 26}, {a: 27}, {a: 28}],
|
|
expectedStages: ["IXSCAN", "SINGLE_SHARD", "FETCH", "SHARDING_FILTER"],
|
|
});
|
|
|
|
// Verify that the query can be covered when index provides range value for a field, but the
|
|
// corresponding shard key field is hashed.
|
|
coll.createIndex({a: 1, b: 1, c: 1});
|
|
validateFindCmdOutputAndPlan({
|
|
filter: {a: {$gt: 25, $lt: 29}},
|
|
projection: {a: 1, c: 1, _id: 0},
|
|
hint: {a: 1, b: 1, c: 1},
|
|
expectedOutput: [
|
|
{a: 26, c: 6},
|
|
{a: 27, c: 7},
|
|
{a: 28, c: 8},
|
|
],
|
|
expectedStages: ["IXSCAN", "SINGLE_SHARD", "SHARDING_FILTER"],
|
|
stagesNotExpected: ["FETCH"],
|
|
});
|
|
|
|
// Verify that the query can be covered if all the shard key fields are provided by the index, even
|
|
// if the order is different.
|
|
coll.createIndex({a: 1, c: 1, b: "hashed"});
|
|
validateFindCmdOutputAndPlan({
|
|
filter: {a: {$gt: 25, $lt: 29}},
|
|
projection: {a: 1, c: 1, _id: 0},
|
|
hint: {a: 1, c: 1, b: "hashed"},
|
|
expectedOutput: [
|
|
{a: 26, c: 6},
|
|
{a: 27, c: 7},
|
|
{a: 28, c: 8},
|
|
],
|
|
expectedStages: ["IXSCAN", "SINGLE_SHARD", "SHARDING_FILTER"],
|
|
stagesNotExpected: ["FETCH"],
|
|
});
|
|
|
|
st.stop();
|