/** * 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"]}}}}, }), ); // 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: {$mergeObjects: "$a"}}}}), ErrorCodes.FailedToParse, ); assert.includes(err.errmsg, "Unrecognized window function, $mergeObjects"); 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");