SERVER-92984 Implement as and valueAs in $reduce (#45102)

GitOrigin-RevId: 81dd379a07a5bba351aa26bc279a20e024ed2501
This commit is contained in:
Felipe Farinon 2025-12-11 16:46:28 -05:00 committed by MongoDB Bot
parent ce07e58cc4
commit 2765e94225
3 changed files with 231 additions and 20 deletions

View File

@ -108,12 +108,157 @@ if (FeatureFlagUtil.isPresentAndEnabled(db, "ExposeArrayIndexInMapFilterReduce")
apiVersion: "1",
apiStrict: true,
});
//
// Test 'as' and 'asValue'.
//
test({$reduce: {input: "$simple", initialValue: 0, in: {$add: ["$$this", "$$value"]}}}, 6);
test({$reduce: {input: "$simple", initialValue: 0, as: "elem", in: {$add: ["$$elem", "$$value"]}}}, 6);
test(
{$reduce: {input: "$simple", initialValue: 0, as: "elem", valueAs: "acc", in: {$add: ["$$elem", "$$acc"]}}},
6,
);
test({$reduce: {input: "$simple", initialValue: 0, valueAs: "acc", in: {$add: ["$$this", "$$acc"]}}}, 6);
// Check nested operators.
pipeline = {
$reduce: {
input: "$matrix",
initialValue: 1,
as: "elem1",
valueAs: "acc1",
in: {
$multiply: [
"$$acc1",
{
$reduce: {
input: "$$elem1",
initialValue: 0,
as: "elem2",
valueAs: "acc2",
in: {$add: ["$$acc2", "$$elem2", "$$IDX"]},
},
},
],
},
},
};
test(pipeline, 4374);
// Check nested variable shadowing.
pipeline = {
$reduce: {
input: "$matrix",
initialValue: 1,
as: "elem",
valueAs: "acc",
in: {
$multiply: [
"$$acc",
{
$reduce: {
input: "$$elem",
initialValue: 0,
as: "elem",
valueAs: "acc",
in: {$add: ["$$acc", "$$elem", "$$IDX"]},
},
},
],
},
},
};
test(pipeline, 4374);
//
// Test error conditions of 'as' and 'valueAs'.
//
// Can't use defaults $$this/$$value if the new parameters are defined.
testError({$reduce: {input: "$simple", initialValue: 0, as: "elem", in: {$add: ["$$this", "$$value"]}}}, 17276);
testError(
{$reduce: {input: "$simple", initialValue: 0, valueAs: "elem", in: {$add: ["$$this", "$$value"]}}},
17276,
);
// Can't use non-user definable names on new parameters.
testError(
{$reduce: {input: "$simple", initialValue: 0, as: "THIS", in: {$add: ["$$THIS", "$$value"]}}},
ErrorCodes.FailedToParse,
);
testError(
{$reduce: {input: "$simple", initialValue: 0, as: "^", in: {$add: ["$$^", "$$value"]}}},
ErrorCodes.FailedToParse,
);
testError(
{$reduce: {input: "$simple", initialValue: 0, valueAs: "VALUE", in: {$add: ["$$this", "$$VALUE"]}}},
ErrorCodes.FailedToParse,
);
testError(
{$reduce: {input: "$simple", initialValue: 0, valueAs: "^", in: {$add: ["$$this", "$$^"]}}},
ErrorCodes.FailedToParse,
);
// Can't use defined variables in the non-'in' arguments.
testError({$reduce: {input: "$$i", initialValue: [], in: [], as: "i"}}, 17276);
testError({$reduce: {input: "$simple", initialValue: "$$i", in: [], as: "i"}}, 17276);
testError({$reduce: {input: "$simple", initialValue: ["$$i"], in: [], as: "i"}}, 17276);
testError({$reduce: {input: "$$i", initialValue: [], in: [], valueAs: "i"}}, 17276);
testError({$reduce: {input: "$simple", initialValue: "$$i", in: [], valueAs: "i"}}, 17276);
testError({$reduce: {input: "$simple", initialValue: ["$$i"], in: [], valueAs: "i"}}, 17276);
// Can't reuse same variable.
testError(
{$reduce: {input: "$simple", initialValue: 0, as: "elem", valueAs: "elem", in: {$add: ["$$elem", "$$elem"]}}},
9298401,
);
testError(
{
$reduce: {
input: "$simple",
initialValue: 0,
as: "elem",
arrayIndexAs: "elem",
in: {$add: ["$$elem", "$$elem"]},
},
},
9298401,
);
testError(
{
$reduce: {
input: "$simple",
initialValue: 0,
valueAs: "elem",
arrayIndexAs: "elem",
in: {$add: ["$$elem", "$$elem"]},
},
},
9298401,
);
// Can't use new parameters in API Version 1 with apiStrict.
pipeline = {$reduce: {input: "$simple", initialValue: 0, as: "elem", in: {$add: ["$$elem", "$$value"]}}};
testError(pipeline, ErrorCodes.APIStrictError, {
apiVersion: "1",
apiStrict: true,
});
pipeline = {$reduce: {input: "$simple", initialValue: 0, valueAs: "acc", in: {$add: ["$$this", "$$acc"]}}};
testError(pipeline, ErrorCodes.APIStrictError, {
apiVersion: "1",
apiStrict: true,
});
} else {
// TODO(SERVER-90514): remove these tests when the new features are enabled by default.
testError(
{$reduce: {input: "$simple", initialValue: 0, in: {$add: [{$multiply: ["$$this", "$$IDX"]}, "$$value"]}}},
17276,
);
testError(
{$reduce: {input: "$simple", initialValue: 0, as: "elem", valueAs: "acc", in: {$add: ["$$elem", "$$acc"]}}},
40076,
);
testError(
{
$reduce: {

View File

@ -2846,14 +2846,11 @@ intrusive_ptr<Expression> ExpressionReduce::parse(ExpressionContext* const expCt
expCtx->getVersionContext(),
serverGlobalParams.featureCompatibility.acquireFCVSnapshot());
// vpsSub is used only to parse 'in', which must have access to $$this and $$value.
VariablesParseState vpsSub(vps);
auto thisVar = vpsSub.defineVariable("this");
auto valueVar = vpsSub.defineVariable("value");
BSONElement inputElem;
BSONElement initialElem;
BSONElement inElem;
BSONElement asElem;
BSONElement valueAsElem;
BSONElement arrayIndexAsElem;
for (auto&& elem : expr.Obj()) {
@ -2864,6 +2861,18 @@ intrusive_ptr<Expression> ExpressionReduce::parse(ExpressionContext* const expCt
initialElem = elem;
} else if (field == "in") {
inElem = elem;
} else if (isExposeArrayIndexEnabled && field == "as") {
assertLanguageFeatureIsAllowed(expCtx->getOperationContext(),
"as argument of $reduce operator",
AllowedWithApiStrict::kNeverInVersion1,
AllowedWithClientType::kAny);
asElem = elem;
} else if (isExposeArrayIndexEnabled && field == "valueAs") {
assertLanguageFeatureIsAllowed(expCtx->getOperationContext(),
"valueAs argument of $reduce operator",
AllowedWithApiStrict::kNeverInVersion1,
AllowedWithClientType::kAny);
valueAsElem = elem;
} else if (isExposeArrayIndexEnabled && field == "arrayIndexAs") {
assertLanguageFeatureIsAllowed(expCtx->getOperationContext(),
"arrayIndexAs argument of $reduce operator",
@ -2878,25 +2887,68 @@ intrusive_ptr<Expression> ExpressionReduce::parse(ExpressionContext* const expCt
uassert(40078, "$reduce requires 'initialValue' to be specified", initialElem);
uassert(40079, "$reduce requires 'in' to be specified", inElem);
// Parse "arrayIndexAs". If "arrayIndexAs" is not specified, then write to "IDX" by default.
// "vpsSub" gets our variables, "vps" doesn't.
VariablesParseState vpsSub(vps);
auto parseVariableDefinition = [&vpsSub](const BSONElement& elem, StringData defaultName) {
boost::optional<std::string> name;
if (elem) {
name = elem.str();
variableValidation::validateNameForUserWrite(*name);
}
Variables::Id id = vpsSub.defineVariable(!name ? defaultName : *name);
return std::make_pair(name, id);
};
// Parse "as". If is not specified, use "this" by default.
boost::optional<std::string> thisName;
Variables::Id thisId;
if (isExposeArrayIndexEnabled) {
std::tie(thisName, thisId) = parseVariableDefinition(asElem, "this");
} else {
// Keep previous behavior if feature flag is disabled.
thisId = vpsSub.defineVariable("this");
}
// Parse "valueAs". If is not specified, use "value" by default.
boost::optional<std::string> valueName;
Variables::Id valueId;
if (isExposeArrayIndexEnabled) {
std::tie(valueName, valueId) = parseVariableDefinition(valueAsElem, "value");
} else {
// Keep previous behavior if feature flag is disabled.
valueId = vpsSub.defineVariable("value");
}
// Parse "arrayIndexAs". If is not specified, use "IDX" by default.
boost::optional<std::string> idxName;
boost::optional<Variables::Id> idxId;
if (isExposeArrayIndexEnabled) {
if (arrayIndexAsElem) {
idxName = arrayIndexAsElem.str();
variableValidation::validateNameForUserWrite(*idxName);
}
idxId = vpsSub.defineVariable(!idxName ? "IDX" : *idxName);
std::tie(idxName, idxId) = parseVariableDefinition(arrayIndexAsElem, "IDX");
}
// Validate uniqueness of the user-defined variables.
boost::optional<std::string> repeatedName;
if (thisName && (thisName == valueName || thisName == idxName)) {
repeatedName = *thisName;
} else if (valueName && (valueName == idxName)) {
repeatedName = *valueName;
}
uassert(9298401,
str::stream() << "Cannot define variables with the same name " << *repeatedName,
!repeatedName.has_value());
return make_intrusive<ExpressionReduce>(expCtx,
parseOperand(expCtx, inputElem, vps),
parseOperand(expCtx, initialElem, vps),
parseOperand(expCtx, inElem, vpsSub),
std::move(idxName),
idxId,
thisVar,
valueVar);
std::move(thisName),
thisId,
std::move(valueName),
valueId);
}
Value ExpressionReduce::evaluate(const Document& root, Variables* variables) const {
@ -2911,13 +2963,15 @@ intrusive_ptr<Expression> ExpressionReduce::optimize() {
}
Value ExpressionReduce::serialize(const SerializationOptions& options) const {
return Value(
Document{{"$reduce",
Document{{"input", _children[_kInput]->serialize(options)},
{"initialValue", _children[_kInitial]->serialize(options)},
{"arrayIndexAs",
_idxName ? Value(options.serializeIdentifier(*_idxName)) : Value()},
{"in", _children[_kIn]->serialize(options)}}}});
return Value(Document{
{"$reduce",
Document{
{"input", _children[_kInput]->serialize(options)},
{"initialValue", _children[_kInitial]->serialize(options)},
{"as", _thisName ? Value(options.serializeIdentifier(*_thisName)) : Value()},
{"valueAs", _valueName ? Value(options.serializeIdentifier(*_valueName)) : Value()},
{"arrayIndexAs", _idxName ? Value(options.serializeIdentifier(*_idxName)) : Value()},
{"in", _children[_kIn]->serialize(options)}}}});
}
/* ------------------------ ExpressionReplaceBase ------------------------ */

View File

@ -3110,10 +3110,14 @@ public:
boost::intrusive_ptr<Expression> in,
const boost::optional<std::string>& idxName,
const boost::optional<Variables::Id>& idxId,
const boost::optional<std::string>& thisName,
Variables::Id thisVar,
const boost::optional<std::string>& valueName,
Variables::Id valueVar)
: Expression(expCtx, {std::move(input), std::move(initial), std::move(in)}),
_thisName(thisName),
_thisVar(thisVar),
_valueName(valueName),
_valueVar(valueVar),
_idxName(std::move(idxName)),
_idxId(idxId) {
@ -3170,7 +3174,9 @@ public:
cloneChild(_kIn),
_idxName,
_idxId,
_thisName,
_thisVar,
_valueName,
_valueVar);
}
@ -3179,8 +3185,14 @@ private:
static constexpr size_t _kInitial = 1;
static constexpr size_t _kIn = 2;
// Name of the variable provided in the 'as' argument, boost::none if not provided.
boost::optional<std::string> _thisName;
Variables::Id _thisVar;
// Name of the variable provided in the 'valueAs' argument, boost::none if not provided.
boost::optional<std::string> _valueName;
Variables::Id _valueVar;
// Name of the variable provided in the 'arrayIndexAs' argument, boost::none if not provided.
boost::optional<std::string> _idxName;
// ID of the variable that represents the array index, boost::none if the feature is not