// @tags: [ // assumes_superuser_permissions, // requires_fastcount, // requires_non_retryable_commands, // # applyOps uses the oplog that require replication support // requires_replication, // # Uses $v: 2 update oplog entries, only available in 4.7+. // requires_fcv_47, // ] (function() { "use strict"; load("jstests/libs/get_index_helpers.js"); var t = db.apply_ops1; t.drop(); // // Input validation tests // // Empty array of operations. assert.commandWorked(db.adminCommand({applyOps: []}), 'applyOps should not fail on empty array of operations'); // Non-array type for operations. assert.commandFailed(db.adminCommand({applyOps: "not an array"}), 'applyOps should fail on non-array type for operations'); // Missing 'op' field in an operation. assert.commandFailed(db.adminCommand({applyOps: [{ns: t.getFullName()}]}), 'applyOps should fail on operation without "op" field'); // Non-string 'op' field in an operation. assert.commandFailed(db.adminCommand({applyOps: [{op: 12345, ns: t.getFullName()}]}), 'applyOps should fail on operation with non-string "op" field'); // Empty 'op' field value in an operation. assert.commandFailed(db.adminCommand({applyOps: [{op: '', ns: t.getFullName()}]}), 'applyOps should fail on operation with empty "op" field value'); // Missing 'ns' field in an operation. assert.commandFailed(db.adminCommand({applyOps: [{op: 'c'}]}), 'applyOps should fail on operation without "ns" field'); // Non-string 'ns' field in an operation. assert.commandFailed(db.adminCommand({applyOps: [{op: 'c', ns: 12345}]}), 'applyOps should fail on operation with non-string "ns" field'); // Empty 'ns' field value in an operation of type 'n' (noop). assert.commandWorked(db.adminCommand({applyOps: [{op: 'n', ns: ''}]}), 'applyOps should work on no op operation with empty "ns" field value'); // Missing dbname in 'ns' field. assert.commandFailed(db.adminCommand({applyOps: [{op: 'd', ns: t.getName(), o: {_id: 1}}]})); // Missing 'o' field value in an operation of type 'c' (command). assert.commandFailed(db.adminCommand({applyOps: [{op: 'c', ns: t.getFullName()}]}), 'applyOps should fail on command operation without "o" field'); // Non-object 'o' field value in an operation of type 'c' (command). assert.commandFailed(db.adminCommand({applyOps: [{op: 'c', ns: t.getFullName(), o: 'bar'}]}), 'applyOps should fail on command operation with non-object "o" field'); // Empty object 'o' field value in an operation of type 'c' (command). assert.commandFailed(db.adminCommand({applyOps: [{op: 'c', ns: t.getFullName(), o: {}}]}), 'applyOps should fail on command operation with empty object "o" field'); // Unknown key in 'o' field value in an operation of type 'c' (command). assert.commandFailed(db.adminCommand({applyOps: [{op: 'c', ns: t.getFullName(), o: {a: 1}}]}), 'applyOps should fail on command operation on unknown key in "o" field'); // Empty 'ns' field value in operation type other than 'n'. assert.commandFailed(db.adminCommand({applyOps: [{op: 'c', ns: ''}]}), 'applyOps should fail on non-"n" operation type with empty "ns" field value'); // Excessively nested applyOps commands gracefully fail. assert.commandFailed(db.adminCommand({ "applyOps": [{ "ts": {"$timestamp": {"t": 1, "i": 100}}, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": {"$timestamp": {"t": 1, "i": 100}}, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": {"$timestamp": {"t": 1, "i": 100}}, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": {"$timestamp": {"t": 1, "i": 100}}, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": {"$timestamp": {"t": 1, "i": 100}}, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": {"$timestamp": {"t": 1, "i": 100}}, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": {"$timestamp": {"t": 1, "i": 100}}, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": { "$timestamp": {"t": 1, "i": 100} }, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": { "$timestamp": {"t": 1, "i": 100} }, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": { "$timestamp": { "t": 1, "i": 100 } }, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [{ "ts": { "$timestamp": { "t": 1, "i": 100 } }, "h": 0, "v": 2, "op": "c", "ns": "test.$cmd", "o": { "applyOps": [] } }] } }] } }] } }] } }] } }] } }] } }] } }] } }] } }] }), "Excessively nested applyOps should be rejected"); // Valid 'ns' field value in unknown operation type 'x'. assert.commandFailed(db.adminCommand({applyOps: [{op: 'x', ns: t.getFullName()}]}), 'applyOps should fail on unknown operation type "x" with valid "ns" value'); assert.eq(0, t.find().count(), "Non-zero amount of documents in collection to start"); /** * Test function for running CRUD operations on non-existent namespaces using various * combinations of invalid namespaces (collection/database), allowAtomic and alwaysUpsert, * and nesting. * * Leave 'expectedErrorCode' undefined if this command is expected to run successfully. */ function testCrudOperationOnNonExistentNamespace(optype, o, o2, expectedErrorCode) { expectedErrorCode = expectedErrorCode || ErrorCodes.OK; const t2 = db.getSiblingDB('apply_ops1_no_such_db').getCollection('t'); [t, t2].forEach(coll => { const op = {op: optype, ns: coll.getFullName(), o: o, o2: o2}; [false, true].forEach(nested => { const opToRun = nested ? {op: 'c', ns: 'test.$cmd', o: {applyOps: [op]}, o2: {}} : op; [false, true].forEach(allowAtomic => { [false, true].forEach(alwaysUpsert => { const cmd = { applyOps: [opToRun], allowAtomic: allowAtomic, alwaysUpsert: alwaysUpsert }; jsTestLog('Testing applyOps on non-existent namespace: ' + tojson(cmd)); if (expectedErrorCode === ErrorCodes.OK) { assert.commandWorked(db.adminCommand(cmd)); } else { assert.commandFailedWithCode(db.adminCommand(cmd), expectedErrorCode); } }); }); }); }); } // Insert and update operations on non-existent collections/databases should return // NamespaceNotFound. testCrudOperationOnNonExistentNamespace('i', {_id: 0}, {}, ErrorCodes.NamespaceNotFound); testCrudOperationOnNonExistentNamespace('u', {x: 0}, {_id: 0}, ErrorCodes.NamespaceNotFound); // TODO(SERVER-46221): These oplog entries are inserted as given. After SERVER-21700 and with // steady-state oplog constraint enforcement on, they will result in secondary crashes. We // will need to have applyOps apply operations as executed rather than as given for this to // work properly. if (false) { // Delete operations on non-existent collections/databases should return OK for idempotency // reasons. testCrudOperationOnNonExistentNamespace('d', {_id: 0}, {}); } assert.commandWorked(db.createCollection(t.getName())); var a = assert.commandWorked( db.adminCommand({applyOps: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}]})); assert.eq(1, t.find().count(), "Valid insert failed"); assert.eq(true, a.results[0], "Bad result value for valid insert"); // TODO(SERVER-46221): Duplicate inserts result in invalid oplog entries, as above. if (false) { a = assert.commandWorked( db.adminCommand({applyOps: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}]})); assert.eq(1, t.find().count(), "Duplicate insert failed"); assert.eq(true, a.results[0], "Bad result value for duplicate insert"); } var o = {_id: 5, x: 17}; assert.eq(o, t.findOne(), "Mismatching document inserted."); // 'o' field is an empty array. assert.commandFailed(db.adminCommand({applyOps: [{op: 'i', ns: t.getFullName(), o: []}]}), 'applyOps should fail on insert of object with empty array element'); var res = assert.commandWorked(db.runCommand({ applyOps: [ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 18}}}, {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 19}}} ] })); o.x++; o.x++; assert.eq(1, t.find().count(), "Updates increased number of documents"); assert.eq(o, t.findOne(), "Document doesn't match expected"); assert.eq(true, res.results[0], "Bad result value for valid update"); assert.eq(true, res.results[1], "Bad result value for valid update"); // preCondition fully matches res = db.runCommand({ applyOps: [ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 20}}}, {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 21}}} ], preCondition: [{ns: t.getFullName(), q: {_id: 5}, res: {x: 19}}] }); // The use of preCondition requires applyOps to run atomically. Therefore, it is incompatible // with {allowAtomic: false}. assert.commandFailedWithCode( db.runCommand({ applyOps: [{op: 'u', ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 22}}}], preCondition: [{ns: t.getFullName(), q: {_id: 5}, res: {x: 21}}], allowAtomic: false, }), ErrorCodes.InvalidOptions, 'applyOps should fail when preCondition is present and atomicAllowed is false.'); // The use of preCondition is also incompatible with operations that include commands. assert.commandFailedWithCode( db.runCommand({ applyOps: [{op: 'c', ns: t.getCollection('$cmd').getFullName(), o: {applyOps: []}}], preCondition: [{ns: t.getFullName(), q: {_id: 5}, res: {x: 21}}], }), ErrorCodes.InvalidOptions, 'applyOps should fail when preCondition is present and operations includes commands.'); o.x++; o.x++; assert.eq(1, t.find().count(), "Updates increased number of documents"); assert.eq(o, t.findOne(), "Document doesn't match expected"); assert.eq(true, res.results[0], "Bad result value for valid update"); assert.eq(true, res.results[1], "Bad result value for valid update"); // preCondition doesn't match ns res = db.runCommand({ applyOps: [ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 22}}}, {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 23}}} ], preCondition: [{ns: "foo.otherName", q: {_id: 5}, res: {x: 21}}] }); assert.eq(o, t.findOne(), "preCondition didn't match, but ops were still applied"); // preCondition doesn't match query res = db.runCommand({ applyOps: [ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 22}}}, {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 23}}} ], preCondition: [{ns: t.getFullName(), q: {_id: 5}, res: {x: 19}}] }); assert.eq(o, t.findOne(), "preCondition didn't match, but ops were still applied"); res = db.runCommand({ applyOps: [ {op: "u", ns: t.getFullName(), o2: {_id: 5}, o: {$set: {x: 22}}}, {op: "u", ns: t.getFullName(), o2: {_id: 6}, o: {$set: {x: 23}}} ] }); assert.eq(true, res.results[0], "Valid update failed"); assert.eq(true, res.results[1], "Valid update failed"); // Ops with transaction numbers are valid. const lsid = { "id": UUID("3eea4a58-6018-40b6-8743-6a55783bf902"), "uid": BinData(0, "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") }; res = db.runCommand({ applyOps: [ { op: "i", ns: t.getFullName(), o: {_id: 7, x: 24}, lsid: lsid, txnNumber: NumberLong(1), stmtId: NumberInt(0) }, { op: "u", ns: t.getFullName(), o2: {_id: 8}, o: {$set: {x: 25}}, lsid: lsid, txnNumber: NumberLong(1), stmtId: NumberInt(1) }, { op: "d", ns: t.getFullName(), o: {_id: 7}, lsid: lsid, txnNumber: NumberLong(2), stmtId: NumberInt(0) }, ] }); assert.eq(true, res.results[0], "Valid insert with transaction number failed"); assert.eq(true, res.results[1], "Valid update with transaction number failed"); assert.eq(true, res.results[2], "Valid delete with transaction number failed"); // When applying a "u" (update) op, we default to 'UpdateNode' update semantics, and $set // operations add new fields in lexicographic order. res = assert.commandWorked(db.adminCommand({ applyOps: [ {"op": "i", "ns": t.getFullName(), "o": {_id: 9}}, {"op": "u", "ns": t.getFullName(), "o2": {_id: 9}, "o": {$set: {z: 1, a: 2}}} ] })); assert.eq(t.findOne({_id: 9}), {_id: 9, a: 2, z: 1}); // Note: 'a' and 'z' have been sorted. // 'ModifierInterface' semantics are not supported, so an update with {$v: 0} should fail. res = assert.commandFailed(db.adminCommand({ applyOps: [ {"op": "i", "ns": t.getFullName(), "o": {_id: 7}}, { "op": "u", "ns": t.getFullName(), "o2": {_id: 7}, "o": {$v: NumberInt(0), $set: {z: 1, a: 2}} } ] })); assert.eq(res.code, 4772604); // When we explicitly specify {$v: 1}, we should get 'UpdateNode' update semantics, and $set // operations get performed in lexicographic order. res = assert.commandWorked(db.adminCommand({ applyOps: [ {"op": "i", "ns": t.getFullName(), "o": {_id: 10}}, { "op": "u", "ns": t.getFullName(), "o2": {_id: 10}, "o": {$v: NumberInt(1), $set: {z: 1, a: 2}} } ] })); assert.eq(t.findOne({_id: 10}), {_id: 10, a: 2, z: 1}); // Note: 'a' and 'z' have been sorted. // {$v: 2} entries encode diffs differently, and operations are applied in the order specified // rather than in lexicographic order. res = assert.commandWorked(db.adminCommand({ applyOps: [ {"op": "i", "ns": t.getFullName(), "o": {_id: 11, deleteField: 1}}, { "op": "u", "ns": t.getFullName(), "o2": {_id: 11}, // The diff indicates that 'deleteField' will be removed and 'newField' will be added // with value "foo". "o": {$v: NumberInt(2), diff: {d: {deleteField: false}, i: {newField: "foo"}}} } ] })); assert.eq(t.findOne({_id: 11}), {_id: 11, newField: "foo"}); // {$v: 3} does not exist yet, and we check that trying to use it throws an error. res = assert.commandFailed(db.adminCommand({ applyOps: [ {"op": "i", "ns": t.getFullName(), "o": {_id: 12}}, { "op": "u", "ns": t.getFullName(), "o2": {_id: 12}, "o": {$v: NumberInt(3), diff: {d: {deleteField: false}}} } ] })); assert.eq(res.code, 4772604); })();