mongo/jstests/core/query/expr/expr.js

347 lines
10 KiB
JavaScript

// Tests for $expr in the CRUD commands.
//
// @tags: [
// # The test runs commands that are not allowed with security token: mapReduce.
// not_allowed_with_signed_security_token,
// does_not_support_stepdowns,
// requires_getmore,
// requires_non_retryable_writes,
// # We added a new test case for a bug we only fixed in 6.0+: SERVER-64764.
// requires_fcv_60,
// # Uses mapReduce command.
// requires_scripting,
// # Does not support multiplanning, because it makes explain fail
// does_not_support_multiplanning_single_solutions,
// requires_getmore,
// # This test relies on query commands returning specific batch-sized responses.
// assumes_no_implicit_cursor_exhaustion,
// # Test relies on mapReduce or unsupported $expr behaviors (e.g. $divide error codes) on time-series.
// exclude_from_timeseries_crud_passthrough,
// ]
import "jstests/libs/query/sbe_assert_error_override.js";
import {getExecutionStats} from "jstests/libs/query/analyze_plan.js";
const coll = db.expr;
//
// $expr in aggregate.
//
coll.drop();
assert.commandWorked(coll.insert({_id: 0, a: 0}));
assert.eq(1, coll.aggregate([{$match: {$expr: {$eq: ["$a", 0]}}}]).itcount());
assert.eq(1, coll.aggregate([{$match: {$expr: {$eq: ["$$ROOT", {_id: 0, 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.commandWorked(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.commandWorked(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.commandWorked(coll.insert({a: 0}));
assert.eq(1, coll.find({$expr: {$eq: ["$a", 0]}}).itcount());
// $expr with time zone expression across getMore (SERVER-31664).
coll.drop();
assert.commandWorked(coll.insert({a: ISODate("2017-10-01T22:00:00")}));
let res = assert.commandWorked(
db.runCommand({
find: coll.getName(),
filter: {$expr: {$eq: [1, {$dayOfMonth: {date: "$a", timezone: "America/New_York"}}]}},
batchSize: 0,
}),
);
assert.eq(0, res.cursor.firstBatch.length);
let cursorId = res.cursor.id;
res = assert.commandWorked(db.runCommand({getMore: cursorId, collection: coll.getName()}));
assert.eq(1, res.cursor.nextBatch.length);
// $expr with unbound variable throws.
assert.throws(function () {
coll.find({$expr: {$eq: ["$a", "$$unbound"]}}).itcount();
});
// $and with $expr child containing an invalid expression throws.
assert.throws(function () {
coll.find({$and: [{a: 0}, {$expr: {$anyElementTrue: undefined}}]}).itcount();
});
// $or with $expr child containing an invalid expression throws.
assert.throws(function () {
coll.find({$or: [{a: 0}, {$expr: {$anyElementTrue: undefined}}]}).itcount();
});
// $nor with $expr child containing an invalid expression throws.
assert.throws(function () {
coll.find({$nor: [{a: 0}, {$expr: {$anyElementTrue: undefined}}]}).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 which causes a runtime error should be caught be explain and reported as an error in the
// 'executionSuccess' field.
let explain = coll.find({$expr: {$divide: [1, "$a"]}}).explain("executionStats");
let executionStats = getExecutionStats(explain).filter((stats) => !stats.executionSuccess)[0];
assert.eq(executionStats.executionSuccess, false, explain);
assert.errorCodeEq(executionStats.errorCode, [16609, ErrorCodes.TypeMismatch], explain);
// $expr is not allowed in $elemMatch projection.
coll.drop();
assert.commandWorked(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.commandWorked(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.commandWorked(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.commandWorked(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.
coll.drop();
assert.commandWorked(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 the $geoNear stage.
//
coll.drop();
assert.commandWorked(coll.insert({geo: {type: "Point", coordinates: [0, 0]}, a: 0}));
assert.commandWorked(coll.createIndex({geo: "2dsphere"}));
assert.eq(
1,
coll
.aggregate({
$geoNear: {
near: {type: "Point", coordinates: [0, 0]},
distanceField: "dist",
spherical: true,
query: {$expr: {$eq: ["$a", 0]}},
},
})
.toArray().length,
);
assert.throws(() =>
coll.aggregate({
$geoNear: {
near: {type: "Point", coordinates: [0, 0]},
distanceField: "dist",
spherical: true,
query: {$expr: {$eq: ["$a", "$$unbound"]}},
},
}),
);
assert.throws(() =>
coll.aggregate({
$geoNear: {
near: {type: "Point", coordinates: [0, 0]},
distanceField: "dist",
spherical: true,
query: {$expr: {$divide: [1, "$a"]}},
},
}),
);
//
// $expr in mapReduce.
//
coll.drop();
assert.commandWorked(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.commandWorked(coll.insert({_id: 0, a: 0}));
let writeRes = coll.remove({_id: 0, $expr: {$eq: ["$a", 0]}});
assert.commandWorked(writeRes);
assert.eq(1, writeRes.nRemoved);
assert.writeError(coll.remove({_id: 0, $expr: {$eq: ["$a", "$$unbound"]}}));
assert.commandWorked(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.commandWorked(coll.insert({_id: 0}));
assert.commandWorked(coll.insert({_id: 1}));
writeRes = db.runCommand({
delete: coll.getName(),
deletes: [
{q: {_id: 0}, limit: 1},
{q: {$expr: "$$unbound"}, limit: 1},
],
});
assert.commandWorkedIgnoringWriteErrors(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.commandWorked(coll.insert({_id: 0, a: 0}));
assert.commandWorked(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.commandWorked(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.commandWorked(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.
coll.drop();
assert.commandWorked(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.commandWorked(coll.insert({_id: 0}));
assert.commandWorked(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.commandWorkedIgnoringWriteErrors(writeRes);
assert.eq(writeRes.writeErrors[0].code, 17276, tojson(writeRes));
assert.eq(writeRes.n, 1, tojson(writeRes));