mirror of https://github.com/mongodb/mongo
SERVER-92984 Implement as and valueAs in $reduce (#45102)
GitOrigin-RevId: 81dd379a07a5bba351aa26bc279a20e024ed2501
This commit is contained in:
parent
ce07e58cc4
commit
2765e94225
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 ------------------------ */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue