/** * Test to verify that hashed indexes can cover projections when appropriate. The queries can be * covered when neither the query predicate nor projection uses a hashed field. * * @tags: [ * assumes_unsharded_collection, * # Assumes some queries will use a collection scan. * assumes_no_implicit_index_creation, * ] */ (function() { "use strict"; load("jstests/aggregation/extras/utils.js"); // For arrayEq(). load("jstests/libs/analyze_plan.js"); // For assertStagesForExplainOfCommand(). const coll = db.compound_hashed_index; coll.drop(); for (let i = 0; i < 100; i++) { assert.commandWorked(coll.insert({a: i, b: (i % 13), c: NumberInt(i % 10)})); } /** * Runs find command with the 'filter' and 'projection' provided in the input, 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 in the plan returned. */ function validateFindCmdOutputAndPlan({filter, projection, expectedOutput, expectedStages}) { const cmdObj = {find: coll.getName(), filter: filter, projection: projection}; if (expectedOutput) { 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(arrayEq(expectedOutput, ouputArray), ouputArray); } return assertStagesForExplainOfCommand( {coll: coll, cmdObj: cmdObj, expectedStages: expectedStages}); } /** * Runs count command with the 'filter' and 'projection' provided in the input, then validates * that the output returned matches 'expectedOutput'. Also runs explain() command on the same count * command and validates that all the 'expectedStages' are present in the plan returned. */ function validateCountCmdOutputAndPlan({filter, expectedOutput, expectedStages}) { const cmdObj = {count: coll.getName(), query: filter}; const res = assert.commandWorked(coll.runCommand(cmdObj)); assert.eq(res.n, expectedOutput); assertStagesForExplainOfCommand({coll: coll, cmdObj: cmdObj, expectedStages: expectedStages}); } /** * Tests when hashed field is a prefix. */ assert.commandWorked(coll.createIndex({b: "hashed", c: -1, a: 1})); // Verify that queries cannot be covered with hashed field is a prefix. validateFindCmdOutputAndPlan( {filter: {c: 1}, projection: {a: 1, _id: 0}, expectedStages: ['COLLSCAN']}); /** * Tests when hashed field is not a prefix. */ assert.commandWorked(coll.createIndex({a: 1, b: "hashed", c: -1})); // Verify that query doesn't get covered when projecting a hashed field. validateFindCmdOutputAndPlan({ filter: {a: 26}, projection: {b: 1, _id: 0}, expectedOutput: [{b: 0}], expectedStages: ['FETCH', 'IXSCAN'] }); // Verify that query doesn't get covered when query is on a hashed field. This is to avoid the // possibility of hash collision. If two different fields produce the same hash value, there is no // way to distinguish them without fetching the document. validateFindCmdOutputAndPlan({ filter: {a: 26, b: 0}, projection: {c: 1, _id: 0}, expectedOutput: [{c: 6}], expectedStages: ['FETCH', 'IXSCAN'] }); // Verify that query gets covered when neither query nor project use hashed field. validateFindCmdOutputAndPlan({ filter: {a: {$gt: 24, $lt: 27}}, projection: {c: 1, _id: 0}, expectedOutput: [{c: 5}, {c: 6}], expectedStages: ['IXSCAN', 'PROJECTION_COVERED'] }); // Verify that an empty query with a coverable projection always uses a COLLSCAN. validateFindCmdOutputAndPlan( {filter: {}, projection: {a: 1, _id: 0}, expectedStages: ['COLLSCAN']}); // Verify that COUNT_SCAN cannot be used when query is on a hashed field. validateCountCmdOutputAndPlan( {filter: {a: 26, b: 0}, expectedStages: ['FETCH', 'IXSCAN'], expectedOutput: 1}); // Verify that a count operation with range query on a non-hashed prefix field can use // COUNT_SCAN. validateCountCmdOutputAndPlan( {filter: {a: {$gt: 25, $lt: 29}}, expectedStages: ["COUNT_SCAN"], expectedOutput: 3}); })();