Files
mongo/jstests/core/explain_find_and_modify.js

348 lines
14 KiB
JavaScript

/**
* 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.writeOK(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",
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.");
}
})();