395 lines
14 KiB
JavaScript
395 lines
14 KiB
JavaScript
/**
|
|
* Test the behavior of $derivative.
|
|
*/
|
|
(function() {
|
|
"use strict";
|
|
|
|
const coll = db.setWindowFields_derivative;
|
|
|
|
// The default window is usually [unbounded, unbounded], but this would be surprising for
|
|
// $derivative, so instead it has no default (it requires an explicit window).
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: 0, y: 0},
|
|
{time: 1, y: 42},
|
|
{time: 3, y: 67},
|
|
{time: 7, y: 99},
|
|
{time: 10, y: 20},
|
|
]));
|
|
let result = coll.runCommand({
|
|
explain: {
|
|
aggregate: coll.getName(),
|
|
cursor: {},
|
|
pipeline: [
|
|
{
|
|
$setWindowFields: {
|
|
sortBy: {time: 1},
|
|
output: {
|
|
dy: {$derivative: {input: "$y"}},
|
|
}
|
|
}
|
|
},
|
|
]
|
|
}
|
|
});
|
|
assert.commandFailedWithCode(
|
|
result, ErrorCodes.FailedToParse, "$derivative requires explicit window bounds");
|
|
|
|
// $derivative never compares values from separate partitions.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{sensor: "A", time: 0, y: 1},
|
|
{sensor: "A", time: 1, y: 2},
|
|
{sensor: "A", time: 2, y: 1},
|
|
{sensor: "A", time: 3, y: 4},
|
|
|
|
{sensor: "B", time: 0, y: 100},
|
|
{sensor: "B", time: 1, y: 105},
|
|
{sensor: "B", time: 2, y: 107},
|
|
{sensor: "B", time: 3, y: 104},
|
|
]));
|
|
result = coll.aggregate([
|
|
{
|
|
$setWindowFields: {
|
|
partitionBy: "$sensor",
|
|
sortBy: {time: 1},
|
|
output: {
|
|
dy: {$derivative: {input: "$y"}, window: {documents: [-1, 0]}},
|
|
}
|
|
}
|
|
},
|
|
{$unset: "_id"},
|
|
])
|
|
.toArray();
|
|
assert.sameMembers(result, [
|
|
{sensor: "A", time: 0, y: 1, dy: null},
|
|
{sensor: "A", time: 1, y: 2, dy: +1},
|
|
{sensor: "A", time: 2, y: 1, dy: -1},
|
|
{sensor: "A", time: 3, y: 4, dy: +3},
|
|
|
|
{sensor: "B", time: 0, y: 100, dy: null},
|
|
{sensor: "B", time: 1, y: 105, dy: +5},
|
|
{sensor: "B", time: 2, y: 107, dy: +2},
|
|
{sensor: "B", time: 3, y: 104, dy: -3},
|
|
]);
|
|
|
|
// When either endpoint lies outside the partition, we use the first/last document in the partition
|
|
// instead.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: 0, y: 100},
|
|
{time: 1, y: 105},
|
|
{time: 2, y: 108},
|
|
{time: 3, y: 108},
|
|
{time: 4, y: 115},
|
|
{time: 5, y: 115},
|
|
{time: 6, y: 118},
|
|
{time: 7, y: 118},
|
|
]));
|
|
result = coll.aggregate([
|
|
{
|
|
$setWindowFields: {
|
|
sortBy: {time: 1},
|
|
output: {
|
|
dy: {$derivative: {input: "$y"}, window: {documents: [-3, +1]}},
|
|
}
|
|
}
|
|
},
|
|
{$unset: "_id"},
|
|
{$sort: {time: 1}},
|
|
])
|
|
.toArray();
|
|
assert.docEq(
|
|
[
|
|
// The first document looks behind 3, but can't go any further back than time: 0.
|
|
// It also looks ahead 1. So the points it compares are time: 0 and time: 1.
|
|
{time: 0, y: 100, dy: +5},
|
|
// The second document gets time: 0 and time: 2.
|
|
{time: 1, y: 105, dy: +8 / 2},
|
|
// The third gets time: 0 and time: 3.
|
|
{time: 2, y: 108, dy: +8 / 3},
|
|
// This is the first document whose left endpoint lies within the partition.
|
|
// So this one, and the next few, all have fully-populated windows.
|
|
{time: 3, y: 108, dy: +15 / 4},
|
|
{time: 4, y: 115, dy: +10 / 4},
|
|
{time: 5, y: 115, dy: +10 / 4},
|
|
{time: 6, y: 118, dy: +10 / 4},
|
|
// For the last document, there is no document at offset +1, so it sees
|
|
// time: 4 and time: 7.
|
|
{time: 7, y: 118, dy: +3 / 3},
|
|
],
|
|
result);
|
|
// Because the derivative is the same irrespective of sort order (as long as we reexpress the
|
|
// bounds) we can compare this result with the result of the previous aggregation.
|
|
const resultDesc =
|
|
coll.aggregate([
|
|
{
|
|
$setWindowFields: {
|
|
sortBy: {time: -1},
|
|
output: {
|
|
dy: {$derivative: {input: "$y"}, window: {documents: [-1, +3]}},
|
|
}
|
|
}
|
|
},
|
|
{$unset: "_id"},
|
|
{$sort: {time: 1}},
|
|
])
|
|
.toArray();
|
|
assert.docEq(resultDesc, result);
|
|
|
|
// Example with range-based bounds.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: 0, y: 10},
|
|
{time: 10, y: 12},
|
|
{time: 11, y: 15},
|
|
{time: 12, y: 19},
|
|
{time: 13, y: 24},
|
|
{time: 20, y: 30},
|
|
]));
|
|
result = coll.aggregate([
|
|
{
|
|
$setWindowFields: {
|
|
sortBy: {time: 1},
|
|
output: {
|
|
dy: {$derivative: {input: "$y"}, window: {range: [-10, 0]}},
|
|
}
|
|
}
|
|
},
|
|
{$unset: "_id"},
|
|
{$sort: {time: 1}},
|
|
])
|
|
.toArray();
|
|
assert.docEq(
|
|
[
|
|
{time: 0, y: 10, dy: null},
|
|
{time: 10, y: 12, dy: (12 - 10) / (10 - 0)},
|
|
{time: 11, y: 15, dy: (15 - 12) / (11 - 10)},
|
|
{time: 12, y: 19, dy: (19 - 12) / (12 - 10)},
|
|
{time: 13, y: 24, dy: (24 - 12) / (13 - 10)},
|
|
{time: 20, y: 30, dy: (30 - 12) / (20 - 10)},
|
|
],
|
|
result);
|
|
|
|
// 'unit' only supports 'week' and smaller.
|
|
coll.drop();
|
|
function derivativeStage(unit) {
|
|
const stage = {
|
|
$setWindowFields: {
|
|
sortBy: {time: 1},
|
|
output: {
|
|
dy: {
|
|
$derivative: {
|
|
input: "$y",
|
|
},
|
|
window: {documents: [-1, 0]}
|
|
},
|
|
}
|
|
}
|
|
};
|
|
if (unit) {
|
|
stage.$setWindowFields.output.dy.$derivative.unit = unit;
|
|
}
|
|
return stage;
|
|
}
|
|
function explainUnit(unit) {
|
|
return coll.runCommand(
|
|
{explain: {aggregate: coll.getName(), cursor: {}, pipeline: [derivativeStage(unit)]}});
|
|
}
|
|
assert.commandFailedWithCode(explainUnit('year'), 5490710);
|
|
assert.commandFailedWithCode(explainUnit('quarter'), 5490710);
|
|
assert.commandFailedWithCode(explainUnit('month'), 5490710);
|
|
assert.commandWorked(explainUnit('week'));
|
|
assert.commandWorked(explainUnit('day'));
|
|
assert.commandWorked(explainUnit('hour'));
|
|
assert.commandWorked(explainUnit('minute'));
|
|
assert.commandWorked(explainUnit('second'));
|
|
assert.commandWorked(explainUnit('millisecond'));
|
|
|
|
// When the time field is numeric, 'unit' is not allowed.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: 0, y: 100},
|
|
{time: 1, y: 100},
|
|
{time: 2, y: 100},
|
|
]));
|
|
assert.throwsWithCode(() => coll.aggregate(derivativeStage('millisecond')).toArray(), 5624900);
|
|
result = coll.aggregate([derivativeStage(), {$unset: '_id'}]).toArray();
|
|
assert.sameMembers(result, [
|
|
{time: 0, y: 100, dy: null},
|
|
{time: 1, y: 100, dy: 0},
|
|
{time: 2, y: 100, dy: 0},
|
|
]);
|
|
|
|
// When the time field is a Date, 'unit' is required.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: ISODate("2020-01-01T00:00:00.000Z"), y: 5},
|
|
{time: ISODate("2020-01-01T00:00:00.001Z"), y: 4},
|
|
{time: ISODate("2020-01-01T00:00:00.002Z"), y: 6},
|
|
{time: ISODate("2020-01-01T00:00:00.003Z"), y: 5},
|
|
]));
|
|
assert.throwsWithCode(() => coll.aggregate(derivativeStage()).toArray(), 5624901);
|
|
result = coll.aggregate([derivativeStage('millisecond'), {$unset: '_id'}]).toArray();
|
|
assert.sameMembers(result, [
|
|
{time: ISODate("2020-01-01T00:00:00.000Z"), y: 5, dy: null},
|
|
{time: ISODate("2020-01-01T00:00:00.001Z"), y: 4, dy: -1},
|
|
{time: ISODate("2020-01-01T00:00:00.002Z"), y: 6, dy: +2},
|
|
{time: ISODate("2020-01-01T00:00:00.003Z"), y: 5, dy: -1},
|
|
]);
|
|
|
|
// The change per minute is 60*1000 larger than the change per millisecond.
|
|
result = coll.aggregate([derivativeStage('minute'), {$unset: "_id"}]).toArray();
|
|
assert.sameMembers(result, [
|
|
// 'unit' applied to an ISODate expresses the output in terms of that unit.
|
|
{time: ISODate("2020-01-01T00:00:00.000Z"), y: 5, dy: null},
|
|
{time: ISODate("2020-01-01T00:00:00.001Z"), y: 4, dy: -1 * 60 * 1000},
|
|
{time: ISODate("2020-01-01T00:00:00.002Z"), y: 6, dy: +2 * 60 * 1000},
|
|
{time: ISODate("2020-01-01T00:00:00.003Z"), y: 5, dy: -1 * 60 * 1000},
|
|
]);
|
|
|
|
// Going the other direction: if the events are spaced far apart, expressing the answer in
|
|
// change-per-millisecond makes the result small.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: ISODate("2020-01-01T00:00:00.000Z"), y: 5},
|
|
{time: ISODate("2020-01-01T00:01:00.000Z"), y: 4},
|
|
{time: ISODate("2020-01-01T00:02:00.000Z"), y: 6},
|
|
{time: ISODate("2020-01-01T00:03:00.000Z"), y: 5},
|
|
]));
|
|
result = coll.aggregate([
|
|
{
|
|
$setWindowFields: {
|
|
sortBy: {time: 1},
|
|
output: {
|
|
dy: {
|
|
$derivative: {input: "$y", unit: 'millisecond'},
|
|
window: {documents: [-1, 0]}
|
|
},
|
|
}
|
|
}
|
|
},
|
|
{$unset: "_id"},
|
|
])
|
|
.toArray();
|
|
assert.sameMembers(result, [
|
|
{time: ISODate("2020-01-01T00:00:00.000Z"), y: 5, dy: null},
|
|
{time: ISODate("2020-01-01T00:01:00.000Z"), y: 4, dy: -1 / (60 * 1000)},
|
|
{time: ISODate("2020-01-01T00:02:00.000Z"), y: 6, dy: +2 / (60 * 1000)},
|
|
{time: ISODate("2020-01-01T00:03:00.000Z"), y: 5, dy: -1 / (60 * 1000)},
|
|
]);
|
|
|
|
// When the sortBy field is a mixture of dates and numbers, it's an error:
|
|
// whether or not you specify unit, either the date or the number values
|
|
// will be an invalid type.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: ISODate("2020-01-01T00:00:00.000Z"), y: 0},
|
|
{time: ISODate("2020-01-01T00:01:00.000Z"), y: 0},
|
|
{time: 12, y: 0},
|
|
{time: 13, y: 0},
|
|
]));
|
|
assert.throwsWithCode(() => coll.aggregate(derivativeStage()).toArray(), 5624901);
|
|
assert.throwsWithCode(() => coll.aggregate(derivativeStage('second')).toArray(), 5624900);
|
|
|
|
// Some examples of unbounded windows.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: 0, y: 0},
|
|
{time: 1, y: 1},
|
|
{time: 2, y: 4},
|
|
{time: 3, y: 9},
|
|
]));
|
|
result = coll.aggregate([
|
|
{
|
|
$setWindowFields: {
|
|
sortBy: {time: 1},
|
|
output: {
|
|
dy: {
|
|
$derivative: {input: "$y"},
|
|
window: {
|
|
documents: ['unbounded', 'unbounded'],
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{$unset: '_id'},
|
|
])
|
|
.toArray();
|
|
assert.sameMembers(result, [
|
|
{time: 0, y: 0, dy: 9 / 3},
|
|
{time: 1, y: 1, dy: 9 / 3},
|
|
{time: 2, y: 4, dy: 9 / 3},
|
|
{time: 3, y: 9, dy: 9 / 3},
|
|
]);
|
|
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: ISODate('2020-01-01T00:00:00Z'), y: 0},
|
|
{time: ISODate('2020-01-01T00:00:01Z'), y: 1},
|
|
{time: ISODate('2020-01-01T00:00:02Z'), y: 4},
|
|
{time: ISODate('2020-01-01T00:00:03Z'), y: 9},
|
|
]));
|
|
result = coll.aggregate([
|
|
{
|
|
$setWindowFields: {
|
|
sortBy: {time: 1},
|
|
output: {
|
|
dy: {
|
|
$derivative: {input: "$y", unit: 'second'},
|
|
window: {
|
|
documents: ['unbounded', 'unbounded'],
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{$unset: '_id'},
|
|
])
|
|
.toArray();
|
|
assert.sameMembers(result, [
|
|
{time: ISODate('2020-01-01T00:00:00Z'), y: 0, dy: 9 / 3},
|
|
{time: ISODate('2020-01-01T00:00:01Z'), y: 1, dy: 9 / 3},
|
|
{time: ISODate('2020-01-01T00:00:02Z'), y: 4, dy: 9 / 3},
|
|
{time: ISODate('2020-01-01T00:00:03Z'), y: 9, dy: 9 / 3},
|
|
]);
|
|
|
|
// Example with time-based bounds.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert([
|
|
{time: ISODate("2020-01-01T00:00:00"), y: 10},
|
|
{time: ISODate("2020-01-01T00:00:10"), y: 12},
|
|
{time: ISODate("2020-01-01T00:00:11"), y: 15},
|
|
{time: ISODate("2020-01-01T00:00:12"), y: 19},
|
|
{time: ISODate("2020-01-01T00:00:13"), y: 24},
|
|
{time: ISODate("2020-01-01T00:00:20"), y: 30},
|
|
]));
|
|
result = coll.aggregate([
|
|
{
|
|
$setWindowFields: {
|
|
sortBy: {time: 1},
|
|
output: {
|
|
dy: {
|
|
$derivative: {input: "$y", unit: 'second'},
|
|
window: {range: [-10, 0], unit: 'second'}
|
|
},
|
|
}
|
|
}
|
|
},
|
|
{$unset: "_id"},
|
|
{$sort: {time: 1}},
|
|
])
|
|
.toArray();
|
|
assert.docEq(
|
|
[
|
|
{time: ISODate("2020-01-01T00:00:00.00Z"), y: 10, dy: null},
|
|
{time: ISODate("2020-01-01T00:00:10.00Z"), y: 12, dy: (12 - 10) / (10 - 0)},
|
|
{time: ISODate("2020-01-01T00:00:11.00Z"), y: 15, dy: (15 - 12) / (11 - 10)},
|
|
{time: ISODate("2020-01-01T00:00:12.00Z"), y: 19, dy: (19 - 12) / (12 - 10)},
|
|
{time: ISODate("2020-01-01T00:00:13.00Z"), y: 24, dy: (24 - 12) / (13 - 10)},
|
|
{time: ISODate("2020-01-01T00:00:20.00Z"), y: 30, dy: (30 - 12) / (20 - 10)},
|
|
],
|
|
result);
|
|
})();
|