285 lines
10 KiB
JavaScript
285 lines
10 KiB
JavaScript
/**
|
|
* Test the syntax of $fill.
|
|
* @tags: [
|
|
* # We're testing the explain plan, not the query results, so the facet passthrough would fail.
|
|
* do_not_wrap_aggregations_in_facets,
|
|
* # This feature flag adjusts the desugaring a bit - requesting 'outputSortKeyMetadata' from the
|
|
* # $sort stage.
|
|
* requires_fcv_81,
|
|
* featureFlagRankFusionBasic,
|
|
* ]
|
|
*/
|
|
|
|
import {anyEq, desugarSingleStageAggregation} from "jstests/aggregation/extras/utils.js";
|
|
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
|
|
|
|
const coll = db[jsTestName()];
|
|
coll.drop();
|
|
|
|
function buildAndRunCommand(stage) {
|
|
return db.runCommand({aggregate: coll.getName(), pipeline: [stage], cursor: {}});
|
|
}
|
|
|
|
// Fail if not an object.
|
|
assert.commandFailedWithCode(buildAndRunCommand({$fill: "test"}), ErrorCodes.FailedToParse);
|
|
|
|
// Fail if 'output' is missing.
|
|
assert.commandFailedWithCode(buildAndRunCommand({$fill: {}}), ErrorCodes.IDLFailedToParse);
|
|
|
|
// Fail if 'output' is present but empty.
|
|
assert.commandFailedWithCode(buildAndRunCommand({$fill: {output: {}}}), 6050203);
|
|
|
|
// Fail on invalid method.
|
|
assert.commandFailedWithCode(buildAndRunCommand({$fill: {output: {test: {method: "random"}}}}), 6050202);
|
|
|
|
// Fail on invalid fill specification.
|
|
assert.commandFailedWithCode(buildAndRunCommand({$fill: {output: {test: "random"}}}), 6050200);
|
|
assert.commandFailedWithCode(
|
|
buildAndRunCommand({$fill: {output: {test: {method: "locf", second: "locf"}}}}),
|
|
ErrorCodes.IDLUnknownField,
|
|
);
|
|
|
|
// Fail if both 'partitionBy' and 'partitionByFields' are specified.
|
|
assert.commandFailedWithCode(
|
|
buildAndRunCommand({
|
|
$fill: {
|
|
output: {test: {method: "locf"}},
|
|
partitionBy: {test: "$test"},
|
|
partitionByFields: ["$test"],
|
|
},
|
|
}),
|
|
6050204,
|
|
);
|
|
|
|
// Fail if 'sortBy' is invalid when using 'output.test.value'.
|
|
assert.commandFailedWithCode(buildAndRunCommand({$fill: {output: {test: {value: "foo"}}, sortBy: {"$obj": 1}}}), 16410);
|
|
|
|
// Fail if 'partitionBy' is invalid when using 'output.test.value'.
|
|
assert.commandFailedWithCode(
|
|
buildAndRunCommand({$fill: {output: {test: {value: "foo"}}, partitionBy: {$part: "foobar"}}}),
|
|
168,
|
|
);
|
|
|
|
// Fail if 'partitionByFields' is invalid when using 'output.test.value'.
|
|
assert.commandFailedWithCode(
|
|
buildAndRunCommand({$fill: {output: {test: {value: "foo"}}, partitionByFields: [""]}}),
|
|
40352,
|
|
);
|
|
|
|
// Fail if linearFill does not receive a sortBy field.
|
|
assert.commandFailedWithCode(
|
|
buildAndRunCommand({
|
|
$fill: {output: {test: {method: "linear"}}, partitionBy: {part: "$part", partTwo: "$partTwo"}},
|
|
}),
|
|
605001,
|
|
);
|
|
|
|
// Test that we desugar correctly.
|
|
// Format is [[$fill spec], [Desugared pipeline], [field list that contains UUIDs]]
|
|
// Not all test cases have a spec that generates UUIDs, the third array will be empty for those
|
|
// tests.
|
|
let testCases = [
|
|
[
|
|
{$fill: {output: {val: {method: "locf"}}}},
|
|
[{"$_internalSetWindowFields": {"output": {"val": {"$locf": "$val"}}}}],
|
|
[],
|
|
], // 0
|
|
[
|
|
{$fill: {sortBy: {key: 1}, output: {val: {method: "linear"}}}},
|
|
[
|
|
{"$sort": {"sortKey": {"key": 1}, "outputSortKeyMetadata": true}},
|
|
{
|
|
"$_internalSetWindowFields": {"sortBy": {"key": 1}, "output": {"val": {"$linearFill": "$val"}}},
|
|
},
|
|
],
|
|
[],
|
|
], // 1
|
|
[{$fill: {output: {val: {value: 5}}}}, [{"$addFields": {"val": {$ifNull: ["$val", {"$const": 5}]}}}], []], // 2
|
|
[{$fill: {output: {val: {value: "$test"}}}}, [{"$addFields": {"val": {$ifNull: ["$val", "$test"]}}}], []], // 3
|
|
[
|
|
{$fill: {output: {val: {value: "$test"}, second: {method: "locf"}}}},
|
|
[
|
|
{"$_internalSetWindowFields": {"output": {"second": {"$locf": "$second"}}}},
|
|
{"$addFields": {"val": {$ifNull: ["$val", "$test"]}}},
|
|
],
|
|
[],
|
|
], // 4
|
|
[
|
|
{
|
|
$fill: {
|
|
output: {
|
|
val: {value: "$test"},
|
|
second: {method: "locf"},
|
|
third: {value: {$add: ["$val", "$second"]}},
|
|
fourth: {method: "locf"},
|
|
},
|
|
},
|
|
},
|
|
[
|
|
{
|
|
"$_internalSetWindowFields": {
|
|
"output": {"second": {"$locf": "$second"}, "fourth": {"$locf": "$fourth"}},
|
|
},
|
|
},
|
|
{
|
|
"$addFields": {
|
|
"val": {$ifNull: ["$val", "$test"]},
|
|
"third": {$ifNull: ["$third", {"$add": ["$val", "$second"]}]},
|
|
},
|
|
},
|
|
],
|
|
[],
|
|
], // 5
|
|
[
|
|
{$fill: {output: {val: {method: "locf"}}, partitionByFields: ["part", "partTwo"]}},
|
|
[
|
|
{"$addFields": {"UUIDPLACEHOLDER": {"part": "$part", "partTwo": "$partTwo"}}},
|
|
{"$sort": {"sortKey": {"UUIDPLACEHOLDER": 1}, "outputSortKeyMetadata": true}},
|
|
{
|
|
"$_internalSetWindowFields": {"partitionBy": "$UUIDPLACEHOLDER", "output": {"val": {"$locf": "$val"}}},
|
|
},
|
|
{"$project": {"UUIDPLACEHOLDER": false, "_id": true}},
|
|
],
|
|
[
|
|
[0, "$addFields", true],
|
|
[1, "$sort", "sortKey", true],
|
|
[2, "$_internalSetWindowFields", "partitionBy", false],
|
|
[3, "$project", true],
|
|
],
|
|
], // 6
|
|
[
|
|
{
|
|
$fill: {output: {val: {method: "locf"}}, partitionBy: {part: "$part", partTwo: "$partTwo"}},
|
|
},
|
|
[
|
|
{"$addFields": {"UUIDPLACEHOLDER": {"part": "$part", "partTwo": "$partTwo"}}},
|
|
{"$sort": {"sortKey": {"UUIDPLACEHOLDER": 1}, "outputSortKeyMetadata": true}},
|
|
{
|
|
"$_internalSetWindowFields": {"partitionBy": "$UUIDPLACEHOLDER", "output": {"val": {"$locf": "$val"}}},
|
|
},
|
|
{"$project": {"UUIDPLACEHOLDER": false, "_id": true}},
|
|
],
|
|
[
|
|
[0, "$addFields", true],
|
|
[1, "$sort", "sortKey", true],
|
|
[2, "$_internalSetWindowFields", "partitionBy", false],
|
|
[3, "$project", true],
|
|
],
|
|
], // 7
|
|
[
|
|
{
|
|
$fill: {
|
|
output: {val: {method: "locf"}},
|
|
sortBy: {key: 1},
|
|
partitionBy: {part: "$part", partTwo: "$partTwo"},
|
|
},
|
|
},
|
|
[
|
|
{"$addFields": {"UUIDPLACEHOLDER": {"part": "$part", "partTwo": "$partTwo"}}},
|
|
{"$sort": {"sortKey": {"UUIDPLACEHOLDER": 1, "key": 1}, "outputSortKeyMetadata": true}},
|
|
{
|
|
"$_internalSetWindowFields": {
|
|
"partitionBy": "$UUIDPLACEHOLDER",
|
|
"sortBy": {"key": 1},
|
|
"output": {"val": {"$locf": "$val"}},
|
|
},
|
|
},
|
|
{"$project": {"UUIDPLACEHOLDER": false, "_id": true}},
|
|
],
|
|
[
|
|
[0, "$addFields", true],
|
|
[1, "$sort", "sortKey", true],
|
|
[2, "$_internalSetWindowFields", "partitionBy", false],
|
|
[3, "$project", true],
|
|
],
|
|
], // 8
|
|
[
|
|
{
|
|
$fill: {
|
|
output: {val: {method: "locf"}, second: {value: 7}},
|
|
sortBy: {key: 1},
|
|
partitionBy: {part: "$part", partTwo: "$partTwo"},
|
|
},
|
|
},
|
|
[
|
|
{"$addFields": {"UUIDPLACEHOLDER": {"part": "$part", "partTwo": "$partTwo"}}},
|
|
{"$sort": {"sortKey": {"UUIDPLACEHOLDER": 1, "key": 1}, "outputSortKeyMetadata": true}},
|
|
{
|
|
"$_internalSetWindowFields": {
|
|
"partitionBy": "$UUIDPLACEHOLDER",
|
|
"sortBy": {"key": 1},
|
|
"output": {"val": {"$locf": "$val"}},
|
|
},
|
|
},
|
|
{"$project": {"UUIDPLACEHOLDER": false, "_id": true}},
|
|
{"$addFields": {"second": {$ifNull: ["$second", {"$const": 7}]}}},
|
|
],
|
|
[
|
|
[0, "$addFields", true],
|
|
[1, "$sort", "sortKey", true],
|
|
[2, "$_internalSetWindowFields", "partitionBy", false],
|
|
[3, "$project", true],
|
|
],
|
|
], // 9
|
|
[
|
|
{
|
|
$fill: {
|
|
output: {val: {value: "foo"}},
|
|
sortBy: {key: 1},
|
|
partitionBy: {part: "$part", partTwo: "$partTwo"},
|
|
},
|
|
},
|
|
[{"$addFields": {"val": {$ifNull: ["$val", {"$const": "foo"}]}}}],
|
|
[],
|
|
], // 10
|
|
];
|
|
|
|
function modifyObjectAtPath(orig, path) {
|
|
if (typeof path[0] == "boolean") {
|
|
// The first key in the object needs to be replaced.
|
|
if (path[0]) {
|
|
const firstKey = Object.keys(orig)[0];
|
|
const val = orig[firstKey];
|
|
delete orig[firstKey];
|
|
orig["UUIDPLACEHOLDER"] = val;
|
|
} else {
|
|
// Orig is a string. If the first character is a '$', keep it.
|
|
if (orig[0] === "$") {
|
|
return "$UUIDPLACEHOLDER";
|
|
}
|
|
return "UUIDPLACEHOLDER";
|
|
}
|
|
} else if (typeof path[0] == "number" || typeof path[0] == "string") {
|
|
// Orig is an array. Operate on an element of the array.
|
|
orig[path[0]] = modifyObjectAtPath(orig[path[0]], path.slice(1));
|
|
} else {
|
|
// Sanity guard.
|
|
assert(false, "Unexpected type in path " + typeof path[0] + "\n" + tojson(path[0]));
|
|
}
|
|
|
|
return orig;
|
|
}
|
|
|
|
// TODO(SERVER-18047): Remove database creation once explain behavior is unified between replica
|
|
// sets and sharded clusters for non-existent dbs.
|
|
if (FixtureHelpers.isMongos(db) || TestData.testingReplicaSetEndpoint) {
|
|
// Create database
|
|
assert.commandWorked(db.adminCommand({"enableSharding": db.getName()}));
|
|
}
|
|
|
|
for (let i = 0; i < testCases.length; i++) {
|
|
let result = desugarSingleStageAggregation(db, coll, testCases[i][0]);
|
|
// $setWindowFields generates random fieldnames. Use the paths in the test case to
|
|
// replace the UUID with "UUIDPLACEHOLDER".
|
|
if (testCases[i][2].length != 0) {
|
|
for (let pathNum = 0; pathNum < testCases[i][2].length; pathNum++) {
|
|
result = modifyObjectAtPath(result, testCases[i][2][pathNum]);
|
|
}
|
|
}
|
|
|
|
assert(
|
|
anyEq(result, testCases[i][1], false, null, "UUIDPLACEHOLDER"),
|
|
"Test case " + i + " failed.\n" + "Expected:\n" + tojson(testCases[i][1]) + "\nGot:\n" + tojson(result),
|
|
);
|
|
}
|