/** * Test that $shift works as a window function. */ import {documentEq} from "jstests/aggregation/extras/utils.js"; const coll = db[jsTestName()]; coll.drop(); const nDocs = 10; for (let i = 0; i < nDocs; i++) { assert.commandWorked( coll.insert({ one: i, partition: i % 2, partitionSeq: Math.trunc(i / 2), }), ); } const lastDoc = nDocs - 1; const lastDocInPartition = nDocs / 2 - 1; const origDocs = coll.find().sort({_id: 1}); function verifyResults(results, valueFunction) { for (let i = 0; i < results.length; i++) { // Use Object.assign to make a copy instead of pass a reference. const correctDoc = valueFunction(i, Object.assign({}, origDocs[i])); assert( documentEq(correctDoc, results[i]), "Got: " + tojson(results[i]) + "\nExpected: " + tojson(correctDoc) + "\n at position " + i + "\n", ); } } // Run an unpartitioned shift query using the specified offset and default expression. function runShiftQuery(shiftBy, defaultVal) { return coll .aggregate([ { $setWindowFields: { sortBy: {one: 1}, output: {a: {$shift: {by: shiftBy, output: "$one", default: defaultVal}}}, }, }, {$sort: {_id: 1}}, ]) .toArray(); } // Run an unpartitioned shift query using the specified offset and the default default expression. function runShiftQueryWithoutDefault(shiftBy) { return coll .aggregate([ { $setWindowFields: {sortBy: {one: 1}, output: {a: {$shift: {by: shiftBy, output: "$one"}}}}, }, {$sort: {_id: 1}}, ]) .toArray(); } // Test left shift with default. let result = runShiftQuery(-1, -10); verifyResults(result, function (num, baseObj) { if (baseObj.one == 0) baseObj.a = -10; else baseObj.a = baseObj.one - 1; return baseObj; }); // Test left shift without default. result = runShiftQueryWithoutDefault(-1); verifyResults(result, function (num, baseObj) { if (baseObj.one == 0) baseObj.a = null; else baseObj.a = baseObj.one - 1; return baseObj; }); // Test 0 shift with default. result = runShiftQuery(0); verifyResults(result, function (num, baseObj) { baseObj.a = baseObj.one; return baseObj; }); // Test 0 shift without default. result = runShiftQueryWithoutDefault(0); verifyResults(result, function (num, baseObj) { baseObj.a = baseObj.one; return baseObj; }); // Test right shift with default. result = runShiftQuery(1, -10); verifyResults(result, function (num, baseObj) { if (baseObj.one == lastDoc) baseObj.a = -10; else baseObj.a = baseObj.one + 1; return baseObj; }); // Test right shift without default. result = runShiftQueryWithoutDefault(1); verifyResults(result, function (num, baseObj) { if (baseObj.one == lastDoc) baseObj.a = null; else baseObj.a = baseObj.one + 1; return baseObj; }); // Run an unpartitioned shift query using the specified offset with descending order. function runShiftQueryDescending(shiftBy) { return coll .aggregate([ { $setWindowFields: {sortBy: {one: -1}, output: {a: {$shift: {by: shiftBy, output: "$one"}}}}, }, {$sort: {_id: 1}}, ]) .toArray(); } // Test right shift with descending sort. result = runShiftQueryDescending(1); verifyResults(result, function (num, baseObj) { if (baseObj.one == 0) baseObj.a = null; else baseObj.a = baseObj.one - 1; return baseObj; }); // Test 0 shift with descending sort. result = runShiftQueryDescending(0); verifyResults(result, function (num, baseObj) { baseObj.a = baseObj.one; return baseObj; }); // Test left shift with descending sort. result = runShiftQueryDescending(-1); verifyResults(result, function (num, baseObj) { if (baseObj.one == lastDoc) baseObj.a = null; else baseObj.a = baseObj.one + 1; return baseObj; }); // Run a shift query partitioned over "$partition" using the specified shift and default // default expression. // // Partitioning is odd/even. function runPartitionedShiftQuery(shiftBy) { return coll .aggregate([ { $setWindowFields: { partitionBy: "$partition", sortBy: {one: 1}, output: {a: {$shift: {by: shiftBy, output: "$one"}}}, }, }, {$sort: {_id: 1}}, ]) .toArray(); } // Test partitioned left shift. result = runPartitionedShiftQuery(-1); verifyResults(result, function (num, baseObj) { if (baseObj.partitionSeq == 0) baseObj.a = null; else // partitioning is even/odd. baseObj.a = baseObj.one - 2; return baseObj; }); // Test partitioned right shift. result = runPartitionedShiftQuery(1); verifyResults(result, function (num, baseObj) { if (baseObj.partitionSeq == lastDocInPartition) baseObj.a = null; else // partitioning is even/odd. baseObj.a = baseObj.one + 2; return baseObj; }); // Test partitioned 0 shift. result = runPartitionedShiftQuery(0); verifyResults(result, function (num, baseObj) { baseObj.a = baseObj.one; return baseObj; }); // Run a shift query partitioned over "$partition" using the specified shift and default // default expression with a descending sort. // // Partitioning is odd/even. function runPartitionedShiftQueryDescending(shiftBy) { return coll .aggregate([ { $setWindowFields: { partitionBy: "$partition", sortBy: {one: -1}, output: {a: {$shift: {by: shiftBy, output: "$one"}}}, }, }, {$sort: {_id: 1}}, ]) .toArray(); } // Test partitioned left shift with descending sort. result = runPartitionedShiftQueryDescending(-1); verifyResults(result, function (num, baseObj) { if (baseObj.partitionSeq == lastDocInPartition) baseObj.a = null; else // partitioning is even/odd. baseObj.a = baseObj.one + 2; return baseObj; }); // Test partitioned right shift with descending sort. result = runPartitionedShiftQueryDescending(1); verifyResults(result, function (num, baseObj) { if (baseObj.partitionSeq == 0) baseObj.a = null; else // partitioning is even/odd. baseObj.a = baseObj.one - 2; return baseObj; }); // Test partitioned 0 shift with descending sort. result = runPartitionedShiftQuery(0); verifyResults(result, function (num, baseObj) { baseObj.a = baseObj.one; return baseObj; }); // Test $shift with default value. coll.drop(); assert.commandWorked(coll.insert([{_id: 1}, {_id: 2}])); result = coll .aggregate([ { $setWindowFields: {sortBy: {_id: 1}, output: {a: {$shift: {output: "$b", by: 1, default: "c"}}}}, }, ]) .toArray(); assert.eq( [ {_id: 1, a: "c"}, {_id: 2, a: "c"}, ], result, result, ); /* Parsing tests */ // "by" is required. assert.commandFailedWithCode( coll.runCommand({ aggregate: coll.getName(), pipeline: [{$setWindowFields: {sortBy: {one: 1}, output: {a: {$shift: {output: "$one"}}}}}], cursor: {}, }), ErrorCodes.FailedToParse, ); // Can't accept a string for "by". assert.commandFailedWithCode( coll.runCommand({ aggregate: coll.getName(), pipeline: [{$setWindowFields: {sortBy: {one: 1}, output: {a: {$shift: {by: "1", output: "$one"}}}}}], cursor: {}, }), ErrorCodes.FailedToParse, ); // Can't accept an expression for "by". assert.commandFailedWithCode( coll.runCommand({ aggregate: coll.getName(), pipeline: [ { $setWindowFields: {sortBy: {one: 1}, output: {a: {$shift: {by: {$sum: [1, 1]}, output: "$one"}}}}, }, ], cursor: {}, }), ErrorCodes.FailedToParse, ); // Can't accept a float for "by". assert.commandFailedWithCode( coll.runCommand({ aggregate: coll.getName(), pipeline: [{$setWindowFields: {sortBy: {one: 1}, output: {a: {$shift: {by: 1.1, output: "$one"}}}}}], cursor: {}, }), ErrorCodes.FailedToParse, ); // Can't accept a float for "by" ... unless it converts to int without loss of precision. assert.commandWorked( coll.runCommand({ aggregate: coll.getName(), pipeline: [{$setWindowFields: {sortBy: {one: 1}, output: {a: {$shift: {by: 1.0, output: "$one"}}}}}], cursor: {}, }), ); // "output" is required. assert.commandFailedWithCode( coll.runCommand({ aggregate: coll.getName(), pipeline: [{$setWindowFields: {sortBy: {one: 1}, output: {a: {$shift: {by: 1}}}}}], cursor: {}, }), ErrorCodes.FailedToParse, ); // "default" must evaluate to a constant. assert.commandFailedWithCode( coll.runCommand({ aggregate: coll.getName(), pipeline: [ { $setWindowFields: {sortBy: {one: 1}, output: {a: {$shift: {by: 1, output: "$one", default: "$one"}}}}, }, ], cursor: {}, }), ErrorCodes.FailedToParse, ); // "default" may be an arbitrary expression as long as it evaluates to a constant. assert.commandWorked( coll.runCommand({ aggregate: coll.getName(), pipeline: [ { $setWindowFields: { sortBy: {one: 1}, output: {a: {$shift: {by: 1, output: "$one", default: {$add: [1, 1]}}}}, }, }, ], cursor: {}, }), ); // "sortBy" is required for $shift. assert.commandFailedWithCode( coll.runCommand({ aggregate: coll.getName(), pipeline: [{$setWindowFields: {output: {a: {$shift: {by: 1, output: "$one"}}}}}], cursor: {}, }), ErrorCodes.FailedToParse, );