summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Storch <david.storch@10gen.com>2017-05-23 19:32:11 -0400
committerDavid Storch <david.storch@10gen.com>2017-05-30 10:23:43 -0400
commita702053750a5a17071e539fc29910809881ad7c7 (patch)
tree4771179e617cc647d289539522b71d70592e82f9
parenta24c0780aed84a78dfa1634ae7d81f8b20bc0d68 (diff)
downloadmongo-a702053750a5a17071e539fc29910809881ad7c7.tar.gz
SERVER-27115 Allow $match to swap across renames expressed using $map.
-rw-r--r--jstests/aggregation/match_swapping_renamed_fields.js131
-rw-r--r--src/mongo/db/matcher/expression_algo.cpp6
-rw-r--r--src/mongo/db/pipeline/expression.cpp71
-rw-r--r--src/mongo/db/pipeline/expression.h53
-rw-r--r--src/mongo/db/pipeline/expression_test.cpp173
-rw-r--r--src/mongo/db/pipeline/parsed_inclusion_projection.cpp28
-rw-r--r--src/mongo/db/pipeline/pipeline_test.cpp21
7 files changed, 449 insertions, 34 deletions
diff --git a/jstests/aggregation/match_swapping_renamed_fields.js b/jstests/aggregation/match_swapping_renamed_fields.js
index c6bc4afa524..efbd2664442 100644
--- a/jstests/aggregation/match_swapping_renamed_fields.js
+++ b/jstests/aggregation/match_swapping_renamed_fields.js
@@ -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));
}());
diff --git a/src/mongo/db/matcher/expression_algo.cpp b/src/mongo/db/matcher/expression_algo.cpp
index a648d1a4b28..12fe0224258 100644
--- a/src/mongo/db/matcher/expression_algo.cpp
+++ b/src/mongo/db/matcher/expression_algo.cpp
@@ -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);
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp
index 9db31bba0d4..9bd1682783f 100644
--- a/src/mongo/db/pipeline/expression.cpp
+++ b/src/mongo/db/pipeline/expression.cpp
@@ -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);
diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h
index 124e4916c1d..4cb5a302191 100644
--- a/src/mongo/db/pipeline/expression.h
+++ b/src/mongo/db/pipeline/expression.h
@@ -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(){};
/**
@@ -108,6 +121,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,
diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp
index 45b00c69fa1..4ed04c8cd0c 100644
--- a/src/mongo/db/pipeline/expression_test.cpp
+++ b/src/mongo/db/pipeline/expression_test.cpp
@@ -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") {}
diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection.cpp b/src/mongo/db/pipeline/parsed_inclusion_projection.cpp
index 6c552f2aacb..45a9998e3ff 100644
--- a/src/mongo/db/pipeline/parsed_inclusion_projection.cpp
+++ b/src/mongo/db/pipeline/parsed_inclusion_projection.cpp
@@ -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());
+
+ for (auto&& rename : exprComputedPaths.renames) {
+ (*renamedPaths)[rename.first] = rename.second;
}
-
- computedPaths->insert(FieldPath::getFullyQualifiedPath(_pathToNode, computedPair.first));
}
for (auto&& childPair : _children) {
childPair.second->addComputedPaths(computedPaths, renamedPaths);
diff --git a/src/mongo/db/pipeline/pipeline_test.cpp b/src/mongo/db/pipeline/pipeline_test.cpp
index 3a1aae261be..03d0f137612 100644
--- a/src/mongo/db/pipeline/pipeline_test.cpp
+++ b/src/mongo/db/pipeline/pipeline_test.cpp
@@ -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 {