705 lines
23 KiB
JavaScript
705 lines
23 KiB
JavaScript
// Tests that commands like find, aggregate and update accepts a 'let' parameter which defines
|
|
// variables for use in expressions within the command.
|
|
// Before 7.2, $rand was evaluated more than once in sharded find (SERVER-75927).
|
|
// @tags: [
|
|
// # TODO SERVER-123918: remove the multiversion incompatible tag.
|
|
// multiversion_incompatible,
|
|
// # Requires a batch size greater than one.
|
|
// does_not_support_config_fuzzer,
|
|
// requires_fcv_81,
|
|
// requires_getmore,
|
|
// # Explain will return different plan than expected when a collection becomes a time-series
|
|
// # collection. Also, query shape will be different.
|
|
// exclude_from_timeseries_crud_passthrough,
|
|
// # Primary-driven index builds must have batched writes enabled which config.image_collection
|
|
// # does not support.
|
|
// primary_driven_index_builds_incompatible_with_retryable_writes,
|
|
// ]
|
|
//
|
|
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
|
|
import {getPlanStage, getSingleNodeExplain, planHasStage} from "jstests/libs/query/analyze_plan.js";
|
|
import {checkSbeFullFeatureFlagEnabled} from "jstests/libs/query/sbe_util.js";
|
|
|
|
const testDB = db.getSiblingDB("command_let_variables");
|
|
const coll = testDB.command_let_variables;
|
|
coll.drop();
|
|
|
|
const isMongos = FixtureHelpers.isMongos(testDB);
|
|
const isCollSharded = FixtureHelpers.isSharded(coll);
|
|
|
|
const testDocs = [
|
|
{
|
|
_id: 1,
|
|
Species: "Blackbird (Turdus merula)",
|
|
population_trends: [
|
|
{term: {start: 1970, end: 2014}, pct_change: -16, annual: -0.38, trend: "no change"},
|
|
{term: {start: 2009, end: 2014}, pct_change: -2, annual: -0.36, trend: "no change"},
|
|
],
|
|
},
|
|
{
|
|
_id: 2,
|
|
Species: "Bullfinch (Pyrrhula pyrrhula)",
|
|
population_trends: [
|
|
{term: {start: 1970, end: 2014}, pct_change: -39, annual: -1.13, trend: "no change"},
|
|
{term: {start: 2009, end: 2014}, pct_change: 12, annual: 2.38, trend: "weak increase"},
|
|
],
|
|
},
|
|
{
|
|
_id: 3,
|
|
Species: "Chaffinch (Fringilla coelebs)",
|
|
population_trends: [
|
|
{term: {start: 1970, end: 2014}, pct_change: 27, annual: 0.55, trend: "no change"},
|
|
{term: {start: 2009, end: 2014}, pct_change: -7, annual: -1.49, trend: "weak decline"},
|
|
],
|
|
},
|
|
{
|
|
_id: 4,
|
|
Species: "Song Thrush (Turdus philomelos)",
|
|
population_trends: [
|
|
{term: {start: 1970, end: 2014}, pct_change: -53, annual: -1.7, trend: "weak decline"},
|
|
{term: {start: 2009, end: 2014}, pct_change: -4, annual: -0.88, trend: "no change"},
|
|
],
|
|
},
|
|
];
|
|
|
|
assert.commandWorked(coll.insert(testDocs));
|
|
|
|
// Aggregate tests
|
|
const pipeline = [
|
|
{$project: {_id: 0}},
|
|
{$unwind: "$population_trends"},
|
|
{$match: {$expr: {$eq: ["$population_trends.trend", "$$target_trend"]}}},
|
|
{$sort: {Species: 1}},
|
|
];
|
|
let expectedResults = [
|
|
{
|
|
Species: "Bullfinch (Pyrrhula pyrrhula)",
|
|
population_trends: {term: {start: 2009, end: 2014}, pct_change: 12, annual: 2.38, trend: "weak increase"},
|
|
},
|
|
];
|
|
|
|
assert.eq(coll.aggregate(pipeline, {let: {target_trend: "weak increase"}}).toArray(), expectedResults);
|
|
|
|
expectedResults = [
|
|
{
|
|
Species: "Chaffinch (Fringilla coelebs)",
|
|
population_trends: {term: {start: 2009, end: 2014}, pct_change: -7, annual: -1.49, trend: "weak decline"},
|
|
},
|
|
{
|
|
Species: "Song Thrush (Turdus philomelos)",
|
|
population_trends: {term: {start: 1970, end: 2014}, pct_change: -53, annual: -1.7, trend: "weak decline"},
|
|
},
|
|
];
|
|
assert.eq(coll.aggregate(pipeline, {let: {target_trend: "weak decline"}}).toArray(), expectedResults);
|
|
|
|
// Test that running explain on the agg command works as expected.
|
|
let explain = assert.commandWorked(
|
|
testDB.runCommand({
|
|
explain: {aggregate: coll.getName(), pipeline, let: {target_trend: "weak decline"}, cursor: {}},
|
|
verbosity: "executionStats",
|
|
}),
|
|
);
|
|
|
|
if (!isMongos) {
|
|
if (checkSbeFullFeatureFlagEnabled(testDB)) {
|
|
// $unwind should be pushed down to SBE.
|
|
assert(planHasStage(testDB, explain, "UNWIND"), explain);
|
|
} else {
|
|
explain = getSingleNodeExplain(explain);
|
|
assert(explain.hasOwnProperty("stages"), explain);
|
|
assert.neq(explain.stages.length, 0, explain);
|
|
let lastStage = explain.stages[explain.stages.length - 1];
|
|
assert.eq(lastStage.nReturned, 2, explain);
|
|
}
|
|
}
|
|
|
|
if (!isMongos && !TestData.testingReplicaSetEndpoint) {
|
|
// Test that if runtimeConstants and let are both specified, both will coexist.
|
|
// Runtime constants are not allowed on mongos passthroughs.
|
|
// Must set 'fromRouter: true' as otherwise 'runtimeConstants' is disallowed on mongod.
|
|
let constants = {
|
|
localNow: new Date(),
|
|
clusterTime: new Timestamp(0, 0),
|
|
};
|
|
|
|
assert.eq(
|
|
coll
|
|
.aggregate(pipeline, {
|
|
runtimeConstants: constants,
|
|
let: {target_trend: "weak decline"},
|
|
fromRouter: true,
|
|
})
|
|
.toArray(),
|
|
expectedResults,
|
|
);
|
|
|
|
// Test that undefined let params in the pipeline fail gracefully.
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({
|
|
aggregate: coll.getName(),
|
|
pipeline: pipeline,
|
|
runtimeConstants: constants,
|
|
cursor: {},
|
|
let: {cat: "not_a_bird"},
|
|
fromRouter: true,
|
|
}),
|
|
17276,
|
|
);
|
|
// Test null and empty let parameters
|
|
const pipeline_no_lets = [
|
|
{$project: {_id: 0}},
|
|
{$unwind: "$population_trends"},
|
|
{$match: {$expr: {$eq: ["$population_trends.trend", "weak decline"]}}},
|
|
{$sort: {Species: 1}},
|
|
];
|
|
assert.eq(
|
|
coll.aggregate(pipeline_no_lets, {runtimeConstants: constants, let: {}, fromRouter: true}).toArray(),
|
|
expectedResults,
|
|
);
|
|
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
aggregate: coll.getName(),
|
|
pipeline: pipeline_no_lets,
|
|
runtimeConstants: constants,
|
|
cursor: {},
|
|
let: null,
|
|
fromRouter: true,
|
|
}),
|
|
);
|
|
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({
|
|
aggregate: coll.getName(),
|
|
pipeline: pipeline_no_lets,
|
|
runtimeConstants: constants,
|
|
cursor: {},
|
|
let: 1,
|
|
fromRouter: true,
|
|
}),
|
|
ErrorCodes.TypeMismatch,
|
|
);
|
|
}
|
|
|
|
// Test that $project stage can use 'let' variables
|
|
assert.eq(
|
|
testDB.runCommand({
|
|
aggregate: coll.getName(),
|
|
pipeline: [
|
|
{
|
|
$project: {
|
|
"var": {
|
|
$let: {
|
|
vars: {variable: "INNER"},
|
|
"in": {
|
|
$cond: {
|
|
"if": {$eq: [{$substr: ["$Species", 0, 1]}, "B"]},
|
|
then: "$$variable",
|
|
"else": "---",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{$match: {$expr: {$eq: ["$var", "INNER"]}}},
|
|
],
|
|
cursor: {},
|
|
}).cursor.firstBatch.length,
|
|
2,
|
|
);
|
|
|
|
// Test that $project stage can access command-level 'let' variables.
|
|
assert.eq(
|
|
testDB.runCommand({
|
|
aggregate: coll.getName(),
|
|
pipeline: [
|
|
{
|
|
$project: {
|
|
"var": {
|
|
$cond: {
|
|
"if": {$eq: [{$substr: ["$Species", 0, 1]}, "B"]},
|
|
then: "$$variable",
|
|
"else": "---",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{$match: {$expr: {$eq: ["$var", "OUTER"]}}},
|
|
],
|
|
cursor: {},
|
|
"let": {variable: "OUTER"},
|
|
}).cursor.firstBatch.length,
|
|
2,
|
|
);
|
|
|
|
// Test that $project stage can use stage-level and command-level 'let' variables in same command.
|
|
assert.eq(
|
|
testDB.runCommand({
|
|
aggregate: coll.getName(),
|
|
pipeline: [
|
|
{
|
|
$project: {
|
|
"var": {
|
|
$let: {
|
|
vars: {innerVar: "INNER"},
|
|
"in": {
|
|
$cond: {
|
|
"if": {$eq: [{$substr: ["$Species", 0, 1]}, "B"]},
|
|
then: {$concat: ["$$innerVar", "$$outerVar"]},
|
|
"else": "---",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{$match: {$expr: {$eq: ["$var", "INNEROUTER"]}}},
|
|
],
|
|
cursor: {},
|
|
"let": {outerVar: "OUTER"},
|
|
}).cursor.firstBatch.length,
|
|
2,
|
|
);
|
|
|
|
// Test that $project stage follows variable scoping rules with stage-level and command-level 'let'
|
|
// variables.
|
|
assert.eq(
|
|
testDB.runCommand({
|
|
aggregate: coll.getName(),
|
|
pipeline: [
|
|
{
|
|
$project: {
|
|
"var": {
|
|
$let: {
|
|
vars: {variable: "INNER"},
|
|
"in": {
|
|
$cond: {
|
|
"if": {$eq: [{$substr: ["$Species", 0, 1]}, "B"]},
|
|
then: "$$variable",
|
|
"else": "---",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{$match: {$expr: {$eq: ["$var", "INNER"]}}},
|
|
],
|
|
cursor: {},
|
|
"let": {variable: "OUTER"},
|
|
}).cursor.firstBatch.length,
|
|
2,
|
|
);
|
|
|
|
// Test that the find command works correctly with a let parameter argument.
|
|
let result = assert.commandWorked(
|
|
testDB.runCommand({
|
|
find: coll.getName(),
|
|
let: {target_species: "Song Thrush (Turdus philomelos)"},
|
|
filter: {$expr: {$eq: ["$Species", "$$target_species"]}},
|
|
projection: {_id: 0},
|
|
}),
|
|
).cursor.firstBatch;
|
|
expectedResults = {
|
|
Species: "Song Thrush (Turdus philomelos)",
|
|
population_trends: [
|
|
{term: {start: 1970, end: 2014}, pct_change: -53, annual: -1.7, trend: "weak decline"},
|
|
{term: {start: 2009, end: 2014}, pct_change: -4, annual: -0.88, trend: "no change"},
|
|
],
|
|
};
|
|
assert.eq(result.length, 1);
|
|
assert.eq(expectedResults, result[0]);
|
|
|
|
// Test that let parameters work as expected when the find is run as an explain.
|
|
explain = assert.commandWorked(
|
|
testDB.runCommand({
|
|
explain: {
|
|
find: coll.getName(),
|
|
let: {target_species: "Song Thrush (Turdus philomelos)"},
|
|
filter: {$expr: {$eq: ["$Species", "$$target_species"]}},
|
|
projection: {_id: 0},
|
|
},
|
|
verbosity: "executionStats",
|
|
}),
|
|
);
|
|
if (!isMongos) {
|
|
assert.eq(explain.executionStats.nReturned, 1, explain);
|
|
}
|
|
|
|
// Delete tests with let params will delete a record, assert that a point-wise find yields an
|
|
// empty result, and then restore the collection state for further tests down the line. We can't
|
|
// exercise a multi-delete here (limit: 0) because of failures in sharded txn passthrough tests.
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
delete: coll.getName(),
|
|
let: {target_species: "Song Thrush (Turdus philomelos)"},
|
|
deletes: [{q: {$and: [{_id: 4}, {$expr: {$eq: ["$Species", "$$target_species"]}}]}, limit: 1}],
|
|
}),
|
|
);
|
|
|
|
result = assert.commandWorked(testDB.runCommand({find: coll.getName(), filter: {$expr: {$eq: ["$_id", "4"]}}})).cursor
|
|
.firstBatch;
|
|
assert.eq(result.length, 0);
|
|
|
|
assert.commandWorked(coll.insert({_id: 4, Species: "bird_to_remove"}));
|
|
|
|
// Test that explain of a delete command works as expected with 'let' parameters.
|
|
explain = assert.commandWorked(
|
|
testDB.runCommand({
|
|
explain: {
|
|
delete: coll.getName(),
|
|
let: {target_species: "bird_to_remove"},
|
|
deletes: [{q: {$and: [{_id: 4}, {$expr: {$eq: ["$Species", "$$target_species"]}}]}, limit: 1}],
|
|
},
|
|
verbosity: "executionStats",
|
|
}),
|
|
);
|
|
if (!isMongos) {
|
|
let deleteStage = getPlanStage(explain.executionStats.executionStages, "DELETE");
|
|
assert.eq(deleteStage.nWouldDelete, 1, explain);
|
|
}
|
|
|
|
// Test that the .remove() shell helper supports let parameters.
|
|
result = assert.commandWorked(
|
|
coll.remove(
|
|
{$and: [{_id: 4}, {$expr: {$eq: ["$Species", "$$target_species"]}}]},
|
|
{justOne: true, let: {target_species: "bird_to_remove"}},
|
|
),
|
|
);
|
|
assert.eq(result.nRemoved, 1);
|
|
|
|
result = assert.commandWorked(testDB.runCommand({find: coll.getName(), filter: {$expr: {$eq: ["$_id", "4"]}}})).cursor
|
|
.firstBatch;
|
|
assert.eq(result.length, 0);
|
|
|
|
// Test that reserved names are not allowed as let variable names.
|
|
[
|
|
{name: "Reserved", errorCode: ErrorCodes.FailedToParse},
|
|
{name: "REMOVE", errorCode: ErrorCodes.FailedToParse},
|
|
{name: "ROOT", errorCode: ErrorCodes.FailedToParse},
|
|
{name: "NOW", errorCode: 4738901},
|
|
{name: "CLUSTER_TIME", errorCode: 4738901},
|
|
{name: "IS_MR", errorCode: 4738901},
|
|
{name: "JS_SCOPE", errorCode: 4738901},
|
|
].forEach((caseInfo) => {
|
|
const reservedName = caseInfo.name;
|
|
const expectedError = caseInfo.errorCode;
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({aggregate: coll.getName(), pipeline: [], cursor: {}, let: {[reservedName]: "failure"}}),
|
|
expectedError,
|
|
`Expected an aggregate with the variable ${reservedName} to fail.`,
|
|
);
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({update: coll.getName(), updates: [{q: {}, u: []}], let: {[reservedName]: "failure"}}),
|
|
expectedError,
|
|
`Expected a pipeline style update with the variable ${reservedName} to fail.`,
|
|
);
|
|
});
|
|
|
|
// Test that let variables can be used within views.
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
create: "core-viewColl",
|
|
viewOn: coll.getName(),
|
|
pipeline: [{$match: {Species: "Song Thrush (Turdus philomelos)"}}],
|
|
}),
|
|
);
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
aggregate: "core-viewColl",
|
|
pipeline: [{$addFields: {var: "$$variable"}}],
|
|
let: {variable: "Song Thrush"},
|
|
cursor: {},
|
|
}),
|
|
);
|
|
|
|
assert.commandWorked(coll.insert({_id: 5, Species: "spy_bird"}));
|
|
|
|
// Test that explain of findAndModify works correctly with let parameters.
|
|
explain = assert.commandWorked(
|
|
testDB.runCommand({
|
|
explain: {
|
|
findAndModify: coll.getName(),
|
|
let: {target_species: "spy_bird"},
|
|
// Querying on _id field for sharded collection passthroughs.
|
|
query: {$and: [{_id: 5}, {$expr: {$eq: ["$Species", "$$target_species"]}}]},
|
|
update: {Species: "questionable_bird"},
|
|
new: true,
|
|
},
|
|
verbosity: "executionStats",
|
|
}),
|
|
);
|
|
if (!isMongos) {
|
|
let updateStage = getPlanStage(explain.executionStats.executionStages, "UPDATE");
|
|
assert.eq(updateStage.nMatched, 1, explain);
|
|
assert.eq(updateStage.nWouldModify, 1, explain);
|
|
}
|
|
|
|
// Test that findAndModify works correctly with let parameter arguments.
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({
|
|
findAndModify: coll.getName(),
|
|
let: {target_species: "spy_bird"},
|
|
// Querying on _id field for sharded collection passthroughs.
|
|
query: {$and: [{_id: 5}, {$expr: {$eq: ["$Species", "$$target_species"]}}]},
|
|
update: {Species: "questionable_bird"},
|
|
new: true,
|
|
}),
|
|
);
|
|
expectedResults = {
|
|
_id: 5,
|
|
Species: "questionable_bird",
|
|
};
|
|
assert.eq(expectedResults, result.value, result);
|
|
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({
|
|
findAndModify: coll.getName(),
|
|
let: {species_name: "not_a_bird", realSpecies: "dino"},
|
|
// Querying on _id field for sharded collection passthroughs.
|
|
query: {$and: [{_id: 5}, {$expr: {$eq: ["$Species", "questionable_bird"]}}]},
|
|
update: [{$project: {Species: "$$species_name"}}, {$addFields: {suspect: "$$realSpecies"}}],
|
|
new: true,
|
|
}),
|
|
);
|
|
expectedResults = {
|
|
_id: 5,
|
|
Species: "not_a_bird",
|
|
suspect: "dino",
|
|
};
|
|
assert.eq(expectedResults, result.value, result);
|
|
|
|
// Test that explain of update works correctly with let parameters.
|
|
explain = assert.commandWorked(
|
|
testDB.runCommand({
|
|
explain: {
|
|
update: coll.getName(),
|
|
updates: [
|
|
{
|
|
q: {_id: 3, $expr: {$eq: ["$Species", "$$target_species"]}},
|
|
u: [{$set: {Species: "$$new_name"}}],
|
|
},
|
|
],
|
|
let: {target_species: "Chaffinch (Fringilla coelebs)", new_name: "Chaffinch"},
|
|
},
|
|
verbosity: "executionStats",
|
|
}),
|
|
);
|
|
if (!isMongos) {
|
|
let updateStage = getPlanStage(explain.executionStats.executionStages, "UPDATE");
|
|
assert.eq(updateStage.nMatched, 1, explain);
|
|
assert.eq(updateStage.nWouldModify, 1, explain);
|
|
}
|
|
|
|
// Test that update respects different parameters in both the query and update part.
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({
|
|
update: coll.getName(),
|
|
updates: [
|
|
{
|
|
q: {_id: 3, $expr: {$eq: ["$Species", "$$target_species"]}},
|
|
u: [{$set: {Species: "$$new_name"}}],
|
|
},
|
|
],
|
|
let: {target_species: "Chaffinch (Fringilla coelebs)", new_name: "Chaffinch"},
|
|
}),
|
|
);
|
|
assert.eq(result.n, 1);
|
|
assert.eq(result.nModified, 1);
|
|
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({find: coll.getName(), filter: {$expr: {$eq: ["$Species", "Chaffinch (Fringilla coelebs)"]}}}),
|
|
);
|
|
assert.eq(result.cursor.firstBatch.length, 0);
|
|
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({find: coll.getName(), filter: {$expr: {$eq: ["$Species", "Chaffinch"]}}}),
|
|
);
|
|
assert.eq(result.cursor.firstBatch.length, 1);
|
|
|
|
// Test that update respects runtime constants and parameters.
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({
|
|
update: coll.getName(),
|
|
updates: [
|
|
{
|
|
q: {_id: 3, $expr: {$eq: ["$Species", "$$target_species"]}},
|
|
u: [{$set: {Timestamp: "$$NOW"}}, {$set: {Species: "$$new_name"}}],
|
|
},
|
|
],
|
|
let: {target_species: "Chaffinch", new_name: "Pied Piper"},
|
|
}),
|
|
);
|
|
assert.eq(result.n, 1);
|
|
assert.eq(result.nModified, 1);
|
|
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({find: coll.getName(), filter: {$expr: {$eq: ["$Species", "Chaffinch"]}}}),
|
|
);
|
|
assert.eq(result.cursor.firstBatch.length, 0, result);
|
|
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({find: coll.getName(), filter: {$expr: {$eq: ["$Species", "Pied Piper"]}}}),
|
|
);
|
|
assert.eq(result.cursor.firstBatch.length, 1, result);
|
|
|
|
// This forces a multi-statement transaction to commit if this test is running in one of the
|
|
// multi-statement transaction passthrough suites. We need to do this to ensure the updates
|
|
// above commit before running an update that will fail, as the failed update aborts the entire
|
|
// transaction and rolls back the updates above.
|
|
assert.commandWorked(testDB.runCommand({ping: 1}));
|
|
|
|
// Test that undefined let params in the update's query part fail gracefully.
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({
|
|
update: coll.getName(),
|
|
updates: [
|
|
{
|
|
q: {$expr: {$eq: ["$Species", "$$target_species"]}},
|
|
u: [{$set: {Species: "Homo Erectus"}}],
|
|
},
|
|
],
|
|
let: {cat: "not_a_bird"},
|
|
}),
|
|
17276,
|
|
);
|
|
|
|
// Test that undefined let params in the update's update part fail gracefully.
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({
|
|
update: coll.getName(),
|
|
updates: [
|
|
{
|
|
q: {_id: 3, $expr: {$eq: ["$Species", "Chaffinch (Fringilla coelebs)"]}},
|
|
u: [{$set: {Species: "$$new_name"}}],
|
|
},
|
|
],
|
|
let: {cat: "not_a_bird"},
|
|
}),
|
|
17276,
|
|
);
|
|
|
|
// Test that the .update() shell helper supports let parameters.
|
|
result = assert.commandWorked(
|
|
coll.update({_id: 3, $expr: {$eq: ["$Species", "$$target_species"]}}, [{$set: {Species: "$$new_name"}}], {
|
|
let: {target_species: "Pied Piper", new_name: "Chaffinch"},
|
|
}),
|
|
);
|
|
assert.eq(result.nMatched, 1);
|
|
assert.eq(result.nModified, 1);
|
|
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({find: coll.getName(), filter: {$expr: {$eq: ["$Species", "Pied Piper"]}}}),
|
|
);
|
|
assert.eq(result.cursor.firstBatch.length, 0, result);
|
|
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({find: coll.getName(), filter: {$expr: {$eq: ["$Species", "Chaffinch"]}}}),
|
|
);
|
|
assert.eq(result.cursor.firstBatch.length, 1, result);
|
|
|
|
// Test that let variables can be initialized with an expression.
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({
|
|
find: coll.getName(),
|
|
filter: {},
|
|
projection: {_id: "$$a"},
|
|
limit: 1,
|
|
let: {a: {$add: [2, 3]}},
|
|
}),
|
|
).cursor.firstBatch;
|
|
assert.eq(result, [{_id: 5}]);
|
|
|
|
// Test that the expression cannot refer to any fields or variables.
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({
|
|
find: coll.getName(),
|
|
filter: {},
|
|
let: {a: "$_id"},
|
|
}),
|
|
4890500,
|
|
);
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({
|
|
find: coll.getName(),
|
|
filter: {},
|
|
let: {a: "$$oops_undefined_var"},
|
|
}),
|
|
17276,
|
|
);
|
|
|
|
// Test that each expression can refer to previously defined let variables.
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({
|
|
find: coll.getName(),
|
|
filter: {},
|
|
projection: {_id: "$$c"},
|
|
limit: 1,
|
|
let: {
|
|
a: 2,
|
|
b: {$add: ["$$a", 3]},
|
|
c: {$multiply: ["$$a", "$$b"]},
|
|
},
|
|
}),
|
|
).cursor.firstBatch;
|
|
assert.eq([{_id: (2 + 3) * 2}], result);
|
|
|
|
// Test that $rand is allowed in the initializer.
|
|
result = assert.commandWorked(
|
|
testDB.runCommand({
|
|
find: coll.getName(),
|
|
filter: {},
|
|
projection: {_id: "$$r"},
|
|
limit: 1,
|
|
let: {r: {$rand: {}}},
|
|
}),
|
|
).cursor.firstBatch[0]._id;
|
|
assert.between(0, result, 1);
|
|
|
|
// Test that each initializer expression is evaluated separately.
|
|
{
|
|
const values = assert.commandWorked(
|
|
testDB.runCommand({
|
|
find: coll.getName(),
|
|
filter: {},
|
|
projection: {_id: ["$$a", "$$b", "$$c", "$$d"]},
|
|
limit: 1,
|
|
let: {
|
|
a: {$rand: {}},
|
|
b: {$rand: {}},
|
|
c: {$rand: {}},
|
|
d: {$rand: {}},
|
|
},
|
|
}),
|
|
).cursor.firstBatch[0]._id;
|
|
|
|
const deduped = [...new Set(values)];
|
|
assert.eq(values.length, deduped.length, `Expected all distinct values: ${values}`);
|
|
}
|
|
|
|
// Test that the expressions are evaluated once up front.
|
|
const values = assert
|
|
.commandWorked(
|
|
testDB.runCommand({
|
|
find: coll.getName(),
|
|
filter: {},
|
|
projection: {_id: "$$r"},
|
|
let: {r: {$rand: {}}},
|
|
}),
|
|
)
|
|
.cursor.firstBatch.map((doc) => doc._id);
|
|
assert.gt(values.length, 1);
|
|
|
|
const deduped = [...new Set(values)];
|
|
assert.eq(1, deduped.length, `Expected all identical values: ${deduped}`);
|
|
|
|
// Test that expressions wrapped with $literal are serialized correctly when run in sharded cluster
|
|
// environments.
|
|
result = coll
|
|
.aggregate([{$match: {$expr: {$eq: ["$_id", 2]}}}, {$project: {a: "$$b"}}], {
|
|
let: {b: {$literal: "$notAFieldPath"}},
|
|
})
|
|
.toArray();
|
|
assert.eq(result, [{_id: 2, a: "$notAFieldPath"}]);
|