mongo/jstests/aggregation/optimization/hoist_computation_function.js
Vesko Karaganev 04de6c9fb5 SERVER-124097 Report that $function might use RNG (#53943)
GitOrigin-RevId: f2ab725f6045ca3cbb5da91bc23f54f03e3057f2
2026-05-20 09:31:16 +00:00

75 lines
2.8 KiB
JavaScript

/**
* Tests that $set stages containing $function expressions are not hoisted before $lookup, because
* $function may have side-effects or depend on execution order.
*
* @tags: [
* requires_scripting,
* requires_pipeline_optimization,
* # Uses a knob (internalQueryTransformHoistPolicy) that does not exist on older binaries.
* multiversion_incompatible,
* assumes_unsharded_collection,
* assumes_stable_shard_list,
* # Wrapping in $facet changes the explain structure so the stage-order check below cannot
* # inspect the original pipeline stages.
* do_not_wrap_aggregations_in_facets,
* # featureFlagImprovedDepsAnalysis behavior can differ across FCV boundaries.
* cannot_run_during_upgrade_downgrade,
* ]
*/
import {it} from "jstests/libs/mochalite.js";
db.hoist_computation_function_secondary.drop();
assert.commandWorked(db.hoist_computation_function_secondary.insert({}));
it("$function side effects: outer $set with $function not hoisted before $lookup", function () {
const coll = db.hoist_computation_function;
coll.drop();
coll.insertOne({c: 1});
const readDistinctTime = function () {
var now = Date.now();
while (Date.now() === now) {}
return new Date(now);
};
const timeFunction = {$function: {body: readDistinctTime, args: [], lang: "js"}};
// The first $set is nested inside the $lookup sub-pipeline and captures time1.
// It spins until the clock ticks so time1 is at a strictly earlier millisecond.
// The outer $set captures time2 after the lookup completes.
// If the optimizer hoisted the outer $set before the $lookup, time2 < time1, which is wrong.
const pipeline = [
{
$lookup: {
from: "hoist_computation_function_secondary",
pipeline: [{$set: {time1: timeFunction}}],
as: "result",
},
},
{$set: {time2: timeFunction}},
];
// Verify the outer $set is not hoisted before the $lookup.
// In a mongos passthrough the stages live under explain.shards[...].stages, not at the top
// level, so explain.stages is absent; skip the stage-order check and rely on the timing
// check below for correctness.
const explain = coll.explain().aggregate(pipeline);
if (explain.stages) {
const stages = explain.stages.map((s) => Object.keys(s)[0]);
assert.eq(
stages.filter((s) => s === "$lookup" || s === "$set"),
["$lookup", "$set"],
{stages},
);
}
// Verify execution order: time1 (set inside the lookup) must precede time2 (set after).
const [
{
result: [{time1}],
time2,
},
] = coll.aggregate(pipeline).toArray();
assert.lt(time1.getTime(), time2.getTime(), tojson({time1, time2}));
});