0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-12-01 09:32:32 +01:00

SERVER-27115 Allow $match to swap across renames expressed using $map.

This commit is contained in:
David Storch 2017-05-23 19:32:11 -04:00
parent a24c0780ae
commit a702053750
7 changed files with 449 additions and 34 deletions

View File

@ -18,20 +18,20 @@
let pipeline = [{$project: {_id: 0, z: "$a", c: 1}}, {$match: {z: {$gt: 1}}}];
assert.eq(2, coll.aggregate(pipeline).itcount());
let explain = coll.explain().aggregate(pipeline);
assert.neq(null, getAggPlanStage(explain, "IXSCAN"));
assert.neq(null, getAggPlanStage(explain, "IXSCAN"), tojson(explain));
// Test that a $match can result in index usage after moving past a field renamed by $addFields.
pipeline = [{$addFields: {z: "$a"}}, {$match: {z: {$gt: 1}}}];
assert.eq(2, coll.aggregate(pipeline).itcount());
explain = coll.explain().aggregate(pipeline);
assert.neq(null, getAggPlanStage(explain, "IXSCAN"));
assert.neq(null, getAggPlanStage(explain, "IXSCAN"), tojson(explain));
// Test that a $match with $type can result in index usage after moving past a field renamed by
// $project.
pipeline = [{$project: {_id: 0, z: "$a", c: 1}}, {$match: {z: {$type: "number"}}}];
assert.eq(3, coll.aggregate(pipeline).itcount());
explain = coll.explain().aggregate(pipeline);
assert.neq(null, getAggPlanStage(explain, "IXSCAN"));
assert.neq(null, getAggPlanStage(explain, "IXSCAN"), tojson(explain));
// Test that a partially dependent match can split, with a rename applied, resulting in index
// usage.
@ -39,7 +39,7 @@
[{$project: {z: "$a", zz: {$sum: ["$a", "$b"]}}}, {$match: {z: {$gt: 1}, zz: {$lt: 5}}}];
assert.eq(1, coll.aggregate(pipeline).itcount());
explain = coll.explain().aggregate(pipeline);
assert.neq(null, getAggPlanStage(explain, "IXSCAN"));
assert.neq(null, getAggPlanStage(explain, "IXSCAN"), tojson(explain));
// Test that a match can swap past several renames, resulting in index usage.
pipeline = [
@ -50,5 +50,126 @@
];
assert.eq(2, coll.aggregate(pipeline).itcount());
explain = coll.explain().aggregate(pipeline);
assert.neq(null, getAggPlanStage(explain, "IXSCAN"));
assert.neq(null, getAggPlanStage(explain, "IXSCAN"), tojson(explain));
coll.drop();
assert.writeOK(coll.insert({_id: 0, a: [{b: 1, c: 1}, {b: 2, c: 2}]}));
assert.writeOK(coll.insert({_id: 1, a: [{b: 3, c: 3}, {b: 4, c: 4}]}));
assert.commandWorked(coll.createIndex({"a.b": 1, "a.c": 1}));
// Test that a $match can result in index usage after moving past a dotted array path renamed by
// a $map inside a $project.
pipeline = [
{$project: {d: {$map: {input: "$a", as: "iter", in : {e: "$$iter.b", f: "$$iter.c"}}}}},
{$match: {"d.e": 1, "d.f": 2}}
];
assert.eq([{_id: 0, d: [{e: 1, f: 1}, {e: 2, f: 2}]}], coll.aggregate(pipeline).toArray());
explain = coll.explain().aggregate(pipeline);
let ixscan = getAggPlanStage(explain, "IXSCAN");
assert.neq(null, ixscan, tojson(explain));
assert.eq({"a.b": 1, "a.c": 1}, ixscan.keyPattern, tojson(ixscan));
// Test that a $match can result in index usage after moving past a dotted array path renamed by
// a $map inside an $addFields. This time the match expression is partially dependent and should
// get split.
pipeline = [
{
$addFields:
{d: {$map: {input: "$a", as: "iter", in : {e: "$$iter.b", f: "$$iter.c"}}}, g: 2}
},
{$match: {"d.e": 1, g: 2}}
];
assert.eq([{_id: 0, a: [{b: 1, c: 1}, {b: 2, c: 2}], d: [{e: 1, f: 1}, {e: 2, f: 2}], g: 2}],
coll.aggregate(pipeline).toArray());
explain = coll.explain().aggregate(pipeline);
ixscan = getAggPlanStage(explain, "IXSCAN");
assert.neq(null, ixscan, tojson(explain));
assert.eq({"a.b": 1, "a.c": 1}, ixscan.keyPattern, tojson(ixscan));
// Test that match swapping behaves correctly when a $map contains a rename but also computes a
// new field.
pipeline = [
{
$addFields:
{d: {$map: {input: "$a", as: "iter", in : {e: "$$iter.b", f: {$literal: 99}}}}}
},
{$match: {"d.e": 1, "d.f": 99}}
];
assert.eq([{_id: 0, a: [{b: 1, c: 1}, {b: 2, c: 2}], d: [{e: 1, f: 99}, {e: 2, f: 99}]}],
coll.aggregate(pipeline).toArray());
explain = coll.explain().aggregate(pipeline);
ixscan = getAggPlanStage(explain, "IXSCAN");
assert.neq(null, ixscan, tojson(explain));
assert.eq({"a.b": 1, "a.c": 1}, ixscan.keyPattern, tojson(ixscan));
coll.drop();
assert.writeOK(coll.insert({_id: 0, a: [{b: [{c: 1}, {c: 2}]}, {b: [{c: 3}, {c: 4}]}]}));
assert.writeOK(coll.insert({_id: 1, a: [{b: [{c: 5}, {c: 6}]}, {b: [{c: 7}, {c: 8}]}]}));
assert.commandWorked(coll.createIndex({"a.b.c": 1}));
// Test that a $match can result in index usage by moving past a rename of a field inside
// two-levels of arrays. The rename is expressed using nested $map inside a $project.
pipeline = [
{
$project: {
d: {
$map: {
input: "$a",
as: "iterOuter",
in : {
e: {
$map: {
input: "$$iterOuter.b",
as: "iterInner",
in : {f: "$$iterInner.c"}
}
}
}
}
}
}
},
{$match: {"d.e.f": 7}}
];
assert.eq([{_id: 1, d: [{e: [{f: 5}, {f: 6}]}, {e: [{f: 7}, {f: 8}]}]}],
coll.aggregate(pipeline).toArray());
explain = coll.explain().aggregate(pipeline);
ixscan = getAggPlanStage(explain, "IXSCAN");
assert.neq(null, ixscan, tojson(explain));
assert.eq({"a.b.c": 1}, ixscan.keyPattern, tojson(ixscan));
// Test that a $match can result in index usage by moving past a rename of a field inside
// two-levels of arrays. The rename is expressed using nested $map inside an $addFields.
pipeline = [
{
$addFields: {
d: {
$map: {
input: "$a",
as: "iterOuter",
in : {
b: {
$map: {
input: "$$iterOuter.b",
as: "iterInner",
in : {c: "$$iterInner.c"}
}
}
}
}
}
}
},
{$match: {"d.b.c": 7}}
];
assert.eq([{
_id: 1,
a: [{b: [{c: 5}, {c: 6}]}, {b: [{c: 7}, {c: 8}]}],
d: [{b: [{c: 5}, {c: 6}]}, {b: [{c: 7}, {c: 8}]}]
}],
coll.aggregate(pipeline).toArray());
explain = coll.explain().aggregate(pipeline);
ixscan = getAggPlanStage(explain, "IXSCAN");
assert.neq(null, ixscan, tojson(explain));
assert.eq({"a.b.c": 1}, ixscan.keyPattern, tojson(ixscan));
}());

View File

@ -431,12 +431,6 @@ std::pair<unique_ptr<MatchExpression>, unique_ptr<MatchExpression>> splitMatchEx
unique_ptr<MatchExpression> expr,
const std::set<std::string>& fields,
const StringMap<std::string>& renames) {
// TODO SERVER-27115: Currently renames from dotted fields are not supported, but this
// restriction can be relaxed.
for (auto&& rename : renames) {
invariant(rename.second.find('.') == std::string::npos);
}
auto splitExpr = splitMatchExpressionByWithoutRenames(std::move(expr), fields);
if (splitExpr.first) {
applyRenamesToExpression(splitExpr.first.get(), renames);

View File

@ -1311,6 +1311,23 @@ Value ExpressionObject::serialize(bool explain) const {
return outputDoc.freezeToValue();
}
Expression::ComputedPaths ExpressionObject::getComputedPaths(const std::string& exprFieldPath,
Variables::Id renamingVar) const {
ComputedPaths outputPaths;
for (auto&& pair : _expressions) {
auto exprComputedPaths = pair.second->getComputedPaths(pair.first, renamingVar);
for (auto&& renames : exprComputedPaths.renames) {
auto newPath = FieldPath::getFullyQualifiedPath(exprFieldPath, renames.first);
outputPaths.renames[std::move(newPath)] = renames.second;
}
for (auto&& path : exprComputedPaths.paths) {
outputPaths.paths.insert(FieldPath::getFullyQualifiedPath(exprFieldPath, path));
}
}
return outputPaths;
}
/* --------------------- ExpressionFieldPath --------------------------- */
// this is the old deprecated version only used by tests not using variables
@ -1438,6 +1455,31 @@ Value ExpressionFieldPath::serialize(bool explain) const {
}
}
Expression::ComputedPaths ExpressionFieldPath::getComputedPaths(const std::string& exprFieldPath,
Variables::Id renamingVar) const {
// An expression field path is either considered a rename or a computed path. We need to find
// out which case we fall into.
//
// The caller has told us that renames must have 'varId' as the first component. We also check
// that there is only one additional component---no dotted field paths are allowed! This is
// because dotted ExpressionFieldPaths can actually reshape the document rather than just
// changing the field names. This can happen only if there are arrays along the dotted path.
//
// For example, suppose you have document {a: [{b: 1}, {b: 2}]}. The projection {"c.d": "$a.b"}
// does *not* perform the strict rename to yield document {c: [{d: 1}, {d: 2}]}. Instead, it
// results in the document {c: {d: [1, 2]}}. Due to this reshaping, matches expressed over "a.b"
// before the $project is applied may not have the same behavior when expressed over "c.d" after
// the $project is applied.
ComputedPaths outputPaths;
if (_variable == renamingVar && _fieldPath.getPathLength() == 2u) {
outputPaths.renames[exprFieldPath] = _fieldPath.tail().fullPath();
} else {
outputPaths.paths.insert(exprFieldPath);
}
return outputPaths;
}
/* ------------------------- ExpressionFilter ----------------------------- */
REGISTER_EXPRESSION(filter, ExpressionFilter::parse);
@ -1774,6 +1816,35 @@ void ExpressionMap::addDependencies(DepsTracker* deps) const {
_each->addDependencies(deps);
}
Expression::ComputedPaths ExpressionMap::getComputedPaths(const std::string& exprFieldPath,
Variables::Id renamingVar) const {
auto inputFieldPath = dynamic_cast<ExpressionFieldPath*>(_input.get());
if (!inputFieldPath) {
return {{exprFieldPath}, {}};
}
auto inputComputedPaths = inputFieldPath->getComputedPaths("", renamingVar);
if (inputComputedPaths.renames.empty()) {
return {{exprFieldPath}, {}};
}
invariant(inputComputedPaths.renames.size() == 1u);
auto fieldPathRenameIter = inputComputedPaths.renames.find("");
invariant(fieldPathRenameIter != inputComputedPaths.renames.end());
const auto& oldArrayName = fieldPathRenameIter->second;
auto eachComputedPaths = _each->getComputedPaths(exprFieldPath, _varId);
if (eachComputedPaths.renames.empty()) {
return {{exprFieldPath}, {}};
}
// Append the name of the array to the beginning of the old field path.
for (auto&& rename : eachComputedPaths.renames) {
eachComputedPaths.renames[rename.first] =
FieldPath::getFullyQualifiedPath(oldArrayName, rename.second);
}
return eachComputedPaths;
}
/* ------------------------- ExpressionMeta ----------------------------- */
REGISTER_EXPRESSION(meta, ExpressionMeta::parse);

View File

@ -70,6 +70,19 @@ public:
using Parser = stdx::function<boost::intrusive_ptr<Expression>(
const boost::intrusive_ptr<ExpressionContext>&, BSONElement, const VariablesParseState&)>;
/**
* Represents new paths computed by an expression. Computed paths are partitioned into renames
* and non-renames. See the comments for Expression::getComputedPaths() for more information.
*/
struct ComputedPaths {
// Non-rename computed paths.
std::set<std::string> paths;
// Mappings from the old name of a path before applying this expression, to the new one
// after applying this expression.
StringMap<std::string> renames;
};
virtual ~Expression(){};
/**
@ -107,6 +120,35 @@ public:
*/
virtual Value evaluate(const Document& root) const = 0;
/**
* Returns information about the paths computed by this expression. This only needs to be
* overridden by expressions that have renaming semantics, where optimization code could take
* advantage of knowledge of these renames.
*
* Partitions paths involved in this expression into the set of computed paths and the set of
* ("new" => "old") rename mappings. Here "new" refers to the name of the path after applying
* this expression, whereas "old" refers to the name of the path before applying this
* expression.
*
* The 'exprFieldPath' is the field path at which the result of this expression will be stored.
* This is used to determine the value of the "new" path created by the rename.
*
* The 'renamingVar' is needed for checking whether a field path is a rename. For example, at
* the top level only field paths that begin with the ROOT variable, as in "$$ROOT.path", are
* renames. A field path such as "$$var.path" is not a rename.
*
* Now consider the example of a rename expressed via a $map:
*
* {$map: {input: "$array", as: "iter", in: {...}}}
*
* In this case, only field paths inside the "in" clause beginning with "iter", such as
* "$$iter.path", are renames.
*/
virtual ComputedPaths getComputedPaths(const std::string& exprFieldPath,
Variables::Id renamingVar = Variables::kRootId) const {
return {{exprFieldPath}, {}};
}
/**
* Parses a BSON Object that could represent an object literal or a functional expression like
* $add.
@ -746,9 +788,8 @@ public:
return _fieldPath;
}
Variables::Id getVariableId() const {
return _variable;
}
ComputedPaths getComputedPaths(const std::string& exprFieldPath,
Variables::Id renamingVar) const final;
private:
ExpressionFieldPath(const boost::intrusive_ptr<ExpressionContext>& expCtx,
@ -956,6 +997,9 @@ public:
BSONElement expr,
const VariablesParseState& vps);
ComputedPaths getComputedPaths(const std::string& exprFieldPath,
Variables::Id renamingVar) const final;
private:
ExpressionMap(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
@ -1106,6 +1150,9 @@ public:
return _expressions;
}
ComputedPaths getComputedPaths(const std::string& exprFieldPath,
Variables::Id renamingVar) const final;
private:
ExpressionObject(
const boost::intrusive_ptr<ExpressionContext>& expCtx,

View File

@ -4201,6 +4201,179 @@ class Null : public ExpectedResultBase {
} // namespace AllAnyElements
namespace GetComputedPathsTest {
TEST(GetComputedPathsTest, ExpressionFieldPathDoesNotCountAsRenameWhenUsingRemoveBuiltin) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto expr = ExpressionFieldPath::parse(expCtx, "$$REMOVE", expCtx->variablesParseState);
auto computedPaths = expr->getComputedPaths("a", Variables::kRootId);
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("a"), 1u);
ASSERT(computedPaths.renames.empty());
}
TEST(GetComputedPathsTest, ExpressionFieldPathDoesNotCountAsRenameWhenOnlyRoot) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto expr = ExpressionFieldPath::parse(expCtx, "$$ROOT", expCtx->variablesParseState);
auto computedPaths = expr->getComputedPaths("a", Variables::kRootId);
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("a"), 1u);
ASSERT(computedPaths.renames.empty());
}
TEST(GetComputedPathsTest, ExpressionFieldPathDoesNotCountAsRenameWithNonMatchingUserVariable) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
expCtx->variablesParseState.defineVariable("userVar");
auto expr = ExpressionFieldPath::parse(expCtx, "$$userVar.b", expCtx->variablesParseState);
auto computedPaths = expr->getComputedPaths("a", Variables::kRootId);
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("a"), 1u);
ASSERT(computedPaths.renames.empty());
}
TEST(GetComputedPathsTest, ExpressionFieldPathDoesNotCountAsRenameWhenDotted) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto expr = ExpressionFieldPath::parse(expCtx, "$a.b", expCtx->variablesParseState);
auto computedPaths = expr->getComputedPaths("c", Variables::kRootId);
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("c"), 1u);
ASSERT(computedPaths.renames.empty());
}
TEST(GetComputedPathsTest, ExpressionFieldPathDoesCountAsRename) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto expr = ExpressionFieldPath::parse(expCtx, "$a", expCtx->variablesParseState);
auto computedPaths = expr->getComputedPaths("b", Variables::kRootId);
ASSERT(computedPaths.paths.empty());
ASSERT_EQ(computedPaths.renames.size(), 1u);
ASSERT_EQ(computedPaths.renames["b"], "a");
}
TEST(GetComputedPathsTest, ExpressionFieldPathDoesCountAsRenameWithExplicitRoot) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto expr = ExpressionFieldPath::parse(expCtx, "$$ROOT.a", expCtx->variablesParseState);
auto computedPaths = expr->getComputedPaths("b", Variables::kRootId);
ASSERT(computedPaths.paths.empty());
ASSERT_EQ(computedPaths.renames.size(), 1u);
ASSERT_EQ(computedPaths.renames["b"], "a");
}
TEST(GetComputedPathsTest, ExpressionFieldPathDoesCountAsRenameWithExplicitCurrent) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto expr = ExpressionFieldPath::parse(expCtx, "$$CURRENT.a", expCtx->variablesParseState);
auto computedPaths = expr->getComputedPaths("b", Variables::kRootId);
ASSERT(computedPaths.paths.empty());
ASSERT_EQ(computedPaths.renames.size(), 1u);
ASSERT_EQ(computedPaths.renames["b"], "a");
}
TEST(GetComputedPathsTest, ExpressionFieldPathDoesCountAsRenameWithMatchingUserVariable) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto varId = expCtx->variablesParseState.defineVariable("userVar");
auto expr = ExpressionFieldPath::parse(expCtx, "$$userVar.a", expCtx->variablesParseState);
auto computedPaths = expr->getComputedPaths("b", varId);
ASSERT(computedPaths.paths.empty());
ASSERT_EQ(computedPaths.renames.size(), 1u);
ASSERT_EQ(computedPaths.renames["b"], "a");
}
TEST(GetComputedPathsTest, ExpressionObjectCorrectlyReportsComputedPaths) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto specObject = fromjson("{a: '$b', c: {$add: [1, 3]}}");
auto expr = Expression::parseObject(expCtx, specObject, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionObject*>(expr.get()));
auto computedPaths = expr->getComputedPaths("d");
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("d.c"), 1u);
ASSERT_EQ(computedPaths.renames.size(), 1u);
ASSERT_EQ(computedPaths.renames["d.a"], "b");
}
TEST(GetComputedPathsTest, ExpressionObjectCorrectlyReportsComputedPathsNested) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto specObject = fromjson(
"{a: {b: '$c'},"
"d: {$map: {input: '$e', as: 'iter', in: {f: '$$iter.g'}}}}");
auto expr = Expression::parseObject(expCtx, specObject, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionObject*>(expr.get()));
auto computedPaths = expr->getComputedPaths("h");
ASSERT(computedPaths.paths.empty());
ASSERT_EQ(computedPaths.renames.size(), 2u);
ASSERT_EQ(computedPaths.renames["h.a.b"], "c");
ASSERT_EQ(computedPaths.renames["h.d.f"], "e.g");
}
TEST(GetComputedPathsTest, ExpressionMapCorrectlyReportsComputedPaths) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto specObject =
fromjson("{$map: {input: '$a', as: 'iter', in: {b: '$$iter.c', d: {$add: [1, 2]}}}}");
auto expr = Expression::parseObject(expCtx, specObject, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionMap*>(expr.get()));
auto computedPaths = expr->getComputedPaths("e");
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("e.d"), 1u);
ASSERT_EQ(computedPaths.renames.size(), 1u);
ASSERT_EQ(computedPaths.renames["e.b"], "a.c");
}
TEST(GetComputedPathsTest, ExpressionMapCorrectlyReportsComputedPathsWithDefaultVarName) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto specObject = fromjson("{$map: {input: '$a', in: {b: '$$this.c', d: {$add: [1, 2]}}}}");
auto expr = Expression::parseObject(expCtx, specObject, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionMap*>(expr.get()));
auto computedPaths = expr->getComputedPaths("e");
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("e.d"), 1u);
ASSERT_EQ(computedPaths.renames.size(), 1u);
ASSERT_EQ(computedPaths.renames["e.b"], "a.c");
}
TEST(GetComputedPathsTest, ExpressionMapCorrectlyReportsComputedPathsWithNestedExprObject) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto specObject = fromjson("{$map: {input: '$a', in: {b: {c: '$$this.d'}}}}");
auto expr = Expression::parseObject(expCtx, specObject, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionMap*>(expr.get()));
auto computedPaths = expr->getComputedPaths("e");
ASSERT(computedPaths.paths.empty());
ASSERT_EQ(computedPaths.renames.size(), 1u);
ASSERT_EQ(computedPaths.renames["e.b.c"], "a.d");
}
TEST(GetComputedPathsTest, ExpressionMapNotConsideredRenameWithWrongRootVariable) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto specObject = fromjson("{$map: {input: '$a', as: 'iter', in: {b: '$c'}}}");
auto expr = Expression::parseObject(expCtx, specObject, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionMap*>(expr.get()));
auto computedPaths = expr->getComputedPaths("d");
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("d"), 1u);
ASSERT(computedPaths.renames.empty());
}
TEST(GetComputedPathsTest, ExpressionMapNotConsideredRenameWithWrongVariableNoExpressionObject) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto specObject = fromjson("{$map: {input: '$a', as: 'iter', in: '$b'}}");
auto expr = Expression::parseObject(expCtx, specObject, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionMap*>(expr.get()));
auto computedPaths = expr->getComputedPaths("d");
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("d"), 1u);
ASSERT(computedPaths.renames.empty());
}
TEST(GetComputedPathsTest, ExpressionMapNotConsideredRenameWithDottedInputPath) {
intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
auto specObject = fromjson("{$map: {input: '$a.b', as: 'iter', in: {c: '$$iter.d'}}}}");
auto expr = Expression::parseObject(expCtx, specObject, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionMap*>(expr.get()));
auto computedPaths = expr->getComputedPaths("e");
ASSERT_EQ(computedPaths.paths.size(), 1u);
ASSERT_EQ(computedPaths.paths.count("e"), 1u);
ASSERT(computedPaths.renames.empty());
}
} // namespace GetComputedPathsTest
class All : public Suite {
public:
All() : Suite("expression") {}

View File

@ -242,27 +242,15 @@ void InclusionNode::addPreservedPaths(std::set<std::string>* preservedPaths) con
void InclusionNode::addComputedPaths(std::set<std::string>* computedPaths,
StringMap<std::string>* renamedPaths) const {
for (auto&& computedPair : _expressions) {
auto exprFieldPath = dynamic_cast<ExpressionFieldPath*>(computedPair.second.get());
if (exprFieldPath) {
const auto& fieldPath = exprFieldPath->getFieldPath();
// Make sure that the first path component is CURRENT/ROOT. If this is not explicitly
// provided by the user, we expect the system to have filled it in as the first path
// component.
//
// TODO SERVER-27115: Support field paths that have multiple components.
if (exprFieldPath->getVariableId() == Variables::kRootId &&
fieldPath.getPathLength() == 2u) {
// Found a renamed path. Record it and continue, since we don't want to also put
// renamed paths inside the 'computedPaths' set.
std::string oldPath = fieldPath.tail().fullPath();
std::string newPath =
FieldPath::getFullyQualifiedPath(_pathToNode, computedPair.first);
(*renamedPaths)[std::move(newPath)] = std::move(oldPath);
continue;
}
}
// The expression's path is the concatenation of the path to this inclusion node,
// plus the field name associated with the expression.
auto exprPath = FieldPath::getFullyQualifiedPath(_pathToNode, computedPair.first);
auto exprComputedPaths = computedPair.second->getComputedPaths(exprPath);
computedPaths->insert(exprComputedPaths.paths.begin(), exprComputedPaths.paths.end());
computedPaths->insert(FieldPath::getFullyQualifiedPath(_pathToNode, computedPair.first));
for (auto&& rename : exprComputedPaths.renames) {
(*renamedPaths)[rename.first] = rename.second;
}
}
for (auto&& childPair : _children) {
childPair.second->addComputedPaths(computedPaths, renamedPaths);

View File

@ -896,6 +896,27 @@ TEST(PipelineOptimizationTest, MatchWithTypeShouldMoveAcrossRename) {
assertPipelineOptimizesTo(inputPipe, outputPipe);
}
TEST(PipelineOptimizationTest, MatchOnArrayFieldCanSplitAcrossRenameWithMapAndProject) {
string inputPipe =
"[{$project: {d: {$map: {input: '$a', as: 'iter', in: {e: '$$iter.b', f: {$add: "
"['$$iter.c', 1]}}}}}}, {$match: {'d.e': 1, 'd.f': 1}}]";
string outputPipe =
"[{$match: {'a.b': {$eq: 1}}}, {$project: {_id: true, d: {$map: {input: '$a', as: 'iter', "
"in: {e: '$$iter.b', f: {$add: ['$$iter.c', {$const: 1}]}}}}}}, {$match: {'d.f': {$eq: "
"1}}}]";
assertPipelineOptimizesTo(inputPipe, outputPipe);
}
TEST(PipelineOptimizationTest, MatchOnArrayFieldCanSplitAcrossRenameWithMapAndAddFields) {
string inputPipe =
"[{$addFields: {d: {$map: {input: '$a', as: 'iter', in: {e: '$$iter.b', f: {$add: "
"['$$iter.c', 1]}}}}}}, {$match: {'d.e': 1, 'd.f': 1}}]";
string outputPipe =
"[{$match: {'a.b': {$eq: 1}}}, {$addFields: {d: {$map: {input: '$a', as: 'iter', in: {e: "
"'$$iter.b', f: {$add: ['$$iter.c', {$const: 1}]}}}}}}, {$match: {'d.f': {$eq: 1}}}]";
assertPipelineOptimizesTo(inputPipe, outputPipe);
}
} // namespace Local
namespace Sharded {