Files
mongo/jstests/aggregation/sources/setWindowFields/range.js
Zac 591928c619 SERVER-108478 JS formatted by prettier and remove clang-format (#39656)
GitOrigin-RevId: 6c8f6aded47f260aa4f7c231b17dae3302cb1e04
2025-08-21 17:27:09 +00:00

275 lines
8.5 KiB
JavaScript

/**
* Test range-based window bounds.
*/
const coll = db.setWindowFields_range;
coll.drop();
assert.commandWorked(coll.insert([{x: 0}, {x: 1}, {x: 1.5}, {x: 2}, {x: 3}, {x: 100}, {x: 100}, {x: 101}]));
// Make a setWindowFields stage with the given bounds.
function range(lower, upper) {
return {
$setWindowFields: {
partitionBy: "$partition",
sortBy: {x: 1},
output: {
y: {$push: "$x", window: {range: [lower, upper]}},
},
},
};
}
// Run the pipeline, and unset _id.
function run(pipeline) {
return coll.aggregate([...pipeline, {$unset: "_id"}]).toArray();
}
// The documents are not evenly spaced, so the window varies in size.
assert.sameMembers(run([range(-1, 0)]), [
{x: 0, y: [0]},
{x: 1, y: [0, 1]},
{x: 1.5, y: [1, 1.5]},
{x: 2, y: [1, 1.5, 2]},
{x: 3, y: [2, 3]},
// '0' means the current document and those that tie with it.
{x: 100, y: [100, 100]},
{x: 100, y: [100, 100]},
{x: 101, y: [100, 100, 101]},
]);
// One or both endpoints can be unbounded.
assert.sameMembers(run([range("unbounded", 0)]), [
{x: 0, y: [0]},
{x: 1, y: [0, 1]},
{x: 1.5, y: [0, 1, 1.5]},
{x: 2, y: [0, 1, 1.5, 2]},
{x: 3, y: [0, 1, 1.5, 2, 3]},
// '0' means current document and those that tie with it.
{x: 100, y: [0, 1, 1.5, 2, 3, 100, 100]},
{x: 100, y: [0, 1, 1.5, 2, 3, 100, 100]},
{x: 101, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
]);
assert.sameMembers(run([range(0, "unbounded")]), [
{x: 0, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
{x: 1, y: [1, 1.5, 2, 3, 100, 100, 101]},
{x: 1.5, y: [1.5, 2, 3, 100, 100, 101]},
{x: 2, y: [2, 3, 100, 100, 101]},
{x: 3, y: [3, 100, 100, 101]},
// '0' means current document and those that tie with it.
{x: 100, y: [100, 100, 101]},
{x: 100, y: [100, 100, 101]},
{x: 101, y: [101]},
]);
assert.sameMembers(run([range("unbounded", "unbounded")]), [
{x: 0, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
{x: 1, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
{x: 1.5, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
{x: 2, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
{x: 3, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
{x: 100, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
{x: 100, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
{x: 101, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
]);
// Unlike '0', 'current' always means the current document.
assert.sameMembers(run([range("current", "current"), {$match: {x: 100}}]), [
{x: 100, y: [100]},
{x: 100, y: [100]},
]);
assert.sameMembers(run([range("current", +1), {$match: {x: 100}}]), [
{x: 100, y: [100, 100, 101]},
{x: 100, y: [100, 101]},
]);
assert.sameMembers(run([range(-97, "current"), {$match: {x: 100}}]), [
{x: 100, y: [3, 100]},
{x: 100, y: [3, 100, 100]},
]);
// The window doesn't have to contain the current document.
// This also means the window can be empty.
assert.sameMembers(run([range(-1, -1)]), [
// Near the partition boundary, no documents fall in the window.
{x: 0, y: []},
{x: 1, y: [0]},
// The window can also be empty in the middle of a partition, because of gaps.
// Here, the only value that would fit is 0.5, which doesn't occur.
{x: 1.5, y: []},
{x: 2, y: [1]},
{x: 3, y: [2]},
{x: 100, y: []},
{x: 100, y: []},
{x: 101, y: [100, 100]},
]);
assert.sameMembers(run([range(+1, +1)]), [
{x: 0, y: [1]},
{x: 1, y: [2]},
{x: 1.5, y: []},
{x: 2, y: [3]},
{x: 3, y: []},
{x: 100, y: [101]},
{x: 100, y: [101]},
{x: 101, y: []},
]);
// The window can be empty even if it's unbounded on one side.
assert.sameMembers(run([range("unbounded", -99)]), [
{x: 0, y: []},
{x: 1, y: []},
{x: 1.5, y: []},
{x: 2, y: []},
{x: 3, y: []},
{x: 100, y: [0, 1]},
{x: 100, y: [0, 1]},
{x: 101, y: [0, 1, 1.5, 2]},
]);
assert.sameMembers(run([range(+99, "unbounded")]), [
{x: 0, y: [100, 100, 101]},
{x: 1, y: [100, 100, 101]},
{x: 1.5, y: [101]},
{x: 2, y: [101]},
{x: 3, y: []},
{x: 100, y: []},
{x: 100, y: []},
{x: 101, y: []},
]);
// Range-based windows reset between partitions.
assert.commandWorked(coll.updateMany({}, {$set: {partition: "A"}}));
assert.commandWorked(
coll.insert([
{partition: "B", x: 101},
{partition: "B", x: 102},
{partition: "B", x: 103},
]),
);
assert.sameMembers(run([range(-5, 0)]), [
{partition: "A", x: 0, y: [0]},
{partition: "A", x: 1, y: [0, 1]},
{partition: "A", x: 1.5, y: [0, 1, 1.5]},
{partition: "A", x: 2, y: [0, 1, 1.5, 2]},
{partition: "A", x: 3, y: [0, 1, 1.5, 2, 3]},
{partition: "A", x: 100, y: [100, 100]},
{partition: "A", x: 100, y: [100, 100]},
{partition: "A", x: 101, y: [100, 100, 101]},
{partition: "B", x: 101, y: [101]},
{partition: "B", x: 102, y: [101, 102]},
{partition: "B", x: 103, y: [101, 102, 103]},
]);
assert.commandWorked(coll.deleteMany({partition: "B"}));
assert.commandWorked(coll.updateMany({}, [{$unset: "partition"}]));
// Empty window vs no window:
// If no documents fall in the window, we evaluate the accumulator on zero documents.
// This makes sense for $push (and $sum), which has an identity element.
// But if the current document's sortBy is non-numeric, we really can't define a window at all,
// so it's an error.
assert.sameMembers(run([range(+999, +999)]), [
{x: 0, y: []},
{x: 1, y: []},
{x: 1.5, y: []},
{x: 2, y: []},
{x: 3, y: []},
{x: 100, y: []},
{x: 100, y: []},
{x: 101, y: []},
]);
coll.insert([{}, {x: null}, {x: ""}, {x: {}}]);
let error;
error = assert.throws(() => run([range(+999, +999)]));
assert.includes(error.message, "Invalid range: Expected the sortBy field to be a number");
error = assert.throws(() => run([range(-999, +999)]));
assert.includes(error.message, "Invalid range: Expected the sortBy field to be a number");
error = assert.throws(() => run([range("unbounded", "unbounded")]));
assert.includes(error.message, "Invalid range: Expected the sortBy field to be a number");
// Another case, involving ties and expiration.
coll.drop();
coll.insert([{x: 0}, {x: 0}, {x: 0}, {x: 0}, {x: 3}, {x: 3}, {x: 3}]);
assert.sameMembers(run([range("unbounded", -3)]), [
{x: 0, y: []},
{x: 0, y: []},
{x: 0, y: []},
{x: 0, y: []},
{x: 3, y: [0, 0, 0, 0]},
{x: 3, y: [0, 0, 0, 0]},
{x: 3, y: [0, 0, 0, 0]},
]);
// Test variable evaluation for input expressions.
assert.sameMembers(
run([
{
$setWindowFields: {
partitionBy: "$partition",
sortBy: {x: 1},
output: {
y: {
$sum: {$filter: {input: [], as: "num", cond: {$gte: ["$$num", 2]}}},
window: {range: [-1, 1]},
},
},
},
},
{$unset: "_id"},
]),
[
{x: 0, y: 0},
{x: 0, y: 0},
{x: 0, y: 0},
{x: 0, y: 0},
{x: 3, y: 0},
{x: 3, y: 0},
{x: 3, y: 0},
],
);
// Test that all values in the executors are cleared between partitions.
coll.drop();
// Create values such that not all will be removed from the first partition and one will be removed
// from the second.
assert.commandWorked(
coll.insert([
{partitionBy: 1, time: new Date(2020, 1, 1, 0, 30, 0, 0), temp: 10},
{partitionBy: 1, time: new Date(2020, 1, 1, 1, 31, 0, 0), temp: 11},
{partitionBy: 1, time: new Date(2020, 1, 1, 1, 32, 0, 0), temp: 12},
{partitionBy: 1, time: new Date(2020, 1, 1, 1, 33, 0, 0), temp: 13},
{partitionBy: 2, time: new Date(2020, 1, 1, 2, 31, 0, 0), temp: 5},
{partitionBy: 2, time: new Date(2020, 1, 1, 2, 35, 0, 0), temp: 6},
{partitionBy: 2, time: new Date(2020, 1, 1, 3, 34, 0, 0), temp: 2},
]),
);
const pipeline = [
{
$setWindowFields: {
partitionBy: "$partitionBy",
sortBy: {time: 1},
output: {min: {$min: "$temp", window: {range: [-1, 0], unit: "hour"}}},
},
},
];
assert.commandWorked(db.runCommand({aggregate: coll.getName(), pipeline: pipeline, cursor: {}}));
// Test the behavior with regards NaN as the sort value.
coll.drop();
assert.commandWorked(coll.insert([{x: NaN}, {x: 0}]));
assert.sameMembers(
run([
{
$setWindowFields: {
sortBy: {x: 1},
output: {
y: {$push: "$x", window: {range: [0, 1]}},
},
},
},
{$unset: "_id"},
]),
[
{x: NaN, y: [NaN]},
{x: 0, y: [0]},
],
);