diff options
author | David Storch <david.storch@10gen.com> | 2017-05-23 19:32:11 -0400 |
---|---|---|
committer | David Storch <david.storch@10gen.com> | 2017-05-30 10:23:43 -0400 |
commit | a702053750a5a17071e539fc29910809881ad7c7 (patch) | |
tree | 4771179e617cc647d289539522b71d70592e82f9 | |
parent | a24c0780aed84a78dfa1634ae7d81f8b20bc0d68 (diff) | |
download | mongo-a702053750a5a17071e539fc29910809881ad7c7.tar.gz |
SERVER-27115 Allow $match to swap across renames expressed using $map.
-rw-r--r-- | jstests/aggregation/match_swapping_renamed_fields.js | 131 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_algo.cpp | 6 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 71 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 53 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_test.cpp | 173 | ||||
-rw-r--r-- | src/mongo/db/pipeline/parsed_inclusion_projection.cpp | 28 | ||||
-rw-r--r-- | src/mongo/db/pipeline/pipeline_test.cpp | 21 |
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 { |