// Tests for $expr in the CRUD commands. (function() { "use strict"; const coll = db.expr; const isMaster = db.runCommand("ismaster"); assert.commandWorked(isMaster); const isMongos = (isMaster.msg === "isdbgrid"); // // $expr in aggregate. // coll.drop(); assert.writeOK(coll.insert({a: 0})); assert.eq(1, coll.aggregate([{$match: {$expr: {$eq: ["$a", 0]}}}]).itcount()); assert.throws(function() { coll.aggregate([{$match: {$expr: {$eq: ["$a", "$$unbound"]}}}]); }); assert.throws(function() { coll.aggregate([{$match: {$expr: {$divide: [1, "$a"]}}}]); }); // // $expr in count. // coll.drop(); assert.writeOK(coll.insert({a: 0})); assert.eq(1, coll.find({$expr: {$eq: ["$a", 0]}}).count()); assert.throws(function() { coll.find({$expr: {$eq: ["$a", "$$unbound"]}}).count(); }); assert.throws(function() { coll.find({$expr: {$divide: [1, "$a"]}}).count(); }); // // $expr in distinct. // coll.drop(); assert.writeOK(coll.insert({a: 0})); assert.eq(1, coll.distinct("a", {$expr: {$eq: ["$a", 0]}}).length); assert.throws(function() { coll.distinct("a", {$expr: {$eq: ["$a", "$$unbound"]}}); }); assert.throws(function() { coll.distinct("a", {$expr: {$divide: [1, "$a"]}}); }); // // $expr in find. // // $expr is allowed in query. coll.drop(); assert.writeOK(coll.insert({a: 0})); assert.eq(1, coll.find({$expr: {$eq: ["$a", 0]}}).itcount()); // $expr with unbound variable throws. assert.throws(function() { coll.find({$expr: {$eq: ["$a", "$$unbound"]}}).itcount(); }); // $expr with division by zero throws. assert.throws(function() { coll.find({$expr: {$divide: [1, "$a"]}}).itcount(); }); // $expr is allowed in find with explain. assert.commandWorked(coll.find({$expr: {$eq: ["$a", 0]}}).explain()); // $expr with unbound variable in find with explain throws. assert.throws(function() { coll.find({$expr: {$eq: ["$a", "$$unbound"]}}).explain(); }); // $expr with division by zero in find with explain with executionStats throws. assert.throws(function() { coll.find({$expr: {$divide: [1, "$a"]}}).explain("executionStats"); }); // $expr is not allowed in $elemMatch projection. coll.drop(); assert.writeOK(coll.insert({a: [{b: 5}]})); assert.throws(function() { coll.find({}, {a: {$elemMatch: {$expr: {$eq: ["$b", 5]}}}}).itcount(); }); // // $expr in findAndModify. // // $expr is allowed in the query when upsert=false. coll.drop(); assert.writeOK(coll.insert({_id: 0, a: 0})); assert.eq({_id: 0, a: 0, b: 6}, coll.findAndModify( {query: {_id: 0, $expr: {$eq: ["$a", 0]}}, update: {$set: {b: 6}}, new: true})); // $expr with unbound variable throws. assert.throws(function() { coll.findAndModify( {query: {_id: 0, $expr: {$eq: ["$a", "$$unbound"]}}, update: {$set: {b: 6}}}); }); // $expr with division by zero throws. assert.throws(function() { coll.findAndModify({query: {_id: 0, $expr: {$divide: [1, "$a"]}}, update: {$set: {b: 6}}}); }); // $expr is not allowed in the query when upsert=true. coll.drop(); assert.writeOK(coll.insert({_id: 0, a: 0})); assert.throws(function() { coll.findAndModify( {query: {_id: 0, $expr: {$eq: ["$a", 0]}}, update: {$set: {b: 6}}, upsert: true}); }); // $expr is not allowed in $pull filter. coll.drop(); assert.writeOK(coll.insert({_id: 0, a: [{b: 5}]})); assert.throws(function() { coll.findAndModify({query: {_id: 0}, update: {$pull: {a: {$expr: {$eq: ["$b", 5]}}}}}); }); // $expr is not allowed in arrayFilters. if (db.getMongo().writeMode() === "commands") { coll.drop(); assert.writeOK(coll.insert({_id: 0, a: [{b: 5}]})); assert.throws(function() { coll.findAndModify({ query: {_id: 0}, update: {$set: {"a.$[i].b": 6}}, arrayFilters: [{"i.b": 5, $expr: {$eq: ["$i.b", 5]}}] }); }); } // // $expr in geoNear. // coll.drop(); assert.writeOK(coll.insert({geo: {type: "Point", coordinates: [0, 0]}, a: 0})); assert.commandWorked(coll.ensureIndex({geo: "2dsphere"})); assert.eq(1, assert .commandWorked(db.runCommand({ geoNear: coll.getName(), near: {type: "Point", coordinates: [0, 0]}, spherical: true, query: {$expr: {$eq: ["$a", 0]}} })) .results.length); assert.commandFailed(db.runCommand({ geoNear: coll.getName(), near: {type: "Point", coordinates: [0, 0]}, spherical: true, query: {$expr: {$eq: ["$a", "$$unbound"]}} })); assert.commandFailed(db.runCommand({ geoNear: coll.getName(), near: {type: "Point", coordinates: [0, 0]}, spherical: true, query: {$expr: {$divide: [1, "$a"]}} })); // // $expr in group. // // The group command is not permitted in sharded collections. if (!isMongos) { coll.drop(); assert.writeOK(coll.insert({a: 0})); assert.eq([{a: 0, count: 1}], coll.group({ cond: {$expr: {$eq: ["$a", 0]}}, key: {a: 1}, initial: {count: 0}, reduce: function(curr, result) { result.count += 1; } })); assert.throws(function() { coll.group({ cond: {$expr: {$eq: ["$a", "$$unbound"]}}, key: {a: 1}, initial: {count: 0}, reduce: function(curr, result) { result.count += 1; } }); }); assert.throws(function() { coll.group({ cond: {$expr: {$divide: [1, "$a"]}}, key: {a: 1}, initial: {count: 0}, reduce: function(curr, result) { result.count += 1; } }); }); } // // $expr in mapReduce. // coll.drop(); assert.writeOK(coll.insert({a: 0})); let mapReduceOut = coll.mapReduce( function() { emit(this.a, 1); }, function(key, values) { return Array.sum(values); }, {out: {inline: 1}, query: {$expr: {$eq: ["$a", 0]}}}); assert.commandWorked(mapReduceOut); assert.eq(mapReduceOut.results.length, 1, tojson(mapReduceOut)); assert.throws(function() { coll.mapReduce( function() { emit(this.a, 1); }, function(key, values) { return Array.sum(values); }, {out: {inline: 1}, query: {$expr: {$eq: ["$a", "$$unbound"]}}}); }); assert.throws(function() { coll.mapReduce( function() { emit(this.a, 1); }, function(key, values) { return Array.sum(values); }, {out: {inline: 1}, query: {$expr: {$divide: [1, "$a"]}}}); }); // // $expr in remove. // coll.drop(); assert.writeOK(coll.insert({_id: 0, a: 0})); let writeRes = coll.remove({_id: 0, $expr: {$eq: ["$a", 0]}}); assert.writeOK(writeRes); assert.eq(1, writeRes.nRemoved); assert.writeError(coll.remove({_id: 0, $expr: {$eq: ["$a", "$$unbound"]}})); assert.writeOK(coll.insert({_id: 0, a: 0})); assert.writeError(coll.remove({_id: 0, $expr: {$divide: [1, "$a"]}})); // Any writes preceding the write that fails to parse are executed. coll.drop(); assert.writeOK(coll.insert({_id: 0})); assert.writeOK(coll.insert({_id: 1})); writeRes = db.runCommand({ delete: coll.getName(), deletes: [{q: {_id: 0}, limit: 1}, {q: {$expr: "$$unbound"}, limit: 1}] }); assert.commandWorked(writeRes); assert.eq(writeRes.writeErrors[0].code, 17276, tojson(writeRes)); assert.eq(writeRes.n, 1, tojson(writeRes)); // // $expr in update. // // $expr is allowed in the query when upsert=false. coll.drop(); assert.writeOK(coll.insert({_id: 0, a: 0})); assert.writeOK(coll.update({_id: 0, $expr: {$eq: ["$a", 0]}}, {$set: {b: 6}})); assert.eq({_id: 0, a: 0, b: 6}, coll.findOne({_id: 0})); // $expr with unbound variable fails. assert.writeError(coll.update({_id: 0, $expr: {$eq: ["$a", "$$unbound"]}}, {$set: {b: 6}})); // $expr with division by zero fails. assert.writeError(coll.update({_id: 0, $expr: {$divide: [1, "$a"]}}, {$set: {b: 6}})); // $expr is not allowed in the query when upsert=true. coll.drop(); assert.writeOK(coll.insert({_id: 0, a: 5})); assert.writeError( coll.update({_id: 0, $expr: {$eq: ["$a", 5]}}, {$set: {b: 6}}, {upsert: true})); // $expr is not allowed in $pull filter. coll.drop(); assert.writeOK(coll.insert({_id: 0, a: [{b: 5}]})); assert.writeError(coll.update({_id: 0}, {$pull: {a: {$expr: {$eq: ["$b", 5]}}}})); // $expr is not allowed in arrayFilters. if (db.getMongo().writeMode() === "commands") { coll.drop(); assert.writeOK(coll.insert({_id: 0, a: [{b: 5}]})); assert.writeError(coll.update({_id: 0}, {$set: {"a.$[i].b": 6}}, {arrayFilters: [{"i.b": 5, $expr: {$eq: ["$i.b", 5]}}]})); } // Any writes preceding the write that fails to parse are executed. coll.drop(); assert.writeOK(coll.insert({_id: 0})); assert.writeOK(coll.insert({_id: 1})); writeRes = db.runCommand({ update: coll.getName(), updates: [{q: {_id: 0}, u: {$set: {b: 6}}}, {q: {$expr: "$$unbound"}, u: {$set: {b: 6}}}] }); assert.commandWorked(writeRes); assert.eq(writeRes.writeErrors[0].code, 17276, tojson(writeRes)); assert.eq(writeRes.n, 1, tojson(writeRes)); })();