diff --git a/src/common/function/FunctionManager.cpp b/src/common/function/FunctionManager.cpp index 07765e8be8a..c55d867544e 100644 --- a/src/common/function/FunctionManager.cpp +++ b/src/common/function/FunctionManager.cpp @@ -2017,7 +2017,7 @@ FunctionManager::FunctionManager() { // More information of encoding could be found in `NebulaKeyUtils.h` auto &attr = functions_["none_direct_dst"]; attr.minArity_ = 1; - attr.maxArity_ = 1; + attr.maxArity_ = 2; attr.isAlwaysPure_ = true; attr.body_ = [](const auto &args) -> Value { switch (args[0].get().type()) { @@ -2035,6 +2035,13 @@ FunctionManager::FunctionManager() { case Value::Type::LIST: { const auto &listVal = args[0].get().getList().values; if (listVal.empty()) { + if (args.size() == 2) { + if (args[1].get().type() == Value::Type::VERTEX) { + const auto &v = args[1].get().getVertex(); + return v.vid; + } + return Value::kNullBadType; + } return Value::kNullBadType; } auto &lastVal = listVal.back(); diff --git a/src/graph/context/ast/CypherAstContext.h b/src/graph/context/ast/CypherAstContext.h index 1b7ca85c235..47b9a184bf2 100644 --- a/src/graph/context/ast/CypherAstContext.h +++ b/src/graph/context/ast/CypherAstContext.h @@ -51,6 +51,8 @@ struct EdgeInfo { MatchEdge::Direction direction{MatchEdge::Direction::OUT_EDGE}; std::vector types; std::string alias; + // use for construct path struct + std::string innerAlias; const MapExpression* props{nullptr}; Expression* filter{nullptr}; }; diff --git a/src/graph/executor/query/TraverseExecutor.cpp b/src/graph/executor/query/TraverseExecutor.cpp index 3c34b8c6255..9425917b66d 100644 --- a/src/graph/executor/query/TraverseExecutor.cpp +++ b/src/graph/executor/query/TraverseExecutor.cpp @@ -24,6 +24,7 @@ namespace graph { folly::Future TraverseExecutor::execute() { range_ = traverse_->stepRange(); + genPath_ = traverse_->genPath(); NG_RETURN_IF_ERROR(buildRequestVids()); if (vids_.empty()) { DataSet emptyDs; @@ -374,9 +375,12 @@ std::vector TraverseExecutor::buildZeroStepPath() { for (auto& p : prevPaths) { Row row = p; List edgeList; - edgeList.values.emplace_back(vertex); row.values.emplace_back(vertex); - row.values.emplace_back(std::move(edgeList)); + row.values.emplace_back(edgeList); + if (genPath_) { + edgeList.values.emplace_back(vertex); + row.values.emplace_back(std::move(edgeList)); + } result.emplace_back(std::move(row)); } } @@ -384,9 +388,12 @@ std::vector TraverseExecutor::buildZeroStepPath() { for (auto& vertex : initVertices_) { Row row; List edgeList; - edgeList.values.emplace_back(vertex); row.values.emplace_back(vertex); - row.values.emplace_back(std::move(edgeList)); + row.values.emplace_back(edgeList); + if (genPath_) { + edgeList.values.emplace_back(vertex); + row.values.emplace_back(std::move(edgeList)); + } result.emplace_back(std::move(row)); } } @@ -477,38 +484,53 @@ std::vector TraverseExecutor::buildPath(const Value& initVertex, return std::vector(); } - std::vector result; - result.reserve(adjEdges.size()); + std::vector oneStepPath; + oneStepPath.reserve(adjEdges.size()); for (auto& edge : adjEdges) { List edgeList; edgeList.values.emplace_back(edge); Row row; row.values.emplace_back(src); - row.values.emplace_back(std::move(edgeList)); - result.emplace_back(std::move(row)); + // only contain edges + row.values.emplace_back(edgeList); + if (genPath_) { + // contain nodes & edges + row.values.emplace_back(std::move(edgeList)); + } + oneStepPath.emplace_back(std::move(row)); } if (maxStep == 1) { if (traverse_->trackPrevPath()) { - return joinPrevPath(initVertex, result); + return joinPrevPath(initVertex, oneStepPath); } - return result; + return oneStepPath; } size_t step = 2; std::vector newResult; std::queue*> queue; + std::queue*> edgeListQueue; std::list>> holder; for (auto& edge : adjEdges) { auto ptr = std::make_unique>(std::vector({edge})); queue.emplace(ptr.get()); + edgeListQueue.emplace(ptr.get()); holder.emplace_back(std::move(ptr)); } - size_t adjSize = queue.size(); - while (!queue.empty()) { - auto edgeListPtr = queue.front(); + + size_t adjSize = edgeListQueue.size(); + while (!edgeListQueue.empty()) { + auto edgeListPtr = edgeListQueue.front(); auto& dst = edgeListPtr->back().getEdge().dst; - queue.pop(); + edgeListQueue.pop(); + + std::vector* vertexEdgeListPtr = nullptr; + if (genPath_) { + vertexEdgeListPtr = queue.front(); + queue.pop(); + } + --adjSize; auto dstIter = adjList_.find(dst); if (dstIter == adjList_.end()) { @@ -516,7 +538,7 @@ std::vector TraverseExecutor::buildPath(const Value& initVertex, if (++step > maxStep) { break; } - adjSize = queue.size(); + adjSize = edgeListQueue.size(); } continue; } @@ -527,29 +549,44 @@ std::vector TraverseExecutor::buildPath(const Value& initVertex, continue; } auto newEdgeListPtr = std::make_unique>(*edgeListPtr); - newEdgeListPtr->emplace_back(dstIter->first); newEdgeListPtr->emplace_back(edge); + std::unique_ptr> newVertexEdgeListPtr = nullptr; + if (genPath_) { + newVertexEdgeListPtr = std::make_unique>(*vertexEdgeListPtr); + newVertexEdgeListPtr->emplace_back(dstIter->first); + newVertexEdgeListPtr->emplace_back(edge); + } + if (step >= minStep) { Row row; row.values.emplace_back(src); + // only contain edges row.values.emplace_back(List(*newEdgeListPtr)); + if (genPath_) { + // contain nodes & edges + row.values.emplace_back(List(*newVertexEdgeListPtr)); + } newResult.emplace_back(std::move(row)); } - queue.emplace(newEdgeListPtr.get()); + edgeListQueue.emplace(newEdgeListPtr.get()); holder.emplace_back(std::move(newEdgeListPtr)); + if (genPath_ && newVertexEdgeListPtr != nullptr) { + queue.emplace(newVertexEdgeListPtr.get()); + holder.emplace_back(std::move(newVertexEdgeListPtr)); + } } if (adjSize == 0) { if (++step > maxStep) { break; } - adjSize = queue.size(); + adjSize = edgeListQueue.size(); } } if (minStep <= 1) { newResult.insert(newResult.begin(), - std::make_move_iterator(result.begin()), - std::make_move_iterator(result.end())); + std::make_move_iterator(oneStepPath.begin()), + std::make_move_iterator(oneStepPath.end())); } if (traverse_->trackPrevPath()) { return joinPrevPath(initVertex, newResult); @@ -570,8 +607,7 @@ std::vector TraverseExecutor::joinPrevPath(const Value& initVertex, if (!hasSameEdgeInPath(prevPath, p)) { // copy Row row = prevPath; - row.values.emplace_back(p.values.front()); - row.values.emplace_back(p.values.back()); + row.values.insert(row.values.end(), p.values.begin(), p.values.end()); newPaths.emplace_back(std::move(row)); } } diff --git a/src/graph/executor/query/TraverseExecutor.h b/src/graph/executor/query/TraverseExecutor.h index 629718b34b5..e8cda9c798f 100644 --- a/src/graph/executor/query/TraverseExecutor.h +++ b/src/graph/executor/query/TraverseExecutor.h @@ -93,6 +93,7 @@ class TraverseExecutor final : public StorageAccessExecutor { private: ObjectPool objPool_; + bool genPath_{false}; VidHashSet vids_; std::vector initVertices_; DataSet result_; diff --git a/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.cpp b/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.cpp index 5b0364b7dfd..b9211632d1f 100644 --- a/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.cpp +++ b/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.cpp @@ -52,6 +52,7 @@ StatusOr RemoveAppendVerticesBelowJoinRule::transform( auto& avNodeAlias = appendVertices->nodeAlias(); auto& tvEdgeAlias = traverse->edgeAlias(); + auto& tvNodeAlias = traverse->nodeAlias(); auto& leftExprs = join->hashKeys(); auto& rightExprs = join->probeKeys(); @@ -148,6 +149,7 @@ StatusOr RemoveAppendVerticesBelowJoinRule::transform( // and let the new left/inner join use it as right expr auto* args = ArgumentList::make(pool); args->addArgument(InputPropertyExpression::make(pool, tvEdgeAlias)); + args->addArgument(InputPropertyExpression::make(pool, tvNodeAlias)); auto* newPrjExpr = FunctionCallExpression::make(pool, "none_direct_dst", args); auto oldYieldColumns = project->columns()->columns(); diff --git a/src/graph/planner/match/MatchPathPlanner.cpp b/src/graph/planner/match/MatchPathPlanner.cpp index 741c18f785b..24f4f6b5c94 100644 --- a/src/graph/planner/match/MatchPathPlanner.cpp +++ b/src/graph/planner/match/MatchPathPlanner.cpp @@ -26,13 +26,17 @@ MatchPathPlanner::MatchPathPlanner(CypherClauseContextBase* ctx, const Path& pat static std::vector genTraverseColNames(const std::vector& inputCols, const NodeInfo& node, const EdgeInfo& edge, - bool trackPrev) { + bool trackPrev, + bool genPath = false) { std::vector cols; if (trackPrev) { cols = inputCols; } cols.emplace_back(node.alias); cols.emplace_back(edge.alias); + if (genPath) { + cols.emplace_back(edge.innerAlias); + } return cols; } @@ -47,9 +51,12 @@ static std::vector genAppendVColNames(const std::vectoraddArgument(InputPropertyExpression::make(pool, edge.alias)); + args->addArgument(InputPropertyExpression::make(pool, node.alias)); return FunctionCallExpression::make(pool, "none_direct_dst", args); } @@ -218,13 +225,15 @@ Status MatchPathPlanner::leftExpandFromNode(size_t startIndex, SubPlan& subplan) traverse->setEdgeDirection(edge.direction); traverse->setStepRange(stepRange); traverse->setDedup(); + traverse->setGenPath(path_.genPath); // If start from end of the path pattern, the first traverse would not // track the previous path, otherwise, it should. bool trackPrevPath = (startIndex + 1 == nodeInfos.size() ? i != startIndex : true); traverse->setTrackPrevPath(trackPrevPath); - traverse->setColNames(genTraverseColNames(subplan.root->colNames(), node, edge, trackPrevPath)); + traverse->setColNames( + genTraverseColNames(subplan.root->colNames(), node, edge, trackPrevPath, path_.genPath)); subplan.root = traverse; - nextTraverseStart = genNextTraverseStart(qctx->objPool(), edge); + nextTraverseStart = genNextTraverseStart(qctx->objPool(), edge, node); if (expandInto) { // TODO(shylock) optimize to embed filter to Traverse auto* startVid = nodeId(qctx->objPool(), dst); @@ -290,10 +299,11 @@ Status MatchPathPlanner::rightExpandFromNode(size_t startIndex, SubPlan& subplan traverse->setStepRange(stepRange); traverse->setDedup(); traverse->setTrackPrevPath(i != startIndex); + traverse->setGenPath(path_.genPath); traverse->setColNames( - genTraverseColNames(subplan.root->colNames(), node, edge, i != startIndex)); + genTraverseColNames(subplan.root->colNames(), node, edge, i != startIndex, path_.genPath)); subplan.root = traverse; - nextTraverseStart = genNextTraverseStart(qctx->objPool(), edge); + nextTraverseStart = genNextTraverseStart(qctx->objPool(), edge, node); if (expandInto) { auto* startVid = nodeId(qctx->objPool(), dst); auto* endVid = nextTraverseStart; diff --git a/src/graph/planner/match/MatchSolver.cpp b/src/graph/planner/match/MatchSolver.cpp index 8f91271ddb0..8687d906893 100644 --- a/src/graph/planner/match/MatchSolver.cpp +++ b/src/graph/planner/match/MatchSolver.cpp @@ -231,21 +231,17 @@ static YieldColumn* buildVertexColumn(ObjectPool* pool, const std::string& alias return new YieldColumn(InputPropertyExpression::make(pool, alias), alias); } -static YieldColumn* buildEdgeColumn(QueryContext* qctx, const EdgeInfo& edge) { +static YieldColumn* buildEdgeColumn(QueryContext* qctx, const EdgeInfo& edge, bool genPath) { Expression* expr = nullptr; + const std::string& edgeName = genPath ? edge.innerAlias : edge.alias; auto pool = qctx->objPool(); if (edge.range == nullptr) { expr = SubscriptExpression::make( - pool, InputPropertyExpression::make(pool, edge.alias), ConstantExpression::make(pool, 0)); + pool, InputPropertyExpression::make(pool, edgeName), ConstantExpression::make(pool, 0)); } else { - auto innerVar = qctx->vctx()->anonVarGen()->getVar(); - auto* args = ArgumentList::make(pool); - args->addArgument(VariableExpression::makeInner(pool, innerVar)); - auto* filter = FunctionCallExpression::make(pool, "is_edge", args); - expr = ListComprehensionExpression::make( - pool, innerVar, InputPropertyExpression::make(pool, edge.alias), filter); + expr = InputPropertyExpression::make(pool, edgeName); } - return new YieldColumn(expr, edge.alias); + return new YieldColumn(expr, edgeName); } // static @@ -262,16 +258,23 @@ void MatchSolver::buildProjectColumns(QueryContext* qctx, const Path& path, SubP } }; - auto addEdge = [columns, &colNames, qctx](auto& edgeInfo) { + auto addEdge = [columns, &colNames, qctx](auto& edgeInfo, bool genPath = false) { if (!edgeInfo.alias.empty() && !edgeInfo.anonymous) { - columns->addColumn(buildEdgeColumn(qctx, edgeInfo)); - colNames.emplace_back(edgeInfo.alias); + columns->addColumn(buildEdgeColumn(qctx, edgeInfo, genPath)); + if (genPath) { + colNames.emplace_back(edgeInfo.innerAlias); + } else { + colNames.emplace_back(edgeInfo.alias); + } } }; for (size_t i = 0; i < edgeInfos.size(); i++) { addNode(nodeInfos[i]); addEdge(edgeInfos[i]); + if (path.genPath) { + addEdge(edgeInfos[i], true); + } } // last vertex diff --git a/src/graph/planner/plan/Query.cpp b/src/graph/planner/plan/Query.cpp index 8d50e1f09a4..5a64b3e0c8d 100644 --- a/src/graph/planner/plan/Query.cpp +++ b/src/graph/planner/plan/Query.cpp @@ -832,6 +832,7 @@ void Traverse::cloneMembers(const Traverse& g) { if (g.tagFilter_ != nullptr) { setTagFilter(g.tagFilter_->clone()); } + genPath_ = g.genPath(); } std::unique_ptr Traverse::explain() const { diff --git a/src/graph/planner/plan/Query.h b/src/graph/planner/plan/Query.h index 60f8dadf859..c130d4361d7 100644 --- a/src/graph/planner/plan/Query.h +++ b/src/graph/planner/plan/Query.h @@ -1713,10 +1713,18 @@ class Traverse final : public GetNeighbors { firstStepFilter_ = filter; } + void setGenPath(bool genPath) { + genPath_ = genPath; + } + Expression* tagFilter() const { return tagFilter_; } + bool genPath() const { + return genPath_; + } + void setTagFilter(Expression* tagFilter) { tagFilter_ = tagFilter; } @@ -1738,6 +1746,7 @@ class Traverse final : public GetNeighbors { // Push down filter in first step Expression* firstStepFilter_{nullptr}; Expression* tagFilter_{nullptr}; + bool genPath_{false}; }; // Append vertices to a path. diff --git a/src/graph/validator/MatchValidator.cpp b/src/graph/validator/MatchValidator.cpp index a49e4fd7ed2..561322b6aaa 100644 --- a/src/graph/validator/MatchValidator.cpp +++ b/src/graph/validator/MatchValidator.cpp @@ -181,7 +181,11 @@ Status MatchValidator::buildPathExpr(const MatchPath *path, auto pathBuild = PathBuildExpression::make(pool); for (size_t i = 0; i < edgeInfos.size(); ++i) { pathBuild->add(InputPropertyExpression::make(pool, nodeInfos[i].alias)); - pathBuild->add(InputPropertyExpression::make(pool, edgeInfos[i].alias)); + if (pathType == MatchPath::PathType::kDefault) { + pathBuild->add(InputPropertyExpression::make(pool, edgeInfos[i].innerAlias)); + } else { + pathBuild->add(InputPropertyExpression::make(pool, edgeInfos[i].alias)); + } } pathBuild->add(InputPropertyExpression::make(pool, nodeInfos.back().alias)); pathInfo.pathBuild = std::move(pathBuild); @@ -332,6 +336,7 @@ Status MatchValidator::buildEdgeInfo(const MatchPath *path, edgeInfos[i].anonymous = anonymous; edgeInfos[i].direction = direction; edgeInfos[i].alias = alias; + edgeInfos[i].innerAlias = "_" + alias; edgeInfos[i].props = props; edgeInfos[i].filter = filter; } diff --git a/src/graph/visitor/PrunePropertiesVisitor.cpp b/src/graph/visitor/PrunePropertiesVisitor.cpp index 9daed3f1626..8cae8f06e1a 100644 --- a/src/graph/visitor/PrunePropertiesVisitor.cpp +++ b/src/graph/visitor/PrunePropertiesVisitor.cpp @@ -218,8 +218,15 @@ void PrunePropertiesVisitor::visitCurrent(Traverse *node) { // The number of output columns of the Traverse operator is at least two(starting point and edge), // which is by design. DCHECK_GE(colNames.size(), 2); - auto &nodeAlias = colNames[colNames.size() - 2]; - auto &edgeAlias = colNames.back(); + size_t nodeIndex = colNames.size() - 2; + size_t edgeIndex = colNames.size() - 1; + if (node->genPath()) { + // if genPath, traverse operator's last column stores list of alternating vertices and edges + nodeIndex = nodeIndex - 1; + edgeIndex = edgeIndex - 1; + } + auto &nodeAlias = colNames[nodeIndex]; + auto &edgeAlias = colNames[edgeIndex]; if (node->vFilter() != nullptr) { status_ = extractPropsFromExpr(node->vFilter(), nodeAlias); @@ -242,13 +249,22 @@ void PrunePropertiesVisitor::pruneCurrent(Traverse *node) { // The number of output columns of the Traverse operator is at least two(starting point and edge), // which is by design. DCHECK_GE(colNames.size(), 2); - auto &nodeAlias = colNames[colNames.size() - 2]; - auto &edgeAlias = colNames.back(); - auto *vertexProps = node->vertexProps(); + size_t nodeIndex = colNames.size() - 2; + size_t edgeIndex = colNames.size() - 1; + size_t innerEdgeIndex = edgeIndex; + if (node->genPath()) { + // if genPath, traverse operator's last column stores list of alternating vertices and edges + nodeIndex = nodeIndex - 1; + edgeIndex = edgeIndex - 1; + } + auto &nodeAlias = colNames[nodeIndex]; + auto &edgeAlias = colNames[edgeIndex]; + auto &innerEdgeAlias = colNames[innerEdgeIndex]; auto &colsSet = propsUsed_.colsSet; auto &vertexPropsMap = propsUsed_.vertexPropsMap; auto &edgePropsMap = propsUsed_.edgePropsMap; + auto *vertexProps = node->vertexProps(); if (colsSet.find(nodeAlias) == colsSet.end()) { auto aliasIter = vertexPropsMap.find(nodeAlias); if (aliasIter == vertexPropsMap.end()) { @@ -295,7 +311,7 @@ void PrunePropertiesVisitor::pruneCurrent(Traverse *node) { } auto *edgeProps = node->edgeProps(); - if (colsSet.find(edgeAlias) != colsSet.end()) { + if (colsSet.find(edgeAlias) != colsSet.end() || colsSet.find(innerEdgeAlias) != colsSet.end()) { // all edge properties are used return; } diff --git a/tests/tck/features/optimizer/PrunePropertiesRule.feature b/tests/tck/features/optimizer/PrunePropertiesRule.feature index 0c4f38f3e80..5731e68177c 100644 --- a/tests/tck/features/optimizer/PrunePropertiesRule.feature +++ b/tests/tck/features/optimizer/PrunePropertiesRule.feature @@ -411,7 +411,7 @@ Feature: Prune Properties rule | 14 | IndexScan | 2 | | | 2 | Start | | | | 11 | Project | 9 | | - | 9 | Traverse | 8 | { "vertexProps": "" } | + | 9 | Traverse | 8 | | | 8 | Argument | | | @distonly @@ -446,9 +446,8 @@ Feature: Prune Properties rule | 24 | And the execution plan should be: | id | name | dependencies | operator info | - | 7 | Aggregate | 6 | | - | 6 | Project | 5 | | - | 5 | AppendVertices | 4 | { "props": "[{\"props\":[\"age\"] }]" } | + | 7 | Aggregate | 8 | | + | 8 | AppendVertices | 4 | { "props": "[{\"props\":[\"age\"] }]" } | | 4 | Traverse | 2 | {"vertexProps": "", "edgeProps": "[{ \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | | 2 | Dedup | 1 | | | 1 | PassThrough | 3 | | @@ -464,9 +463,8 @@ Feature: Prune Properties rule | 24 | And the execution plan should be: | id | name | dependencies | operator info | - | 7 | Aggregate | 6 | | - | 6 | Project | 5 | | - | 5 | AppendVertices | 4 | { "props": "[{\"props\":[\"_tag\"] }, {\"props\":[\"_tag\"] }, {\"props\":[\"_tag\"] }]" } | + | 7 | Aggregate | 8 | | + | 8 | AppendVertices | 4 | { "props": "[{\"props\":[\"_tag\"] }, {\"props\":[\"_tag\"] }, {\"props\":[\"_tag\"] }]" } | | 4 | Traverse | 2 | {"vertexProps": "" , "edgeProps": "[{ \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | | 2 | Dedup | 1 | | | 1 | PassThrough | 3 | | @@ -588,7 +586,7 @@ Feature: Prune Properties rule | 1 | PassThrough | 3 | | | 3 | Start | | | | 26 | Project | 14 | | - | 14 | Traverse | 13 | {"vertexProps": "", "edgeProps": "[{ \"props\": [\"_src\", \"_type\", \"_rank\", \"_dst\", \"start_year\", \"end_year\"]}]" } | + | 14 | Traverse | 13 | {"edgeProps": "[{ \"props\": [\"_src\", \"_type\", \"_rank\", \"_dst\", \"start_year\", \"end_year\"]}]" } | | 13 | Traverse | 12 | {"vertexProps": "", "edgeProps": "[{ \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | | 12 | Traverse | 11 | {"vertexProps": "", "edgeProps": "[{ \"props\": [\"_type\", \"_rank\", \"_dst\"]}, { \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | | 11 | Argument | | | diff --git a/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature b/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature index 2d767926e47..c04a130e4b6 100644 --- a/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature +++ b/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature @@ -58,7 +58,7 @@ Feature: Remove AppendVertices Below Join | 2 | Dedup | 1 | | | 1 | PassThrough | 3 | | | 3 | Start | | | - | 15 | Project | 14 | {"columns": ["$-.friend AS friend", "$-.friend2 AS friend2", "none_direct_dst($-.__VAR_3) AS friendTeam"]} | + | 15 | Project | 14 | {"columns": ["$-.friend AS friend", "$-.friend2 AS friend2", "none_direct_dst($-.__VAR_3,$-.friend2) AS friendTeam"]} | | 14 | Traverse | 12 | | | 12 | Traverse | 11 | | | 11 | Argument | | | @@ -76,16 +76,16 @@ Feature: Remove AppendVertices Below Join | me | both | he | | ("Tony Parker") | ("Manu Ginobili") | ("Tim Duncan") | And the execution plan should be: - | id | name | dependencies | operator info | - | 13 | HashInnerJoin | 6,12 | {"hashKeys": ["_joinkey($-.both)"], "probeKeys": ["$-.both"]} | - | 6 | Project | 5 | | - | 5 | AppendVertices | 15 | | - | 15 | Traverse | 2 | | - | 2 | Dedup | 1 | | - | 1 | PassThrough | 3 | | - | 3 | Start | | | - | 12 | Project | 16 | {"columns": ["$-.he AS he", "none_direct_dst($-.__VAR_1) AS both"]} | - | 16 | Traverse | 8 | | - | 8 | Dedup | 7 | | - | 7 | PassThrough | 9 | | - | 9 | Start | | | + | id | name | dependencies | operator info | + | 13 | HashInnerJoin | 6,12 | {"hashKeys": ["_joinkey($-.both)"], "probeKeys": ["$-.both"]} | + | 6 | Project | 5 | | + | 5 | AppendVertices | 15 | | + | 15 | Traverse | 2 | | + | 2 | Dedup | 1 | | + | 1 | PassThrough | 3 | | + | 3 | Start | | | + | 12 | Project | 16 | {"columns": ["$-.he AS he", "none_direct_dst($-.__VAR_1,$-.he) AS both"]} | + | 16 | Traverse | 8 | | + | 8 | Dedup | 7 | | + | 7 | PassThrough | 9 | | + | 9 | Start | | |