diff options
author | David Percy <david.percy@mongodb.com> | 2021-03-05 21:21:47 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-04-19 19:15:48 +0000 |
commit | c3209ebb1db49656dcc3d1edf3635f7c7e96add5 (patch) | |
tree | b3f920a3df53b171145371c734ba9185dc3e3bdc /jstests | |
parent | 481bade9c40d662bf463291645bbf9fddce3a11f (diff) | |
download | mongo-c3209ebb1db49656dcc3d1edf3635f7c7e96add5.tar.gz |
SERVER-54294 Support range-based bounds for window functions
Diffstat (limited to 'jstests')
-rw-r--r-- | jstests/aggregation/sources/setWindowFields/parse.js | 35 | ||||
-rw-r--r-- | jstests/aggregation/sources/setWindowFields/range.js | 238 |
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]}, +]); +})(); |