Files
mongo/jstests/aggregation/sources/setWindowFields/parse.js
natalie-hill fe20ecd3ff SERVER-83884 Add window function for mergeObjects accumulator (#46866)
GitOrigin-RevId: 165b85cf3cebd97c46ef06cf093de381a8cdb06f
2026-01-28 20:15:09 +00:00

273 lines
9.8 KiB
JavaScript

/**
* Test the user-facing syntax of $setWindowFields. For example:
* - Which options are allowed?
* - When is an expression expected, vs a constant?
* - Which window functions accept bounds?
* - When something is not allowed, what error message do we expect?
*/
const coll = db.setWindowFields_parse;
coll.drop();
function run(stage, extraCommandArgs = {}) {
return coll.runCommand(Object.merge({aggregate: coll.getName(), pipeline: [stage], cursor: {}}, extraCommandArgs));
}
// Test that the stage spec must be an object.
assert.commandFailedWithCode(run({$setWindowFields: "invalid"}), ErrorCodes.FailedToParse);
// Test that the stage parameters are the correct type.
assert.commandFailedWithCode(run({$setWindowFields: {sortBy: "invalid"}}), ErrorCodes.TypeMismatch);
assert.commandFailedWithCode(run({$setWindowFields: {output: "invalid"}}), ErrorCodes.TypeMismatch);
// Test that parsing fails for an invalid partitionBy expression.
assert.commandFailedWithCode(
run({$setWindowFields: {partitionBy: {$notAnOperator: 1}, output: {}}}),
ErrorCodes.InvalidPipelineOperator,
);
// Since partitionBy can be any expression, it can be a variable.
assert.commandWorked(run({$setWindowFields: {partitionBy: "$$NOW", output: {}}}));
assert.commandWorked(run({$setWindowFields: {partitionBy: "$$myobj.a", output: {}}}, {let: {myobj: {a: 456}}}));
// Test that parsing fails for unrecognized parameters.
assert.commandFailedWithCode(run({$setWindowFields: {what_is_this: 1}}), ErrorCodes.IDLUnknownField);
// Test for a successful parse, ignoring the response documents.
assert.commandWorked(
run({
$setWindowFields: {
partitionBy: "$state",
sortBy: {city: 1},
output: {a: {$sum: 1, window: {documents: ["unbounded", "current"]}}},
},
}),
);
function runWindowFunction(spec) {
// Include a single-field sortBy in this helper to allow all kinds of bounds.
return run({$setWindowFields: {sortBy: {ts: 1}, output: {v: spec}}});
}
// The most basic case: $sum everything.
assert.commandWorked(runWindowFunction({$sum: "$a"}));
// That's equivalent to bounds of [unbounded, unbounded].
assert.commandWorked(runWindowFunction({$sum: "$a", window: {documents: ["unbounded", "unbounded"]}}));
// Extra arguments to a window function are rejected.
assert.commandFailedWithCode(
runWindowFunction({abcde: 1}),
ErrorCodes.FailedToParse,
"Window function $sum found an unknown argument: abcde",
);
// Bounds can be bounded, or bounded on one side.
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: [-2, +4]}}));
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: [-3, "unbounded"]}}));
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: ["unbounded", +5]}}));
assert.commandWorked(runWindowFunction({"$max": "$a", window: {documents: [-3, "unbounded"]}}));
// Range-based bounds:
assert.commandWorked(runWindowFunction({$sum: "$a", window: {range: ["unbounded", "unbounded"]}}));
assert.commandWorked(runWindowFunction({$sum: "$a", window: {range: [-2, +4]}}));
assert.commandWorked(runWindowFunction({$sum: "$a", window: {range: [-3, "unbounded"]}}));
assert.commandWorked(runWindowFunction({$sum: "$a", window: {range: ["unbounded", +5]}}));
assert.commandWorked(runWindowFunction({$sum: "$a", window: {range: [NumberDecimal("1.42"), NumberLong(5)]}}));
// Time-based bounds:
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {range: [-3, "unbounded"], unit: "hour"}}));
// Numeric bounds can be a constant expression:
let expr = {$add: [2, 2]};
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: [expr, expr]}}));
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {range: [expr, expr]}}));
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {range: [expr, expr], unit: "hour"}}));
// But 'current' and 'unbounded' are not expressions: they're more like keywords.
assert.commandFailedWithCode(
runWindowFunction({"$sum": "$a", window: {documents: [{$const: "current"}, 3]}}),
ErrorCodes.FailedToParse,
"Numeric document-based bounds must be an integer",
);
assert.commandFailedWithCode(
runWindowFunction({"$sum": "$a", range: [{$const: "current"}, 3]}),
ErrorCodes.FailedToParse,
"Range-based bounds expression must be a number",
);
// Bounds must not be backwards.
function badBounds(bounds) {
assert.commandFailedWithCode(
runWindowFunction(Object.merge({"$sum": "$a"}, {window: bounds})),
5339900,
"Lower bound must not exceed upper bound",
);
}
badBounds({documents: [+1, -1]});
badBounds({range: [+1, -1]});
badBounds({range: [+1, -1], unit: "day"});
badBounds({documents: ["current", -1]});
badBounds({range: ["current", -1]});
badBounds({range: ["current", -1], unit: "day"});
badBounds({documents: [+1, "current"]});
badBounds({range: [+1, "current"]});
badBounds({range: [+1, "current"], unit: "day"});
// Any bound besides [unbounded, unbounded] requires a sort:
// - document-based
assert.commandWorked(
run({
$setWindowFields: {output: {v: {$sum: "$a", window: {documents: ["unbounded", "unbounded"]}}}},
}),
);
assert.commandFailedWithCode(
run({
$setWindowFields: {output: {v: {$sum: "$a", window: {documents: ["unbounded", "current"]}}}},
}),
5339901,
"Document-based bounds require a sortBy",
);
// - range-based
assert.commandFailedWithCode(
run({
$setWindowFields: {output: {v: {$sum: "$a", window: {range: ["unbounded", "unbounded"]}}}},
}),
5339902,
"Range-based bounds require sortBy a single field",
);
assert.commandFailedWithCode(
run({
$setWindowFields: {
sortBy: {a: 1, b: 1},
output: {v: {$sum: "$a", window: {range: ["unbounded", "unbounded"]}}},
},
}),
5339902,
"Range-based bounds require sortBy a single field",
);
assert.commandFailedWithCode(
run({$setWindowFields: {output: {v: {$sum: "$a", window: {range: ["unbounded", "current"]}}}}}),
5339902,
"Range-based bounds require sortBy a single field",
);
assert.commandFailedWithCode(
run({
$setWindowFields: {
sortBy: {a: 1, b: 1},
output: {v: {$sum: "$a", window: {range: ["unbounded", "current"]}}},
},
}),
5339902,
"Range-based bounds require sortBy a single field",
);
// - time-based
assert.commandFailedWithCode(
run({
$setWindowFields: {output: {v: {$sum: "$a", window: {range: ["unbounded", "unbounded"], unit: "second"}}}},
}),
5339902,
);
assert.commandFailedWithCode(
run({
$setWindowFields: {
sortBy: {a: 1, b: 1},
output: {v: {$sum: "$a", window: {range: ["unbounded", "unbounded"], unit: "second"}}},
},
}),
5339902,
);
assert.commandFailedWithCode(
run({
$setWindowFields: {output: {v: {$sum: "$a", window: {range: ["unbounded", "current"], unit: "second"}}}},
}),
5339902,
"Range-based bounds require sortBy a single field",
);
assert.commandFailedWithCode(
run({
$setWindowFields: {
sortBy: {a: 1, b: 1},
output: {v: {$sum: "$a", window: {range: ["unbounded", "current"], unit: "second"}}},
},
}),
5339902,
"Range-based bounds require sortBy a single field",
);
// Variety of accumulators:
assert.commandWorked(
run({
$setWindowFields: {sortBy: {ts: 1}, output: {v: {$sum: "$a", window: {documents: ["unbounded", "current"]}}}},
}),
);
assert.commandWorked(
run({
$setWindowFields: {sortBy: {ts: 1}, output: {v: {$avg: "$a", window: {documents: ["unbounded", "current"]}}}},
}),
);
assert.commandWorked(
run({
$setWindowFields: {sortBy: {ts: 1}, output: {v: {$max: "$a", window: {documents: ["unbounded", "current"]}}}},
}),
);
assert.commandWorked(
run({
$setWindowFields: {sortBy: {ts: 1}, output: {v: {$min: "$a", window: {documents: ["unbounded", "current"]}}}},
}),
);
assert.commandWorked(
run({
$setWindowFields: {output: {v: {$mergeObjects: "$a"}}},
}),
);
// Not every accumulator is automatically a window function.
let err = assert.commandFailedWithCode(
run({$setWindowFields: {output: {a: {b: {$sum: "$a"}}}}}),
ErrorCodes.FailedToParse,
);
assert.includes(err.errmsg, "Expected a $-prefixed window function, b");
err = assert.commandFailedWithCode(
run({$setWindowFields: {output: {total: {sum: "$x", window: {documents: [-1, 1]}}}}}),
ErrorCodes.FailedToParse,
);
assert.includes(err.errmsg, "Expected a $-prefixed window function, sum");
err = assert.commandFailedWithCode(run({$setWindowFields: {output: {total: {}}}}), ErrorCodes.FailedToParse);
assert.includes(err.errmsg, "Expected a $-prefixed window function");
err = assert.commandFailedWithCode(
run({$setWindowFields: {output: {v: {$accumulator: "$a"}}}}),
ErrorCodes.FailedToParse,
);
assert.includes(err.errmsg, "Unrecognized window function, $accumulator");
err = assert.commandFailedWithCode(
run({
$setWindowFields: {output: {total: {$summ: "$x", window: {documents: ["unbounded", "current"]}}}},
}),
ErrorCodes.FailedToParse,
);
assert.includes(err.errmsg, "Unrecognized window function, $summ");
err = assert.commandFailedWithCode(
run({
$setWindowFields: {output: {total: {$summ: "$x", windoww: {documents: ["unbounded", "current"]}}}},
}),
ErrorCodes.FailedToParse,
);
assert.includes(err.errmsg, "Unrecognized window function, $summ");
// Test that an empty object is a valid projected field.
assert.commandWorked(coll.insert({}));
assert.commandWorked(run({$setWindowFields: {output: {v: {$max: {mergeObjects: {}}}}}}));
// However conflicting field paths is always an error.
err = assert.commandFailedWithCode(run({$setWindowFields: {output: {a: {$sum: 1}, "a.b": {$sum: 1}}}}), 6307900);
assert.includes(err.errmsg, "specification contains two conflicting paths");