// Cannot implicitly shard accessed collections because of collection existing when none // expected. // @tags: [assumes_no_implicit_collection_creation_after_drop] /** * Test correctness of explaining findAndModify. Asserts the following: * * 1. Explaining findAndModify should never create a database. * 2. Explaining findAndModify should never create a collection. * 3. Explaining findAndModify should not work with an invalid findAndModify command object. * 4. Explaining findAndModify should not modify any contents of the collection. * 5. The reported stats should reflect how the command would be executed. */ (function() { "use strict"; var cName = "explain_find_and_modify"; var t = db.getCollection(cName); // Different types of findAndModify explain requests. var explainRemove = {explain: {findAndModify: cName, remove: true, query: {_id: 0}}}; var explainUpdate = {explain: {findAndModify: cName, update: {$inc: {i: 1}}, query: {_id: 0}}}; var explainUpsert = { explain: {findAndModify: cName, update: {$inc: {i: 1}}, query: {_id: 0}, upsert: true} }; // 1. Explaining findAndModify should never create a database. // Make sure this one doesn't exist before we start. assert.commandWorked(db.getSiblingDB(cName).runCommand({dropDatabase: 1})); var newDB = db.getSiblingDB(cName); // Explain the command, ensuring the database is not created. var err_msg = "Explaining findAndModify on a non-existent database should return an error."; assert.commandFailed(newDB.runCommand(explainRemove), err_msg); assertDBDoesNotExist(newDB, "Explaining a remove should not create a database."); assert.commandFailed(newDB.runCommand(explainUpsert), err_msg); assertDBDoesNotExist(newDB, "Explaining an upsert should not create a database."); // 2. Explaining findAndModify should never create a collection. // Insert a document to make sure the database exists. t.insert({'will': 'be dropped'}); // Make sure the collection doesn't exist. t.drop(); // Explain the command, ensuring the collection is not created. assert.commandWorked(db.runCommand(explainRemove)); assertCollDoesNotExist(cName, "explaining a remove should not create a new collection."); assert.commandWorked(db.runCommand(explainUpsert)); assertCollDoesNotExist(cName, "explaining an upsert should not create a new collection."); assert.commandWorked(db.runCommand(Object.merge(explainUpsert, {fields: {x: 1}}))); assertCollDoesNotExist(cName, "explaining an upsert should not create a new collection."); // 3. Explaining findAndModify should not work with an invalid findAndModify command object. // Specifying both remove and new is illegal. assert.commandFailed(db.runCommand({remove: true, new: true})); // 4. Explaining findAndModify should not modify any contents of the collection. var onlyDoc = {_id: 0, i: 1}; assert.commandWorked(t.insert(onlyDoc)); // Explaining a delete should not delete anything. var matchingRemoveCmd = {findAndModify: cName, remove: true, query: {_id: onlyDoc._id}}; var res = db.runCommand({explain: matchingRemoveCmd}); assert.commandWorked(res); assert.eq(t.find().itcount(), 1, "Explaining a remove should not remove any documents."); // Explaining an update should not update anything. var matchingUpdateCmd = {findAndModify: cName, update: {x: "x"}, query: {_id: onlyDoc._id}}; var res = db.runCommand({explain: matchingUpdateCmd}); assert.commandWorked(res); assert.eq(t.findOne(), onlyDoc, "Explaining an update should not update any documents."); // Explaining an upsert should not insert anything. var matchingUpsertCmd = {findAndModify: cName, update: {x: "x"}, query: {_id: "non-match"}, upsert: true}; var res = db.runCommand({explain: matchingUpsertCmd}); assert.commandWorked(res); assert.eq(t.find().itcount(), 1, "Explaining an upsert should not insert any documents."); // 5. The reported stats should reflect how it would execute and what it would modify. var isMongos = db.runCommand({isdbgrid: 1}).isdbgrid; // List out the command to be explained, and the expected results of that explain. var testCases = [ // -------------------------------------- Removes ---------------------------------------- { // Non-matching remove command. cmd: {remove: true, query: {_id: "no-match"}}, expectedResult: { executionStats: { nReturned: 0, executionSuccess: true, executionStages: {stage: "DELETE", nWouldDelete: 0} } } }, { // Matching remove command. cmd: {remove: true, query: {_id: onlyDoc._id}}, expectedResult: { executionStats: { nReturned: 1, executionSuccess: true, executionStages: {stage: "DELETE", nWouldDelete: 1} } } }, // -------------------------------------- Updates ---------------------------------------- { // Non-matching update query. cmd: {update: {$inc: {i: 1}}, query: {_id: "no-match"}}, expectedResult: { executionStats: { nReturned: 0, executionSuccess: true, executionStages: {stage: "UPDATE", nWouldModify: 0, wouldInsert: false} } } }, { // Non-matching update query, returning new doc. cmd: {update: {$inc: {i: 1}}, query: {_id: "no-match"}, new: true}, expectedResult: { executionStats: { nReturned: 0, executionSuccess: true, executionStages: {stage: "UPDATE", nWouldModify: 0, wouldInsert: false} } } }, { // Matching update query. cmd: {update: {$inc: {i: 1}}, query: {_id: onlyDoc._id}}, expectedResult: { executionStats: { nReturned: 1, executionSuccess: true, executionStages: {stage: "UPDATE", nWouldModify: 1, wouldInsert: false} } } }, { // Matching update query, returning new doc. cmd: {update: {$inc: {i: 1}}, query: {_id: onlyDoc._id}, new: true}, expectedResult: { executionStats: { nReturned: 1, executionSuccess: true, executionStages: {stage: "UPDATE", nWouldModify: 1, wouldInsert: false} } } }, // -------------------------------------- Upserts ---------------------------------------- { // Non-matching upsert query. cmd: {update: {$inc: {i: 1}}, upsert: true, query: {_id: "no-match"}}, expectedResult: { executionStats: { nReturned: 0, executionSuccess: true, executionStages: {stage: "UPDATE", nWouldModify: 0, wouldInsert: true} } } }, { // Non-matching upsert query, returning new doc. cmd: {update: {$inc: {i: 1}}, upsert: true, query: {_id: "no-match"}, new: true}, expectedResult: { executionStats: { nReturned: 1, executionSuccess: true, executionStages: {stage: "UPDATE", nWouldModify: 0, wouldInsert: true} } } }, { // Matching upsert query, returning new doc. cmd: {update: {$inc: {i: 1}}, upsert: true, query: {_id: onlyDoc._id}, new: true}, expectedResult: { executionStats: { nReturned: 1, executionSuccess: true, executionStages: {stage: "UPDATE", nWouldModify: 1, wouldInsert: false} } } } ]; // Apply all the same test cases, this time adding a projection stage. testCases = testCases.concat(testCases.map(function makeProjection(testCase) { return { cmd: Object.merge(testCase.cmd, {fields: {i: 0}}), expectedResult: { executionStats: { // nReturned Shouldn't change. nReturned: testCase.expectedResult.executionStats.nReturned, executionStages: { stage: "PROJECTION_DEFAULT", transformBy: {i: 0}, // put previous root stage under projection stage. inputStage: testCase.expectedResult.executionStats.executionStages } } } }; })); // Actually assert on the test cases. testCases.forEach(function(testCase) { assertExplainMatchedAllVerbosities(testCase.cmd, testCase.expectedResult); }); // ----------------------------------------- Helpers ----------------------------------------- /** * Helper to make this test work in the sharding passthrough suite. * * Transforms the explain output so that if it came from a mongos, it will be modified * to have the same format as though it had come from a mongod. */ function transformIfSharded(explainOut) { if (!isMongos) { return explainOut; } // Asserts that the explain command ran on a single shard and modifies the given // explain output to have a top-level UPDATE or DELETE stage by removing the // top-level SINGLE_SHARD stage. function replace(outerKey, innerKey) { assert(explainOut.hasOwnProperty(outerKey)); assert(explainOut[outerKey].hasOwnProperty(innerKey)); var shardStage = explainOut[outerKey][innerKey]; assert.eq("SINGLE_SHARD", shardStage.stage); assert.eq(1, shardStage.shards.length); Object.extend(explainOut[outerKey], shardStage.shards[0], false); } replace("queryPlanner", "winningPlan"); replace("executionStats", "executionStages"); return explainOut; } /** * Assert the results from running the explain match the expected results. * * Since we aren't expecting a perfect match (we only specify a subset of the fields we expect * to match), recursively go through the expected results, and make sure each one has a * corresponding field on the actual results, and that their values match. * Example doc for expectedMatches: * {executionStats: {nReturned: 0, executionStages: {isEOF: 1}}} */ function assertExplainResultsMatch(explainOut, expectedMatches, preMsg, currentPath) { // This is only used recursively, to keep track of where we are in the document. var isRootLevel = typeof currentPath === "undefined"; Object.keys(expectedMatches).forEach(function(key) { var totalFieldName = isRootLevel ? key : currentPath + "." + key; assert(explainOut.hasOwnProperty(key), preMsg + "Explain's output does not have a value for " + key); if (typeof expectedMatches[key] === "object") { // Sub-doc, recurse to match on it's fields assertExplainResultsMatch( explainOut[key], expectedMatches[key], preMsg, totalFieldName); } else { assert.eq(explainOut[key], expectedMatches[key], preMsg + "Explain's " + totalFieldName + " (" + explainOut[key] + ")" + " does not match expected value (" + expectedMatches[key] + ")."); } }); } /** * Assert that running explain on the given findAndModify command matches the expected results, * on all the different verbosities (but just assert the command worked on the lowest verbosity, * since it doesn't have any useful stats). */ function assertExplainMatchedAllVerbosities(findAndModifyArgs, expectedResult) { ["queryPlanner", "executionStats", "allPlansExecution"].forEach(function(verbosityMode) { var cmd = { explain: Object.merge({findAndModify: cName}, findAndModifyArgs), verbosity: verbosityMode }; var msg = "Error after running command: " + tojson(cmd) + ": "; var explainOut = db.runCommand(cmd); assert.commandWorked(explainOut, "command: " + tojson(cmd)); // Don't check explain results for queryPlanner mode, as that doesn't have any of the // interesting stats. if (verbosityMode !== "queryPlanner") { explainOut = transformIfSharded(explainOut); assertExplainResultsMatch(explainOut, expectedResult, msg); } }); } function assertDBDoesNotExist(db, msg) { assert.eq(db.getMongo().getDBNames().indexOf(db.getName()), -1, msg + "db " + db.getName() + " exists."); } function assertCollDoesNotExist(cName, msg) { assert.eq(db.getCollectionNames().indexOf(cName), -1, msg + "collection " + cName + " exists."); } })();