diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 97cf436339e..83af18d2433 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -19,7 +19,7 @@ jobs: lint: name: lint if: ${{ contains(github.event.pull_request.labels.*.name, 'ready-for-testing') && github.event.pull_request.merged != true }} - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 with: @@ -31,7 +31,7 @@ jobs: uses: apache/skywalking-eyes/header@main - name: Ensure clang-format-10 is available run: | - command -v clang-format-10 > /dev/null || (apt-get update && apt-get install -y clang-format-10) + command -v clang-format-10 > /dev/null || (sudo apt update && sudo apt install -y clang-format-10) - name: Cpplint run: | ln -snf $PWD/.linters/cpp/hooks/pre-commit.sh $PWD/.linters/cpp/pre-commit.sh diff --git a/src/common/datatypes/Edge.h b/src/common/datatypes/Edge.h index 4f9973538bd..e873c62689a 100644 --- a/src/common/datatypes/Edge.h +++ b/src/common/datatypes/Edge.h @@ -20,6 +20,7 @@ struct Edge { std::string name; EdgeRanking ranking; std::unordered_map props; + std::atomic refcnt{1}; Edge() {} Edge(Edge&& v) noexcept @@ -44,6 +45,13 @@ struct Edge { ranking(std::move(r)), props(std::move(p)) {} + size_t ref() { + return ++refcnt; + } + size_t unref() { + return --refcnt; + } + void clear(); void __clear() { @@ -57,6 +65,10 @@ struct Edge { bool operator==(const Edge& rhs) const; + bool operator!=(const Edge& rhs) const { + return !(*this == rhs); + } + void format() { if (type < 0) { reverse(); diff --git a/src/common/datatypes/Path.h b/src/common/datatypes/Path.h index f0ac650b2f4..5dbedb3b9d0 100644 --- a/src/common/datatypes/Path.h +++ b/src/common/datatypes/Path.h @@ -91,7 +91,7 @@ struct Step { if (dst != rhs.dst) { return dst < rhs.dst; } - if (type != rhs.dst) { + if (type != rhs.type) { return type < rhs.type; } if (ranking != rhs.ranking) { diff --git a/src/common/datatypes/Value.cpp b/src/common/datatypes/Value.cpp index c49adf90290..324756777f1 100644 --- a/src/common/datatypes/Value.cpp +++ b/src/common/datatypes/Value.cpp @@ -463,9 +463,9 @@ void Value::setVertex(Vertex&& v) { setV(std::move(v)); } -void Value::setVertex(std::unique_ptr&& v) { +void Value::setVertex(Vertex* v) { clear(); - setV(std::move(v)); + setV(v); } void Value::setEdge(const Edge& v) { @@ -478,9 +478,9 @@ void Value::setEdge(Edge&& v) { setE(std::move(v)); } -void Value::setEdge(std::unique_ptr&& v) { +void Value::setEdge(Edge* v) { clear(); - setE(std::move(v)); + setE(v); } void Value::setPath(const Path& v) { @@ -620,7 +620,7 @@ const Vertex& Value::getVertex() const { const Vertex* Value::getVertexPtr() const { CHECK_EQ(type_, Type::VERTEX); - return value_.vVal.get(); + return value_.vVal; } const Edge& Value::getEdge() const { @@ -630,7 +630,7 @@ const Edge& Value::getEdge() const { const Edge* Value::getEdgePtr() const { CHECK_EQ(type_, Type::EDGE); - return value_.eVal.get(); + return value_.eVal; } const Path& Value::getPath() const { @@ -945,11 +945,23 @@ void Value::clearSlow() { break; } case Type::VERTEX: { - destruct(value_.vVal); + if (value_.vVal) { + if (value_.vVal->unref() == 0) { + delete value_.vVal; + } + value_.vVal = nullptr; + type_ = Type::__EMPTY__; + } break; } case Type::EDGE: { - destruct(value_.eVal); + if (value_.eVal) { + if (value_.eVal->unref() == 0) { + delete value_.eVal; + } + value_.eVal = nullptr; + type_ = Type::__EMPTY__; + } break; } case Type::PATH: { @@ -1026,12 +1038,18 @@ Value& Value::operator=(Value&& rhs) noexcept { break; } case Type::VERTEX: { - setV(std::move(rhs.value_.vVal)); - break; + value_.vVal = rhs.value_.vVal; + type_ = Type::VERTEX; + rhs.value_.vVal = nullptr; + rhs.type_ = Type::__EMPTY__; + return *this; } case Type::EDGE: { - setE(std::move(rhs.value_.eVal)); - break; + value_.eVal = rhs.value_.eVal; + type_ = Type::EDGE; + rhs.value_.eVal = nullptr; + rhs.type_ = Type::__EMPTY__; + return *this; } case Type::PATH: { setP(std::move(rhs.value_.pVal)); @@ -1246,44 +1264,36 @@ void Value::setDT(DateTime&& v) { new (std::addressof(value_.dtVal)) DateTime(std::move(v)); } -void Value::setV(const std::unique_ptr& v) { - type_ = Type::VERTEX; - new (std::addressof(value_.vVal)) std::unique_ptr(new Vertex(*v)); -} - -void Value::setV(std::unique_ptr&& v) { +void Value::setV(Vertex* v) { type_ = Type::VERTEX; - new (std::addressof(value_.vVal)) std::unique_ptr(std::move(v)); + value_.vVal = v; + value_.vVal->ref(); } void Value::setV(const Vertex& v) { type_ = Type::VERTEX; - new (std::addressof(value_.vVal)) std::unique_ptr(new Vertex(v)); + new (std::addressof(value_.vVal)) Vertex*(new Vertex(v)); } void Value::setV(Vertex&& v) { type_ = Type::VERTEX; - new (std::addressof(value_.vVal)) std::unique_ptr(new Vertex(std::move(v))); + new (std::addressof(value_.vVal)) Vertex*(new Vertex(std::move(v))); } -void Value::setE(const std::unique_ptr& v) { +void Value::setE(Edge* v) { type_ = Type::EDGE; - new (std::addressof(value_.eVal)) std::unique_ptr(new Edge(*v)); -} - -void Value::setE(std::unique_ptr&& v) { - type_ = Type::EDGE; - new (std::addressof(value_.eVal)) std::unique_ptr(std::move(v)); + value_.eVal = v; + value_.eVal->ref(); } void Value::setE(const Edge& v) { type_ = Type::EDGE; - new (std::addressof(value_.eVal)) std::unique_ptr(new Edge(v)); + new (std::addressof(value_.eVal)) Edge*(new Edge(v)); } void Value::setE(Edge&& v) { type_ = Type::EDGE; - new (std::addressof(value_.eVal)) std::unique_ptr(new Edge(std::move(v))); + new (std::addressof(value_.eVal)) Edge*(new Edge(std::move(v))); } void Value::setP(const std::unique_ptr& v) { @@ -1902,9 +1912,15 @@ Value Value::equal(const Value& v) const { return getDateTime() == v.getDateTime(); } case Value::Type::VERTEX: { + if (value_.vVal == v.value_.vVal) { + return true; + } return getVertex() == v.getVertex(); } case Value::Type::EDGE: { + if (value_.eVal == v.value_.eVal) { + return true; + } return getEdge() == v.getEdge(); } case Value::Type::PATH: { @@ -2737,9 +2753,15 @@ bool Value::equals(const Value& rhs) const { return getDateTime() == rhs.getDateTime(); } case Value::Type::VERTEX: { + if (value_.vVal == rhs.value_.vVal) { + return true; + } return getVertex() == rhs.getVertex(); } case Value::Type::EDGE: { + if (value_.eVal == rhs.value_.eVal) { + return true; + } return getEdge() == rhs.getEdge(); } case Value::Type::PATH: { diff --git a/src/common/datatypes/Value.h b/src/common/datatypes/Value.h index b55397a1566..936a4cf4a3b 100644 --- a/src/common/datatypes/Value.h +++ b/src/common/datatypes/Value.h @@ -256,10 +256,10 @@ struct Value { void setDateTime(DateTime&& v); void setVertex(const Vertex& v); void setVertex(Vertex&& v); - void setVertex(std::unique_ptr&& v); + void setVertex(Vertex* v); void setEdge(const Edge& v); void setEdge(Edge&& v); - void setEdge(std::unique_ptr&& v); + void setEdge(Edge* v); void setPath(const Path& v); void setPath(Path&& v); void setPath(std::unique_ptr&& v); @@ -391,8 +391,8 @@ struct Value { Date dVal; Time tVal; DateTime dtVal; - std::unique_ptr vVal; - std::unique_ptr eVal; + Vertex* vVal; + Edge* eVal; std::unique_ptr pVal; std::unique_ptr lVal; std::unique_ptr mVal; @@ -437,13 +437,11 @@ struct Value { void setDT(const DateTime& v); void setDT(DateTime&& v); // Vertex value - void setV(const std::unique_ptr& v); - void setV(std::unique_ptr&& v); + void setV(Vertex* v); void setV(const Vertex& v); void setV(Vertex&& v); // Edge value - void setE(const std::unique_ptr& v); - void setE(std::unique_ptr&& v); + void setE(Edge* v); void setE(const Edge& v); void setE(Edge&& v); // Path value diff --git a/src/common/datatypes/ValueOps-inl.h b/src/common/datatypes/ValueOps-inl.h index 53db26fa22c..d9ab04320f4 100644 --- a/src/common/datatypes/ValueOps-inl.h +++ b/src/common/datatypes/ValueOps-inl.h @@ -368,10 +368,10 @@ void Cpp2Ops::read(Protocol* proto, nebula::Value* obj) { } case 9: { if (readState.fieldType == apache::thrift::protocol::T_STRUCT) { - obj->setVertex(nebula::Vertex()); - auto ptr = std::make_unique(); - Cpp2Ops::read(proto, ptr.get()); - obj->setVertex(std::move(ptr)); + auto* ptr = new nebula::Vertex(); + Cpp2Ops::read(proto, ptr); + obj->setVertex(ptr); + ptr->unref(); } else { proto->skip(readState.fieldType); } @@ -379,10 +379,10 @@ void Cpp2Ops::read(Protocol* proto, nebula::Value* obj) { } case 10: { if (readState.fieldType == apache::thrift::protocol::T_STRUCT) { - obj->setEdge(nebula::Edge()); - auto ptr = std::make_unique(); - Cpp2Ops::read(proto, ptr.get()); - obj->setEdge(std::move(ptr)); + auto* ptr = new nebula::Edge(); + Cpp2Ops::read(proto, ptr); + obj->setEdge(ptr); + ptr->unref(); } else { proto->skip(readState.fieldType); } diff --git a/src/common/datatypes/Vertex.h b/src/common/datatypes/Vertex.h index 1239121b7bb..41f4cfab554 100644 --- a/src/common/datatypes/Vertex.h +++ b/src/common/datatypes/Vertex.h @@ -61,12 +61,20 @@ struct Tag { struct Vertex { Value vid; std::vector tags; + std::atomic refcnt{1}; Vertex() = default; Vertex(const Vertex& v) : vid(v.vid), tags(v.tags) {} Vertex(Vertex&& v) noexcept : vid(std::move(v.vid)), tags(std::move(v.tags)) {} Vertex(Value id, std::vector t) : vid(std::move(id)), tags(std::move(t)) {} + size_t ref() { + return ++refcnt; + } + size_t unref() { + return --refcnt; + } + void clear() { vid.clear(); tags.clear(); @@ -89,6 +97,10 @@ struct Vertex { return vid == rhs.vid && tags == rhs.tags; } + bool operator!=(const Vertex& rhs) const { + return vid != rhs.vid || tags != rhs.tags; + } + bool operator<(const Vertex& rhs) const; bool contains(const Value& key) const; diff --git a/src/common/expression/PredicateExpression.cpp b/src/common/expression/PredicateExpression.cpp index 16390619f24..d2ede313a12 100644 --- a/src/common/expression/PredicateExpression.cpp +++ b/src/common/expression/PredicateExpression.cpp @@ -19,10 +19,14 @@ std::unordered_map PredicateExpression:: const Value& PredicateExpression::evalExists(ExpressionContext& ctx) { DCHECK(collection_->kind() == Expression::Kind::kAttribute || collection_->kind() == Expression::Kind::kSubscript || - collection_->kind() == Expression::Kind::kLabelTagProperty); - - if (collection_->kind() == Expression::Kind::kLabelTagProperty) { - result_ = !collection_->eval(ctx).isNull(); + collection_->kind() == Expression::Kind::kLabelTagProperty || + collection_->kind() == Expression::Kind::kTagProperty) + << "actual kind: " << collection_->kind() << ", toString: " << toString(); + + if (collection_->kind() == Expression::Kind::kLabelTagProperty || + collection_->kind() == Expression::Kind::kTagProperty) { + auto v = collection_->eval(ctx); + result_ = (!v.isNull()) && (!v.empty()); return result_; } diff --git a/src/common/function/FunctionManager.cpp b/src/common/function/FunctionManager.cpp index 646e09dc911..048ba23769d 100644 --- a/src/common/function/FunctionManager.cpp +++ b/src/common/function/FunctionManager.cpp @@ -2004,6 +2004,11 @@ FunctionManager::FunctionManager() { }; } { + // `none_direct_dst` always return the dstId of an edge key + // without considering the direction of the edge type. + // The encoding of the edge key is: + // type(1) + partId(3) + srcId(*) + edgeType(4) + edgeRank(8) + dstId(*) + placeHolder(1) + // More information of encoding could be found in `NebulaKeyUtils.h` auto &attr = functions_["none_direct_dst"]; attr.minArity_ = 1; attr.maxArity_ = 1; diff --git a/src/graph/context/Iterator.cpp b/src/graph/context/Iterator.cpp index 6d4afad6838..4079f190144 100644 --- a/src/graph/context/Iterator.cpp +++ b/src/graph/context/Iterator.cpp @@ -439,12 +439,15 @@ const Value& GetNeighborsIter::getEdgeProp(const std::string& edge, const std::s return currentEdge_->values[propIndex->second]; } -Value GetNeighborsIter::getVertex(const std::string& name) const { +Value GetNeighborsIter::getVertex(const std::string& name) { UNUSED(name); if (!valid()) { return Value::kNullValue; } auto vidVal = getColumn(0); + if (!prevVertex_.empty() && prevVertex_.getVertex().vid == vidVal) { + return prevVertex_; + } Vertex vertex; vertex.vid = vidVal; @@ -470,7 +473,8 @@ Value GetNeighborsIter::getVertex(const std::string& name) const { } vertex.tags.emplace_back(std::move(tag)); } - return Value(std::move(vertex)); + prevVertex_ = Value(std::move(vertex)); + return prevVertex_; } std::vector GetNeighborsIter::vids() { @@ -735,7 +739,7 @@ StatusOr SequentialIter::getColumnIndex(const std::string& col) con return index->second; } -Value SequentialIter::getVertex(const std::string& name) const { +Value SequentialIter::getVertex(const std::string& name) { return getColumn(name); } @@ -852,7 +856,7 @@ const Value& PropIter::getProp(const std::string& name, const std::string& prop) } } -Value PropIter::getVertex(const std::string& name) const { +Value PropIter::getVertex(const std::string& name) { UNUSED(name); if (!valid()) { return Value::kNullValue; diff --git a/src/graph/context/Iterator.h b/src/graph/context/Iterator.h index 9727680336c..1b4119f0a32 100644 --- a/src/graph/context/Iterator.h +++ b/src/graph/context/Iterator.h @@ -145,7 +145,7 @@ class Iterator { return Value::kEmpty; } - virtual Value getVertex(const std::string& name = "") const { + virtual Value getVertex(const std::string& name = "") { UNUSED(name); return Value(); } @@ -319,7 +319,7 @@ class GetNeighborsIter final : public Iterator { const Value& getEdgeProp(const std::string& edge, const std::string& prop) const override; - Value getVertex(const std::string& name = "") const override; + Value getVertex(const std::string& name = "") override; Value getEdge() const override; @@ -420,6 +420,7 @@ class GetNeighborsIter final : public Iterator { boost::dynamic_bitset<> bitset_; int64_t bitIdx_{-1}; + Value prevVertex_; }; class SequentialIter : public Iterator { @@ -497,7 +498,7 @@ class SequentialIter : public Iterator { StatusOr getColumnIndex(const std::string& col) const override; - Value getVertex(const std::string& name = "") const override; + Value getVertex(const std::string& name = "") override; Value getEdge() const override; @@ -548,7 +549,7 @@ class PropIter final : public SequentialIter { StatusOr getColumnIndex(const std::string& col) const override; - Value getVertex(const std::string& name = "") const override; + Value getVertex(const std::string& name = "") override; Value getEdge() const override; diff --git a/src/graph/context/ast/CypherAstContext.h b/src/graph/context/ast/CypherAstContext.h index eeb0857d5b0..193996a0a5d 100644 --- a/src/graph/context/ast/CypherAstContext.h +++ b/src/graph/context/ast/CypherAstContext.h @@ -55,7 +55,27 @@ struct EdgeInfo { Expression* filter{nullptr}; }; -enum class AliasType : int8_t { kNode, kEdge, kPath, kEdgeList, kDefault }; +enum class AliasType : int8_t { kNode, kEdge, kPath, kNodeList, kEdgeList, kRuntime }; + +struct AliasTypeName { + static std::string get(AliasType type) { + switch (type) { + case AliasType::kNode: + return "Node"; + case AliasType::kEdge: + return "Edge"; + case AliasType::kPath: + return "Path"; + case AliasType::kNodeList: + return "NodeList"; + case AliasType::kEdgeList: + return "EdgeList"; + case AliasType::kRuntime: + return "Runtime"; + } + return "Error"; // should not reach here + } +}; struct ScanInfo { Expression* filter{nullptr}; @@ -83,6 +103,10 @@ struct Path final { // "(v)-[:like]->()" in (v)-[:like]->() std::string collectVariable; + // Flag for pattern predicate + bool isPred{false}; + bool isAntiPred{false}; + enum PathType : int8_t { kDefault, kAllShortest, kSingleShortest }; PathType pathType{PathType::kDefault}; }; diff --git a/src/graph/executor/CMakeLists.txt b/src/graph/executor/CMakeLists.txt index d33f3b6afa1..0984a387d30 100644 --- a/src/graph/executor/CMakeLists.txt +++ b/src/graph/executor/CMakeLists.txt @@ -40,6 +40,7 @@ nebula_add_library( query/TraverseExecutor.cpp query/AppendVerticesExecutor.cpp query/RollUpApplyExecutor.cpp + query/PatternApplyExecutor.cpp query/GetDstBySrcExecutor.cpp algo/BFSShortestPathExecutor.cpp algo/MultiShortestPathExecutor.cpp diff --git a/src/graph/executor/Executor.cpp b/src/graph/executor/Executor.cpp index 8c0afcb6861..06588f2d1b4 100644 --- a/src/graph/executor/Executor.cpp +++ b/src/graph/executor/Executor.cpp @@ -80,6 +80,7 @@ #include "graph/executor/query/LeftJoinExecutor.h" #include "graph/executor/query/LimitExecutor.h" #include "graph/executor/query/MinusExecutor.h" +#include "graph/executor/query/PatternApplyExecutor.h" #include "graph/executor/query/ProjectExecutor.h" #include "graph/executor/query/RollUpApplyExecutor.h" #include "graph/executor/query/SampleExecutor.h" @@ -542,6 +543,9 @@ Executor *Executor::makeExecutor(QueryContext *qctx, const PlanNode *node) { case PlanNode::Kind::kRollUpApply: { return pool->makeAndAdd(node, qctx); } + case PlanNode::Kind::kPatternApply: { + return pool->makeAndAdd(node, qctx); + } case PlanNode::Kind::kArgument: { return pool->makeAndAdd(node, qctx); } diff --git a/src/graph/executor/logic/ArgumentExecutor.cpp b/src/graph/executor/logic/ArgumentExecutor.cpp index 61b4c6b6cbd..eab46383f17 100644 --- a/src/graph/executor/logic/ArgumentExecutor.cpp +++ b/src/graph/executor/logic/ArgumentExecutor.cpp @@ -24,7 +24,9 @@ folly::Future ArgumentExecutor::execute() { std::unordered_set unique; for (; iter->valid(); iter->next()) { auto &val = iter->getColumn(alias); - if (!val.isVertex()) { + if (val.isNull()) { + continue; + } else if (!val.isVertex()) { return Status::Error("Argument only support vertex, but got %s, which is type %s, ", val.toString().c_str(), val.typeName().c_str()); diff --git a/src/graph/executor/query/PatternApplyExecutor.cpp b/src/graph/executor/query/PatternApplyExecutor.cpp new file mode 100644 index 00000000000..826e1c0c86b --- /dev/null +++ b/src/graph/executor/query/PatternApplyExecutor.cpp @@ -0,0 +1,154 @@ +/* Copyright (c) 2022 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#include "graph/executor/query/PatternApplyExecutor.h" + +#include "graph/context/Iterator.h" +#include "graph/context/QueryExpressionContext.h" +#include "graph/planner/plan/Query.h" + +namespace nebula { +namespace graph { + +folly::Future PatternApplyExecutor::execute() { + SCOPED_TIMER(&execTime_); + return patternApply(); +} + +Status PatternApplyExecutor::checkBiInputDataSets() { + auto* patternApply = asNode(node()); + lhsIter_ = ectx_->getResult(patternApply->leftInputVar()).iter(); + DCHECK(!!lhsIter_); + if (lhsIter_->isGetNeighborsIter() || lhsIter_->isDefaultIter()) { + std::stringstream ss; + ss << "PatternApply executor does not support " << lhsIter_->kind(); + return Status::Error(ss.str()); + } + rhsIter_ = ectx_->getResult(patternApply->rightInputVar()).iter(); + DCHECK(!!rhsIter_); + if (rhsIter_->isGetNeighborsIter() || rhsIter_->isDefaultIter()) { + std::stringstream ss; + ss << "PatternApply executor does not support " << rhsIter_->kind(); + return Status::Error(ss.str()); + } + isAntiPred_ = patternApply->isAntiPredicate(); + + return Status::OK(); +} + +void PatternApplyExecutor::collectValidKeys(const std::vector& keyCols, + Iterator* iter, + std::unordered_set& validKeys) const { + QueryExpressionContext ctx(ectx_); + for (; iter->valid(); iter->next()) { + List list; + list.values.reserve(keyCols.size()); + for (auto& col : keyCols) { + Value val = col->eval(ctx(iter)); + list.values.emplace_back(std::move(val)); + } + validKeys.emplace(std::move(list)); + } +} + +void PatternApplyExecutor::collectValidKey(Expression* keyCol, + Iterator* iter, + std::unordered_set& validKey) const { + QueryExpressionContext ctx(ectx_); + for (; iter->valid(); iter->next()) { + auto& val = keyCol->eval(ctx(iter)); + validKey.emplace(val); + } +} + +DataSet PatternApplyExecutor::applyZeroKey(Iterator* appliedIter, const bool allValid) { + DataSet ds; + ds.rows.reserve(appliedIter->size()); + QueryExpressionContext ctx(ectx_); + for (; appliedIter->valid(); appliedIter->next()) { + Row row = mv_ ? appliedIter->moveRow() : *appliedIter->row(); + if (allValid) { + ds.rows.emplace_back(std::move(row)); + } + } + return ds; +} + +DataSet PatternApplyExecutor::applySingleKey(Expression* appliedKey, + Iterator* appliedIter, + const std::unordered_set& validKey) { + DataSet ds; + ds.rows.reserve(appliedIter->size()); + QueryExpressionContext ctx(ectx_); + for (; appliedIter->valid(); appliedIter->next()) { + auto& val = appliedKey->eval(ctx(appliedIter)); + bool applyFlag = (validKey.find(val) != validKey.end()) ^ isAntiPred_; + if (applyFlag) { + Row row = mv_ ? appliedIter->moveRow() : *appliedIter->row(); + ds.rows.emplace_back(std::move(row)); + } + } + return ds; +} + +DataSet PatternApplyExecutor::applyMultiKey(std::vector appliedKeys, + Iterator* appliedIter, + const std::unordered_set& validKeys) { + DataSet ds; + ds.rows.reserve(appliedIter->size()); + QueryExpressionContext ctx(ectx_); + for (; appliedIter->valid(); appliedIter->next()) { + List list; + list.values.reserve(appliedKeys.size()); + for (auto& col : appliedKeys) { + Value val = col->eval(ctx(appliedIter)); + list.values.emplace_back(std::move(val)); + } + + bool applyFlag = (validKeys.find(list) != validKeys.end()) ^ isAntiPred_; + if (applyFlag) { + Row row = mv_ ? appliedIter->moveRow() : *appliedIter->row(); + ds.rows.emplace_back(std::move(row)); + } + } + return ds; +} + +folly::Future PatternApplyExecutor::patternApply() { + auto* patternApplyNode = asNode(node()); + NG_RETURN_IF_ERROR(checkBiInputDataSets()); + + DataSet result; + mv_ = movable(node()->inputVars()[0]); + auto keyCols = patternApplyNode->keyCols(); + if (keyCols.size() == 0) { + // Reverse the valid flag if the pattern predicate is an anti-predicate + applyZeroKey(lhsIter_.get(), (rhsIter_->size() > 0) ^ isAntiPred_); + } else if (keyCols.size() == 1) { + std::unordered_set validKey; + collectValidKey(keyCols[0]->clone(), rhsIter_.get(), validKey); + result = applySingleKey(keyCols[0]->clone(), lhsIter_.get(), validKey); + } else { + // Copy the keyCols to refresh the inside propIndex_ cache + auto cloneExpr = [](std::vector exprs) { + std::vector applyColsCopy; + applyColsCopy.reserve(exprs.size()); + for (auto& expr : exprs) { + applyColsCopy.emplace_back(expr->clone()); + } + return applyColsCopy; + }; + + std::unordered_set validKeys; + collectValidKeys(cloneExpr(keyCols), rhsIter_.get(), validKeys); + result = applyMultiKey(cloneExpr(keyCols), lhsIter_.get(), validKeys); + } + + result.colNames = patternApplyNode->colNames(); + return finish(ResultBuilder().value(Value(std::move(result))).build()); +} + +} // namespace graph +} // namespace nebula diff --git a/src/graph/executor/query/PatternApplyExecutor.h b/src/graph/executor/query/PatternApplyExecutor.h new file mode 100644 index 00000000000..0adbcf4cc10 --- /dev/null +++ b/src/graph/executor/query/PatternApplyExecutor.h @@ -0,0 +1,52 @@ +/* Copyright (c) 2022 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#pragma once + +#include "graph/executor/Executor.h" + +namespace nebula { +namespace graph { + +class PatternApplyExecutor : public Executor { + public: + PatternApplyExecutor(const PlanNode* node, QueryContext* qctx) + : Executor("PatternApplyExecutor", node, qctx) {} + + folly::Future execute() override; + + protected: + Status checkBiInputDataSets(); + + void collectValidKeys(const std::vector& keyCols, + Iterator* iter, + std::unordered_set& validKeys) const; + + void collectValidKey(Expression* keyCol, + Iterator* iter, + std::unordered_set& validKey) const; + + DataSet applyZeroKey(Iterator* appliedIter, const bool allValid); + + DataSet applySingleKey(Expression* appliedCol, + Iterator* appliedIter, + const std::unordered_set& validKey); + + DataSet applyMultiKey(std::vector appliedKeys, + Iterator* appliedIter, + const std::unordered_set& validKeys); + + folly::Future patternApply(); + std::unique_ptr lhsIter_; + std::unique_ptr rhsIter_; + + // Should apply the reverse when the pattern is an anti-predicate + bool isAntiPred_{false}; + // Check if the apply side dataset movable + bool mv_{false}; +}; + +} // namespace graph +} // namespace nebula diff --git a/src/graph/optimizer/CMakeLists.txt b/src/graph/optimizer/CMakeLists.txt index cae7300d687..b99c495d16c 100644 --- a/src/graph/optimizer/CMakeLists.txt +++ b/src/graph/optimizer/CMakeLists.txt @@ -54,6 +54,8 @@ nebula_add_library( rule/EliminateAppendVerticesRule.cpp rule/PushLimitDownScanEdgesRule.cpp rule/RemoveProjectDedupBeforeGetDstBySrcRule.cpp + rule/PushFilterDownTraverseRule.cpp + rule/RemoveAppendVerticesBelowJoinRule.cpp ) nebula_add_subdirectory(test) diff --git a/src/graph/optimizer/OptGroup.cpp b/src/graph/optimizer/OptGroup.cpp index 569b5ec43c8..340881b224d 100644 --- a/src/graph/optimizer/OptGroup.cpp +++ b/src/graph/optimizer/OptGroup.cpp @@ -119,7 +119,9 @@ void OptGroup::addGroupNode(OptGroupNode *groupNode) { if (outputVar_.empty()) { outputVar_ = groupNode->node()->outputVar(); } else { - DCHECK_EQ(outputVar_, groupNode->node()->outputVar()); + DCHECK_EQ(outputVar_, groupNode->node()->outputVar()) + << "outputVar_: " << outputVar_ << "groupNode plan: " << groupNode->node()->toString() + << ", groupNode->node()->outputVar(): " << groupNode->node()->outputVar(); } groupNodes_.emplace_back(groupNode); groupNode->node()->updateSymbols(); diff --git a/src/graph/optimizer/rule/PushEFilterDownRule.cpp b/src/graph/optimizer/rule/PushEFilterDownRule.cpp index 3b5df8904d2..0af06f4a4b3 100644 --- a/src/graph/optimizer/rule/PushEFilterDownRule.cpp +++ b/src/graph/optimizer/rule/PushEFilterDownRule.cpp @@ -53,8 +53,12 @@ StatusOr PushEFilterDownRule::transform( auto pool = qctx->objPool(); auto eFilter = traverse->eFilter()->clone(); - eFilter = rewriteStarEdge( - eFilter, traverse->space(), *DCHECK_NOTNULL(traverse->edgeProps()), qctx->schemaMng(), pool); + eFilter = rewriteStarEdge(eFilter, + traverse->space(), + *DCHECK_NOTNULL(traverse->edgeProps()), + qctx->schemaMng(), + traverse->edgeDirection() == storage::cpp2::EdgeDirection::BOTH, + pool); if (eFilter == nullptr) { return TransformResult::noTransform(); } @@ -93,6 +97,7 @@ std::string PushEFilterDownRule::toString() const { GraphSpaceID spaceId, const std::vector &edges, meta::SchemaManager *schemaMng, + bool isBothDirection, ObjectPool *pool) { graph::RewriteVisitor::Matcher matcher = [](const Expression *exp) -> bool { switch (exp->kind()) { @@ -113,12 +118,12 @@ std::string PushEFilterDownRule::toString() const { } }; graph::RewriteVisitor::Rewriter rewriter = - [spaceId, &edges, schemaMng, pool](const Expression *exp) -> Expression * { + [spaceId, &edges, schemaMng, isBothDirection, pool](const Expression *exp) -> Expression * { auto *propertyExpr = static_cast(exp); DCHECK_EQ(propertyExpr->sym(), "*"); DCHECK(!edges.empty()); Expression *ret = nullptr; - if (edges.size() == 1) { + if (edges.size() == 1 || (isBothDirection && edges.size() == 2)) { ret = rewriteStarEdge(propertyExpr, spaceId, edges.front(), schemaMng, pool); if (ret == nullptr) { return nullptr; @@ -126,6 +131,9 @@ std::string PushEFilterDownRule::toString() const { } else { auto args = ArgumentList::make(pool); for (auto &edge : edges) { + if (isBothDirection && edge.get_type() < 0) { + continue; + } auto reEdgeExp = rewriteStarEdge(propertyExpr, spaceId, edge, schemaMng, pool); if (reEdgeExp == nullptr) { return nullptr; @@ -144,7 +152,7 @@ std::string PushEFilterDownRule::toString() const { const storage::cpp2::EdgeProp &edge, meta::SchemaManager *schemaMng, ObjectPool *pool) { - auto edgeNameResult = schemaMng->toEdgeName(spaceId, edge.get_type()); + auto edgeNameResult = schemaMng->toEdgeName(spaceId, std::abs(edge.get_type())); if (!edgeNameResult.ok()) { return nullptr; } diff --git a/src/graph/optimizer/rule/PushEFilterDownRule.h b/src/graph/optimizer/rule/PushEFilterDownRule.h index cb2b415d738..4add56e8ef3 100644 --- a/src/graph/optimizer/rule/PushEFilterDownRule.h +++ b/src/graph/optimizer/rule/PushEFilterDownRule.h @@ -26,6 +26,7 @@ class PushEFilterDownRule final : public OptRule { GraphSpaceID spaceId, const std::vector &edges, meta::SchemaManager *schemaMng, + bool isBothDirection, ObjectPool *pool); static Expression *rewriteStarEdge(const PropertyExpression *exp, GraphSpaceID spaceId, diff --git a/src/graph/optimizer/rule/PushFilterDownTraverseRule.cpp b/src/graph/optimizer/rule/PushFilterDownTraverseRule.cpp new file mode 100644 index 00000000000..0ad74b1dd01 --- /dev/null +++ b/src/graph/optimizer/rule/PushFilterDownTraverseRule.cpp @@ -0,0 +1,152 @@ +/* Copyright (c) 2022 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#include "graph/optimizer/rule/PushFilterDownTraverseRule.h" + +#include "common/expression/ConstantExpression.h" +#include "common/expression/Expression.h" +#include "graph/optimizer/OptContext.h" +#include "graph/optimizer/OptGroup.h" +#include "graph/planner/plan/PlanNode.h" +#include "graph/planner/plan/Query.h" +#include "graph/util/ExpressionUtils.h" +#include "graph/visitor/ExtractFilterExprVisitor.h" + +using nebula::Expression; +using nebula::graph::Filter; +using nebula::graph::PlanNode; +using nebula::graph::QueryContext; +using nebula::graph::Traverse; + +namespace nebula { +namespace opt { + +std::unique_ptr PushFilterDownTraverseRule::kInstance = + std::unique_ptr(new PushFilterDownTraverseRule()); + +PushFilterDownTraverseRule::PushFilterDownTraverseRule() { + RuleSet::QueryRules().addRule(this); +} + +const Pattern& PushFilterDownTraverseRule::pattern() const { + static Pattern pattern = + Pattern::create(PlanNode::Kind::kFilter, + {Pattern::create(PlanNode::Kind::kAppendVertices, + {Pattern::create(PlanNode::Kind::kTraverse)})}); + return pattern; +} + +bool PushFilterDownTraverseRule::match(OptContext* ctx, const MatchedResult& matched) const { + if (!OptRule::match(ctx, matched)) { + return false; + } + DCHECK_EQ(matched.dependencies[0].dependencies[0].node->node()->kind(), + PlanNode::Kind::kTraverse); + auto traverse = + static_cast(matched.dependencies[0].dependencies[0].node->node()); + return traverse->isOneStep(); +} + +StatusOr PushFilterDownTraverseRule::transform( + OptContext* ctx, const MatchedResult& matched) const { + auto* filterGroupNode = matched.node; + auto* filterGroup = filterGroupNode->group(); + auto* filter = static_cast(filterGroupNode->node()); + auto* condition = filter->condition(); + + auto* avGroupNode = matched.dependencies[0].node; + auto* av = static_cast(avGroupNode->node()); + + auto* tvGroupNode = matched.dependencies[0].dependencies[0].node; + auto* tv = static_cast(tvGroupNode->node()); + auto& edgeAlias = tv->edgeAlias(); + + auto qctx = ctx->qctx(); + auto pool = qctx->objPool(); + + // Pick the expr looks like `$-.e[0].likeness + auto picker = [&edgeAlias](const Expression* e) -> bool { + // TODO(jie): Handle the strange exists expr. e.g. exists(e.likeness) + auto exprs = graph::ExpressionUtils::collectAll(e, {Expression::Kind::kPredicate}); + for (auto* expr : exprs) { + if (static_cast(expr)->name() == "exists") { + return false; + } + } + + auto varProps = graph::ExpressionUtils::collectAll( + e, {Expression::Kind::kInputProperty, Expression::Kind::kVarProperty}); + if (varProps.empty()) { + return false; + } + for (auto* expr : varProps) { + DCHECK(graph::ExpressionUtils::isPropertyExpr(expr)); + auto& propName = static_cast(expr)->prop(); + if (propName != edgeAlias) return false; + } + return true; + }; + Expression* filterPicked = nullptr; + Expression* filterUnpicked = nullptr; + graph::ExpressionUtils::splitFilter(condition, picker, &filterPicked, &filterUnpicked); + + if (!filterPicked) { + return TransformResult::noTransform(); + } + auto* newFilterPicked = + graph::ExpressionUtils::rewriteEdgePropertyFilter(pool, edgeAlias, filterPicked->clone()); + + Filter* newFilter = nullptr; + OptGroupNode* newFilterGroupNode = nullptr; + if (filterUnpicked) { + newFilter = Filter::make(qctx, nullptr, filterUnpicked); + newFilter->setOutputVar(filter->outputVar()); + newFilter->setColNames(filter->colNames()); + newFilterGroupNode = OptGroupNode::create(ctx, newFilter, filterGroup); + } + + auto* newAv = static_cast(av->clone()); + + OptGroupNode* newAvGroupNode = nullptr; + if (newFilterGroupNode) { + auto* newAvGroup = OptGroup::create(ctx); + newAvGroupNode = newAvGroup->makeGroupNode(newAv); + newFilterGroupNode->dependsOn(newAvGroup); + newFilter->setInputVar(newAv->outputVar()); + } else { + newAvGroupNode = OptGroupNode::create(ctx, newAv, filterGroup); + newAv->setOutputVar(filter->outputVar()); + } + + auto* eFilter = tv->eFilter(); + Expression* newEFilter = eFilter + ? LogicalExpression::makeAnd(pool, newFilterPicked, eFilter->clone()) + : newFilterPicked; + + auto* newTv = static_cast(tv->clone()); + newAv->setInputVar(newTv->outputVar()); + newTv->setEdgeFilter(newEFilter); + + auto* newTvGroup = OptGroup::create(ctx); + newAvGroupNode->dependsOn(newTvGroup); + auto* newTvGroupNode = newTvGroup->makeGroupNode(newTv); + + for (auto dep : tvGroupNode->dependencies()) { + newTvGroupNode->dependsOn(dep); + } + + TransformResult result; + result.eraseCurr = true; + result.newGroupNodes.emplace_back(newFilterGroupNode ? newFilterGroupNode : newAvGroupNode); + + return result; +} + +std::string PushFilterDownTraverseRule::toString() const { + return "PushFilterDownTraverseRule"; +} + +} // namespace opt +} // namespace nebula diff --git a/src/graph/optimizer/rule/PushFilterDownTraverseRule.h b/src/graph/optimizer/rule/PushFilterDownTraverseRule.h new file mode 100644 index 00000000000..459773a2017 --- /dev/null +++ b/src/graph/optimizer/rule/PushFilterDownTraverseRule.h @@ -0,0 +1,43 @@ +/* Copyright (c) 2022 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#pragma once + +#include "graph/optimizer/OptRule.h" + +namespace nebula { +namespace opt { + +/* + * Before: + * Filter(e.likeness > 78) + * | + * AppendVertices + * | + * Traverse + * + * After : + * AppendVertices + * | + * Traverse(eFilter_: *.likeness > 78) + */ +class PushFilterDownTraverseRule final : public OptRule { + public: + const Pattern &pattern() const override; + + bool match(OptContext *ctx, const MatchedResult &matched) const override; + + StatusOr transform(OptContext *ctx, const MatchedResult &matched) const override; + + std::string toString() const override; + + private: + PushFilterDownTraverseRule(); + + static std::unique_ptr kInstance; +}; + +} // namespace opt +} // namespace nebula diff --git a/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.cpp b/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.cpp new file mode 100644 index 00000000000..5b0364b7dfd --- /dev/null +++ b/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.cpp @@ -0,0 +1,205 @@ +// Copyright (c) 2022 vesoft inc. All rights reserved. +// +// This source code is licensed under Apache 2.0 License. + +#include "graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.h" + +#include "graph/optimizer/OptContext.h" +#include "graph/optimizer/OptGroup.h" +#include "graph/planner/plan/PlanNode.h" +#include "graph/planner/plan/Query.h" +#include "graph/util/ExpressionUtils.h" + +using nebula::graph::PlanNode; +using nebula::graph::QueryContext; + +namespace nebula { +namespace opt { + +std::unique_ptr RemoveAppendVerticesBelowJoinRule::kInstance = + std::unique_ptr(new RemoveAppendVerticesBelowJoinRule()); + +RemoveAppendVerticesBelowJoinRule::RemoveAppendVerticesBelowJoinRule() { + RuleSet::QueryRules().addRule(this); +} + +const Pattern& RemoveAppendVerticesBelowJoinRule::pattern() const { + static Pattern pattern = Pattern::create( + {PlanNode::Kind::kHashLeftJoin, PlanNode::Kind::kHashInnerJoin}, + {Pattern::create(PlanNode::Kind::kUnknown), + Pattern::create(PlanNode::Kind::kProject, + {Pattern::create(PlanNode::Kind::kAppendVertices, + {Pattern::create(PlanNode::Kind::kTraverse)})})}); + return pattern; +} + +StatusOr RemoveAppendVerticesBelowJoinRule::transform( + OptContext* octx, const MatchedResult& matched) const { + auto* joinGroupNode = matched.node; + auto* joinGroup = joinGroupNode->group(); + auto* join = static_cast(joinGroupNode->node()); + + auto* projectGroupNode = matched.dependencies[1].node; + auto* project = static_cast(projectGroupNode->node()); + + auto* appendVerticesGroupNode = matched.dependencies[1].dependencies[0].node; + auto appendVertices = + static_cast(matched.dependencies[1].dependencies[0].node->node()); + + auto traverse = static_cast( + matched.dependencies[1].dependencies[0].dependencies[0].node->node()); + + auto& avNodeAlias = appendVertices->nodeAlias(); + + auto& tvEdgeAlias = traverse->edgeAlias(); + + auto& leftExprs = join->hashKeys(); + auto& rightExprs = join->probeKeys(); + + std::vector referAVNodeAliasExprs; + for (auto* rightExpr : rightExprs) { + auto propExprs = graph::ExpressionUtils::collectAll( + rightExpr, {Expression::Kind::kVarProperty, Expression::Kind::kInputProperty}); + for (auto* expr : propExprs) { + auto* propExpr = static_cast(expr); + if (propExpr->prop() == avNodeAlias) { + referAVNodeAliasExprs.push_back(propExpr); + } + } + } + // If avNodeAlias is referred by more than one expr, + // we cannot remove the append vertices which generate the avNodeAlias column + if (referAVNodeAliasExprs.size() > 1) { + return TransformResult::noTransform(); + } + + bool found = false; + size_t rightExprIdx = 0; + for (size_t i = 0; i < rightExprs.size(); ++i) { + auto* rightExpr = rightExprs[i]; + if (rightExpr->kind() != Expression::Kind::kFunctionCall) { + continue; + } + auto* func = static_cast(rightExpr); + if (func->name() != "id" && func->name() != "_joinkey") { + continue; + } + auto& args = func->args()->args(); + DCHECK_EQ(args.size(), 1); + auto* arg = args[0]; + if (arg->kind() != Expression::Kind::kInputProperty) { + continue; + } + auto& alias = static_cast(arg)->prop(); + if (alias != avNodeAlias) continue; + // Must check if left exprs contain the same key + if (*leftExprs[i] != *rightExpr) { + return TransformResult::noTransform(); + } + if (found) { + return TransformResult::noTransform(); + } + rightExprIdx = i; + found = true; + } + if (!found) { + return TransformResult::noTransform(); + } + + auto columns = project->columns()->columns(); + referAVNodeAliasExprs.clear(); + for (auto* column : columns) { + auto propExprs = graph::ExpressionUtils::collectAll( + column->expr(), {Expression::Kind::kVarProperty, Expression::Kind::kInputProperty}); + for (auto* expr : propExprs) { + auto* propExpr = static_cast(expr); + if (propExpr->prop() == avNodeAlias) { + referAVNodeAliasExprs.push_back(propExpr); + } + } + } + // If avNodeAlias is referred by more than one expr, + // we cannot remove the append vertices which generate the avNodeAlias column + if (referAVNodeAliasExprs.size() > 1) { + return TransformResult::noTransform(); + } + + found = false; + size_t prjIdx = 0; + for (size_t i = 0; i < columns.size(); ++i) { + const auto* col = columns[i]; + if (col->expr()->kind() != Expression::Kind::kInputProperty) { + continue; + } + auto* inputProp = static_cast(col->expr()); + if (inputProp->prop() != avNodeAlias) continue; + if (found) { + return TransformResult::noTransform(); + } + prjIdx = i; + found = true; + } + if (!found) { + return TransformResult::noTransform(); + } + + auto* pool = octx->qctx()->objPool(); + // Let the new project generate expr `none_direct_dst($-.tvEdgeAlias)`, + // and let the new left/inner join use it as right expr + auto* args = ArgumentList::make(pool); + args->addArgument(InputPropertyExpression::make(pool, tvEdgeAlias)); + auto* newPrjExpr = FunctionCallExpression::make(pool, "none_direct_dst", args); + + auto oldYieldColumns = project->columns()->columns(); + auto* newYieldColumns = pool->makeAndAdd(); + for (size_t i = 0; i < oldYieldColumns.size(); ++i) { + if (i == prjIdx) { + newYieldColumns->addColumn(new YieldColumn(newPrjExpr, avNodeAlias)); + } else { + newYieldColumns->addColumn(oldYieldColumns[i]->clone().release()); + } + } + auto* newProject = graph::Project::make(octx->qctx(), nullptr, newYieldColumns); + + // $-.`avNodeAlias` + auto* newRightExpr = InputPropertyExpression::make(pool, avNodeAlias); + std::vector newRightExprs; + for (size_t i = 0; i < rightExprs.size(); ++i) { + if (i == rightExprIdx) { + newRightExprs.emplace_back(newRightExpr); + } else { + newRightExprs.emplace_back(rightExprs[i]->clone()); + } + } + graph::HashJoin* newJoin = nullptr; + if (join->kind() == PlanNode::Kind::kHashLeftJoin) { + newJoin = graph::HashLeftJoin::make(octx->qctx(), nullptr, nullptr, leftExprs, newRightExprs); + } else { + newJoin = graph::HashInnerJoin::make(octx->qctx(), nullptr, nullptr, leftExprs, newRightExprs); + } + + TransformResult result; + result.eraseAll = true; + + newProject->setInputVar(appendVertices->inputVar()); + auto newProjectGroup = OptGroup::create(octx); + auto* newProjectGroupNode = newProjectGroup->makeGroupNode(newProject); + newProjectGroupNode->setDeps(appendVerticesGroupNode->dependencies()); + + newJoin->setLeftVar(join->leftInputVar()); + newJoin->setRightVar(newProject->outputVar()); + newJoin->setOutputVar(join->outputVar()); + auto* newJoinGroupNode = OptGroupNode::create(octx, newJoin, joinGroup); + newJoinGroupNode->dependsOn(joinGroupNode->dependencies()[0]); + newJoinGroupNode->dependsOn(newProjectGroup); + + result.newGroupNodes.emplace_back(newJoinGroupNode); + return result; +} + +std::string RemoveAppendVerticesBelowJoinRule::toString() const { + return "RemoveAppendVerticesBelowJoinRule"; +} + +} // namespace opt +} // namespace nebula diff --git a/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.h b/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.h new file mode 100644 index 00000000000..d955ca58950 --- /dev/null +++ b/src/graph/optimizer/rule/RemoveAppendVerticesBelowJoinRule.h @@ -0,0 +1,46 @@ +/* Copyright (c) 2022 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#pragma once + +#include "graph/optimizer/OptRule.h" + +namespace nebula { +namespace opt { +// Before: +// HashLeft/InnerJoin({id(v)}, {id(v)}) +// | | +// ... Project +// | | +// AppendVertices(v) AppendVertices(v) +// | | +// ... Traverse(e) +// +// After: +// HashLeft/InnerJoin({id(v)}, {$-.v}) +// | | +// ... Project(..., none_direct_dst(e) AS v) +// | | +// AppendVertices(v) Traverse(e) +// | +// ... +// +class RemoveAppendVerticesBelowJoinRule final : public OptRule { + public: + const Pattern &pattern() const override; + + StatusOr transform(OptContext *qctx, + const MatchedResult &matched) const override; + + std::string toString() const override; + + private: + RemoveAppendVerticesBelowJoinRule(); + + static std::unique_ptr kInstance; +}; + +} // namespace opt +} // namespace nebula diff --git a/src/graph/optimizer/rule/RemoveNoopProjectRule.cpp b/src/graph/optimizer/rule/RemoveNoopProjectRule.cpp index 19636b6ad27..3b8860d7d2e 100644 --- a/src/graph/optimizer/rule/RemoveNoopProjectRule.cpp +++ b/src/graph/optimizer/rule/RemoveNoopProjectRule.cpp @@ -62,6 +62,7 @@ const std::unordered_set RemoveNoopProjectRule::kQueries{ PlanNode::Kind::kHashInnerJoin, PlanNode::Kind::kCrossJoin, PlanNode::Kind::kRollUpApply, + PlanNode::Kind::kPatternApply, PlanNode::Kind::kArgument, }; diff --git a/src/graph/planner/match/MatchPathPlanner.cpp b/src/graph/planner/match/MatchPathPlanner.cpp index 088d39d1d50..44fb4ecbc11 100644 --- a/src/graph/planner/match/MatchPathPlanner.cpp +++ b/src/graph/planner/match/MatchPathPlanner.cpp @@ -77,7 +77,11 @@ StatusOr MatchPathPlanner::transform(WhereClauseContext* bindWhere, NG_RETURN_IF_ERROR(findStarts(bindWhere, nodeAliasesSeen, startFromEdge, startIndex, subplan)); NG_RETURN_IF_ERROR(expand(startFromEdge, startIndex, subplan)); - MatchSolver::buildProjectColumns(ctx_->qctx, path_, subplan); + // No need to actually build path if the path is just a predicate + if (!path_.isPred) { + MatchSolver::buildProjectColumns(ctx_->qctx, path_, subplan); + } + return subplan; } diff --git a/src/graph/planner/match/MatchPlanner.cpp b/src/graph/planner/match/MatchPlanner.cpp index 353ea04ab0c..72f4cf31581 100644 --- a/src/graph/planner/match/MatchPlanner.cpp +++ b/src/graph/planner/match/MatchPlanner.cpp @@ -72,12 +72,16 @@ Status MatchPlanner::connectMatchPlan(SubPlan& queryPlan, MatchClauseContext* ma for (auto& alias : matchCtx->aliasesGenerated) { auto it = matchCtx->aliasesAvailable.find(alias.first); if (it != matchCtx->aliasesAvailable.end()) { - // Joined type should be same - if (it->second != alias.second) { + // Joined type should be same, + // If any type is kRuntime, leave the type check to runtime, + // Primitive types (Integer, String, etc.) or composite types(List, Map etc.) + // are deduced to kRuntime when cannot be deduced during planning. + if (it->second != alias.second && it->second != AliasType::kRuntime && + alias.second != AliasType::kRuntime) { return Status::SemanticError(fmt::format("{} binding to different type: {} vs {}", alias.first, - AliasTypeName[static_cast(alias.second)], - AliasTypeName[static_cast(it->second)])); + AliasTypeName::get(alias.second), + AliasTypeName::get(it->second))); } // Joined On EdgeList is not supported if (alias.second == AliasType::kEdgeList) { diff --git a/src/graph/planner/match/MatchPlanner.h b/src/graph/planner/match/MatchPlanner.h index 95c0cc7ad14..d61f2932a08 100644 --- a/src/graph/planner/match/MatchPlanner.h +++ b/src/graph/planner/match/MatchPlanner.h @@ -23,7 +23,6 @@ class MatchPlanner final : public Planner { StatusOr transform(AstContext* astCtx) override; private: - static constexpr std::array AliasTypeName = {"Node", "Edge", "Path", "EdgeList", "Default"}; bool tailConnected_{false}; StatusOr genPlan(CypherClauseContextBase* clauseCtx); Status connectMatchPlan(SubPlan& queryPlan, MatchClauseContext* matchCtx); diff --git a/src/graph/planner/match/SegmentsConnector.cpp b/src/graph/planner/match/SegmentsConnector.cpp index c62c26d5e7a..63dcbcebe8b 100644 --- a/src/graph/planner/match/SegmentsConnector.cpp +++ b/src/graph/planner/match/SegmentsConnector.cpp @@ -93,12 +93,34 @@ SubPlan SegmentsConnector::rollUpApply(CypherClauseContextBase* ctx, return newPlan; } +/*static*/ SubPlan SegmentsConnector::patternApply(CypherClauseContextBase* ctx, + const SubPlan& left, + const SubPlan& right, + const graph::Path& path) { + SubPlan newPlan = left; + auto qctx = ctx->qctx; + std::vector keyProps; + for (const auto& col : path.compareVariables) { + keyProps.emplace_back(FunctionCallExpression::make( + qctx->objPool(), "id", {InputPropertyExpression::make(qctx->objPool(), col)})); + } + auto* patternApply = PatternApply::make( + qctx, left.root, DCHECK_NOTNULL(right.root), std::move(keyProps), path.isAntiPred); + // Left side input may be nullptr, which will be filled later + std::vector colNames = + left.root != nullptr ? left.root->colNames() : ctx->inputColNames; + patternApply->setColNames(std::move(colNames)); + newPlan.root = patternApply; + newPlan.tail = (newPlan.tail == nullptr ? patternApply : newPlan.tail); + return newPlan; +} + SubPlan SegmentsConnector::addInput(const SubPlan& left, const SubPlan& right, bool copyColNames) { if (left.root == nullptr) { return right; } SubPlan newPlan = left; - DCHECK(left.root->isSingleInput()); + if (left.tail->isSingleInput()) { auto* mutableLeft = const_cast(left.tail); auto* siLeft = static_cast(mutableLeft); diff --git a/src/graph/planner/match/SegmentsConnector.h b/src/graph/planner/match/SegmentsConnector.h index 9a201f59820..102d7d7a713 100644 --- a/src/graph/planner/match/SegmentsConnector.h +++ b/src/graph/planner/match/SegmentsConnector.h @@ -44,6 +44,11 @@ class SegmentsConnector final { const SubPlan& right, const graph::Path& path); + static SubPlan patternApply(CypherClauseContextBase* ctx, + const SubPlan& left, + const SubPlan& right, + const graph::Path& path); + /* * left->right */ diff --git a/src/graph/planner/match/WhereClausePlanner.cpp b/src/graph/planner/match/WhereClausePlanner.cpp index a6bf3608da0..0da331924e0 100644 --- a/src/graph/planner/match/WhereClausePlanner.cpp +++ b/src/graph/planner/match/WhereClausePlanner.cpp @@ -19,27 +19,37 @@ StatusOr WhereClausePlanner::transform(CypherClauseContextBase* ctx) { } auto* wctx = static_cast(ctx); - SubPlan wherePlan; - if (wctx->filter) { - auto* newFilter = MatchSolver::doRewrite(wctx->qctx, wctx->aliasesAvailable, wctx->filter); - wherePlan.root = Filter::make(wctx->qctx, nullptr, newFilter, needStableFilter_); - wherePlan.tail = wherePlan.root; - - SubPlan subPlan; - // Build plan for pattern from expression + SubPlan plan; + if (!wctx->paths.empty()) { + SubPlan pathsPlan; + // Build plan for pattern expression for (auto& path : wctx->paths) { auto status = MatchPathPlanner(wctx, path).transform(nullptr, {}); NG_RETURN_IF_ERROR(status); - subPlan = SegmentsConnector::rollUpApply(wctx, subPlan, std::move(status).value(), path); - } - if (subPlan.root != nullptr) { - wherePlan = SegmentsConnector::addInput(wherePlan, subPlan, true); + auto pathPlan = std::move(status).value(); + + if (path.isPred) { + // Build plan for pattern predicates + pathsPlan = SegmentsConnector::patternApply(wctx, pathsPlan, pathPlan, path); + } else { + pathsPlan = SegmentsConnector::rollUpApply(wctx, pathsPlan, pathPlan, path); + } } + plan = pathsPlan; + } - return wherePlan; + if (wctx->filter) { + SubPlan wherePlan; + auto* newFilter = MatchSolver::doRewrite(wctx->qctx, wctx->aliasesAvailable, wctx->filter); + wherePlan.root = Filter::make(wctx->qctx, nullptr, newFilter, needStableFilter_); + wherePlan.tail = wherePlan.root; + if (plan.root == nullptr) { + return wherePlan; + } + plan = SegmentsConnector::addInput(wherePlan, plan, true); } - return wherePlan; + return plan; } } // namespace graph } // namespace nebula diff --git a/src/graph/planner/plan/PlanNode.cpp b/src/graph/planner/plan/PlanNode.cpp index 7221f84eef9..72931b7f24c 100644 --- a/src/graph/planner/plan/PlanNode.cpp +++ b/src/graph/planner/plan/PlanNode.cpp @@ -299,6 +299,8 @@ const char* PlanNode::toString(PlanNode::Kind kind) { return "Argument"; case Kind::kRollUpApply: return "RollUpApply"; + case Kind::kPatternApply: + return "PatternApply"; case Kind::kGetDstBySrc: return "GetDstBySrc"; // no default so the compiler will warning when lack diff --git a/src/graph/planner/plan/PlanNode.h b/src/graph/planner/plan/PlanNode.h index 678acc5b271..0b47e93d91e 100644 --- a/src/graph/planner/plan/PlanNode.h +++ b/src/graph/planner/plan/PlanNode.h @@ -69,6 +69,7 @@ class PlanNode { kHashInnerJoin, kCrossJoin, kRollUpApply, + kPatternApply, kArgument, // Logic diff --git a/src/graph/planner/plan/Query.cpp b/src/graph/planner/plan/Query.cpp index 2383987b78a..a2e5fd952fe 100644 --- a/src/graph/planner/plan/Query.cpp +++ b/src/graph/planner/plan/Query.cpp @@ -944,5 +944,41 @@ PlanNode* RollUpApply::clone() const { return newRollUpApply; } +std::unique_ptr PatternApply::explain() const { + auto desc = BinaryInputNode::explain(); + addDescription("keyCols", folly::toJson(util::toJson(keyCols_)), desc.get()); + return desc; +} + +void PatternApply::accept(PlanNodeVisitor* visitor) { + visitor->visit(this); +} + +PatternApply::PatternApply(QueryContext* qctx, + Kind kind, + PlanNode* left, + PlanNode* right, + std::vector keyCols, + bool isAntiPred) + : BinaryInputNode(qctx, kind, left, right), + keyCols_(std::move(keyCols)), + isAntiPred_(isAntiPred) {} + +void PatternApply::cloneMembers(const PatternApply& r) { + BinaryInputNode::cloneMembers(r); + for (const auto* col : r.keyCols_) { + keyCols_.emplace_back(col->clone()); + } + isAntiPred_ = r.isAntiPred_; +} + +PlanNode* PatternApply::clone() const { + auto* lnode = left() ? left()->clone() : nullptr; + auto* rnode = right() ? right()->clone() : nullptr; + auto* newPatternApply = PatternApply::make(qctx_, lnode, rnode, {}); + newPatternApply->cloneMembers(*this); + return newPatternApply; +} + } // namespace graph } // namespace nebula diff --git a/src/graph/planner/plan/Query.h b/src/graph/planner/plan/Query.h index b39108d9f7c..2e188c26d6f 100644 --- a/src/graph/planner/plan/Query.h +++ b/src/graph/planner/plan/Query.h @@ -1538,6 +1538,10 @@ class Traverse final : public GetNeighbors { return range_; } + bool isOneStep() const { + return !range_; + } + // Contains zero step bool zeroStep() const { return range_ != nullptr && range_->min() == 0; @@ -1555,6 +1559,17 @@ class Traverse final : public GetNeighbors { return trackPrevPath_; } + const std::string& nodeAlias() const { + auto& cols = this->colNames(); + DCHECK_GE(cols.size(), 2); + return cols[cols.size() - 2]; + } + + const std::string& edgeAlias() const { + DCHECK(!this->colNames().empty()); + return this->colNames().back(); + } + void setStepRange(MatchStepRange* range) { range_ = range; } @@ -1618,6 +1633,11 @@ class AppendVertices final : public GetVertices { return trackPrevPath_; } + const std::string nodeAlias() const { + DCHECK(!this->colNames().empty()); + return this->colNames().back(); + } + void setVertexFilter(Expression* vFilter) { vFilter_ = vFilter; } @@ -1789,6 +1809,48 @@ class RollUpApply : public BinaryInputNode { InputPropertyExpression* collectCol_; }; +// PatternApply only used by pattern predicate for now +class PatternApply : public BinaryInputNode { + public: + static PatternApply* make(QueryContext* qctx, + PlanNode* left, + PlanNode* right, + std::vector keyCols, + bool isAntiPred = false) { + return qctx->objPool()->makeAndAdd( + qctx, Kind::kPatternApply, left, right, std::move(keyCols), isAntiPred); + } + + const std::vector& keyCols() const { + return keyCols_; + } + + bool isAntiPredicate() const { + return isAntiPred_; + } + + PlanNode* clone() const override; + std::unique_ptr explain() const override; + + void accept(PlanNodeVisitor* visitor) override; + + protected: + friend ObjectPool; + PatternApply(QueryContext* qctx, + Kind kind, + PlanNode* left, + PlanNode* right, + std::vector keyCols, + bool isAntiPred); + + void cloneMembers(const PatternApply&); + + protected: + // Common columns of subplans on both sides + std::vector keyCols_; + bool isAntiPred_{false}; +}; + } // namespace graph } // namespace nebula #endif // GRAPH_PLANNER_PLAN_QUERY_H_ diff --git a/src/graph/util/ExpressionUtils.cpp b/src/graph/util/ExpressionUtils.cpp index d8ce7d8953b..423a8d39e5b 100644 --- a/src/graph/util/ExpressionUtils.cpp +++ b/src/graph/util/ExpressionUtils.cpp @@ -350,6 +350,7 @@ Expression *ExpressionUtils::rewriteInExpr(const Expression *expr) { return const_cast(expr); } std::vector operands; + operands.reserve(values.size()); for (const auto &v : values) { operands.emplace_back( RelationalExpression::makeEQ(pool, inExpr->left(), ConstantExpression::make(pool, v))); @@ -390,6 +391,32 @@ Expression *ExpressionUtils::rewriteStartsWithExpr(const Expression *expr) { return LogicalExpression::makeAnd(pool, resultLeft, resultRight); } +Expression *ExpressionUtils::foldInnerLogicalExpr(const Expression *originExpr) { + auto matcher = [](const Expression *e) -> bool { + return e->kind() == Expression::Kind::kLogicalAnd || e->kind() == Expression::Kind::kLogicalOr; + }; + auto rewriter = [](const Expression *e) -> Expression * { + auto expr = e->clone(); + auto &operands = static_cast(expr)->operands(); + for (auto iter = operands.begin(); iter != operands.end();) { + if (*iter == nullptr) { + operands.erase(iter); + } else { + iter++; + } + } + auto n = operands.size(); + if (n == 0) { + return nullptr; + } else if (n == 1) { + return operands[0]; + } + return expr; + }; + + return RewriteVisitor::transform(originExpr, std::move(matcher), std::move(rewriter)); +} + Expression *ExpressionUtils::rewriteLogicalAndToLogicalOr(const Expression *expr) { DCHECK(expr->kind() == Expression::Kind::kLogicalAnd); @@ -1431,5 +1458,67 @@ bool ExpressionUtils::checkExprDepth(const Expression *expr) { return true; } +/*static*/ +bool ExpressionUtils::isSingleLenExpandExpr(const std::string &edgeAlias, const Expression *expr) { + if (expr->kind() != Expression::Kind::kAttribute) { + return false; + } + auto attributeExpr = static_cast(expr); + auto *left = attributeExpr->left(); + auto *right = attributeExpr->right(); + + if (left->kind() != Expression::Kind::kSubscript) return false; + if (right->kind() != Expression::Kind::kConstant || + !static_cast(right)->value().isStr()) + return false; + + auto subscriptExpr = static_cast(left); + auto *listExpr = subscriptExpr->left(); + auto *idxExpr = subscriptExpr->right(); + if (listExpr->kind() != Expression::Kind::kInputProperty && + listExpr->kind() != Expression::Kind::kVarProperty) { + return false; + } + if (static_cast(listExpr)->prop() != edgeAlias) { + return false; + } + + // NOTE(jie): Just handled `$-.e[0].likeness` for now, whileas the traverse is single length + // expand. + // TODO(jie): Handle `ALL(i IN e WHERE i.likeness > 78)`, whileas the traverse is var len + // expand. + if (idxExpr->kind() != Expression::Kind::kConstant || + static_cast(idxExpr)->value() != 0) { + return false; + } + return true; +} + +// Transform expression `$-.e[0].likeness` to EdgePropertyExpression `like.likeness` +// for more friendly to push down +// \param pool object pool to hold ownership of objects alloacted +// \param edgeAlias the name of edge. e.g. e in pattern -[e]-> +// \param expr the filter expression +/*static*/ Expression *ExpressionUtils::rewriteEdgePropertyFilter(ObjectPool *pool, + const std::string &edgeAlias, + Expression *expr) { + graph::RewriteVisitor::Matcher matcher = [&edgeAlias](const Expression *e) -> bool { + return isSingleLenExpandExpr(edgeAlias, e); + }; + graph::RewriteVisitor::Rewriter rewriter = [pool](const Expression *e) -> Expression * { + DCHECK_EQ(e->kind(), Expression::Kind::kAttribute); + auto attributeExpr = static_cast(e); + auto *right = attributeExpr->right(); + DCHECK_EQ(right->kind(), Expression::Kind::kConstant); + + auto &prop = static_cast(right)->value().getStr(); + + auto *edgePropExpr = EdgePropertyExpression::make(pool, "*", prop); + return edgePropExpr; + }; + + return graph::RewriteVisitor::transform(expr, matcher, rewriter); +} + } // namespace graph } // namespace nebula diff --git a/src/graph/util/ExpressionUtils.h b/src/graph/util/ExpressionUtils.h index 75af5021080..6df24c2f823 100644 --- a/src/graph/util/ExpressionUtils.h +++ b/src/graph/util/ExpressionUtils.h @@ -104,6 +104,8 @@ class ExpressionUtils { // (A or B) and (C or D) => (A and C) or (A and D) or (B and C) or (B or D) static Expression* rewriteLogicalAndToLogicalOr(const Expression* expr); + static Expression* foldInnerLogicalExpr(const Expression* expr); + // Returns the operands of container expressions // For list and set, return the operands // For map, return the keys @@ -228,6 +230,13 @@ class ExpressionUtils { // Whether the whole expression is vertex id predication // e.g. id(v) == 1, id(v) IN [...] static bool isVidPredication(const Expression* expr); + + // Check if the expr looks like `$-.e[0].likeness` + static bool isSingleLenExpandExpr(const std::string& edgeAlias, const Expression* expr); + + static Expression* rewriteEdgePropertyFilter(ObjectPool* pool, + const std::string& edgeAlias, + Expression* expr); }; } // namespace graph diff --git a/src/graph/validator/MatchValidator.cpp b/src/graph/validator/MatchValidator.cpp index aca9729dd8b..eb5d998a374 100644 --- a/src/graph/validator/MatchValidator.cpp +++ b/src/graph/validator/MatchValidator.cpp @@ -5,8 +5,13 @@ #include "graph/validator/MatchValidator.h" +#include "common/expression/FunctionCallExpression.h" +#include "common/expression/LogicalExpression.h" +#include "common/expression/MatchPathPatternExpression.h" +#include "common/expression/UnaryExpression.h" #include "graph/planner/match/MatchSolver.h" #include "graph/util/ExpressionUtils.h" +#include "graph/visitor/DeduceAliasTypeVisitor.h" #include "graph/visitor/ExtractGroupSuiteVisitor.h" #include "graph/visitor/RewriteVisitor.h" #include "graph/visitor/ValidatePatternExpressionVisitor.h" @@ -92,9 +97,13 @@ Status MatchValidator::validateImpl() { NG_RETURN_IF_ERROR(validateFilter(withClause->where()->filter(), *whereClauseCtx)); withClauseCtx->where = std::move(whereClauseCtx); } - // A with pass all named aliases to the next query part. - aliasesAvailable = withClauseCtx->aliasesGenerated; + if (withClause->returnItems()->allNamedAliases()) { + aliasesAvailable.insert(withClauseCtx->aliasesGenerated.begin(), + withClauseCtx->aliasesGenerated.end()); + } else { + aliasesAvailable = withClauseCtx->aliasesGenerated; + } cypherCtx_->queryParts.back().boundary = std::move(withClauseCtx); cypherCtx_->queryParts.emplace_back(); cypherCtx_->queryParts.back().aliasesAvailable = aliasesAvailable; @@ -133,6 +142,9 @@ Status MatchValidator::validatePath(const MatchPath *path, Path &pathInfo) { NG_RETURN_IF_ERROR(buildNodeInfo(path, pathInfo.nodeInfos, dummy)); NG_RETURN_IF_ERROR(buildEdgeInfo(path, pathInfo.edgeInfos, dummy)); NG_RETURN_IF_ERROR(buildPathExpr(path, pathInfo, dummy)); + pathInfo.isPred = path->isPredicate(); + pathInfo.isAntiPred = path->isAntiPredicate(); + return Status::OK(); } @@ -342,8 +354,8 @@ Status MatchValidator::validateFilter(const Expression *filter, } NG_RETURN_IF_ERROR(validateAliases({whereClauseCtx.filter}, whereClauseCtx.aliasesAvailable)); - NG_RETURN_IF_ERROR(validateMatchPathExpr( - whereClauseCtx.filter, whereClauseCtx.aliasesAvailable, whereClauseCtx.paths)); + NG_RETURN_IF_ERROR( + validatePathInWhere(whereClauseCtx, whereClauseCtx.aliasesAvailable, whereClauseCtx.paths)); return Status::OK(); } @@ -546,13 +558,19 @@ Status MatchValidator::validateWith(const WithClause *with, exprs.reserve(withClauseCtx.yield->yieldColumns->size()); for (auto *col : withClauseCtx.yield->yieldColumns->columns()) { auto labelExprs = ExpressionUtils::collectAll(col->expr(), {Expression::Kind::kLabel}); - auto aliasType = AliasType::kDefault; + auto aliasType = AliasType::kRuntime; for (auto *labelExpr : labelExprs) { auto label = static_cast(labelExpr)->name(); if (!withClauseCtx.yield->aliasesAvailable.count(label)) { return Status::SemanticError("Alias `%s` not defined", label.c_str()); } - aliasType = withClauseCtx.yield->aliasesAvailable.at(label); + AliasType inputType = withClauseCtx.yield->aliasesAvailable.at(label); + DeduceAliasTypeVisitor visitor(qctx_, vctx_, space_.id, inputType); + const_cast(col->expr())->accept(&visitor); + if (!visitor.ok()) { + return std::move(visitor).status(); + } + aliasType = visitor.outputType(); } if (col->alias().empty()) { if (col->expr()->kind() == Expression::Kind::kLabel) { @@ -632,7 +650,7 @@ Status MatchValidator::validateUnwind(const UnwindClause *unwindClause, // set z to the same type // else // set z to default - AliasType aliasType = AliasType::kDefault; + AliasType aliasType = AliasType::kRuntime; if (types.size() > 0 && std::adjacent_find(types.begin(), types.end(), std::not_equal_to<>()) == types.end()) { aliasType = types[0]; @@ -922,7 +940,7 @@ Status MatchValidator::checkAlias( const Expression *refExpr, const std::unordered_map &aliasesAvailable) const { auto kind = refExpr->kind(); - AliasType aliasType = AliasType::kDefault; + AliasType aliasType = AliasType::kRuntime; switch (kind) { case Expression::Kind::kLabel: { @@ -1064,7 +1082,118 @@ Status MatchValidator::validateMatchPathExpr( auto *matchPathExprImpl = const_cast( static_cast(matchPathExpr)); // Check variables - NG_RETURN_IF_ERROR(checkMatchPathExpr(matchPathExprImpl, availableAliases)); + NG_RETURN_IF_ERROR(checkMatchPathExpr(matchPathExprImpl->matchPath(), availableAliases)); + // Build path alias + auto &matchPath = matchPathExprImpl->matchPath(); + auto pathAlias = matchPath.toString(); + matchPath.setAlias(new std::string(pathAlias)); + if (matchPathExprImpl->genList() == nullptr) { + // Don't done in expression visitor + Expression *genList = InputPropertyExpression::make(pool, pathAlias); + matchPathExprImpl->setGenList(genList); + } + paths.emplace_back(); + NG_RETURN_IF_ERROR(validatePath(&matchPath, paths.back())); + NG_RETURN_IF_ERROR(buildRollUpPathInfo(&matchPath, paths.back())); + } + return Status::OK(); +} + +bool extractSinglePathPredicate(Expression *expr, std::vector &pathPreds) { + if (expr->kind() == Expression::Kind::kMatchPathPattern) { + auto pred = static_cast(expr)->matchPath().clone(); + pred.setPredicate(); + pathPreds.emplace_back(std::move(pred)); + // Absorb expression into path predicate + return true; + } else if (expr->kind() == Expression::Kind::kUnaryNot) { + auto *operand = static_cast(expr)->operand(); + if (operand->kind() == Expression::Kind::kMatchPathPattern) { + auto pred = static_cast(operand)->matchPath().clone(); + pred.setAntiPredicate(); + pathPreds.emplace_back(std::move(pred)); + // Absorb expression into path predicate + return true; + } else if (operand->kind() == Expression::Kind::kFunctionCall) { + auto funcExpr = static_cast(operand); + if (funcExpr->isFunc("exists")) { + auto args = funcExpr->args()->args(); + DCHECK_EQ(args.size(), 1); + if (args[0]->kind() == Expression::Kind::kMatchPathPattern) { + auto pred = static_cast(args[0])->matchPath().clone(); + pred.setAntiPredicate(); + pathPreds.emplace_back(std::move(pred)); + // Absorb expression into path predicate + return true; + } + } + } + } + // Take no effects + return false; +} + +bool extractMultiPathPredicate(Expression *expr, std::vector &pathPreds) { + if (expr->kind() == Expression::Kind::kLogicalAnd) { + auto &operands = static_cast(expr)->operands(); + for (auto iter = operands.begin(); iter != operands.end();) { + if (extractSinglePathPredicate(*iter, pathPreds)) { + // Should remove this operand bcz it was already absorbed into pathPreds + operands.erase(iter); + } else { + iter++; + } + } + // Alread remove inner predicate operands + return false; + } else { + return extractSinglePathPredicate(expr, pathPreds); + } +} + +Status MatchValidator::validatePathInWhere( + WhereClauseContext &wctx, + const std::unordered_map &availableAliases, + std::vector &paths) { + auto expr = ExpressionUtils::flattenInnerLogicalExpr(wctx.filter); + auto *pool = qctx_->objPool(); + ValidatePatternExpressionVisitor visitor(pool, vctx_); + expr->accept(&visitor); + std::vector pathPreds; + // FIXME(czp): Delete this function and add new expression visitor to cover all general cases + if (extractMultiPathPredicate(expr, pathPreds)) { + wctx.filter = nullptr; + } else { + // Flatten and fold the inner logical expressions that already have operands that can be + // compacted + wctx.filter = + ExpressionUtils::foldInnerLogicalExpr(ExpressionUtils::flattenInnerLogicalExpr(expr)); + } + for (auto &pred : pathPreds) { + NG_RETURN_IF_ERROR(checkMatchPathExpr(pred, availableAliases)); + // Build path alias + auto pathAlias = pred.toString(); + pred.setAlias(new std::string(pathAlias)); + paths.emplace_back(); + NG_RETURN_IF_ERROR(validatePath(&pred, paths.back())); + NG_RETURN_IF_ERROR(buildRollUpPathInfo(&pred, paths.back())); + } + + // All inside pattern expressions are path predicate + if (wctx.filter == nullptr) { + return Status::OK(); + } + + ValidatePatternExpressionVisitor pathExprVisitor(pool, vctx_); + wctx.filter->accept(&pathExprVisitor); + auto matchPathExprs = + ExpressionUtils::collectAll(wctx.filter, {Expression::Kind::kMatchPathPattern}); + for (auto &matchPathExpr : matchPathExprs) { + DCHECK_EQ(matchPathExpr->kind(), Expression::Kind::kMatchPathPattern); + auto *matchPathExprImpl = const_cast( + static_cast(matchPathExpr)); + // Check variables + NG_RETURN_IF_ERROR(checkMatchPathExpr(matchPathExprImpl->matchPath(), availableAliases)); // Build path alias auto &matchPath = matchPathExprImpl->matchPath(); auto pathAlias = matchPath.toString(); @@ -1078,13 +1207,13 @@ Status MatchValidator::validateMatchPathExpr( NG_RETURN_IF_ERROR(validatePath(&matchPath, paths.back())); NG_RETURN_IF_ERROR(buildRollUpPathInfo(&matchPath, paths.back())); } + return Status::OK(); } /*static*/ Status MatchValidator::checkMatchPathExpr( - const MatchPathPatternExpression *expr, + const MatchPath &matchPath, const std::unordered_map &availableAliases) { - const auto &matchPath = expr->matchPath(); if (matchPath.alias() != nullptr) { const auto find = availableAliases.find(*matchPath.alias()); if (find == availableAliases.end()) { @@ -1093,7 +1222,9 @@ Status MatchValidator::validateMatchPathExpr( matchPath.alias()->c_str()); } if (find->second != AliasType::kPath) { - return Status::SemanticError("Alias `%s' should be Path.", matchPath.alias()->c_str()); + return Status::SemanticError("Alias `%s' should be Path, but got type '%s", + matchPath.alias()->c_str(), + AliasTypeName::get(find->second).c_str()); } } for (const auto &node : matchPath.nodes()) { @@ -1109,7 +1240,9 @@ Status MatchValidator::validateMatchPathExpr( node->alias().c_str()); } if (find->second != AliasType::kNode) { - return Status::SemanticError("Alias `%s' should be Node.", node->alias().c_str()); + return Status::SemanticError("Alias `%s' should be Node, but got type '%s", + node->alias().c_str(), + AliasTypeName::get(find->second).c_str()); } } } @@ -1122,7 +1255,9 @@ Status MatchValidator::validateMatchPathExpr( edge->alias().c_str()); } if (find->second != AliasType::kEdge) { - return Status::SemanticError("Alias `%s' should be Edge.", edge->alias().c_str()); + return Status::SemanticError("Alias `%s' should be Edge, but got type '%s'", + edge->alias().c_str(), + AliasTypeName::get(find->second).c_str()); } } } diff --git a/src/graph/validator/MatchValidator.h b/src/graph/validator/MatchValidator.h index b33feede7dc..d9ca0cba82e 100644 --- a/src/graph/validator/MatchValidator.h +++ b/src/graph/validator/MatchValidator.h @@ -98,8 +98,13 @@ class MatchValidator final : public Validator { const std::unordered_map &availableAliases, std::vector &paths); + // Check and extract path in where clause + Status validatePathInWhere(WhereClauseContext &wctx, + const std::unordered_map &availableAliases, + std::vector &paths); + static Status checkMatchPathExpr( - const MatchPathPatternExpression *expr, + const MatchPath &matchPath, const std::unordered_map &availableAliases); static Status buildRollUpPathInfo(const MatchPath *path, Path &pathInfo); diff --git a/src/graph/visitor/CMakeLists.txt b/src/graph/visitor/CMakeLists.txt index cdd75ac16fc..01ccba3cd1a 100644 --- a/src/graph/visitor/CMakeLists.txt +++ b/src/graph/visitor/CMakeLists.txt @@ -18,6 +18,7 @@ nebula_add_library( EvaluableExprVisitor.cpp ExtractGroupSuiteVisitor.cpp ValidatePatternExpressionVisitor.cpp + DeduceAliasTypeVisitor.cpp ) nebula_add_library( diff --git a/src/graph/visitor/DeduceAliasTypeVisitor.cpp b/src/graph/visitor/DeduceAliasTypeVisitor.cpp new file mode 100644 index 00000000000..571eabf3e71 --- /dev/null +++ b/src/graph/visitor/DeduceAliasTypeVisitor.cpp @@ -0,0 +1,102 @@ +/* Copyright (c) 2022 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#include "graph/visitor/DeduceAliasTypeVisitor.h" + +#include +#include + +#include "common/datatypes/DataSet.h" +#include "common/datatypes/Edge.h" +#include "common/datatypes/List.h" +#include "common/datatypes/Map.h" +#include "common/datatypes/Path.h" +#include "common/datatypes/Set.h" +#include "common/function/FunctionManager.h" +#include "graph/context/QueryContext.h" +#include "graph/context/QueryExpressionContext.h" +#include "graph/context/ValidateContext.h" +#include "graph/visitor/EvaluableExprVisitor.h" + +namespace nebula { +namespace graph { + +DeduceAliasTypeVisitor::DeduceAliasTypeVisitor(QueryContext *qctx, + ValidateContext *vctx, + GraphSpaceID space, + AliasType inputType) + : qctx_(qctx), vctx_(vctx), space_(space), inputType_(inputType) {} + +void DeduceAliasTypeVisitor::visit(VertexExpression *expr) { + UNUSED(expr); + outputType_ = AliasType::kNode; +} + +void DeduceAliasTypeVisitor::visit(EdgeExpression *expr) { + UNUSED(expr); + outputType_ = AliasType::kEdge; +} + +void DeduceAliasTypeVisitor::visit(PathBuildExpression *expr) { + UNUSED(expr); + outputType_ = AliasType::kPath; +} + +void DeduceAliasTypeVisitor::visit(FunctionCallExpression *expr) { + std::string funName = expr->name(); + std::transform(funName.begin(), funName.end(), funName.begin(), ::tolower); + if (funName == "nodes") { + outputType_ = AliasType::kNodeList; + } else if (funName == "relationships") { + outputType_ = AliasType::kEdgeList; + } else if (funName == "reversepath") { + outputType_ = AliasType::kPath; + } else if (funName == "startnode" || funName == "endnode") { + outputType_ = AliasType::kNode; + } +} + +void DeduceAliasTypeVisitor::visit(SubscriptExpression *expr) { + Expression *leftExpr = expr->left(); + DeduceAliasTypeVisitor childVisitor(qctx_, vctx_, space_, inputType_); + leftExpr->accept(&childVisitor); + if (!childVisitor.ok()) { + status_ = std::move(childVisitor).status(); + return; + } + inputType_ = childVisitor.outputType(); + // This is not accurate, since there exist List of List...Edges/Nodes, + // may have opportunities when analyze more detail of the expr. + if (inputType_ == AliasType::kEdgeList) { + outputType_ = AliasType::kEdge; + } else if (inputType_ == AliasType::kNodeList) { + outputType_ = AliasType::kNode; + } else { + outputType_ = inputType_; + } +} + +void DeduceAliasTypeVisitor::visit(SubscriptRangeExpression *expr) { + Expression *leftExpr = expr->list(); + DeduceAliasTypeVisitor childVisitor(qctx_, vctx_, space_, inputType_); + leftExpr->accept(&childVisitor); + if (!childVisitor.ok()) { + status_ = std::move(childVisitor).status(); + return; + } + inputType_ = childVisitor.outputType(); + // This is not accurate, since there exist List of List...Edges/Nodes, + // may have opportunities when analyze more detail of the expr. + if (inputType_ == AliasType::kEdgeList) { + outputType_ = AliasType::kEdgeList; + } else if (inputType_ == AliasType::kNodeList) { + outputType_ = AliasType::kNodeList; + } else { + outputType_ = inputType_; + } +} + +} // namespace graph +} // namespace nebula diff --git a/src/graph/visitor/DeduceAliasTypeVisitor.h b/src/graph/visitor/DeduceAliasTypeVisitor.h new file mode 100644 index 00000000000..1b132f6e84e --- /dev/null +++ b/src/graph/visitor/DeduceAliasTypeVisitor.h @@ -0,0 +1,100 @@ +/* Copyright (c) 2022 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#ifndef NEBULA_GRAPH_DEDUCEALIASTYPEVISITOR_H +#define NEBULA_GRAPH_DEDUCEALIASTYPEVISITOR_H + +#include "common/base/Status.h" +#include "common/datatypes/Value.h" +#include "common/expression/ExprVisitor.h" +#include "graph/context/ValidateContext.h" +#include "graph/context/ast/CypherAstContext.h" + +namespace nebula { +namespace graph { + +class QueryContext; + +// An expression visitor enable deducing AliasType when possible. +class DeduceAliasTypeVisitor final : public ExprVisitor { + public: + DeduceAliasTypeVisitor(QueryContext *qctx, + ValidateContext *vctx, + GraphSpaceID space, + AliasType inputType); + + ~DeduceAliasTypeVisitor() = default; + + bool ok() const { + return status_.ok(); + } + + Status status() && { + return std::move(status_); + } + + AliasType outputType() const { + return outputType_; + } + + private: + // Most expression cannot be used to deducing, + // or deduced to primitive types, use the default kRuntime type + void visit(ConstantExpression *) override {} + void visit(UnaryExpression *) override {} + void visit(TypeCastingExpression *) override {} + void visit(LabelExpression *) override {} + void visit(LabelAttributeExpression *) override {} + void visit(ArithmeticExpression *) override {} + void visit(RelationalExpression *) override {} + void visit(AttributeExpression *) override {} + void visit(LogicalExpression *) override {} + void visit(AggregateExpression *) override {} + void visit(UUIDExpression *) override {} + void visit(VariableExpression *) override {} + void visit(VersionedVariableExpression *) override {} + void visit(ListExpression *) override {} + void visit(SetExpression *) override {} + void visit(MapExpression *) override {} + void visit(LabelTagPropertyExpression *) override {} + void visit(TagPropertyExpression *) override {} + void visit(EdgePropertyExpression *) override {} + void visit(InputPropertyExpression *) override {} + void visit(VariablePropertyExpression *) override {} + void visit(DestPropertyExpression *) override {} + void visit(SourcePropertyExpression *) override {} + void visit(EdgeSrcIdExpression *) override {} + void visit(EdgeTypeExpression *) override {} + void visit(EdgeRankExpression *) override {} + void visit(EdgeDstIdExpression *) override {} + void visit(CaseExpression *) override {} + void visit(ColumnExpression *) override {} + void visit(PredicateExpression *) override {} + void visit(ListComprehensionExpression *) override {} + void visit(ReduceExpression *) override {} + void visit(MatchPathPatternExpression *) override {} + + // Expression may have deducing potential + void visit(VertexExpression *expr) override; + void visit(EdgeExpression *expr) override; + void visit(PathBuildExpression *expr) override; + void visit(FunctionCallExpression *expr) override; + void visit(SubscriptExpression *expr) override; + void visit(SubscriptRangeExpression *expr) override; + + private: + QueryContext *qctx_{nullptr}; + ValidateContext *vctx_{nullptr}; + GraphSpaceID space_; + Status status_; + AliasType inputType_{AliasType::kRuntime}; + // Default output type is Runtime + AliasType outputType_{AliasType::kRuntime}; +}; + +} // namespace graph +} // namespace nebula + +#endif // NEBULA_GRAPH_DEDUCEALIASTYPEVISITOR_H diff --git a/src/graph/visitor/PropertyTrackerVisitor.cpp b/src/graph/visitor/PropertyTrackerVisitor.cpp index 293fa77ebe8..2829094f22a 100644 --- a/src/graph/visitor/PropertyTrackerVisitor.cpp +++ b/src/graph/visitor/PropertyTrackerVisitor.cpp @@ -317,8 +317,7 @@ void PropertyTrackerVisitor::visit(AggregateExpression *expr) { std::transform(funName.begin(), funName.end(), funName.begin(), ::tolower); if (funName == "count") { auto kind = expr->arg()->kind(); - if (kind == Expression::Kind::kConstant || kind == Expression::Kind::kInputProperty || - kind == Expression::Kind::kVarProperty) { + if (kind == Expression::Kind::kConstant) { return; } } diff --git a/src/graph/visitor/test/CMakeLists.txt b/src/graph/visitor/test/CMakeLists.txt index e7fa08ce8d9..d3d3acd68f0 100644 --- a/src/graph/visitor/test/CMakeLists.txt +++ b/src/graph/visitor/test/CMakeLists.txt @@ -20,6 +20,7 @@ nebula_add_test( TestMain.cpp FoldConstantExprVisitorTest.cpp DeduceTypeVisitorTest.cpp + DeduceAliasTypeVisitorTest.cpp RewriteUnaryNotExprVisitorTest.cpp RewriteRelExprVisitorTest.cpp FilterTransformTest.cpp diff --git a/src/graph/visitor/test/DeduceAliasTypeVisitorTest.cpp b/src/graph/visitor/test/DeduceAliasTypeVisitorTest.cpp new file mode 100644 index 00000000000..eeee87f39d5 --- /dev/null +++ b/src/graph/visitor/test/DeduceAliasTypeVisitorTest.cpp @@ -0,0 +1,132 @@ +/* Copyright (c) 2022 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#include + +#include "graph/visitor/DeduceAliasTypeVisitor.h" +#include "graph/visitor/test/VisitorTestBase.h" + +namespace nebula { +namespace graph { +class DeduceAliasTypeVisitorTest : public VisitorTestBase {}; + +TEST_F(DeduceAliasTypeVisitorTest, SubscriptExpr) { + { + auto* expr = VertexExpression::make(pool); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kNode); + } + { + auto* expr = EdgeExpression::make(pool); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kEdge); + } + { + auto* expr = PathBuildExpression::make(pool); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kPath); + } + // FunctionCallExpression + { + auto* expr = FunctionCallExpression::make(pool, "nodes"); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kNodeList); + } + { + auto* expr = FunctionCallExpression::make(pool, "relationships"); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kEdgeList); + } + { + auto* expr = FunctionCallExpression::make(pool, "reversepath"); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kPath); + } + { + auto* expr = FunctionCallExpression::make(pool, "startnode"); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kNode); + } + { + auto* expr = FunctionCallExpression::make(pool, "endnode"); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kNode); + } + + // SubscriptExpression + { + auto* items = ExpressionList::make(pool); + auto expr = SubscriptExpression::make( + pool, ListExpression::make(pool, items), ConstantExpression::make(pool, 1)); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kRuntime); + } + { + auto expr = SubscriptExpression::make(pool, + FunctionCallExpression::make(pool, "relationships"), + ConstantExpression::make(pool, 1)); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kEdgeList); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kEdge); + } + { + auto expr = SubscriptExpression::make( + pool, FunctionCallExpression::make(pool, "nodes"), ConstantExpression::make(pool, 1)); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kNodeList); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kNode); + } + + // SubscriptRangeExpression + { + auto* items = ExpressionList::make(pool); + auto expr = SubscriptRangeExpression::make( + pool, ListExpression::make(pool, items), ConstantExpression::make(pool, 1)); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kRuntime); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kRuntime); + } + { + auto expr = SubscriptRangeExpression::make(pool, + FunctionCallExpression::make(pool, "relationships"), + ConstantExpression::make(pool, 1)); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kEdgeList); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kEdgeList); + } + { + auto expr = SubscriptRangeExpression::make( + pool, FunctionCallExpression::make(pool, "nodes"), ConstantExpression::make(pool, 1)); + DeduceAliasTypeVisitor visitor(nullptr, nullptr, 0, AliasType::kNodeList); + expr->accept(&visitor); + EXPECT_TRUE(visitor.ok()); + EXPECT_EQ(visitor.outputType(), AliasType::kNodeList); + } +} + +} // namespace graph +} // namespace nebula diff --git a/src/parser/MatchPath.h b/src/parser/MatchPath.h index c30a74df35c..a0154f6f022 100644 --- a/src/parser/MatchPath.h +++ b/src/parser/MatchPath.h @@ -335,6 +335,23 @@ class MatchPath final { pathType_ = type; } + bool isPredicate() const { + return isPred_; + } + + void setPredicate() { + isPred_ = true; + } + + bool isAntiPredicate() const { + return isPred_ && isAntiPred_; + } + + void setAntiPredicate() { + isPred_ = true; + isAntiPred_ = true; + } + std::string toString() const; MatchPath clone() const { @@ -353,6 +370,9 @@ class MatchPath final { std::vector> nodes_; std::vector> edges_; PathType pathType_{PathType::kDefault}; + + bool isPred_{false}; + bool isAntiPred_{false}; }; } // namespace nebula diff --git a/src/storage/query/QueryBaseProcessor-inl.h b/src/storage/query/QueryBaseProcessor-inl.h index e1291920fce..c459b76d1dd 100644 --- a/src/storage/query/QueryBaseProcessor-inl.h +++ b/src/storage/query/QueryBaseProcessor-inl.h @@ -407,9 +407,11 @@ nebula::cpp2::ErrorCode QueryBaseProcessor::checkExp( if (ret != nebula::cpp2::ErrorCode::SUCCEEDED) { return ret; } - ret = checkExp(predExp->filter(), returned, filtered, updated, allowNoexistentProp); - if (ret != nebula::cpp2::ErrorCode::SUCCEEDED) { - return ret; + if (predExp->hasFilter()) { + ret = checkExp(predExp->filter(), returned, filtered, updated, allowNoexistentProp); + if (ret != nebula::cpp2::ErrorCode::SUCCEEDED) { + return ret; + } } return nebula::cpp2::ErrorCode::SUCCEEDED; } diff --git a/tests/tck/features/bugfix/AliasTypeDeduce.feature b/tests/tck/features/bugfix/AliasTypeDeduce.feature new file mode 100644 index 00000000000..60a1652ff73 --- /dev/null +++ b/tests/tck/features/bugfix/AliasTypeDeduce.feature @@ -0,0 +1,30 @@ +# Copyright (c) 2022 vesoft inc. All rights reserved. +# +# This source code is licensed under Apache 2.0 License. +Feature: Test extract filter + + Background: + Given a graph with space named "nba" + + Scenario: Extract filter bug + When executing query: + """ + match p=allShortestPaths((v1)-[:like*1..3]-(v2)) where id(v1)=="Tim Duncan" and id(v2)=="Tony Parker" with nodes(p) as pathNodes WITH [n IN pathNodes | id(n)] AS personIdsInPath, [idx IN range(1, size(pathNodes)-1) | [prev IN [pathNodes[idx-1]] | [curr IN [pathNodes[idx]] | [prev, curr]]]] AS vertList UNWIND vertList AS c WITH c[0][0][0] AS prev , c[0][0][1] AS curr OPTIONAL MATCH (curr)<-[e:like]-(:player)-[:serve]->(:team)-[:teammate]->(prev) RETURN count(e) AS cnt1, prev, curr + """ + Then the result should be, in any order: + | cnt1 | prev | curr | + | 0 | ("Tim Duncan" :player{age: 42, name: "Tim Duncan"} :bachelor{name: "Tim Duncan", speciality: "psychology"}) | ("Tony Parker" :player{age: 36, name: "Tony Parker"}) | + When executing query: + """ + match p=(a:player)-[e:like*1..3]->(b) where b.player.age>42 with relationships(p)[1] AS e1 match (b)-[:serve]->(c) where c.team.name>"S" and (b)-[e1]->() return count(c) + """ + Then the result should be, in any order: + | count(c) | + | 3225 | + When executing query: + """ + match p=(a:player)-[e:like*1..3]->(b) where b.player.age>42 with relationships(p)[1..2][0] AS e1 match (b)-[:serve]->(c) where c.team.name>"S" and (b)-[e1]->() return count(c) + """ + Then the result should be, in any order: + | count(c) | + | 3225 | diff --git a/tests/tck/features/match/MultiLineMultiQueryParts.feature b/tests/tck/features/match/MultiLineMultiQueryParts.feature index 0049557407e..ec2916b4635 100644 --- a/tests/tck/features/match/MultiLineMultiQueryParts.feature +++ b/tests/tck/features/match/MultiLineMultiQueryParts.feature @@ -294,6 +294,17 @@ Feature: Multi Line Multi Query Parts Then the result should be, in any order: | count | | 4 | + # When the input of argument is NULL + When executing query: + """ + MATCH (v1:player) WHERE id(v1) IN ["Tony Parker", "Tim Duncan"] + OPTIONAL MATCH (v1)-[e:like{likeness:90}]->(v2) MATCH (v2)-[e2:serve]->(v3) + RETURN * + """ + Then the result should be, in any order: + | v1 | e | v2 | e2 | v3 | + | ("Tony Parker" :player{age: 36, name: "Tony Parker"}) | [:like "Tony Parker"->"LaMarcus Aldridge" @0 {}] | ("LaMarcus Aldridge" :player{}) | [:serve "LaMarcus Aldridge"->"Spurs" @0 {}] | ("Spurs" :team{}) | + | ("Tony Parker" :player{age: 36, name: "Tony Parker"}) | [:like "Tony Parker"->"LaMarcus Aldridge" @0 {}] | ("LaMarcus Aldridge" :player{}) | [:serve "LaMarcus Aldridge"->"Trail Blazers" @0 {}] | ("Trail Blazers" :team{}) | Scenario: Multi Line Multi Query Parts When executing query: diff --git a/tests/tck/features/match/MultiQueryParts.feature b/tests/tck/features/match/MultiQueryParts.feature index 31599bcf9f1..4cb98264889 100644 --- a/tests/tck/features/match/MultiQueryParts.feature +++ b/tests/tck/features/match/MultiQueryParts.feature @@ -147,14 +147,14 @@ Feature: Multi Query Parts MATCH (v3:player)-[:like]->(v1)<-[e5]-(v4) where id(v3) == "Tim Duncan" return * """ Then the result should be, in any order, with relax comparison: - | v1 | v2 | e3 | v4 | v3 | e5 | - | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Tim Duncan" @0 {likeness: 95}] | ("Tim Duncan") | ("Tim Duncan") | [:teammate "Tim Duncan"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Manu Ginobili") | [:like "Manu Ginobili"->"Tim Duncan" @0 {likeness: 90}] | ("Tim Duncan") | ("Tim Duncan") | [:teammate "Tim Duncan"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tim Duncan" ) | [:like "Tim Duncan"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tim Duncan" ) | [:like "Tim Duncan"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"LaMarcus Aldridge" @0 {likeness: 90}] | ("LaMarcus Aldridge") | ("Tim Duncan") | [:like "LaMarcus Aldridge"->"Tony Parker" @0 {}] | + | v1 | v2 | e3 | v4 | v3 | e5 | + | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Tim Duncan" @0 {likeness: 95}] | ("Tim Duncan") | ("Tim Duncan") | [:teammate "Tim Duncan"->"Tony Parker" @0] | + | ("Tony Parker") | ("Manu Ginobili") | [:like "Manu Ginobili"->"Tim Duncan" @0 {likeness: 90}] | ("Tim Duncan") | ("Tim Duncan") | [:teammate "Tim Duncan"->"Tony Parker" @0] | + | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0] | + | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0] | + | ("Tony Parker") | ("Tim Duncan" ) | [:like "Tim Duncan"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0] | + | ("Tony Parker") | ("Tim Duncan" ) | [:like "Tim Duncan"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0] | + | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"LaMarcus Aldridge" @0 {likeness: 90}] | ("LaMarcus Aldridge") | ("Tim Duncan") | [:like "LaMarcus Aldridge"->"Tony Parker" @0] | # The redudant Project after HashInnerJoin is removed now And the execution plan should be: | id | name | dependencies | profiling data | operator info | @@ -166,8 +166,7 @@ Feature: Multi Query Parts | 2 | Dedup | 1 | | | | 1 | PassThrough | 3 | | | | 3 | Start | | | | - | 14 | Project | 13 | | | - | 13 | AppendVertices | 12 | | | + | 14 | Project | 12 | | | | 12 | Traverse | 21 | | | | 21 | Traverse | 9 | | | | 9 | Dedup | 8 | | | @@ -181,16 +180,16 @@ Feature: Multi Query Parts OPTIONAL MATCH (v3:player)-[:like]->(v1)<-[e5]-(v4) where id(v3) == "Tim Duncan" return * """ Then the result should be, in any order, with relax comparison: - | v1 | v2 | e3 | v4 | v3 | e5 | - | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"LaMarcus Aldridge" @0 {likeness: 90}] | ("LaMarcus Aldridge") | ("Tim Duncan") | [:like "LaMarcus Aldridge"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tim Duncan") | [:like "Tim Duncan"->"Tony Parker" @0 {likeness: 95}] | ("Tony Parker") | __NULL__ | __NULL__ | - | ("Tony Parker") | ("Tim Duncan") | [:like "Tim Duncan"->"Tony Parker" @0 {likeness: 95}] | ("Tony Parker") | __NULL__ | __NULL__ | - | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Tim Duncan" @0 {likeness: 95}] | ("Tim Duncan") | ("Tim Duncan") | [:teammate "Tim Duncan"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"}) | [:like "Manu Ginobili"->"Tim Duncan" @0 {likeness: 90}] | ("Tim Duncan") | ("Tim Duncan") | [:teammate "Tim Duncan"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tim Duncan") | [:like "Tim Duncan"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0 {}] | - | ("Tony Parker") | ("Tim Duncan") | [:like "Tim Duncan"->"Manu Ginobili" @0 {likeness: 95}] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0 {}] | + | v1 | v2 | e3 | v4 | v3 | e5 | + | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"LaMarcus Aldridge" @0] | ("LaMarcus Aldridge") | ("Tim Duncan") | [:like "LaMarcus Aldridge"->"Tony Parker" @0] | + | ("Tony Parker") | ("Tim Duncan") | [:like "Tim Duncan"->"Tony Parker" @0 ] | ("Tony Parker") | __NULL__ | __NULL__ | + | ("Tony Parker") | ("Tim Duncan") | [:like "Tim Duncan"->"Tony Parker" @0] | ("Tony Parker") | __NULL__ | __NULL__ | + | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Tim Duncan" @0 ] | ("Tim Duncan") | ("Tim Duncan") | [:teammate "Tim Duncan"->"Tony Parker" @0] | + | ("Tony Parker") | ("Manu Ginobili") | [:like "Manu Ginobili"->"Tim Duncan" @0 ] | ("Tim Duncan") | ("Tim Duncan") | [:teammate "Tim Duncan"->"Tony Parker" @0] | + | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Manu Ginobili" @0] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0] | + | ("Tony Parker") | ("Tony Parker") | [:like "Tony Parker"->"Manu Ginobili" @0 ] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0] | + | ("Tony Parker") | ("Tim Duncan") | [:like "Tim Duncan"->"Manu Ginobili" @0 ] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0 ] | + | ("Tony Parker") | ("Tim Duncan") | [:like "Tim Duncan"->"Manu Ginobili" @0] | ("Manu Ginobili") | ("Tim Duncan") | [:teammate "Manu Ginobili"->"Tony Parker" @0] | # The redudant Project after HashLeftJoin is removed now And the execution plan should be: | id | name | dependencies | profiling data | operator info | @@ -203,8 +202,7 @@ Feature: Multi Query Parts | 1 | PassThrough | 3 | | | | 3 | Start | | | | | 14 | Project | 13 | | | - | 13 | AppendVertices | 12 | | | - | 12 | Traverse | 21 | | | + | 13 | Traverse | 21 | | | | 21 | Traverse | 9 | | | | 9 | Dedup | 8 | | | | 8 | PassThrough | 10 | | | diff --git a/tests/tck/features/match/PathExpr.feature b/tests/tck/features/match/PathExpr.feature index 41286a192db..d4366d179c6 100644 --- a/tests/tck/features/match/PathExpr.feature +++ b/tests/tck/features/match/PathExpr.feature @@ -50,22 +50,22 @@ Feature: Basic match """ MATCH (v:player) WITH (v)-[v]-() AS p RETURN p """ - Then a SemanticError should be raised at runtime: Alias `v' should be Edge. + Then a SemanticError should be raised at runtime: Alias `v' should be Edge, but got type 'Node' When executing query: """ MATCH (v:player) UNWIND (v)-[v]-() AS p RETURN p """ - Then a SemanticError should be raised at runtime: Alias `v' should be Edge. + Then a SemanticError should be raised at runtime: Alias `v' should be Edge, but got type 'Node' When executing query: """ MATCH (v:player) WHERE (v)-[v]-() RETURN v """ - Then a SemanticError should be raised at runtime: Alias `v' should be Edge. + Then a SemanticError should be raised at runtime: Alias `v' should be Edge, but got type 'Node' When executing query: """ MATCH (v:player) RETURN (v)-[v]-() """ - Then a SemanticError should be raised at runtime: Alias `v' should be Edge. + Then a SemanticError should be raised at runtime: Alias `v' should be Edge, but got type 'Node' Scenario: In Where When executing query: diff --git a/tests/tck/features/match/With.feature b/tests/tck/features/match/With.feature index 9f76a59e9b7..895853de8ba 100644 --- a/tests/tck/features/match/With.feature +++ b/tests/tck/features/match/With.feature @@ -374,3 +374,16 @@ Feature: With clause Then the result should be, in any order: | e.edgeProp_1_0 | | NULL | + + Scenario: with wildcard after unwind + When executing query: + """ + match p = (v0)-[e0]->(v1) where id(v0) in ["Tim Duncan"] unwind v0 as uv0 with * return e0 limit 5; + """ + Then the result should be, in any order: + | e0 | + | [:serve "Tim Duncan"->"Spurs" @0 {end_year: 2016, start_year: 1997}] | + | [:teammate "Tim Duncan"->"LaMarcus Aldridge" @0 {end_year: 2016, start_year: 2015}] | + | [:teammate "Tim Duncan"->"Tony Parker" @0 {end_year: 2016, start_year: 2001}] | + | [:like "Tim Duncan"->"Tony Parker" @0 {likeness: 95}] | + | [:teammate "Tim Duncan"->"Danny Green" @0 {end_year: 2016, start_year: 2010}] | diff --git a/tests/tck/features/optimizer/PrunePropertiesRule.feature b/tests/tck/features/optimizer/PrunePropertiesRule.feature index c278ee8492d..dfcf1123837 100644 --- a/tests/tck/features/optimizer/PrunePropertiesRule.feature +++ b/tests/tck/features/optimizer/PrunePropertiesRule.feature @@ -364,8 +364,7 @@ Feature: Prune Properties rule | 11 | Project | 10 | | | 10 | AppendVertices | 9 | { "props": "[{\"props\":[\"name\", \"age\", \"_tag\"],\"tagId\":9}, {\"props\":[\"name\", \"speciality\", \"_tag\"],\"tagId\":8}, {\"props\":[\"name\", \"_tag\"],\"tagId\":10}]" } | | 9 | Traverse | 8 | { "vertexProps": "[{\"props\":[\"name\"],\"tagId\":9}]" } | - | 8 | Argument | 0 | | - | 0 | Start | | | + | 8 | Argument | | | When profiling query: """ MATCH (m:player{name:"Tim Duncan"})-[:like]-(n)--() @@ -377,19 +376,17 @@ Feature: Prune Properties rule | scount | | 270 | And the execution plan should be: - | id | name | dependencies | operator info | - | 12 | Aggregate | 13 | | - | 13 | HashInnerJoin | 15, 11 | | - | 15 | Project | 4 | | - | 4 | Traverse | 3 | { "vertexProps": "" } | - | 3 | Traverse | 14 | { "vertexProps": "" } | - | 14 | IndexScan | 2 | | - | 2 | Start | | | - | 11 | Project | 10 | | - | 10 | AppendVertices | 9 | { "props": "[{\"props\":[\"_tag\"],\"tagId\":8}, {\"props\":[\"_tag\"],\"tagId\":9}, {\"props\":[\"_tag\"],\"tagId\":10}]" } | - | 9 | Traverse | 8 | { "vertexProps": "" } | - | 8 | Argument | 0 | | - | 0 | Start | | | + | id | name | dependencies | operator info | + | 12 | Aggregate | 13 | | + | 13 | HashInnerJoin | 15, 11 | | + | 15 | Project | 4 | | + | 4 | Traverse | 3 | { "vertexProps": "" } | + | 3 | Traverse | 14 | { "vertexProps": "" } | + | 14 | IndexScan | 2 | | + | 2 | Start | | | + | 11 | Project | 9 | | + | 9 | Traverse | 8 | { "vertexProps": "" } | + | 8 | Argument | | | @distonly Scenario: return function @@ -422,14 +419,14 @@ Feature: Prune Properties rule | count(v2) | | 24 | And the execution plan should be: - | id | name | dependencies | operator info | - | 7 | Aggregate | 6 | | - | 6 | Project | 5 | | - | 5 | AppendVertices | 4 | { "props": "[{\"props\":[\"_tag\"],\"tagId\":8}, {\"props\":[\"_tag\"],\"tagId\":9}, {\"props\":[\"_tag\"],\"tagId\":10}]" } | - | 4 | Traverse | 2 | {"vertexProps": "", "edgeProps": "[{\"type\": 3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | - | 2 | Dedup | 1 | | - | 1 | PassThrough | 3 | | - | 3 | Start | | | + | id | name | dependencies | operator info | + | 7 | Aggregate | 6 | | + | 6 | Project | 5 | | + | 5 | AppendVertices | 4 | { "props": "[{\"tagId\":8,\"props\":[\"name\",\"speciality\",\"_tag\"]},{\"props\":[\"_tag\",\"name\",\"age\"],\"tagId\":9},{\"tagId\":10,\"props\":[\"name\",\"_tag\"]}]" } | + | 4 | Traverse | 2 | {"vertexProps": "", "edgeProps": "[{\"type\": 3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | + | 2 | Dedup | 1 | | + | 1 | PassThrough | 3 | | + | 3 | Start | | | When profiling query: """ MATCH p = (v1)-[e:like*1..5]->(v2) @@ -533,27 +530,27 @@ Feature: Prune Properties rule | "Spurs" | 11 | | "Hornets" | 3 | And the execution plan should be: - | id | name | dependencies | operator info | - | 21 | Aggregate | 20 | | - | 20 | Aggregate | 19 | | - | 19 | HashLeftJoin | 10, 25 | | - | 10 | Aggregate | 23 | | - | 23 | Project | 22 | | - | 22 | Filter | 29 | | - | 29 | AppendVertices | 28 | { "props": "[{\"props\":[\"name\", \"_tag\"],\"tagId\":10}]" } | - | 28 | Traverse | 27 | {"vertexProps": "[{\"props\":[\"age\"],\"tagId\":9}]", "edgeProps": "[{\"type\": 4, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | - | 27 | Traverse | 26 | {"vertexProps": "", "edgeProps": "[{\"type\": -5, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | - | 26 | Traverse | 2 | {"vertexProps": "", "edgeProps": "[{\"type\": -3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}, {\"type\": 3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | - | 2 | Dedup | 1 | | - | 1 | PassThrough | 3 | | - | 3 | Start | | | - | 25 | Project | 24 | | - | 24 | Filter | 16 | | - | 16 | AppendVertices | 15 | { "props": "[{\"props\":[\"name\", \"_tag\"],\"tagId\":10}]" } | - | 15 | Traverse | 14 | {"vertexProps": "[{\"props\":[\"age\"],\"tagId\":9}]", "edgeProps": "[{\"type\": 4, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | - | 14 | Traverse | 13 | {"vertexProps": "", "edgeProps": "[{\"type\": -3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | - | 13 | Traverse | 11 | {"vertexProps": "", "edgeProps": "[{\"type\": -3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}, {\"type\": 3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | - | 11 | Argument | | | + | id | name | dependencies | operator info | + | 21 | Aggregate | 20 | | + | 20 | Aggregate | 19 | | + | 19 | HashLeftJoin | 10, 25 | | + | 10 | Aggregate | 23 | | + | 23 | Project | 22 | | + | 22 | Filter | 29 | | + | 29 | AppendVertices | 28 | { "props": "[{\"props\":[\"name\", \"_tag\"],\"tagId\":10}]" } | + | 28 | Traverse | 27 | {"vertexProps": "[{\"tagId\":8,\"props\":[\"name\",\"speciality\",\"_tag\"]},{\"tagId\":9,\"props\":[\"name\",\"age\",\"_tag\"]},{\"props\":[\"name\",\"_tag\"],\"tagId\":10}]", "edgeProps": "[{\"type\":4,\"props\":[\"_dst\",\"_type\",\"_rank\"]}]"} | + | 27 | Traverse | 26 | {"vertexProps": "", "edgeProps": "[{\"type\": -5, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | + | 26 | Traverse | 2 | {"vertexProps": "", "edgeProps": "[{\"type\": -3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}, {\"type\": 3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | + | 2 | Dedup | 1 | | + | 1 | PassThrough | 3 | | + | 3 | Start | | | + | 25 | Project | 24 | | + | 24 | Filter | 16 | | + | 16 | AppendVertices | 15 | { "props": "[{\"props\":[\"name\", \"_tag\"],\"tagId\":10}]" } | + | 15 | Traverse | 14 | {"vertexProps": "[{\"tagId\":8,\"props\":[\"name\",\"speciality\",\"_tag\"]},{\"tagId\":9,\"props\":[\"name\",\"age\",\"_tag\"]},{\"props\":[\"name\",\"_tag\"],\"tagId\":10}]", "edgeProps": "[{\"type\":4,\"props\":[\"_dst\",\"_type\",\"_rank\"]}]"} | + | 14 | Traverse | 13 | {"vertexProps": "", "edgeProps": "[{\"type\": -3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | + | 13 | Traverse | 11 | {"vertexProps": "", "edgeProps": "[{\"type\": -3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}, {\"type\": 3, \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | + | 11 | Argument | | | @distonly Scenario: test properties: @@ -878,3 +875,69 @@ Feature: Prune Properties rule | v.player.name | t.errortag.name | properties(v) | t | | "Tim Duncan" | __NULL__ | {age: 42, name: "Tim Duncan", speciality: "psychology"} | ("Tony Parker" :player{age: 36, name: "Tony Parker"}) | | "Tim Duncan" | __NULL__ | {age: 42, name: "Tim Duncan", speciality: "psychology"} | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"}) | + + Scenario: no pruning on agg after unwind + Given a graph with space named "nba" + When executing query: + """ + match (v0:player)-[e0]->(v1) where id(v0) == "Tim Duncan" unwind e0.start_year as a return count(a) + """ + Then the result should be, in any order: + | count(a) | + | 5 | + When executing query: + """ + match (v0:player)-[e0]->(v1) where id(v0) == "Tim Duncan" unwind e0.start_year as a return sum(a) + """ + Then the result should be, in any order: + | sum(a) | + | 10025 | + When executing query: + """ + match (v0:player)-[e0]->(v1) where id(v0) == "Tim Duncan" unwind e0.start_year as a return max(a) + """ + Then the result should be, in any order: + | max(a) | + | 2015 | + When executing query: + """ + match (v0:player)-[e0]->(v1) where id(v0) == "Tim Duncan" unwind e0.start_year as a return min(a) + """ + Then the result should be, in any order: + | min(a) | + | 1997 | + When executing query: + """ + match (v0:player)-[e0]->(v1) where id(v0) == "Tim Duncan" unwind e0.start_year as a return avg(a) + """ + Then the result should be, in any order: + | avg(a) | + | 2005.0 | + When executing query: + """ + match (v0:player)-[e0]->(v1) where id(v0) == "Tim Duncan" unwind e0.start_year as a return std(a) + """ + Then the result should be, in any order: + | std(a) | + | 6.542170893518461 | + When executing query: + """ + match (v0:player)-[e0]->(v1) where id(v0) == "Tim Duncan" unwind e0.start_year as a return std(a) + """ + Then the result should be, in any order: + | std(a) | + | 6.542170893518461 | + When executing query: + """ + match (v0:player)-[e0]->(v1) where id(v0) == "Tim Duncan" unwind e0.start_year as a return bit_or(a) + """ + Then the result should be, in any order: + | bit_or(a) | + | 2015 | + When executing query: + """ + match (v0:player)-[e0]->(v1) where id(v0) == "Tim Duncan" unwind e0.start_year as a return bit_and(a) + """ + Then the result should be, in any order: + | bit_and(a) | + | 1984 | diff --git a/tests/tck/features/optimizer/PushEFilterDownRule.feature b/tests/tck/features/optimizer/PushEFilterDownRule.feature index 96637782021..7366594663c 100644 --- a/tests/tck/features/optimizer/PushEFilterDownRule.feature +++ b/tests/tck/features/optimizer/PushEFilterDownRule.feature @@ -25,6 +25,34 @@ Feature: Push EFilter down rule | 8 | Traverse | 7 | {"edge filter": "", "filter": "(like.likeness==95)"} | | 7 | IndexScan | 0 | | | 0 | Start | | | + When profiling query: + """ + MATCH (v:player{name: 'Tim Duncan'})<-[e:like{likeness: 95}]-() return v.player.name AS name + """ + Then the result should be, in any order: + | name | + | "Tim Duncan" | + And the execution plan should be: + | id | name | dependencies | operator info | + | 5 | Project | 8 | | + | 8 | Traverse | 7 | {"edge filter": "", "filter": "(like.likeness==95)"} | + | 7 | IndexScan | 0 | | + | 0 | Start | | | + When profiling query: + """ + MATCH (v:player{name: 'Tim Duncan'})-[e:like|serve{likeness: 95}]-() return v.player.name AS name + """ + Then the result should be, in any order: + | name | + | "Tim Duncan" | + | "Tim Duncan" | + | "Tim Duncan" | + And the execution plan should be: + | id | name | dependencies | operator info | + | 5 | Project | 8 | | + | 8 | Traverse | 7 | {"edge filter": "", "filter": "(_any(like.likeness,serve.likeness)==95)"} | + | 7 | IndexScan | 0 | | + | 0 | Start | | | When profiling query: """ MATCH (v:player{name: 'Tim Duncan'})-[e:like*1..2{likeness: 95}]->() return v.player.name AS name diff --git a/tests/tck/features/optimizer/PushFilterDownHashInnerJoinRule.feature b/tests/tck/features/optimizer/PushFilterDownHashInnerJoinRule.feature index 43c8c956c38..0fcdd16ade2 100644 --- a/tests/tck/features/optimizer/PushFilterDownHashInnerJoinRule.feature +++ b/tests/tck/features/optimizer/PushFilterDownHashInnerJoinRule.feature @@ -32,21 +32,19 @@ Feature: Push Filter down HashInnerJoin rule | [:like "Tony Parker"->"Tim Duncan" @0 {likeness: 95}] | ("Tony Parker" :player{age: 36, name: "Tony Parker"}) | | [:like "Tim Duncan"->"Tony Parker" @0 {likeness: 95}] | ("Tony Parker" :player{age: 36, name: "Tony Parker"}) | And the execution plan should be: - | id | name | dependencies | operator info | - | 30 | Sort | 14 | | - | 14 | Project | 19 | | - | 19 | HashInnerJoin | 6,22 | | - | 6 | Project | 20 | | - | 20 | AppendVertices | 2 | | - | 2 | Dedup | 1 | | - | 1 | PassThrough | 3 | | - | 3 | Start | | | - | 22 | Project | 21 | | - | 21 | Filter | 10 | { "condition": "($-.e[0].likeness>0)" } | - | 10 | AppendVertices | 9 | | - | 9 | Traverse | 7 | | - | 7 | Argument | 8 | | - | 8 | Start | | | + | id | name | dependencies | operator info | + | 30 | Sort | 14 | | + | 14 | Project | 19 | | + | 19 | HashInnerJoin | 6,22 | | + | 6 | Project | 20 | | + | 20 | AppendVertices | 2 | | + | 2 | Dedup | 1 | | + | 1 | PassThrough | 3 | | + | 3 | Start | | | + | 22 | Project | 10 | | + | 10 | AppendVertices | 9 | | + | 9 | Traverse | 7 | {"filter": "(like.likeness>0)"} | + | 7 | Argument | | | When profiling query: """ MATCH (v:player) @@ -112,22 +110,20 @@ Feature: Push Filter down HashInnerJoin rule | [:like "Tony Parker"->"Tim Duncan" @0 {likeness: 95}] | ("Tony Parker" :player{age: 36, name: "Tony Parker"}) | | [:like "Tim Duncan"->"Tony Parker" @0 {likeness: 95}] | ("Tony Parker" :player{age: 36, name: "Tony Parker"}) | And the execution plan should be: - | id | name | dependencies | operator info | - | 30 | Sort | 14 | | - | 14 | Project | 20 | | - | 20 | HashInnerJoin | 23,25 | | - | 23 | Project | 22 | | - | 22 | Filter | 21 | {"condition": "(v.player.age>0)"} | - | 21 | AppendVertices | 2 | | - | 2 | Dedup | 1 | | - | 1 | PassThrough | 3 | | - | 3 | Start | | | - | 25 | Project | 24 | | - | 24 | Filter | 10 | {"condition": "($-.e[0].likeness>0)"} | - | 10 | AppendVertices | 9 | | - | 9 | Traverse | 7 | | - | 7 | Argument | 8 | | - | 8 | Start | | | + | id | name | dependencies | operator info | + | 30 | Sort | 14 | | + | 14 | Project | 20 | | + | 20 | HashInnerJoin | 23,25 | | + | 23 | Project | 22 | | + | 22 | Filter | 21 | {"condition": "(v.player.age>0)"} | + | 21 | AppendVertices | 2 | | + | 2 | Dedup | 1 | | + | 1 | PassThrough | 3 | | + | 3 | Start | | | + | 25 | Project | 10 | | + | 10 | AppendVertices | 9 | | + | 9 | Traverse | 7 | {"filter": "(like.likeness>0)"} | + | 7 | Argument | | | When profiling query: """ MATCH (v:player) diff --git a/tests/tck/features/optimizer/PushFilterDownTraverseRule.feature b/tests/tck/features/optimizer/PushFilterDownTraverseRule.feature new file mode 100644 index 00000000000..81ef5260791 --- /dev/null +++ b/tests/tck/features/optimizer/PushFilterDownTraverseRule.feature @@ -0,0 +1,96 @@ +# Copyright (c) 2022 vesoft inc. All rights reserved. +# +# This source code is licensed under Apache 2.0 License. +Feature: Push Filter down Traverse rule + + Background: + Given a graph with space named "nba" + + Scenario: push filter down Traverse + When profiling query: + """ + MATCH (v:player)-[e:like]->(v2) WHERE e.likeness > 99 RETURN e.likeness, v2.player.age + """ + Then the result should be, in any order: + | e.likeness | v2.player.age | + | 100 | 31 | + | 100 | 43 | + # The filter `e.likeness>99` is first pushed down to the `eFilter_` of `Traverese` by rule `PushFilterDownTraverse`, + # and then pushed down to the `filter_` of `Traverse` by rule `PushEFilterDown`. + And the execution plan should be: + | id | name | dependencies | operator info | + | 6 | Project | 5 | | + | 5 | AppendVertices | 10 | | + | 10 | Traverse | 2 | {"filter": "(like.likeness>99)"} | + | 2 | Dedup | 9 | | + | 9 | IndexScan | 3 | | + | 3 | Start | | | + When profiling query: + """ + MATCH (person:player)-[:like*1..2]-(friend:player)-[served:serve]->(friendTeam:team) + WHERE id(person) == "Tony Parker" AND id(friend) != "Tony Parker" AND served.start_year > 2010 + WITH DISTINCT friend, friendTeam + OPTIONAL MATCH (friend)<-[:like]-(friend2:player)<-[:like]-(friendTeam) + WITH friendTeam, count(friend2) AS numFriends + RETURN + friendTeam.team.name AS teamName, + numFriends + ORDER BY teamName DESC + LIMIT 8 + """ + Then the result should be, in any order, with relax comparison: + | teamName | numFriends | + | "Warriors" | 0 | + | "Trail Blazers" | 0 | + | "Spurs" | 0 | + | "Rockets" | 0 | + | "Raptors" | 0 | + | "Pistons" | 0 | + | "Lakers" | 0 | + | "Kings" | 0 | + # The filter `served.start_year` is first pushed down to the `eFilter_` of `Traverese` by rule `PushFilterDownTraverse`, + # and then pushed down to the `filter_` of `Traverse` by rule `PushEFilterDown`. + And the execution plan should be: + | id | name | dependencies | profiling data | operator info | + | 21 | TopN | 18 | | | + | 18 | Project | 17 | | | + | 17 | Aggregate | 16 | | | + | 16 | HashLeftJoin | 10,15 | | | + | 10 | Dedup | 28 | | | + | 28 | Project | 22 | | | + | 22 | Filter | 26 | | | + | 26 | AppendVertices | 25 | | | + | 25 | Traverse | 24 | | {"filter": "(serve.start_year>2010)"} | + | 24 | Traverse | 2 | | | + | 2 | Dedup | 1 | | | + | 1 | PassThrough | 3 | | | + | 3 | Start | | | | + | 15 | Project | 14 | | | + | 14 | Traverse | 12 | | | + | 12 | Traverse | 11 | | | + | 11 | Argument | | | | + + Scenario: push filter down Traverse with complex filter + When profiling query: + """ + MATCH (v:player)-[e:like]->(v2) WHERE v.player.age != 35 and (e.likeness + 100) != 199 + RETURN e.likeness, v2.player.age as age + ORDER BY age + LIMIT 3 + """ + Then the result should be, in any order: + | e.likeness | age | + | 90 | 20 | + | 80 | 22 | + | 90 | 23 | + # The filter `e.likeness+100!=99` is first pushed down to the `eFilter_` of `Traverese` by rule `PushFilterDownTraverse`, + # and then pushed down to the `filter_` of `Traverse` by rule `PushEFilterDown`. + And the execution plan should be: + | id | name | dependencies | operator info | + | 11 | TopN | 10 | | + | 10 | Project | 9 | | + | 9 | Filter | 4 | {"condition": "(-.v.player.age!=35)" } | + | 4 | AppendVertices | 12 | | + | 12 | Traverse | 8 | {"filter": "((like.likeness+100)!=199)"} | + | 8 | IndexScan | 2 | | + | 2 | Start | | | diff --git a/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature b/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature new file mode 100644 index 00000000000..2d767926e47 --- /dev/null +++ b/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature @@ -0,0 +1,91 @@ +# Copyright (c) 2022 vesoft inc. All rights reserved. +# +# This source code is licensed under Apache 2.0 License. +Feature: Remove AppendVertices Below Join + + Background: + Given a graph with space named "nba" + + Scenario: Remove AppendVertices below left join + When profiling query: + """ + MATCH (person:player)-[:like*1..2]-(friend:player)-[:serve]->(friendTeam:team) + WHERE id(person) == "Tony Parker" AND id(friend) != "Tony Parker" + WITH DISTINCT friend, friendTeam + OPTIONAL MATCH (friend)<-[:like]-(friend2:player)<-[:like]-(friendTeam) + WITH friendTeam, count(friend2) AS numFriends + RETURN + id(friendTeam) AS teamId, + friendTeam.team.name AS teamName, + numFriends + ORDER BY teamName DESC + """ + Then the result should be, in order, with relax comparison: + | teamId | teamName | numFriends | + | "Warriors" | "Warriors" | 0 | + | "Trail Blazers" | "Trail Blazers" | 0 | + | "Thunders" | "Thunders" | 0 | + | "Suns" | "Suns" | 0 | + | "Spurs" | "Spurs" | 0 | + | "Rockets" | "Rockets" | 0 | + | "Raptors" | "Raptors" | 0 | + | "Pistons" | "Pistons" | 0 | + | "Magic" | "Magic" | 0 | + | "Lakers" | "Lakers" | 0 | + | "Kings" | "Kings" | 0 | + | "Jazz" | "Jazz" | 0 | + | "Hornets" | "Hornets" | 0 | + | "Heat" | "Heat" | 0 | + | "Hawks" | "Hawks" | 0 | + | "Grizzlies" | "Grizzlies" | 0 | + | "Clippers" | "Clippers" | 0 | + | "Celtics" | "Celtics" | 0 | + | "Cavaliers" | "Cavaliers" | 0 | + | "Bulls" | "Bulls" | 0 | + | "76ers" | "76ers" | 0 | + And the execution plan should be: + | id | name | dependencies | operator info | + | 21 | Sort | 18 | | + | 18 | Project | 17 | | + | 17 | Aggregate | 16 | | + | 16 | HashLeftJoin | 10,15 | {"hashKeys": ["_joinkey($-.friendTeam)", "_joinkey($-.friend)"], "probeKeys": ["$-.friendTeam", "_joinkey($-.friend)"]} | + | 10 | Dedup | 28 | | + | 28 | Project | 22 | | + | 22 | Filter | 26 | | + | 26 | AppendVertices | 25 | | + | 25 | Traverse | 24 | | + | 24 | Traverse | 2 | | + | 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"]} | + | 14 | Traverse | 12 | | + | 12 | Traverse | 11 | | + | 11 | Argument | | | + + Scenario: Remove AppendVertices below inner join + When profiling query: + """ + MATCH (me:player)-[:like]->(both) + WHERE id(me) == "Tony Parker" + MATCH (he:player)-[:like]->(both) + WHERE id(he) == "Tim Duncan" + RETURN * + """ + Then the result should be, in order, with relax comparison: + | 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 | | |