348 lines
14 KiB
JavaScript
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.");
|
|
}
|
|
})();
|