Files
mongo/jstests/aggregation/sources/fill/fill_parse.js
Lee Maguire ba9b18039a SERVER-99569 Validate partitionBy and sortBy in $fill (#32252)
GitOrigin-RevId: 17057ba86b4ed0079424c6456092a40ef8b72676
2025-04-09 22:19:11 +00:00

292 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));
}