SERVER-93758 Implement arrayIndexAs in $map/$reduce/$filter (#41170)

Co-authored-by: Parker Felix <parker.felix@mongodb.com>
Co-authored-by: Kyle Booker <107269166+ktbooker@users.noreply.github.com>
Co-authored-by: Kyle Booker <kyle.booker@mongodb.com>
Co-authored-by: Kevin Cherkauer <kevin.cherkauer@mongodb.com>
GitOrigin-RevId: 26ec273fa78c9f83ba9f77d71e04e70c3a83291b
This commit is contained in:
Felipe Farinon 2025-10-15 10:15:34 -04:00 committed by MongoDB Bot
parent 512bac7bd3
commit c91d2ce3bf
12 changed files with 1616 additions and 72 deletions

View File

@ -1,7 +1,6 @@
// Test $filter aggregation expression.
import "jstests/libs/query/sbe_assert_error_override.js";
import {assertArrayEq, assertErrorCode} from "jstests/aggregation/extras/utils.js";
function runAndAssert(filterSpec, expectedResult) {
@ -17,6 +16,11 @@ function runAndAssertThrows(filterSpec, expectedErrorCode) {
let coll = db.agg_filter_expr;
coll.drop();
function test(expression, expected) {
var result = coll.aggregate({$project: {_id: 0, res: expression}}).toArray();
assert.eq(result, [{res: expected}]);
}
assert.commandWorked(
coll.insert([
{_id: 0, c: 1, d: 3, a: [1, 2, 3, 4, 5]},
@ -437,3 +441,26 @@ filterDoc = {
$filter: {input: "$a", cond: {$or: [{$lt: ["$$this", 0]}, {$ln: "$$this"}]}},
};
runAndAssert(filterDoc, expectedResults);
// Nested behavior with two named variables.
test(
{
$filter: {
input: [[-3, 1, 2], [2, 3], [1], [-12, -3]],
as: "outer",
cond: {$ne: [0, {$size: {$filter: {input: "$$outer", as: "inner", cond: {$gte: ["$$inner", 0]}}}}]},
},
},
[[-3, 1, 2], [2, 3], [1]],
);
// Nested behavior for shadowing variables.
test(
{
$filter: {
input: [[-3, 1, 2], [2, 3], [1], [-12, -3]],
cond: {$ne: [0, {$size: {$filter: {input: "$$this", cond: {$gte: ["$$this", 0]}}}}]},
},
},
[[-3, 1, 2], [2, 3], [1]],
);

View File

@ -0,0 +1,160 @@
/**
* Test the behavior of the $filter operator with the arrayIndexAs field.
*
* @tags: [requires_fcv_83]
*/
import {assertErrorCode} from "jstests/aggregation/extras/utils.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
function testError(expression, code, options = {}) {
const projectSpec = {$project: {b: expression}};
assertErrorCode(coll, projectSpec, code, "", options);
}
function test(expression, expected) {
let result = coll.aggregate({$project: {_id: 0, res: expression}}).toArray();
assert.eq(result, [{res: expected}]);
}
let coll = db[jsTestName()];
coll.drop();
if (FeatureFlagUtil.isPresentAndEnabled(db, "ExposeArrayIndexInMapFilterReduce")) {
assert.commandWorked(
coll.insert({
_id: 0,
b: [0, 7, 2, 5, 4, 0],
nested: [[], [0, 1], [2, 3], [4, 5]],
}),
);
let filterDoc = {$filter: {input: "$b", cond: {$eq: ["$$this", "$$IDX"]}}};
test(filterDoc, [0, 2, 4]);
filterDoc = {$filter: {input: "$b", arrayIndexAs: "i", cond: {$eq: ["$$this", "$$i"]}}};
test(filterDoc, [0, 2, 4]);
filterDoc = {$filter: {input: "$b", cond: {$eq: ["$$this", "$$IDX"]}, limit: 2}};
test(filterDoc, [0, 2]);
filterDoc = {$filter: {input: "$b", arrayIndexAs: "i", cond: {$eq: ["$$this", "$$i"]}, limit: 1}};
test(filterDoc, [0]);
// Nested behavior with two named index variables.
test(
{
$filter: {
input: "$nested",
as: "outer",
arrayIndexAs: "i",
cond: {
$ne: [
0,
{
$size: {
$filter: {
input: "$$outer",
as: "inner",
cond: {$eq: ["$$i", "$$inner"]},
},
},
},
],
},
},
},
[
[0, 1],
[2, 3],
],
);
// Nested behavior for shadowing variables.
test(
{
$filter: {
input: "$nested",
as: "outer",
cond: {
$ne: [
0,
{
$size: {
$filter: {
input: "$$outer",
as: "inner",
cond: {$eq: ["$$IDX", "$$inner"]},
},
},
},
],
},
},
},
[[0, 1]],
);
// Nested behavior with named inner variable and default outer variable.
test(
{
$filter: {
input: "$nested",
as: "outer",
cond: {
$ne: [
0,
{
$size: {
$filter: {
input: "$$outer",
as: "inner",
arrayIndexAs: "i",
cond: {
$and: [{$eq: ["$$inner", "$$IDX"]}, {$eq: ["$$inner", "$$i"]}],
},
},
},
},
],
},
limit: 2,
},
},
[[0, 1]],
);
//
// Test error conditions.
//
// Can't use default $$IDX if 'arrayIndexAs' is defined.
testError({$filter: {input: "$b", arrayIndexAs: "i", cond: {$eq: ["$$this", "$$IDX"]}}}, 17276);
// Can't use non-user definable names on 'arrayIndexAs'.
testError(
{$filter: {input: "$b", arrayIndexAs: "IDX", cond: {$eq: ["$$this", "$$IDX"]}}},
ErrorCodes.FailedToParse,
);
testError({$filter: {input: "$b", arrayIndexAs: "^", cond: {$eq: ["$$this", "$$^"]}}}, ErrorCodes.FailedToParse);
testError({$filter: {input: "$b", arrayIndexAs: "", cond: {$eq: ["$$this", "$$IDX"]}}}, ErrorCodes.FailedToParse);
// Can't use variable defined by 'arrayIndexAs' or $$IDX in the non-'cond' arguments.
testError({$filter: {input: "$$i", as: "c", cond: true, limit: 1, arrayIndexAs: "i"}}, 17276);
testError({$filter: {input: "$b", as: "c", cond: true, limit: "$$i", arrayIndexAs: "i"}}, 17276);
testError({$filter: {input: "$$IDX", as: "c", cond: true, limit: 1}}, 17276);
testError({$filter: {input: "$b", as: "c", cond: true, limit: "$$IDX"}}, 17276);
// Can't reuse same variable.
testError({$filter: {input: "$b", as: "i", cond: true, arrayIndexAs: "i"}}, 9375802);
// Can't use 'arrayIndexAs' in API Version 1 with apiStrict.
filterDoc = {$filter: {input: "$b", arrayIndexAs: "i", cond: {$eq: ["$$this", "$$IDX"]}}};
testError(filterDoc, ErrorCodes.APIStrictError, {
apiVersion: "1",
apiStrict: true,
});
} else {
// TODO(SERVER-90514): remove these tests when the new features are enabled by default.
testError({$filter: {input: "$b", cond: {$eq: ["$$this", "$$IDX"]}}}, 17276);
testError({$filter: {input: "$b", arrayIndexAs: "i", cond: {$eq: ["$$this", "$$i"]}}}, 28647);
}

View File

@ -27,6 +27,42 @@ test({$map: {input: "$mixed", as: "var", in: "$$var.a"}}, [1, null, 2, null]); /
test({$map: {input: "$null", as: "var", in: "$$var"}}, null);
// Nested behavior with two named variables.
test(
{
$map: {
input: "$simple",
as: "outer",
in: {$map: {input: "$simple", as: "inner", in: {$add: ["$$inner", "$$outer"]}}},
},
},
[
[2, 3, 4, 5],
[3, 4, 5, 6],
[4, 5, 6, 7],
[5, 6, 7, 8],
],
);
// Nested behavior for shadowing variables.
test({$map: {input: "$simple", in: {$map: {input: "$simple", in: "$$this"}}}}, [
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4],
]);
// Nested behavior with named inner variable and default outer variable.
test({$map: {input: "$simple", as: "outer", in: {$map: {input: "$simple", in: {$add: ["$$this", "$$outer"]}}}}}, [
[2, 3, 4, 5],
[3, 4, 5, 6],
[4, 5, 6, 7],
[5, 6, 7, 8],
]);
// can't use default if 'as' is defined
assertErrorCode(t, {$map: {input: "$simple", as: "value", in: "$$this"}}, 40324);
// can't set ROOT
assertErrorCode(t, {$project: {a: {$map: {input: "$simple", as: "ROOT", in: "$$ROOT"}}}}, ErrorCodes.FailedToParse);

View File

@ -0,0 +1,120 @@
/**
* Test the behavior of the $map operator with the arrayIndexAs field.
*
* @tags: [requires_fcv_83]
*/
import {assertErrorCode} from "jstests/aggregation/extras/utils.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
function testError(expression, code, options = {}) {
const projectSpec = {$project: {b: expression}};
assertErrorCode(coll, projectSpec, code, "", options);
}
function test(expression, expected) {
let result = coll.aggregate({$project: {_id: 0, res: expression}}).toArray();
assert.eq(result, [{res: expected}]);
}
let coll = db[jsTestName()];
coll.drop();
if (FeatureFlagUtil.isPresentAndEnabled(db, "ExposeArrayIndexInMapFilterReduce")) {
assert.commandWorked(
coll.insert({
simple: [1, 2, 3, 4],
a4z: [0, 0, 0, 0],
nested: [{a: 1}, {a: 2}],
mixed: [{a: 1}, {}, {a: 2}, {a: null}],
notArray: 1,
null: null,
}),
);
test({$map: {input: "$mixed", in: "$$IDX"}}, [0, 1, 2, 3]);
test({$map: {input: "$mixed", arrayIndexAs: "σημείο", in: "$$σημείο"}}, [0, 1, 2, 3]);
test(
{
$map: {
input: "$simple",
arrayIndexAs: "idx",
as: "this",
in: {$add: ["$$idx", "$$this"]},
},
},
[1, 3, 5, 7],
);
// Nested behavior with two named index variables.
test(
{
$map: {
input: "$a4z",
arrayIndexAs: "outer",
in: {
$map: {
input: "$a4z",
arrayIndexAs: "inner",
in: {$add: [{$multiply: [4, "$$outer"]}, "$$inner", 1]},
},
},
},
},
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16],
],
);
// Nested behavior for shadowing variables.
test({$map: {input: "$a4z", in: {$map: {input: "$a4z", in: "$$IDX"}}}}, [
[0, 1, 2, 3],
[0, 1, 2, 3],
[0, 1, 2, 3],
[0, 1, 2, 3],
]);
// Nested behavior with named inner variable and default outer variable.
test(
{$map: {input: "$a4z", in: {$map: {input: "$a4z", arrayIndexAs: "inner", in: {$add: ["$$IDX", "$$inner"]}}}}},
[
[0, 1, 2, 3],
[1, 2, 3, 4],
[2, 3, 4, 5],
[3, 4, 5, 6],
],
);
//
// Test error conditions.
//
// Can't use default $$IDX if 'arrayIndexAs' is defined.
testError({$map: {input: "$simple", arrayIndexAs: "index", in: "$$IDX"}}, 17276);
// Can't use non-user definable names on 'arrayIndexAs'.
testError({$map: {input: "$simple", arrayIndexAs: "IDX", in: []}}, ErrorCodes.FailedToParse);
testError({$map: {input: "$simple", arrayIndexAs: "^", in: []}}, ErrorCodes.FailedToParse);
testError({$map: {input: "$simple", arrayIndexAs: "", in: []}}, ErrorCodes.FailedToParse);
// Can't use variable defined by 'arrayIndexAs' or $$IDX in the non-'in' arguments.
testError({$map: {input: "$$IDX", in: {}}}, 17276);
testError({$map: {input: "$$i", arrayIndexAs: "i", in: {}}}, 17276);
// Can't reuse same variable.
testError({$map: {input: "$simple", as: "i", arrayIndexAs: "i", in: "$$i"}}, 9375801);
// Can't use 'arrayIndexAs' in API Version 1 with apiStrict.
testError({$map: {input: "$simple", arrayIndexAs: "i", in: "$$i"}}, ErrorCodes.APIStrictError, {
apiVersion: "1",
apiStrict: true,
});
} else {
// TODO(SERVER-90514): remove these tests when the new features are enabled by default.
testError({$map: {input: "$simple", in: "$$IDX"}}, 17276);
testError({$map: {input: "$simple", arrayIndexAs: "i", in: "$$i"}}, 16879);
}

View File

@ -0,0 +1,128 @@
/**
* Test the behavior of the $reduce operator with the arrayIndexAs field.
*
* @tags: [requires_fcv_83]
*/
import {assertErrorCode} from "jstests/aggregation/extras/utils.js";
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
function testError(expression, code, options = {}) {
const projectSpec = {$project: {b: expression}};
assertErrorCode(coll, projectSpec, code, "", options);
}
function test(expression, expected) {
let result = coll.aggregate({$project: {_id: 0, res: expression}}).toArray();
assert.eq(result, [{res: expected}]);
}
let coll = db[jsTestName()];
coll.drop();
if (FeatureFlagUtil.isPresentAndEnabled(db, "ExposeArrayIndexInMapFilterReduce")) {
assert.commandWorked(
coll.insert({
_id: 0,
simple: [1, 2, 3],
matrix: [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
],
}),
);
test({$reduce: {input: "$simple", initialValue: 0, in: {$add: [{$multiply: ["$$this", "$$IDX"]}, "$$value"]}}}, 8);
test(
{
$reduce: {
input: "$simple",
initialValue: 0,
arrayIndexAs: "i",
in: {$add: [{$multiply: ["$$this", "$$i"]}, "$$value"]},
},
},
8,
);
let pipeline = {
$reduce: {
input: "$matrix",
initialValue: 1,
in: {
$multiply: [
"$$value",
{
$reduce: {
input: "$$this",
initialValue: 0,
in: {$add: ["$$value", "$$this", "$$IDX"]},
},
},
],
},
},
};
test(pipeline, 4374);
//
// Test error conditions.
//
// Can't use default $$IDX if 'arrayIndexAs' is defined.
pipeline = {
$reduce: {
input: "$simple",
initialValue: 0,
arrayIndexAs: "i",
in: {$add: ["$$this", "$$IDX", "$$value"]},
},
};
testError(pipeline, 17276);
// Can't use non-user definable names on 'arrayIndexAs'.
testError({$reduce: {input: "$simple", arrayIndexAs: "IDX", initialValue: [], in: []}}, ErrorCodes.FailedToParse);
testError({$reduce: {input: "$simple", arrayIndexAs: "^", initialValue: [], in: []}}, ErrorCodes.FailedToParse);
testError({$reduce: {input: "$simple", arrayIndexAs: "", initialValue: [], in: []}}, ErrorCodes.FailedToParse);
// Can't use variable defined by 'arrayIndexAs' or $$IDX in the non-'in' arguments.
testError({$reduce: {input: "$$i", initialValue: [], in: [], arrayIndexAs: "i"}}, 17276);
testError({$reduce: {input: "$simple", initialValue: "$$i", in: [], arrayIndexAs: "i"}}, 17276);
testError({$reduce: {input: "$simple", initialValue: ["$$i"], in: [], arrayIndexAs: "i"}}, 17276);
testError({$reduce: {input: "$$IDX", initialValue: [], in: []}}, 17276);
testError({$reduce: {input: "$simple", initialValue: "$$IDX", in: []}}, 17276);
testError({$reduce: {input: "$simple", initialValue: ["$$IDX"], in: []}}, 17276);
// Can't use 'arrayIndexAs' in API Version 1 with apiStrict.
pipeline = {
$reduce: {
input: "$simple",
arrayIndexAs: "i",
in: {
$add: ["$$this", "$$value", "$$i"],
},
},
};
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,
arrayIndexAs: "i",
in: {$add: [{$multiply: ["$$this", "$$i"]}, "$$value"]},
},
},
40076,
);
}

View File

@ -97,6 +97,9 @@ Value evaluate(const ExpressionMap& expr, const Document& root, Variables* varia
for (size_t i = 0; i < input.size(); i++) {
checkForInterrupt();
variables->setValue(expr.getVarId(), input[i]);
if (expr.getIndexVariableId()) {
variables->setValue(*expr.getIndexVariableId(), Value(static_cast<int>(i)));
}
Value toInsert = expr.getEach()->evaluate(root, variables);
if (toInsert.missing()) {
@ -133,17 +136,20 @@ Value evaluate(const ExpressionReduce& expr, const Document& root, Variables* va
size_t memLimit = internalQueryMaxMapFilterReduceBytes.load();
Value accumulatedValue = expr.getInitial()->evaluate(root, variables);
size_t itr = 0;
int32_t prevDepth = -1;
size_t interval = expr.getAccumulatedValueDepthCheckInterval();
for (auto&& elem : inputVal.getArray()) {
auto input = inputVal.getArray();
for (size_t i = 0; i < input.size(); ++i) {
checkForInterrupt();
variables->setValue(expr.getThisVar(), elem);
variables->setValue(expr.getThisVar(), input[i]);
variables->setValue(expr.getValueVar(), accumulatedValue);
if (expr.getIndexVariableId()) {
variables->setValue(*expr.getIndexVariableId(), Value(static_cast<int>(i)));
}
accumulatedValue = expr.getIn()->evaluate(root, variables);
if ((interval > 0) && (itr % interval) == 0 &&
if ((interval > 0) && (i % interval) == 0 &&
(accumulatedValue.isObject() || accumulatedValue.isArray())) {
int32_t depth =
accumulatedValue.depth(2 * BSONDepth::getMaxAllowableDepth() /*maxDepth*/);
@ -164,7 +170,6 @@ Value evaluate(const ExpressionReduce& expr, const Document& root, Variables* va
uasserted(ErrorCodes::ExceededMemoryLimit,
"$reduce would use too much memory and cannot spill");
}
itr++;
}
return accumulatedValue;
@ -222,12 +227,16 @@ Value evaluate(const ExpressionFilter& expr, const Document& root, Variables* va
std::vector<Value> output;
output.reserve(approximateOutputSize);
for (const auto& elem : input) {
for (size_t i = 0; i < input.size(); ++i) {
checkForInterrupt();
variables->setValue(expr.getVariableId(), elem);
variables->setValue(expr.getVariableId(), input[i]);
if (expr.getIndexVariableId()) {
variables->setValue(*expr.getIndexVariableId(), Value(static_cast<int>(i)));
}
if (expr.getCond()->evaluate(root, variables).coerceToBool()) {
output.push_back(elem);
output.push_back(input[i]);
if (remainingLimitCounter && --*remainingLimitCounter == 0) {
return Value(std::move(output));
}

View File

@ -1560,6 +1560,7 @@ mongo_cc_unit_test(
"expression_function_test.cpp",
"expression_hasher_test.cpp",
"expression_let_test.cpp",
"expression_map_reduce_filter_test.cpp",
"expression_nary_test.cpp",
"expression_object_test.cpp",
"expression_or_test.cpp",

View File

@ -65,6 +65,7 @@
#include "mongo/db/pipeline/expression_context.h"
#include "mongo/db/pipeline/expression_parser_gen.h"
#include "mongo/db/pipeline/variable_validation.h"
#include "mongo/db/query/query_feature_flags_gen.h"
#include "mongo/db/query/query_knobs_gen.h"
#include "mongo/db/query/util/make_data_structure.h"
#include "mongo/db/query/util/rank_fusion_util.h"
@ -1874,12 +1875,18 @@ intrusive_ptr<Expression> ExpressionFilter::parse(ExpressionContext* const expCt
uassert(
28646, "$filter only supports an object as its argument", expr.type() == BSONType::object);
const bool isExposeArrayIndexEnabled = expCtx->shouldParserIgnoreFeatureFlagCheck() ||
feature_flags::gFeatureFlagExposeArrayIndexInMapFilterReduce
.isEnabledUseLastLTSFCVWhenUninitialized(
expCtx->getVersionContext(),
serverGlobalParams.featureCompatibility.acquireFCVSnapshot());
// "cond" must be parsed after "as" regardless of BSON order.
BSONElement inputElem;
BSONElement asElem;
BSONElement condElem;
BSONElement limitElem;
BSONElement arrayIndexAsElem;
for (auto elem : expr.Obj()) {
if (elem.fieldNameStringData() == "input") {
@ -1894,6 +1901,12 @@ intrusive_ptr<Expression> ExpressionFilter::parse(ExpressionContext* const expCt
AllowedWithApiStrict::kNeverInVersion1,
AllowedWithClientType::kAny);
limitElem = elem;
} else if (isExposeArrayIndexEnabled && elem.fieldNameStringData() == "arrayIndexAs") {
assertLanguageFeatureIsAllowed(expCtx->getOperationContext(),
"arrayIndexAs argument of $filter operator",
AllowedWithApiStrict::kNeverInVersion1,
AllowedWithClientType::kAny);
arrayIndexAsElem = elem;
} else {
uasserted(28647,
str::stream() << "Unrecognized parameter to $filter: " << elem.fieldName());
@ -1903,32 +1916,50 @@ intrusive_ptr<Expression> ExpressionFilter::parse(ExpressionContext* const expCt
uassert(28648, "Missing 'input' parameter to $filter", !inputElem.eoo());
uassert(28650, "Missing 'cond' parameter to $filter", !condElem.eoo());
// Parse "input", only has outer variables.
intrusive_ptr<Expression> input = parseOperand(expCtx, inputElem, vpsIn);
// "vpsSub" gets our variables, "vpsIn" doesn't.
VariablesParseState vpsSub(vpsIn);
VariablesParseState vpsSub(vpsIn); // vpsSub gets our variable, vpsIn doesn't.
// Parse "as". If "as" is not specified, then use "this" by default.
auto varName = asElem.eoo() ? "this" : asElem.str();
variableValidation::validateNameForUserWrite(varName);
Variables::Id varId = vpsSub.defineVariable(varName);
// Parse "cond", has access to "as" variable.
intrusive_ptr<Expression> cond = parseOperand(expCtx, condElem, vpsSub);
// Parse "arrayIndexAs". If "arrayIndexAs" is not specified, then write to "IDX" by default.
boost::optional<std::string> idxName;
boost::optional<Variables::Id> idxId;
if (isExposeArrayIndexEnabled) {
if (arrayIndexAsElem) {
idxName = arrayIndexAsElem.str();
variableValidation::validateNameForUserWrite(*idxName);
if (limitElem) {
intrusive_ptr<Expression> limit = parseOperand(expCtx, limitElem, vpsIn);
return new ExpressionFilter(
expCtx, std::move(varName), varId, std::move(input), std::move(cond), std::move(limit));
uassert(9375802,
"Can't redefine variable specified in 'as' and 'arrayIndexAs' parameters",
varName != idxName);
}
idxId = vpsSub.defineVariable(!idxName ? "IDX" : *idxName);
}
return new ExpressionFilter(
expCtx, std::move(varName), varId, std::move(input), std::move(cond));
intrusive_ptr<Expression> limit;
if (limitElem) {
limit = parseOperand(expCtx, limitElem, vpsIn);
}
return make_intrusive<ExpressionFilter>(
expCtx,
std::move(varName),
varId,
std::move(idxName),
idxId,
parseOperand(expCtx, inputElem, vpsIn), // Only has access to outer vars.
parseOperand(expCtx, condElem, vpsSub), // Has access to "as" and "arrayIndexAs" vars.
std::move(limit));
}
ExpressionFilter::ExpressionFilter(ExpressionContext* const expCtx,
string varName,
Variables::Id varId,
const boost::optional<std::string>& idxName,
const boost::optional<Variables::Id>& idxId,
intrusive_ptr<Expression> input,
intrusive_ptr<Expression> cond,
intrusive_ptr<Expression> limit)
@ -1937,12 +1968,14 @@ ExpressionFilter::ExpressionFilter(ExpressionContext* const expCtx,
: makeVector(std::move(input), std::move(cond))),
_varName(std::move(varName)),
_varId(varId),
_idxName(std::move(idxName)),
_idxId(idxId),
_limit(_children.size() == 3 ? 2 : boost::optional<size_t>(boost::none)) {
expCtx->setSbeCompatibility(SbeCompatibility::notCompatible);
}
intrusive_ptr<Expression> ExpressionFilter::optimize() {
// TODO handle when _input is constant.
// TODO(SERVER-111215) handle when _input is constant.
_children[_kInput] = _children[_kInput]->optimize();
_children[_kCond] = _children[_kCond]->optimize();
if (_limit)
@ -1952,16 +1985,14 @@ intrusive_ptr<Expression> ExpressionFilter::optimize() {
}
Value ExpressionFilter::serialize(const SerializationOptions& options) const {
if (_limit) {
return Value(
DOC("$filter" << DOC("input" << _children[_kInput]->serialize(options) << "as"
<< options.serializeIdentifier(_varName) << "cond"
<< _children[_kCond]->serialize(options) << "limit"
<< (_children[*_limit])->serialize(options))));
}
return Value(DOC("$filter" << DOC("input" << _children[_kInput]->serialize(options) << "as"
<< options.serializeIdentifier(_varName) << "cond"
<< _children[_kCond]->serialize(options))));
Document{{"$filter",
Document{{"input", _children[_kInput]->serialize(options)},
{"as", options.serializeIdentifier(_varName)},
{"arrayIndexAs",
_idxName ? Value(options.serializeIdentifier(*_idxName)) : Value()},
{"cond", _children[_kCond]->serialize(options)},
{"limit", _limit ? _children[*_limit]->serialize(options) : Value()}}}});
}
Value ExpressionFilter::evaluate(const Document& root, Variables* variables) const {
@ -1989,10 +2020,17 @@ intrusive_ptr<Expression> ExpressionMap::parse(ExpressionContext* const expCtx,
uassert(16878, "$map only supports an object as its argument", expr.type() == BSONType::object);
// "in" must be parsed after "as" regardless of BSON order
const bool isExposeArrayIndexEnabled = expCtx->shouldParserIgnoreFeatureFlagCheck() ||
feature_flags::gFeatureFlagExposeArrayIndexInMapFilterReduce
.isEnabledUseLastLTSFCVWhenUninitialized(
expCtx->getVersionContext(),
serverGlobalParams.featureCompatibility.acquireFCVSnapshot());
// "in" must be parsed after "as" regardless of BSON order.
BSONElement inputElem;
BSONElement asElem;
BSONElement inElem;
BSONElement arrayIndexAsElem;
const BSONObj args = expr.embeddedObject();
for (auto&& arg : args) {
if (arg.fieldNameStringData() == "input") {
@ -2001,6 +2039,12 @@ intrusive_ptr<Expression> ExpressionMap::parse(ExpressionContext* const expCtx,
asElem = arg;
} else if (arg.fieldNameStringData() == "in") {
inElem = arg;
} else if (isExposeArrayIndexEnabled && arg.fieldNameStringData() == "arrayIndexAs") {
assertLanguageFeatureIsAllowed(expCtx->getOperationContext(),
"arrayIndexAs argument of $map operator",
AllowedWithApiStrict::kNeverInVersion1,
AllowedWithClientType::kAny);
arrayIndexAsElem = arg;
} else {
uasserted(16879,
str::stream() << "Unrecognized parameter to $map: " << arg.fieldName());
@ -2010,46 +2054,70 @@ intrusive_ptr<Expression> ExpressionMap::parse(ExpressionContext* const expCtx,
uassert(16880, "Missing 'input' parameter to $map", !inputElem.eoo());
uassert(16882, "Missing 'in' parameter to $map", !inElem.eoo());
// parse "input"
intrusive_ptr<Expression> input =
parseOperand(expCtx, inputElem, vpsIn); // only has outer vars
// "vpsSub" gets our variables, "vpsIn" doesn't.
VariablesParseState vpsSub(vpsIn);
// parse "as"
VariablesParseState vpsSub(vpsIn); // vpsSub gets our vars, vpsIn doesn't.
// If "as" is not specified, then use "this" by default.
// Parse "as". If "as" is not specified, then use "this" by default.
auto varName = asElem.eoo() ? "this" : asElem.str();
variableValidation::validateNameForUserWrite(varName);
Variables::Id varId = vpsSub.defineVariable(varName);
// parse "in"
intrusive_ptr<Expression> in =
parseOperand(expCtx, inElem, vpsSub); // has access to map variable
// Parse "arrayIndexAs". If "arrayIndexAs" is not specified, then write to "IDX" by default.
boost::optional<std::string> idxName;
boost::optional<Variables::Id> idxId;
if (isExposeArrayIndexEnabled) {
if (arrayIndexAsElem) {
idxName = arrayIndexAsElem.str();
variableValidation::validateNameForUserWrite(*idxName);
return new ExpressionMap(expCtx, varName, varId, input, in);
uassert(
9375801, "'as' and 'arrayIndexAs' cannot have the same name", varName != idxName);
}
idxId = vpsSub.defineVariable(!idxName ? "IDX" : *idxName);
}
return make_intrusive<ExpressionMap>(
expCtx,
std::move(varName),
varId,
std::move(idxName),
idxId,
parseOperand(expCtx, inputElem, vpsIn), // Only has access to outer vars.
parseOperand(expCtx, inElem, vpsSub) // Has access to "as" and "arrayIndexAs" vars.
);
}
ExpressionMap::ExpressionMap(ExpressionContext* const expCtx,
const string& varName,
Variables::Id varId,
const boost::optional<std::string>& idxName,
const boost::optional<Variables::Id>& idxId,
intrusive_ptr<Expression> input,
intrusive_ptr<Expression> each)
: Expression(expCtx, {std::move(input), std::move(each)}), _varName(varName), _varId(varId) {
: Expression(expCtx, {std::move(input), std::move(each)}),
_varName(varName),
_varId(varId),
_idxName(std::move(idxName)),
_idxId(idxId) {
expCtx->setSbeCompatibility(SbeCompatibility::notCompatible);
}
intrusive_ptr<Expression> ExpressionMap::optimize() {
// TODO handle when _input is constant
// TODO(SERVER-111215) handle when _input is constant
_children[_kInput] = _children[_kInput]->optimize();
_children[_kEach] = _children[_kEach]->optimize();
return this;
}
Value ExpressionMap::serialize(const SerializationOptions& options) const {
return Value(DOC("$map" << DOC("input" << _children[_kInput]->serialize(options) << "as"
<< options.serializeIdentifier(_varName) << "in"
<< _children[_kEach]->serialize(options))));
return Value(
Document{{"$map",
Document{{"input", _children[_kInput]->serialize(options)},
{"as", options.serializeIdentifier(_varName)},
{"arrayIndexAs",
_idxName ? Value(options.serializeIdentifier(*_idxName)) : Value()},
{"in", _children[_kEach]->serialize(options)}}}});
}
Value ExpressionMap::evaluate(const Document& root, Variables* variables) const {
@ -2767,35 +2835,63 @@ intrusive_ptr<Expression> ExpressionReduce::parse(ExpressionContext* const expCt
<< typeName(expr.type()),
expr.type() == BSONType::object);
const bool isExposeArrayIndexEnabled = expCtx->shouldParserIgnoreFeatureFlagCheck() ||
feature_flags::gFeatureFlagExposeArrayIndexInMapFilterReduce
.isEnabledUseLastLTSFCVWhenUninitialized(
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");
boost::intrusive_ptr<Expression> input;
boost::intrusive_ptr<Expression> initial;
boost::intrusive_ptr<Expression> in;
BSONElement inputElem;
BSONElement initialElem;
BSONElement inElem;
BSONElement arrayIndexAsElem;
for (auto&& elem : expr.Obj()) {
auto field = elem.fieldNameStringData();
if (field == "input") {
input = parseOperand(expCtx, elem, vps);
inputElem = elem;
} else if (field == "initialValue") {
initial = parseOperand(expCtx, elem, vps);
initialElem = elem;
} else if (field == "in") {
in = parseOperand(expCtx, elem, vpsSub);
inElem = elem;
} else if (isExposeArrayIndexEnabled && field == "arrayIndexAs") {
assertLanguageFeatureIsAllowed(expCtx->getOperationContext(),
"arrayIndexAs argument of $reduce operator",
AllowedWithApiStrict::kNeverInVersion1,
AllowedWithClientType::kAny);
arrayIndexAsElem = elem;
} else {
uasserted(40076, str::stream() << "$reduce found an unknown argument: " << field);
}
}
uassert(40077, "$reduce requires 'input' to be specified", inputElem);
uassert(40078, "$reduce requires 'initialValue' to be specified", initialElem);
uassert(40079, "$reduce requires 'in' to be specified", inElem);
uassert(40077, "$reduce requires 'input' to be specified", input);
uassert(40078, "$reduce requires 'initialValue' to be specified", initial);
uassert(40079, "$reduce requires 'in' to be specified", in);
// Parse "arrayIndexAs". If "arrayIndexAs" is not specified, then write to "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);
}
return new ExpressionReduce(
expCtx, std::move(input), std::move(initial), std::move(in), thisVar, valueVar);
return make_intrusive<ExpressionReduce>(expCtx,
parseOperand(expCtx, inputElem, vps),
parseOperand(expCtx, initialElem, vps),
parseOperand(expCtx, inElem, vpsSub),
std::move(idxName),
idxId,
thisVar,
valueVar);
}
Value ExpressionReduce::evaluate(const Document& root, Variables* variables) const {
@ -2810,9 +2906,12 @@ intrusive_ptr<Expression> ExpressionReduce::optimize() {
}
Value ExpressionReduce::serialize(const SerializationOptions& options) const {
return Value(Document{{"$reduce",
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)}}}});
}

View File

@ -2102,6 +2102,8 @@ public:
ExpressionFilter(ExpressionContext* expCtx,
std::string varName,
Variables::Id varId,
const boost::optional<std::string>& idxName,
const boost::optional<Variables::Id>& idxId,
boost::intrusive_ptr<Expression> input,
boost::intrusive_ptr<Expression> cond,
boost::intrusive_ptr<Expression> limit = nullptr);
@ -2118,6 +2120,10 @@ public:
return _varId;
}
const boost::optional<Variables::Id>& getIndexVariableId() const {
return _idxId;
}
bool hasLimit() const {
return this->_limit ? true : false;
}
@ -2138,6 +2144,8 @@ public:
return make_intrusive<ExpressionFilter>(getExpressionContext(),
_varName,
_varId,
_idxName,
_idxId,
cloneChild(_kInput),
cloneChild(_kCond),
_limit ? cloneChild(*_limit) : nullptr);
@ -2153,6 +2161,14 @@ private:
std::string _varName;
// The id of the variable to set.
Variables::Id _varId;
// The name of the variable provided in the 'arrayIndexAs' argument, boost::none if not
// provided.
boost::optional<std::string> _idxName;
// The ID of the variable that represents the array index, boost::none if the feature is not
// enabled.
// TODO(SERVER-90514): make this non-optional when the feature flag is removed.
boost::optional<Variables::Id> _idxId;
// The optional expression determining how many elements should be present in the result array.
boost::optional<size_t> _limit;
@ -2598,6 +2614,8 @@ public:
ExpressionContext* expCtx,
const std::string& varName, // name of variable to set
Variables::Id varId, // id of variable to set
const boost::optional<std::string>& idxName, // name of index to set
const boost::optional<Variables::Id>& idxId, // id of index to set
boost::intrusive_ptr<Expression> input, // yields array to iterate
boost::intrusive_ptr<Expression> each); // yields results to be added to output array
@ -2617,6 +2635,10 @@ public:
return visitor->visit(this);
}
const boost::optional<Variables::Id>& getIndexVariableId() const {
return _idxId;
}
const Expression* getInput() const {
return _children[_kInput].get();
}
@ -2630,8 +2652,13 @@ public:
}
boost::intrusive_ptr<Expression> clone() const final {
return make_intrusive<ExpressionMap>(
getExpressionContext(), _varName, _varId, cloneChild(_kInput), cloneChild(_kEach));
return make_intrusive<ExpressionMap>(getExpressionContext(),
_varName,
_varId,
_idxName,
_idxId,
cloneChild(_kInput),
cloneChild(_kEach));
}
private:
@ -2639,6 +2666,12 @@ private:
static constexpr size_t _kEach = 1;
std::string _varName;
Variables::Id _varId;
// 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
// enabled.
// TODO(SERVER-90514): make this non-optional when the feature flag is removed.
boost::optional<Variables::Id> _idxId;
template <typename H>
friend class ExpressionHashVisitor;
@ -3073,11 +3106,15 @@ public:
boost::intrusive_ptr<Expression> input,
boost::intrusive_ptr<Expression> initial,
boost::intrusive_ptr<Expression> in,
const boost::optional<std::string>& idxName,
const boost::optional<Variables::Id>& idxId,
Variables::Id thisVar,
Variables::Id valueVar)
: Expression(expCtx, {std::move(input), std::move(initial), std::move(in)}),
_thisVar(thisVar),
_valueVar(valueVar) {
_valueVar(valueVar),
_idxName(std::move(idxName)),
_idxId(idxId) {
expCtx->setSbeCompatibility(SbeCompatibility::notCompatible);
}
@ -3096,6 +3133,10 @@ public:
return visitor->visit(this);
}
const boost::optional<Variables::Id>& getIndexVariableId() const {
return _idxId;
}
const Expression* getInput() const {
return _children[_kInput].get();
}
@ -3125,6 +3166,8 @@ public:
cloneChild(_kInput),
cloneChild(_kInitial),
cloneChild(_kIn),
_idxName,
_idxId,
_thisVar,
_valueVar);
}
@ -3136,6 +3179,12 @@ private:
Variables::Id _thisVar;
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
// enabled.
// TODO(SERVER-90514): make this non-optional when the feature flag is removed.
boost::optional<Variables::Id> _idxId;
const size_t _accumulatedValueDepthCheckInterval =
gInternalReduceAccumulatedValueDepthCheckInterval.load();

View File

@ -0,0 +1,788 @@
/**
* Copyright (C) 2025-present MongoDB, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the Server Side Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*/
#include <absl/container/node_hash_map.h>
#include <boost/smart_ptr/intrusive_ptr.hpp>
// IWYU pragma: no_include "boost/container/detail/std_fwd.hpp"
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/bson/bsontypes.h"
#include "mongo/bson/json.h"
#include "mongo/bson/timestamp.h"
#include "mongo/config.h" // IWYU pragma: keep
#include "mongo/db/api_parameters.h"
#include "mongo/db/exec/document_value/document.h"
#include "mongo/db/exec/document_value/document_value_test_util.h"
#include "mongo/db/pipeline/expression.h"
#include "mongo/db/pipeline/expression_context_for_test.h"
#include "mongo/dbtests/dbtests.h" // IWYU pragma: keep
#include "mongo/idl/server_parameter_test_controller.h"
#include "mongo/logv2/log.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/assert_util.h"
#include "mongo/util/decorable.h"
#include <climits>
#include <cmath>
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
namespace mongo {
namespace ExpressionTests {
class ExpressionMapReduceFilterTest : public mongo::unittest::Test {
public:
ExpressionContextForTest& getExpCtx() {
return *_expCtx;
}
// Parse 'json' into an expression of type T.
template <class T>
boost::intrusive_ptr<Expression> parse(StringData json) {
return parse<T>(fromjson(json));
}
// Parse 'bson' into an expression of type T.
template <class T>
boost::intrusive_ptr<Expression> parse(BSONObj bson) {
return T::parse(&getExpCtx(), bson.firstElement(), getExpCtx().variablesParseState);
}
private:
boost::optional<ExpressionContextForTest> _expCtx;
void setUp() override {
_expCtx.emplace();
}
void tearDown() override {
_expCtx.reset();
}
};
// Assert that EXPRESSION throws an exception with the expected error code.
#define ASSERT_CODE(EXPRESSION, EXPECTED_CODE) \
ASSERT_THROWS_CODE(EXPRESSION, DBException, EXPECTED_CODE)
static Document fromJson(const std::string& json) {
return Document(fromjson(json));
}
/* ------------------------- ExpressionMap -------------------------- */
TEST_F(ExpressionMapReduceFilterTest, MapNonArray) {
auto expressionMap = parse<ExpressionMap>("{ $map: {input: 'MongoDB', in: 15213}}");
ASSERT_CODE(expressionMap->evaluate(MutableDocument().freeze(), &getExpCtx().variables), 16883);
}
// Test several parsing errors.
TEST_F(ExpressionMapReduceFilterTest, MapParseConstraints) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
// Uppercase first letter.
ASSERT_CODE(
parse<ExpressionMap>("{ $map: {input: [1, 2, 3], arrayIndexAs: 'Idx', in: '$$Idx'}}"),
ErrorCodes::FailedToParse);
// Identifier starting with a dollar.
ASSERT_CODE(parse<ExpressionMap>("{ $map: {input: [1, 2, 3], arrayIndexAs: '$i', in: '$$$i'}}"),
ErrorCodes::FailedToParse);
// Identifier starting with two dollars.
ASSERT_CODE(
parse<ExpressionMap>("{ $map: {input: [1, 2, 3], arrayIndexAs: '$$i', in: '$$$$i'}}"),
ErrorCodes::FailedToParse);
// Identifier starting with special characters.
ASSERT_CODE(
parse<ExpressionMap>("{ $map: {input: [1, 2, 3], arrayIndexAs: '\\\\a', in: '$$\\\\a'}}"),
ErrorCodes::FailedToParse);
ASSERT_CODE(parse<ExpressionMap>("{ $map: {input: [1, 2, 3], arrayIndexAs: '*a', in: '$$*a'}}"),
ErrorCodes::FailedToParse);
ASSERT_CODE(parse<ExpressionMap>("{ $map: {input: [1, 2, 3], arrayIndexAs: '_a', in: '$$_a'}}"),
ErrorCodes::FailedToParse);
// Identifier with embedded null.
StringData str("fo\0o", 4);
BSONObj query = BSONObjBuilder()
.append("$map",
BSONObjBuilder()
.appendArray("input", BSON_ARRAY(1 << 2 << 3))
.append("arrayIndexAs", str)
.append("in", str)
.obj())
.obj();
ASSERT_CODE(parse<ExpressionMap>(query), ErrorCodes::FailedToParse);
}
TEST(ExpressionMapTest, MapToConstant) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson("{ $map: { input: { $literal: [1, 2, 3]}, in: 1 } }");
auto expressionMap =
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionMap->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(1 << 1 << 1)));
}
TEST(ExpressionMapTest, MapAddOne) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr =
fromjson("{ $map: { input: { $literal: [3, 1, 2]}, as: 'v', in: { $add: ['$$v', 1]} } }");
auto expressionMap =
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionMap->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(4 << 2 << 3)));
}
TEST(ExpressionMapTest, MapDivideZero) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $map: { input: { $literal: [3, 2, 1, 0]}, as: 'v', in: { $divide: [6, '$$v']} } }");
auto expressionMap =
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
ASSERT_THROWS_CODE(expressionMap->evaluate(MutableDocument().freeze(), &expCtx.variables),
DBException,
ErrorCodes::BadValue);
}
TEST(ExpressionMapTest, MapEmptyWithExceptionInit) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr =
fromjson("{ $map: { input: { $literal: []}, as: 'v', in: { $divide: [15445, 0]} } }");
auto expressionMap =
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionMap->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSONArray()));
}
TEST(ExpressionMapTest, MapTypeMismatch) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $map: { input: { $literal: [1, 2, 3]}, as: 'v', in: { $concat: ['$$v', 'MongoDB']} } }");
auto expressionMap =
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
ASSERT_THROWS_CODE(
expressionMap->evaluate(MutableDocument().freeze(), &expCtx.variables), DBException, 16702);
}
TEST(ExpressionMapTest, MapIndices) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson("{ $map: { input: { $literal: [1, 1, 1]}, in: '$$IDX'}}");
auto expressionMap =
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionMap->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(0 << 1 << 2)));
}
TEST(ExpressionMapTest, MapIndicesNamed) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
BSONObj expr =
fromjson("{ $map: { input: { $literal: [1, 1, 1]}, arrayIndexAs: 'i', in: '$$i'}}");
auto expressionMap =
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionMap->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(0 << 1 << 2)));
}
TEST(ExpressionMapTest, MapIndicesNamedFeatureDisabled) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", false);
auto expCtx = ExpressionContextForTest{};
BSONObj expr =
fromjson("{ $map: { input: { $literal: [1, 1, 1]}, arrayIndexAs: 'i', in: '$$i'}}");
ASSERT_THROWS_CODE(
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
DBException,
16879);
}
TEST(ExpressionMapTest, MapIndicesDefaultFeatureDisabled) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", false);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson("{ $map: { input: { $literal: [1, 1, 1]}, in: '$$IDX'}}");
ASSERT_THROWS_CODE(
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
DBException,
17276);
}
TEST(ExpressionMapTest, MapIndicesAPIStrict) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
APIParameters::get(expCtx.getOperationContext()).setAPIVersion("1");
APIParameters::get(expCtx.getOperationContext()).setAPIStrict(true);
BSONObj expr =
fromjson("{ $map: { input: { $literal: [1, 1, 1]}, arrayIndexAs: 'i', in: '$$i'}}");
ASSERT_THROWS_CODE(
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
AssertionException,
ErrorCodes::APIStrictError);
}
TEST(ExpressionMapTest, MapIndicesAPIStrictFeatureDisabled) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", false);
auto expCtx = ExpressionContextForTest{};
APIParameters::get(expCtx.getOperationContext()).setAPIVersion("1");
APIParameters::get(expCtx.getOperationContext()).setAPIStrict(true);
BSONObj expr =
fromjson("{ $map: { input: { $literal: [1, 1, 1]}, arrayIndexAs: 'i', in: '$$i'}}");
ASSERT_THROWS_CODE(
ExpressionMap::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
AssertionException,
16879);
}
/* ------------------------- ExpressionReduce -------------------------- */
// Test several parsing errors.
TEST_F(ExpressionMapReduceFilterTest, ReduceParseConstraints) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
// Identifier with uppercase first letter.
ASSERT_CODE(parse<ExpressionReduce>("{ $reduce: { input: [1, 2, 3], initialValue: 0, "
"arrayIndexAs: 'Idx', in: { $add: ['$$Idx', 1]}}}"),
ErrorCodes::FailedToParse);
// Identifier starting with a dollar.
ASSERT_CODE(parse<ExpressionReduce>("{ $reduce: { input: [1, 2, 3], initialValue: 0, "
"arrayIndexAs: '$i', in: { $add: ['$$$i', 1]}}}"),
ErrorCodes::FailedToParse);
// Identifier starting with two dollars.
ASSERT_CODE(parse<ExpressionReduce>("{ $reduce: { input: [1, 2, 3], initialValue: 0, "
"arrayIndexAs: '$$i', in: { $add: ['$$$$i', 1]}}}"),
ErrorCodes::FailedToParse);
// Identifier starting with special characters.
ASSERT_CODE(parse<ExpressionReduce>("{ $reduce: { input: [1, 2, 3], initialValue: 0, "
"arrayIndexAs: '\\\\a', in: { $add: ['$$\\\\a', 1]}}}"),
ErrorCodes::FailedToParse);
ASSERT_CODE(parse<ExpressionReduce>("{ $reduce: { input: [1, 2, 3], initialValue: 0, "
"arrayIndexAs: '*a', in: { $add: ['$$*a', 1]}}}"),
ErrorCodes::FailedToParse);
ASSERT_CODE(parse<ExpressionReduce>("{ $reduce: { input: [1, 2, 3], initialValue: 0, "
"arrayIndexAs: '_a', in: { $add: ['$$_a', 1]}}}"),
ErrorCodes::FailedToParse);
// Identifier with embedded null.
StringData str("fo\0o", 4);
BSONObj query =
BSONObjBuilder()
.append(
"$reduce",
BSONObjBuilder()
.appendArray("input", BSON_ARRAY(1 << 2 << 3))
.append("initialValue", 0)
.append("arrayIndexAs", str)
.append("in", BSONObjBuilder().appendArray("$add", BSON_ARRAY(str << 1)).obj())
.obj())
.obj();
ASSERT_CODE(parse<ExpressionReduce>(query), ErrorCodes::FailedToParse);
}
TEST(ExpressionReduceTest, ReduceNonArray) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $reduce: { input: 'MongoDB', initialValue: 15213, in: { $add: ['$$value', 1]}}}");
auto expressionReduce =
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
ASSERT_THROWS_CODE(expressionReduce->evaluate(MutableDocument().freeze(), &expCtx.variables),
DBException,
40080);
}
TEST(ExpressionReduceTest, ReduceEmptyArray) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $reduce: { input: { $literal: [] }, initialValue: 15150, in: { $add: [ '$$value', "
"'$$this' ]"
"} } }");
auto expressionReduce =
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionReduce->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(15150));
}
TEST(ExpressionReduceTest, ReduceStringConcat) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $reduce: { input: { $literal: ['a', 'b', 'c']}, initialValue: '', in: {$concat: "
"['$$value', '$$this']} }}");
auto expressionReduce =
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionReduce->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(("abc"_sd)));
}
TEST(ExpressionReduceTest, ReduceSumProduct) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $reduce: { input: {$literal: [1, 2, 3, 4, 5]}, initialValue: { sum: 5, product: 2 }, "
"in: {sum : {$add: ['$$this', '$$value.sum']}, product: {$multiply: ['$$this', "
"'$$value.product']}} } "
"}");
auto expressionReduce =
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionReduce->evaluate(MutableDocument().freeze(), &expCtx.variables);
BSONObj res = fromjson("{sum: 20, product: 240}");
ASSERT_VALUE_EQ(val, Value(res));
}
TEST(ExpressionReduceTest, ReduceEmptyExceptionInitialValue) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $reduce: { input: { $literal: []}, initialValue: {$divide: [48, 0]}, in: {$divide: "
"['$$this', '$$value']} } }");
auto expressionReduce =
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
ASSERT_THROWS_CODE(expressionReduce->evaluate(MutableDocument().freeze(), &expCtx.variables),
DBException,
ErrorCodes::BadValue);
}
TEST(ExpressionReduceTest, ReduceEmptyExceptionExpression) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $reduce: { input: { $literal: []}, initialValue: 15312, in: {$divide: ['$$this', 0]}} "
"}");
auto expressionReduce =
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionReduce->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(15312));
}
TEST(ExpressionReduceTest, ReduceIndicesDefault) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $reduce: { input: { $literal: [1, 1, 1]}, initialValue: 0, in: "
"{$add: ['$$this', "
"'$$value', '$$IDX']}}}");
auto expressionReduce =
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionReduce->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(6));
}
TEST(ExpressionReduceTest, ReduceIndicesNamed) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $reduce: { input: { $literal: [1, 1, 1]}, initialValue: 0, arrayIndexAs: 'i', in: "
"{$add: ['$$this', "
"'$$value', '$$i']}}}");
auto expressionReduce =
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionReduce->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(6));
}
TEST(ExpressionReduceTest, ReduceIndicesAPIStrict) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
APIParameters::get(expCtx.getOperationContext()).setAPIVersion("1");
APIParameters::get(expCtx.getOperationContext()).setAPIStrict(true);
BSONObj expr = fromjson(
"{ $reduce: {input: [1, 2, 3], arrayIndexAs: 'i', in: {$add: ['$$this', '$$value', '$$i']} "
"}}");
ASSERT_THROWS_CODE(
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
AssertionException,
ErrorCodes::APIStrictError);
}
TEST(ExpressionReduceTest, ReduceIndicesAPIStrictFeatureDisabled) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", false);
auto expCtx = ExpressionContextForTest{};
APIParameters::get(expCtx.getOperationContext()).setAPIVersion("1");
APIParameters::get(expCtx.getOperationContext()).setAPIStrict(true);
BSONObj expr = fromjson(
"{ $reduce: {input: [1, 2, 3], arrayIndexAs: 'i', in: {$add: ['$$this', '$$value', '$$i']} "
"}}");
ASSERT_THROWS_CODE(
ExpressionReduce::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
AssertionException,
40076);
}
/* ------------------------- ExpressionFilter -------------------------- */
// Test several parsing errors.
TEST_F(ExpressionMapReduceFilterTest, FilterParseConstraints) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
// Identifier with uppercase first letter.
ASSERT_CODE(
parse<ExpressionFilter>(
"{ $filter: { input: [1, 2, 3], arrayIndexAs: 'Idx', cond: { $lte: [ '$$Idx', 2]}}}"),
ErrorCodes::FailedToParse);
// Identifier starting with a dollar.
ASSERT_CODE(
parse<ExpressionFilter>(
"{ $filter: { input: [1, 2, 3], arrayIndexAs: '$i', cond: { $lte: [ '$$$i', 2]}}}"),
ErrorCodes::FailedToParse);
// Identifier starting with two dollars.
ASSERT_CODE(
parse<ExpressionFilter>(
"{ $filter: { input: [1, 2, 3], arrayIndexAs: '$$i', cond: { $lte: [ '$$$$i', 2]}}}"),
ErrorCodes::FailedToParse);
// Identifier starting with special characters.
ASSERT_CODE(parse<ExpressionFilter>("{ $filter: { input: [1, 2, 3], arrayIndexAs: '\\\\a', "
"cond: { $lte: [ '$$\\\\a', 2]}}}"),
ErrorCodes::FailedToParse);
ASSERT_CODE(
parse<ExpressionFilter>(
"{ $filter: { input: [1, 2, 3], arrayIndexAs: '*a', cond: { $lte: [ '$$*a', 2]}}}"),
ErrorCodes::FailedToParse);
ASSERT_CODE(
parse<ExpressionFilter>(
"{ $filter: { input: [1, 2, 3], arrayIndexAs: '_a', cond: { $lte: [ '$$_a', 2]}}}"),
ErrorCodes::FailedToParse);
// Identifier with embedded null.
StringData str("fo\0o", 4);
BSONObj query =
BSONObjBuilder()
.append("$filter",
BSONObjBuilder()
.appendArray("input", BSON_ARRAY(1 << 2 << 3))
.append("arrayIndexAs", str)
.append("cond",
BSONObjBuilder().appendArray("$lte", BSON_ARRAY(str << 2)).obj())
.obj())
.obj();
ASSERT_CODE(parse<ExpressionFilter>(query), ErrorCodes::FailedToParse);
}
TEST(ExpressionFilterTest, FilterNonArray) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson("{ $filter: { input: 'MongoDB', as: 'v', cond: true}}");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
ASSERT_THROWS_CODE(expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables),
DBException,
28651);
}
TEST(ExpressionFilterTest, FilterInvalidLimit) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: {$literal: [1, 2, 3]}, as: 'v', cond: { $lte: [ '$$v', 2]}, limit: -1 "
"}}");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
ASSERT_THROWS_CODE(expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables),
DBException,
327392);
}
TEST(ExpressionFilterTest, FilterTypeMismatchLimit) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: {$literal: [1, 2, 3]}, as: 'v', cond: { $lte: [ '$$v', 2]}, limit: "
"'Functions are Values' "
"}}");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
ASSERT_THROWS_CODE(expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables),
DBException,
327391);
}
TEST(ExpressionFilterTest, FilterExtraLimit) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr =
fromjson("{ $filter: { input: {$literal: [1, 2, 3]}, as: 'v', cond: true , limit: 5 }}");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(1 << 2 << 3)));
}
TEST(ExpressionFilterTest, FilterNullLimit) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr =
fromjson("{ $filter: { input: {$literal: [1, 2, 3]}, as: 'v', cond: true , limit: null }}");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(1 << 2 << 3)));
}
TEST(ExpressionFilterTest, FilterIntegerComparison) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [1, 2, 3, 4, 5]}, as: 'v', cond: { $lte: [ '$$v', 3 ] } } "
"}");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(1 << 2 << 3)));
}
TEST(ExpressionFilterTest, FilterIsNumber) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [1, 'a', 2, null, 3.1, NumberLong(4), '5']}, as: 'v', "
"cond: { $isNumber: '$$v'}, limit: 3 } }");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(1 << 2 << 3.1)));
}
TEST(ExpressionFilterTest, FilterStringEqualityOnField) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [ {city: 'Pittsburgh', population: 302898}, {city: 'NYC', "
"population: 8336000}]}, as: 'v', cond: { $eq: ['$$v.city', 'Pittsburgh']} } }");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(fromjson("{city: 'Pittsburgh', population: 302898}"))));
}
TEST(ExpressionFilterTest, FilterEmptyWithExceptionCondition) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr =
fromjson("{ $filter: { input: { $literal: []}, as: 'v', cond: { $divide: [1, 0]}} }");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSONArray()));
}
TEST(ExpressionFilterTest, FilterConditionFalse) {
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [0, 'c', NumberLong(12), true]}, as: 'v', cond: false} }");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSONArray()));
}
TEST(ExpressionFilterTest, FilterIndicesDefault) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [0, 4, 2, 7, 4, 0] }, as: 'v', cond: { $eq: ['$$v', "
"'$$IDX']}} }");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(0 << 2 << 4)));
}
TEST(ExpressionFilterTest, FilterIndicesNamed) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [0, 4, 2, 7, 4, 0] }, as: 'v', arrayIndexAs: 'i', cond: { "
"$eq: ['$$v', "
"'$$i']}} }");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(0 << 2 << 4)));
}
TEST(ExpressionFilterTest, FilterIndicesDefaultLimited) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [0, 4, 2, 7, 4, 0] }, as: 'v', cond: { $eq: ['$$v', "
"'$$IDX']}, limit: 2 }}");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(0 << 2)));
}
TEST(ExpressionFilterTest, FilterIndicesNamedLimited) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [0, 4, 2, 7, 4, 0] }, as: 'v', arrayIndexAs: 'i', cond: { "
"$eq: ['$$v', "
"'$$i']}, limit: 1 }}");
auto expressionFilter =
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState);
Value val = expressionFilter->evaluate(MutableDocument().freeze(), &expCtx.variables);
ASSERT_VALUE_EQ(val, Value(BSON_ARRAY(0)));
}
TEST(ExpressionFilterTest, FilterIndicesDefaultFeatureDisabled) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", false);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [0, 4, 2, 7, 4, 0] }, as: 'v', cond: { $eq: ['$$v', "
"'$$IDX']}} }");
ASSERT_THROWS_CODE(
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
DBException,
17276);
}
TEST(ExpressionFilterTest, FilterIndicesNamedFeatureDisabled) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", false);
auto expCtx = ExpressionContextForTest{};
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [0, 4, 2, 7, 4, 0] }, as: 'v', arrayIndexAs: 'i', cond: { "
"$eq: ['$$v', "
"'$$i']}} }");
ASSERT_THROWS_CODE(
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
DBException,
28647);
}
TEST(ExpressionFilterTest, FilterIndicesAPIStrict) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
auto expCtx = ExpressionContextForTest{};
APIParameters::get(expCtx.getOperationContext()).setAPIVersion("1");
APIParameters::get(expCtx.getOperationContext()).setAPIStrict(true);
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [0, 4, 2, 7, 4, 0] }, as: 'v', arrayIndexAs: 'i', cond: { "
"$eq: ['$$v', "
"'$$i']}} }");
ASSERT_THROWS_CODE(
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
AssertionException,
ErrorCodes::APIStrictError);
}
TEST(ExpressionFilterTest, FilterIndicesAPIStrictFeatureDisabled) {
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", false);
auto expCtx = ExpressionContextForTest{};
APIParameters::get(expCtx.getOperationContext()).setAPIVersion("1");
APIParameters::get(expCtx.getOperationContext()).setAPIStrict(true);
BSONObj expr = fromjson(
"{ $filter: { input: { $literal: [0, 4, 2, 7, 4, 0] }, as: 'v', arrayIndexAs: 'i', cond: { "
"$eq: ['$$v', "
"'$$i']}} }");
ASSERT_THROWS_CODE(
ExpressionFilter::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState),
AssertionException,
28647);
}
} // namespace ExpressionTests
} // namespace mongo

View File

@ -2935,6 +2935,29 @@ TEST_F(PipelineOptimizationTest,
assertPipelineOptimizesAndSerializesTo(pipeline, pipeline);
}
TEST_F(PipelineOptimizationTest,
FeatureMatchElemMatchValueOnArrayFieldCanNotSplitAcrossRenameWithMapAndAddFields) {
// The $addFields simply maps an array of objects to one containing their inner 'elementField'
// scalar values . The $match stage on the reshaped array should not be swapped with $project to
// preserve the original $elemMatch semantics.
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
std::string pipeline = R"(
[
{
$addFields: {
"reshapedArray": {
$map: {input: '$arrayField', as: 'iter', arrayIndexAs: 'index', in : "$$iter.elementField"}
},
_id: { "$const": false }
}
},
{$match: {"reshapedArray": {$elemMatch: {$eq: 1}}}}
]
)";
assertPipelineOptimizesAndSerializesTo(pipeline, pipeline);
}
TEST_F(PipelineOptimizationTest,
MatchElemMatchValueOnArrayFieldCanNotSplitAcrossRenameWithDottedProject) {
// The $project stage maps a dotted field path to a simple non-dotted one which is then matched
@ -2994,6 +3017,55 @@ TEST_F(PipelineOptimizationTest,
assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe);
}
TEST_F(PipelineOptimizationTest,
FeatureMatchElemMatchObjectOnArrayFieldCanNotSplitAcrossRenameWithMapAndProject) {
// The $project simply renames 'a.b' & 'a.c' to 'd.e' & 'd.f' but the dependency tracker reports
// the 'd' for $elemMatch as a modified dependency and so $match cannot be swapped with
// $project.
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
std::string inputPipe = R"(
[
{
$project: {
d: {
$map: {input: '$a', as: 'iter', arrayIndexAs: 'index', in : {e: '$$iter.b', f: '$$iter.c'}}
}
}
},
{$match: {d: {$elemMatch: {e: 1, f: 1}}}}
]
)";
std::string outputPipe = R"(
[
{
$project: {
_id: true,
d: {
$map: {input: "$a", as: "iter", arrayIndexAs: "index", in : {e: "$$iter.b", f: "$$iter.c"}}
}
}
},
{$match: {d: {$elemMatch: {$and: [{e: {$eq: 1}}, {f: {$eq: 1}}]}}}}
]
)";
std::string serializedPipe = R"(
[
{
$project: {
_id: true,
d: {
$map: {input: '$a', as: 'iter', arrayIndexAs: 'index', in : {e: '$$iter.b', f: '$$iter.c'}}
}
}
},
{$match: {d: {$elemMatch: {e: 1, f: 1}}}}
]
)";
assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe);
}
// TODO SERVER-74298 The $match can be swapped with $project after renaming.
TEST_F(PipelineOptimizationTest, MatchEqObjectCanNotSplitAcrossRenameWithMapAndProject) {
// The $project simply renames 'a.b' & 'a.c' to 'd.e' & 'd.f' but the dependency tracker reports
@ -3040,6 +3112,53 @@ TEST_F(PipelineOptimizationTest, MatchEqObjectCanNotSplitAcrossRenameWithMapAndP
assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe);
}
TEST_F(PipelineOptimizationTest, FeatureMatchEqObjectCanNotSplitAcrossRenameWithMapAndProject) {
// The $project simply renames 'a.b' & 'a.c' to 'd.e' & 'd.f' but the dependency tracker reports
// the 'd' for $eq as a modified dependency and so $match cannot be swapped with $project.
RAIIServerParameterControllerForTest featureFlagController(
"featureFlagExposeArrayIndexInMapFilterReduce", true);
std::string inputPipe = R"(
[
{
$project: {
d: {
$map: {input: '$a', as: 'i', arrayIndexAs: 'index', in : {e: '$$i.b', f: '$$i.c'}}
}
}
},
{$match: {d: {$eq: {e: 1, f: 1}}}}
]
)";
std::string outputPipe = R"(
[
{
$project: {
_id: true,
d: {
$map: {input: "$a", as: "i", arrayIndexAs: "index", in : {e: "$$i.b", f: "$$i.c"}}
}
}
},
{$match: {d: {$eq: {e: 1, f: 1}}}}
]
)";
std::string serializedPipe = R"(
[
{
$project: {
_id: true,
d: {
$map: {input: '$a', as: 'i', arrayIndexAs: 'index', in : {e: '$$i.b', f: '$$i.c'}}
}
}
},
{$match: {d: {$eq: {e: 1, f: 1}}}}
]
)";
assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe);
}
TEST_F(PipelineOptimizationTest, MatchCannotSwapWithLimit) {
std::string pipeline = "[{$limit: 3}, {$match: {x: {$gt: 0}}}]";
assertPipelineOptimizesAndSerializesTo(pipeline, pipeline);

View File

@ -110,6 +110,14 @@ feature_flags:
version: 8.1
fcv_gated: true
featureFlagExposeArrayIndexInMapFilterReduce:
description:
"Feature flag to enable referencing array indices in $map, $filter, and $reduce
expressions."
cpp_varname: gFeatureFlagExposeArrayIndexInMapFilterReduce
default: false
fcv_gated: true
featureFlagBinDataConvertNumeric:
description: "Feature flag to enable extended BinData support for int, double and long in $convert."
cpp_varname: gFeatureFlagBinDataConvertNumeric