262 lines
10 KiB
JavaScript
262 lines
10 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?
|
|
*/
|
|
|
|
(function() {
|
|
"use strict";
|
|
|
|
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}}), 40415);
|
|
|
|
// 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');
|
|
})();
|