summaryrefslogtreecommitdiff
path: root/jstests
diff options
context:
space:
mode:
Diffstat (limited to 'jstests')
-rw-r--r--jstests/aggregation/sources/setWindowFields/parse.js35
-rw-r--r--jstests/aggregation/sources/setWindowFields/range.js238
2 files changed, 254 insertions, 19 deletions
diff --git a/jstests/aggregation/sources/setWindowFields/parse.js b/jstests/aggregation/sources/setWindowFields/parse.js
index e8532a8bb7f..9adaa172464 100644
--- a/jstests/aggregation/sources/setWindowFields/parse.js
+++ b/jstests/aggregation/sources/setWindowFields/parse.js
@@ -16,10 +16,10 @@ if (!featureEnabled) {
return;
}
-const coll = db[jsTestName()];
+const coll = db.setWindowFields_parse;
coll.drop();
-assert.commandWorked(coll.insert({}));
+assert.commandWorked(coll.insert({ts: 0}));
function run(stage, extraCommandArgs = {}) {
return coll.runCommand(
@@ -79,16 +79,12 @@ assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: ['unbo
assert.commandWorked(runWindowFunction({"$max": "$a", window: {documents: [-3, 'unbounded']}}));
// Range-based bounds:
-assert.commandFailedWithCode(
- runWindowFunction({"$sum": "$a", window: {range: ['unbounded', 'unbounded']}}), 5397901);
-assert.commandFailedWithCode(runWindowFunction({"$sum": "$a", window: {range: [-2, +4]}}), 5397901);
-assert.commandFailedWithCode(runWindowFunction({"$sum": "$a", window: {range: [-3, 'unbounded']}}),
- 5397901);
-assert.commandFailedWithCode(runWindowFunction({"$sum": "$a", window: {range: ['unbounded', +5]}}),
- 5397901);
-assert.commandFailedWithCode(
- runWindowFunction({"$sum": "$a", window: {range: [NumberDecimal('1.42'), NumberLong(5)]}}),
- 5397901);
+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.commandFailedWithCode(
@@ -97,8 +93,7 @@ assert.commandFailedWithCode(
// Numeric bounds can be a constant expression:
let expr = {$add: [2, 2]};
assert.commandWorked(runWindowFunction({"$sum": "$a", window: {documents: [expr, expr]}}));
-assert.commandFailedWithCode(runWindowFunction({"$sum": "$a", window: {range: [expr, expr]}}),
- 5397901);
+assert.commandWorked(runWindowFunction({"$sum": "$a", window: {range: [expr, expr]}}));
assert.commandFailedWithCode(
runWindowFunction({"$sum": "$a", window: {range: [expr, expr], unit: 'hour'}}), 5397902);
// But 'current' and 'unbounded' are not expressions: they're more like keywords.
@@ -146,7 +141,8 @@ assert.commandFailedWithCode(
run({
$setWindowFields: {output: {v: {$sum: "$a", window: {range: ['unbounded', 'unbounded']}}}}
}),
- 5397901);
+ 5339902,
+ 'Range-based bounds require sortBy a single field');
assert.commandFailedWithCode(
run({
$setWindowFields: {
@@ -154,11 +150,12 @@ assert.commandFailedWithCode(
output: {v: {$sum: "$a", window: {range: ['unbounded', 'unbounded']}}}
}
}),
- 5397901);
+ 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 a sortBy a single field');
+ 'Range-based bounds require sortBy a single field');
assert.commandFailedWithCode(
run({
$setWindowFields: {
@@ -174,7 +171,7 @@ assert.commandFailedWithCode(
$setWindowFields:
{output: {v: {$sum: "$a", window: {range: ['unbounded', 'unbounded'], unit: 'second'}}}}
}),
- 5397902);
+ 5339902);
assert.commandFailedWithCode(
run({
$setWindowFields: {
@@ -182,7 +179,7 @@ assert.commandFailedWithCode(
output: {v: {$sum: "$a", window: {range: ['unbounded', 'unbounded'], unit: 'second'}}}
}
}),
- 5397902);
+ 5339902);
assert.commandFailedWithCode(
run({
$setWindowFields:
diff --git a/jstests/aggregation/sources/setWindowFields/range.js b/jstests/aggregation/sources/setWindowFields/range.js
new file mode 100644
index 00000000000..2d69555605d
--- /dev/null
+++ b/jstests/aggregation/sources/setWindowFields/range.js
@@ -0,0 +1,238 @@
+/**
+ * Test range-based window bounds.
+ */
+(function() {
+"use strict";
+
+load("jstests/aggregation/extras/window_function_helpers.js");
+
+const featureEnabled =
+ assert.commandWorked(db.adminCommand({getParameter: 1, featureFlagWindowFunctions: 1}))
+ .featureFlagWindowFunctions.value;
+if (!featureEnabled) {
+ jsTestLog("Skipping test because the window function feature flag is disabled");
+ return;
+}
+
+const coll = db.setWindowFields_range;
+coll.drop();
+
+assert.commandWorked(coll.insert([
+ {x: 0},
+ {x: 1},
+ {x: 1.5},
+ {x: 2},
+ {x: 3},
+ {x: 100},
+ {x: 100},
+ {x: 101},
+]));
+
+// Make a setWindowFields stage with the given bounds.
+function range(lower, upper) {
+ return {
+ $setWindowFields: {
+ partitionBy: "$partition",
+ sortBy: {x: 1},
+ output: {
+ y: {$push: "$x", window: {range: [lower, upper]}},
+ }
+ }
+ };
+}
+
+// Run the pipeline, and unset _id.
+function run(pipeline) {
+ return coll
+ .aggregate([
+ ...pipeline,
+ {$unset: '_id'},
+ ])
+ .toArray();
+}
+
+// The documents are not evenly spaced, so the window varies in size.
+assert.sameMembers(run([range(-1, 0)]), [
+ {x: 0, y: [0]},
+ {x: 1, y: [0, 1]},
+ {x: 1.5, y: [1, 1.5]},
+ {x: 2, y: [1, 1.5, 2]},
+ {x: 3, y: [2, 3]},
+ // '0' means the current document and those that tie with it.
+ {x: 100, y: [100, 100]},
+ {x: 100, y: [100, 100]},
+ {x: 101, y: [100, 100, 101]},
+]);
+
+// One or both endpoints can be unbounded.
+assert.sameMembers(run([range('unbounded', 0)]), [
+ {x: 0, y: [0]},
+ {x: 1, y: [0, 1]},
+ {x: 1.5, y: [0, 1, 1.5]},
+ {x: 2, y: [0, 1, 1.5, 2]},
+ {x: 3, y: [0, 1, 1.5, 2, 3]},
+ // '0' means current document and those that tie with it.
+ {x: 100, y: [0, 1, 1.5, 2, 3, 100, 100]},
+ {x: 100, y: [0, 1, 1.5, 2, 3, 100, 100]},
+ {x: 101, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+]);
+assert.sameMembers(run([range(0, 'unbounded')]), [
+ {x: 0, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+ {x: 1, y: [1, 1.5, 2, 3, 100, 100, 101]},
+ {x: 1.5, y: [1.5, 2, 3, 100, 100, 101]},
+ {x: 2, y: [2, 3, 100, 100, 101]},
+ {x: 3, y: [3, 100, 100, 101]},
+ // '0' means current document and those that tie with it.
+ {x: 100, y: [100, 100, 101]},
+ {x: 100, y: [100, 100, 101]},
+ {x: 101, y: [101]},
+]);
+assert.sameMembers(run([range('unbounded', 'unbounded')]), [
+ {x: 0, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+ {x: 1, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+ {x: 1.5, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+ {x: 2, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+ {x: 3, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+ {x: 100, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+ {x: 100, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+ {x: 101, y: [0, 1, 1.5, 2, 3, 100, 100, 101]},
+]);
+
+// Unlike '0', 'current' always means the current document.
+assert.sameMembers(run([range('current', 'current'), {$match: {x: 100}}]), [
+ {x: 100, y: [100]},
+ {x: 100, y: [100]},
+]);
+assert.sameMembers(run([range('current', +1), {$match: {x: 100}}]), [
+ {x: 100, y: [100, 100, 101]},
+ {x: 100, y: [100, 101]},
+]);
+assert.sameMembers(run([range(-97, 'current'), {$match: {x: 100}}]), [
+ {x: 100, y: [3, 100]},
+ {x: 100, y: [3, 100, 100]},
+]);
+
+// The window doesn't have to contain the current document.
+// This also means the window can be empty.
+assert.sameMembers(run([range(-1, -1)]), [
+ // Near the partition boundary, no documents fall in the window.
+ {x: 0, y: []},
+ {x: 1, y: [0]},
+ // The window can also be empty in the middle of a partition, because of gaps.
+ // Here, the only value that would fit is 0.5, which doesn't occur.
+ {x: 1.5, y: []},
+ {x: 2, y: [1]},
+ {x: 3, y: [2]},
+ {x: 100, y: []},
+ {x: 100, y: []},
+ {x: 101, y: [100, 100]},
+]);
+assert.sameMembers(run([range(+1, +1)]), [
+ {x: 0, y: [1]},
+ {x: 1, y: [2]},
+ {x: 1.5, y: []},
+ {x: 2, y: [3]},
+ {x: 3, y: []},
+ {x: 100, y: [101]},
+ {x: 100, y: [101]},
+ {x: 101, y: []},
+]);
+
+// The window can be empty even if it's unbounded on one side.
+assert.sameMembers(run([range('unbounded', -99)]), [
+ {x: 0, y: []},
+ {x: 1, y: []},
+ {x: 1.5, y: []},
+ {x: 2, y: []},
+ {x: 3, y: []},
+ {x: 100, y: [0, 1]},
+ {x: 100, y: [0, 1]},
+ {x: 101, y: [0, 1, 1.5, 2]},
+]);
+assert.sameMembers(run([range(+99, 'unbounded')]), [
+ {x: 0, y: [100, 100, 101]},
+ {x: 1, y: [100, 100, 101]},
+ {x: 1.5, y: [101]},
+ {x: 2, y: [101]},
+ {x: 3, y: []},
+ {x: 100, y: []},
+ {x: 100, y: []},
+ {x: 101, y: []},
+]);
+
+// Range-based windows reset between partitions.
+assert.commandWorked(coll.updateMany({}, {$set: {partition: "A"}}));
+assert.commandWorked(coll.insert([
+ {partition: "B", x: 101},
+ {partition: "B", x: 102},
+ {partition: "B", x: 103},
+]));
+assert.sameMembers(run([range(-5, 0)]), [
+ {partition: "A", x: 0, y: [0]},
+ {partition: "A", x: 1, y: [0, 1]},
+ {partition: "A", x: 1.5, y: [0, 1, 1.5]},
+ {partition: "A", x: 2, y: [0, 1, 1.5, 2]},
+ {partition: "A", x: 3, y: [0, 1, 1.5, 2, 3]},
+ {partition: "A", x: 100, y: [100, 100]},
+ {partition: "A", x: 100, y: [100, 100]},
+ {partition: "A", x: 101, y: [100, 100, 101]},
+
+ {partition: "B", x: 101, y: [101]},
+ {partition: "B", x: 102, y: [101, 102]},
+ {partition: "B", x: 103, y: [101, 102, 103]},
+]);
+assert.commandWorked(coll.deleteMany({partition: "B"}));
+assert.commandWorked(coll.updateMany({}, [{$unset: 'partition'}]));
+
+// Empty window vs no window:
+// If no documents fall in the window, we evaluate the accumulator on zero documents.
+// This makes sense for $push (and $sum), which has an identity element.
+// But if the current document's sortBy is non-numeric, we really can't define a window at all,
+// so it's an error.
+assert.sameMembers(run([range(+999, +999)]), [
+ {x: 0, y: []},
+ {x: 1, y: []},
+ {x: 1.5, y: []},
+ {x: 2, y: []},
+ {x: 3, y: []},
+ {x: 100, y: []},
+ {x: 100, y: []},
+ {x: 101, y: []},
+]);
+coll.insert([
+ {},
+ {x: null},
+ {x: ''},
+ {x: {}},
+]);
+assert.throws(() => {
+ run([range(+999, +999)]);
+}, [], 'Invalid range: Expected the sortBy field to be a number');
+assert.throws(() => {
+ run([range(-999, +999)]);
+}, [], 'Invalid range: Expected the sortBy field to be a number');
+assert.throws(() => {
+ run([range('unbounded', 'unbounded')]);
+}, [], 'Invalid range: Expected the sortBy field to be a number');
+
+// Another case, involving ties and expiration.
+coll.drop();
+coll.insert([
+ {x: 0},
+ {x: 0},
+ {x: 0},
+ {x: 0},
+ {x: 3},
+ {x: 3},
+ {x: 3},
+]);
+assert.sameMembers(run([range('unbounded', -3)]), [
+ {x: 0, y: []},
+ {x: 0, y: []},
+ {x: 0, y: []},
+ {x: 0, y: []},
+ {x: 3, y: [0, 0, 0, 0]},
+ {x: 3, y: [0, 0, 0, 0]},
+ {x: 3, y: [0, 0, 0, 0]},
+]);
+})();