summaryrefslogtreecommitdiff
path: root/jstests/core/explain_find_and_modify.js
blob: 8b7c65d519ee8fb8ac9ad6c277c34e4c7884bf06 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
// 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.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_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.");
}
})();