Files
mongo/jstests/aggregation/sources/setWindowFields/comprehensive_parse.js

264 lines
8.6 KiB
JavaScript

(function() {
"use strict";
const coll = db[jsTestName()];
coll.drop();
const nDocs = 10;
let currentDate = new Date();
for (let i = 0; i < nDocs; i++) {
assert.commandWorked(coll.insert({
a: i,
date: currentDate,
array: [],
partition: i % 2,
partitionSeq: Math.trunc(i / 2),
}));
const nextDate = currentDate.getDate() + 1;
currentDate.setDate(nextDate);
}
// The list of window functions to test.
const functions = {
sum: {$sum: '$a'},
avg: {$avg: '$a'},
stdDevSamp: {$stdDevSamp: '$a'},
stdDevPop: {$stdDevPop: '$a'},
min: {$min: '$a'},
max: {$max: '$a'},
count: {$count: {}},
derivative: {$derivative: {input: '$a'}},
derivative_date: {$derivative: {input: '$date', unit: 'millisecond'}},
integral: {$integral: {input: '$a'}},
integral_date: {$integral: {input: '$a', unit: 'millisecond'}},
covariancePop: {$covariancePop: ['$a', '$date']},
covarianceSamp: {$covarianceSamp: ['$a', '$date']},
expMovingAvgN: {$expMovingAvg: {input: '$a', N: 3}},
expMovingAvgAlpha: {$expMovingAvg: {input: '$a', alpha: 0.1}},
push: {$push: '$a'},
addToSet: {$addToSet: '$a'},
first: {$first: '$a'},
last: {$last: '$a'},
shift: {$shift: {output: '$a', by: 1, default: 0}},
documentNumber: {$documentNumber: {}},
rank: {$rank: {}},
denseRank: {$denseRank: {}}
};
// The list of window definitions to test.
const windows = {
none: null,
left_unbounded_doc: {documents: ['unbounded', 'current']},
right_unbounded_doc: {documents: ['current', 'unbounded']},
past_doc: {documents: [-1, 'current']},
future_doc: {documents: ['current', 1]},
centered_doc: {documents: [-1, 1]},
full_unbounded_range: {range: ['unbounded', 'unbounded']},
left_unbounded_range: {range: ['unbounded', 'current']},
right_unbounded_range: {range: ['current', 'unbounded']},
past_range: {range: [-2, 'current']},
future_range: {range: ['current', 2]},
centered_range: {range: [-2, 2]},
};
// The list of sort definitions to test.
const sortBys = {
none: null,
asc: {partitionSeq: 1},
desc: {partitionSeq: -1},
asc_date: {date: 1},
desc_date: {date: -1},
multi: {partitionSeq: 1, partition: 1},
};
// The list of partition definitions to test.
const partitionBys = {
none: null,
field: '$partition',
dynamic_array: '$array',
static_array: {$const: []},
};
// Given an element from each of the lists above, construct a
// $setWindowFields stage.
function constructQuery(wf, window, sortBy, partitionBy) {
let pathArg = Object.assign({}, wf);
if (window != null) {
Object.assign(pathArg, {window: window});
}
let setWindowFieldsArg = {};
if (sortBy != null) {
setWindowFieldsArg.sortBy = sortBy;
}
if (partitionBy != null) {
setWindowFieldsArg.partitionBy = partitionBy;
}
setWindowFieldsArg.output = {x: pathArg};
return {$setWindowFields: setWindowFieldsArg};
}
// Given an element of each of the lists above, what is the expected
// result. The output should be 'SKIP', 'OK' or the expected integer
// error code.
function expectedResult(wfType, windowType, sortType, partitionType) {
// Static errors all come first.
// Skip range windows over dates or that are over descending windows.
if (windowType.endsWith('range')) {
if (sortType.endsWith('date') || sortType.startsWith('desc')) {
return 'SKIP';
}
}
// Derivative and integral require an ascending sort
// and an explicit window.
if (wfType.startsWith('derivative')) {
// Derivative requires a sort and an explicit window.
if (sortType == 'none' || windowType == 'none') {
return ErrorCodes.FailedToParse;
}
// Integral requires single column sort
if (sortType == 'multi') {
return ErrorCodes.FailedToParse;
}
} else if (wfType.startsWith('integral')) {
// Integral requires a sort.
if (sortType == 'none') {
return ErrorCodes.FailedToParse;
}
// Integral requires single column sort
if (sortType == 'multi') {
return ErrorCodes.FailedToParse;
}
} else if (wfType.startsWith('expMovingAvg')) {
// $expMovingAvg doesn't accept a window.
if (windowType != 'none') {
return ErrorCodes.FailedToParse;
}
// $expMovingAvg requires a sort.
if (sortType == 'none') {
return ErrorCodes.FailedToParse;
}
} else if (wfType == 'documentNumber' || wfType == 'rank' || wfType == 'denseRank') {
if (windowType != 'none') {
// Rank style window functions take no other arguments.
return 5371601;
}
if (sortType == 'none') {
// %s must be specified with a top level sortBy expression with exactly one element.
return 5371602;
}
if (sortType == 'multi') {
// %s must be specified with a top level sortBy expression with exactly one element.
return 5371602;
}
} else if (wfType == 'shift') {
// $shift requires a sortBy and can't have defined a window.
if (sortType == 'none' || windowType != 'none') {
return ErrorCodes.FailedToParse;
}
}
// Document windows require a sortBy.
if (windowType.endsWith('doc') && sortType == 'none') {
// 'Document-based bounds require a sortBy'.
return 5339901;
}
// Range based windows require a sort over a single field.
if (windowType.endsWith('range') && (sortType == 'none' || sortType == 'multi')) {
// 'Range-based window require sortBy a single field'.
return 5339902;
}
if (partitionType === 'static_array') {
// When we parse $setWindowFields, we check whether partitionBy is a constant; if so we can
// drop the partitionBy clause. However, if the constant value is an array, we want to
// throw an error to make it clear that partitioning by an array is not supported.
return ErrorCodes.TypeMismatch;
}
// Dynamic errors all come after this point.
if (partitionType === 'dynamic_array') {
// At runtime, we raise an error if partitionBy evaluates to an array. We chose not to
// support partitioning by an array because $sort (which has multikey semantics)
// doesn't partition arrays.
return ErrorCodes.TypeMismatch;
}
if (wfType.startsWith('derivative')) {
if (wfType == 'derivative_date' && !sortType.endsWith('date')) {
// "$derivative with unit expects the sortBy field to be a Date".
return 5624900;
}
if (sortType.endsWith('date') && wfType != 'derivative_date') {
// "$derivative where the sortBy is a Date requires a 'unit'.
return 5624901;
}
} else if (wfType.startsWith('integral')) {
if (wfType == 'integral_date' && !sortType.endsWith('date')) {
// "$integral with unit expects the sortBy field to be a Date"
return 5423901;
}
if (sortType.endsWith('date') && wfType != 'integral_date') {
// $integral where the sortBy is a Date requires a 'unit'
return 5423902;
}
}
return ErrorCodes.OK;
}
// Generate all combinations of the elements in the lists above,
// one element per list.
function* makeTests() {
for (const [wfType, wfDefinition] of Object.entries(functions)) {
for (const [windowType, windowDefinition] of Object.entries(windows)) {
for (const [sortType, sortDefinition] of Object.entries(sortBys)) {
for (const [partitionType, partitionDefinition] of Object.entries(partitionBys)) {
let test = {
query: constructQuery(
wfDefinition, windowDefinition, sortDefinition, partitionDefinition),
expectedResult: expectedResult(wfType, windowType, sortType, partitionType)
};
if (test.expectedResult == 'SKIP') {
continue;
}
yield test;
}
}
}
}
}
// Run all the combinations generated in makeTests.
for (const test of makeTests()) {
if (test.expectedResult == ErrorCodes.OK) {
assert.commandWorked(
coll.runCommand({aggregate: coll.getName(), pipeline: [test.query], cursor: {}}));
} else {
assert.commandFailedWithCode(
coll.runCommand({aggregate: coll.getName(), pipeline: [test.query], cursor: {}}),
test.expectedResult);
}
}
})();