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:
parent
a24c0780ae
commit
a702053750
@ -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));
|
||||
}());
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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") {}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user