diff --git a/ci/test.sh b/ci/test.sh
index abb3fe13b14af1eba48346cf0b28da02ccb31cc1..5dcb5285afd4ba661211e41a0b6c432537282fef 100755
--- a/ci/test.sh
+++ b/ci/test.sh
@@ -84,8 +84,6 @@ function run_test() {
         $PROJ_DIR/tests/admin/* \
         $PROJ_DIR/tests/maintain/* \
         $PROJ_DIR/tests/mutate/* \
-        $PROJ_DIR/tests/query/stateless/test_new_go.py \
-        $PROJ_DIR/tests/query/stateless/test_new_groupby.py \
         $PROJ_DIR/tests/query/v1/* \
         $PROJ_DIR/tests/query/v2/* \
         $PROJ_DIR/tests/query/stateless/test_schema.py \
diff --git a/src/context/ExecutionContext.cpp b/src/context/ExecutionContext.cpp
index dd8c05c107d4bd7c9ffd5270e010c55c1e0d35ef..04936ec547de23b4b75fe4880b4273055a2b25b8 100644
--- a/src/context/ExecutionContext.cpp
+++ b/src/context/ExecutionContext.cpp
@@ -10,6 +10,7 @@ namespace nebula {
 namespace graph {
 constexpr int64_t ExecutionContext::kLatestVersion;
 constexpr int64_t ExecutionContext::kOldestVersion;
+constexpr int64_t ExecutionContext::kPreviousOneVersion;
 
 void ExecutionContext::setValue(const std::string& name, Value&& val) {
     ResultBuilder builder;
diff --git a/src/context/ExecutionContext.h b/src/context/ExecutionContext.h
index 25160e4769b56838e703314509949cb9b90707d8..a57d768ba4c8e2c1b464c09d6cfb64b47c824316 100644
--- a/src/context/ExecutionContext.h
+++ b/src/context/ExecutionContext.h
@@ -35,6 +35,7 @@ public:
     // 1 is the oldest, 2 is the second elder, and so on
     static constexpr int64_t kLatestVersion = 0;
     static constexpr int64_t kOldestVersion = 1;
+    static constexpr int64_t kPreviousOneVersion = -1;
 
     ExecutionContext() = default;
 
diff --git a/src/context/Iterator.cpp b/src/context/Iterator.cpp
index 9433b7665846aa00defe1a86c9783a09e79a807e..4be610629c9212b1fa9e20b1b9978e02890f36f3 100644
--- a/src/context/Iterator.cpp
+++ b/src/context/Iterator.cpp
@@ -15,7 +15,7 @@ namespace std {
 bool equal_to<const nebula::graph::LogicalRow*>::operator()(
     const nebula::graph::LogicalRow* lhs,
     const nebula::graph::LogicalRow* rhs) const {
-    DCHECK_EQ(lhs->kind(), rhs->kind());
+    DCHECK_EQ(lhs->kind(), rhs->kind()) << lhs->kind() << " vs. " << rhs->kind();
     switch (lhs->kind()) {
         case nebula::graph::LogicalRow::Kind::kSequential:
         case nebula::graph::LogicalRow::Kind::kJoin: {
diff --git a/src/exec/query/DataCollectExecutor.cpp b/src/exec/query/DataCollectExecutor.cpp
index 2afd2a1dff95bc61f1d3e7ea9ef6a30e8c3ddc02..b5a1e39d2ef8be3920dbc9ee9d38050523536033 100644
--- a/src/exec/query/DataCollectExecutor.cpp
+++ b/src/exec/query/DataCollectExecutor.cpp
@@ -33,6 +33,10 @@ folly::Future<Status> DataCollectExecutor::doCollect() {
             NG_RETURN_IF_ERROR(rowBasedMove(vars));
             break;
         }
+        case DataCollect::CollectKind::kMToN: {
+            NG_RETURN_IF_ERROR(collectMToN(vars, dc->mToN(), dc->distinct()));
+            break;
+        }
         default:
             LOG(FATAL) << "Unknown data collect type: " << static_cast<int64_t>(dc->collectKind());
     }
@@ -106,5 +110,37 @@ Status DataCollectExecutor::rowBasedMove(const std::vector<std::string>& vars) {
     result_.setDataSet(std::move(ds));
     return Status::OK();
 }
+
+Status DataCollectExecutor::collectMToN(const std::vector<std::string>& vars,
+                                        StepClause::MToN* mToN,
+                                        bool distinct) {
+    DataSet ds;
+    ds.colNames = std::move(colNames_);
+    DCHECK(!ds.colNames.empty());
+    std::unordered_set<const LogicalRow*> unique;
+    // itersHolder keep life cycle of iters util this method return.
+    std::vector<std::unique_ptr<Iterator>> itersHolder;
+    for (auto& var : vars) {
+        auto& hist = ectx_->getHistory(var);
+        DCHECK_GE(mToN->mSteps, 1);
+        for (auto i = mToN->mSteps - 1; i < mToN->nSteps; ++i) {
+            auto iter = hist[i].iter();
+            if (iter->isSequentialIter()) {
+                auto* seqIter = static_cast<SequentialIter*>(iter.get());
+                for (; seqIter->valid(); seqIter->next()) {
+                    if (distinct && !unique.emplace(seqIter->row()).second) {
+                            continue;
+                    }
+                    ds.rows.emplace_back(seqIter->moveRow());
+                }
+            } else {
+                return Status::Error("Iterator should be kind of SequentialIter.");
+            }
+            itersHolder.emplace_back(std::move(iter));
+        }
+    }
+    result_.setDataSet(std::move(ds));
+    return Status::OK();
+}
 }  // namespace graph
 }  // namespace nebula
diff --git a/src/exec/query/DataCollectExecutor.h b/src/exec/query/DataCollectExecutor.h
index 89db4c69a49a593f5271a7c31259861624d73d72..91e51849556e3e3778f28bdf2d9936f3851edb0d 100644
--- a/src/exec/query/DataCollectExecutor.h
+++ b/src/exec/query/DataCollectExecutor.h
@@ -25,6 +25,8 @@ private:
 
     Status rowBasedMove(const std::vector<std::string>& vars);
 
+    Status collectMToN(const std::vector<std::string>& vars, StepClause::MToN* mToN, bool distinct);
+
     std::vector<std::string>    colNames_;
     Value                       result_;
 };
diff --git a/src/exec/query/ProjectExecutor.cpp b/src/exec/query/ProjectExecutor.cpp
index 3b2caa1a23e270b494aa7dbb16952fe4232ff937..c000799d928ad19a1d9ae8d5a8fb97e871c62e58 100644
--- a/src/exec/query/ProjectExecutor.cpp
+++ b/src/exec/query/ProjectExecutor.cpp
@@ -33,6 +33,7 @@ folly::Future<Status> ProjectExecutor::execute() {
         }
         ds.rows.emplace_back(std::move(row));
     }
+    VLOG(1) << node()->varName() << ":" << ds;
     return finish(ResultBuilder().value(Value(std::move(ds))).finish());
 }
 
diff --git a/src/parser/Clauses.cpp b/src/parser/Clauses.cpp
index 3de5c9a53a25edcdeb41bc866c1473275b3751e3..d90865cbc9b195635d4026d45320ad2b5feb3e68 100644
--- a/src/parser/Clauses.cpp
+++ b/src/parser/Clauses.cpp
@@ -13,10 +13,13 @@ namespace nebula {
 std::string StepClause::toString() const {
     std::string buf;
     buf.reserve(256);
-    if (isUpto()) {
-        buf += "UPTO ";
+    if (isMToN()) {
+        buf += std::to_string(mToN_->mSteps);
+        buf += " TO ";
+        buf += std::to_string(mToN_->nSteps);
+    } else {
+        buf += std::to_string(steps_);
     }
-    buf += std::to_string(steps_);
     buf += " STEPS";
     return buf;
 }
diff --git a/src/parser/Clauses.h b/src/parser/Clauses.h
index 813662b250cbb6650fc39f890754363a05698ca5..173d7ad31f2bb6355018a7acd58cfffac1c4050d 100644
--- a/src/parser/Clauses.h
+++ b/src/parser/Clauses.h
@@ -14,24 +14,38 @@
 namespace nebula {
 class StepClause final {
 public:
-    explicit StepClause(uint64_t steps = 1, bool isUpto = false) {
+    struct MToN {
+        uint32_t mSteps;
+        uint32_t nSteps;
+    };
+
+    explicit StepClause(uint32_t steps = 1) {
         steps_ = steps;
-        isUpto_ = isUpto;
+    }
+
+    StepClause(uint32_t m, uint32_t n) {
+        mToN_ = std::make_unique<MToN>();
+        mToN_->mSteps = m;
+        mToN_->nSteps = n;
     }
 
     uint32_t steps() const {
         return steps_;
     }
 
-    bool isUpto() const {
-        return isUpto_;
+    MToN* mToN() const {
+        return mToN_.get();
+    }
+
+    bool isMToN() const {
+        return mToN_ != nullptr;
     }
 
     std::string toString() const;
 
 private:
     uint32_t                                    steps_{1};
-    bool                                        isUpto_{false};
+    std::unique_ptr<MToN>                       mToN_;
 };
 
 
diff --git a/src/parser/parser.yy b/src/parser/parser.yy
index d1d2d32b77e0481cfa2c1f251399a9f75171aa38..da6db7c84f2923e711bb63295907584a0068addc 100644
--- a/src/parser/parser.yy
+++ b/src/parser/parser.yy
@@ -728,8 +728,8 @@ step_clause
     | legal_integer KW_STEPS {
         $$ = new StepClause($1);
     }
-    | KW_UPTO legal_integer KW_STEPS {
-        $$ = new StepClause($2, true);
+    | legal_integer KW_TO legal_integer KW_STEPS {
+        $$ = new StepClause($1, $3);
     }
     ;
 
diff --git a/src/parser/test/ParserTest.cpp b/src/parser/test/ParserTest.cpp
index 4d3d7de3c0a3b6f64ca595a928c433b55716da51..4529d6ac1da42a1683653e14a323940c84c9d6fb 100644
--- a/src/parser/test/ParserTest.cpp
+++ b/src/parser/test/ParserTest.cpp
@@ -73,12 +73,6 @@ TEST(Parser, Go) {
         auto result = parser.parse(query);
         ASSERT_TRUE(result.ok()) << result.status();
     }
-    {
-        GQLParser parser;
-        std::string query = "GO UPTO 2 STEPS FROM \"1\" OVER friend";
-        auto result = parser.parse(query);
-        ASSERT_TRUE(result.ok()) << result.status();
-    }
     {
         GQLParser parser;
         std::string query = "GO FROM \"1\" OVER friend";
diff --git a/src/planner/PlanNode.cpp b/src/planner/PlanNode.cpp
index b52cbd9fe8d412eaf7185d586bc0bc624760ece0..449be015bae9bea57f28f1481af24637143e8d8f 100644
--- a/src/planner/PlanNode.cpp
+++ b/src/planner/PlanNode.cpp
@@ -8,6 +8,7 @@
 
 #include "common/interface/gen-cpp2/graph_types.h"
 #include "planner/ExecutionPlan.h"
+#include "util/ToJson.h"
 
 namespace nebula {
 namespace graph {
@@ -138,6 +139,7 @@ std::unique_ptr<cpp2::PlanNodeDescription> PlanNode::explain() const {
     desc->set_id(id_);
     desc->set_name(toString(kind_));
     desc->set_output_var(outputVar_);
+    addDescription("colNames", folly::toJson(util::toJson(colNames_)), desc.get());
     return desc;
 }
 
@@ -155,7 +157,6 @@ std::unique_ptr<cpp2::PlanNodeDescription> SingleDependencyNode::explain() const
 
 std::unique_ptr<cpp2::PlanNodeDescription> SingleInputNode::explain() const {
     auto desc = SingleDependencyNode::explain();
-    DCHECK(!desc->__isset.description);
     addDescription("inputVar", inputVar_, desc.get());
     return desc;
 }
@@ -164,7 +165,6 @@ std::unique_ptr<cpp2::PlanNodeDescription> BiInputNode::explain() const {
     auto desc = PlanNode::explain();
     DCHECK(!desc->__isset.dependencies);
     desc->set_dependencies({left_->id(), right_->id()});
-    DCHECK(!desc->__isset.description);
     addDescription("leftVar", leftVar_, desc.get());
     addDescription("rightVar", rightVar_, desc.get());
     return desc;
diff --git a/src/planner/PlanNode.h b/src/planner/PlanNode.h
index 94119d67efd94d9ec831749c994de30ae811fb87..833a263fc0aaaee5068f821a74e151bcc0afc6a1 100644
--- a/src/planner/PlanNode.h
+++ b/src/planner/PlanNode.h
@@ -152,7 +152,7 @@ std::ostream& operator<<(std::ostream& os, PlanNode::Kind kind);
 // Dependencies will cover the inputs, For example bi input require bi dependencies as least,
 // but single dependencies may don't need any inputs (I.E admin plan node)
 // Single dependecy without input
-// It's useful for addmin plan node
+// It's useful for admin plan node
 class SingleDependencyNode : public PlanNode {
 public:
     const PlanNode* dep() const {
diff --git a/src/planner/Query.cpp b/src/planner/Query.cpp
index 41ee29c374bbe43819ecede6d8d06e72fcfdb6b3..fc6efb03ae3f3ee52404edd52b804605ceccb1bf 100644
--- a/src/planner/Query.cpp
+++ b/src/planner/Query.cpp
@@ -40,7 +40,7 @@ std::unique_ptr<cpp2::PlanNodeDescription> GetNeighbors::explain() const {
     addDescription(
         "statProps", statProps_ ? folly::toJson(util::toJson(*statProps_)) : "", desc.get());
     addDescription("exprs", exprs_ ? folly::toJson(util::toJson(*exprs_)) : "", desc.get());
-    addDescription("random", folly::to<std::string>(random_), desc.get());
+    addDescription("random", util::toJson(random_), desc.get());
     return desc;
 }
 
@@ -100,7 +100,7 @@ std::unique_ptr<cpp2::PlanNodeDescription> Aggregate::explain() const {
     folly::dynamic itemArr = folly::dynamic::array();
     for (const auto &item : groupItems_) {
         folly::dynamic itemObj = folly::dynamic::object();
-        itemObj.insert("distinct", item.distinct);
+        itemObj.insert("distinct", util::toJson(item.distinct));
         itemObj.insert("funcType", static_cast<uint8_t>(item.func));
         itemObj.insert("expr", item.expr ? item.expr->toString() : "");
         itemArr.push_back(itemObj);
diff --git a/src/planner/Query.h b/src/planner/Query.h
index 253fce58d1533b194296b0d2e58a60fea3865cfe..98306dec5b5f8961547e3bf48b030fa121f0769b 100644
--- a/src/planner/Query.h
+++ b/src/planner/Query.h
@@ -707,6 +707,7 @@ public:
     enum class CollectKind : uint8_t {
         kSubgraph,
         kRowBasedMove,
+        kMToN,
     };
 
     static DataCollect* make(ExecutionPlan* plan,
@@ -716,6 +717,14 @@ public:
         return new DataCollect(plan, input, collectKind, std::move(vars));
     }
 
+    void setMToN(StepClause::MToN* mToN) {
+        mToN_ = mToN;
+    }
+
+    void setDistinct(bool distinct) {
+        distinct_ = distinct;
+    }
+
     CollectKind collectKind() const {
         return collectKind_;
     }
@@ -724,6 +733,14 @@ public:
         return vars_;
     }
 
+    StepClause::MToN* mToN() const {
+        return mToN_;
+    }
+
+    bool distinct() const {
+        return distinct_;
+    }
+
     std::unique_ptr<cpp2::PlanNodeDescription> explain() const override;
 
 private:
@@ -739,6 +756,9 @@ private:
 private:
     CollectKind                 collectKind_;
     std::vector<std::string>    vars_;
+    // using for m to n steps
+    StepClause::MToN*           mToN_{nullptr};
+    bool                        distinct_{false};
 };
 
 /**
diff --git a/src/validator/GetSubgraphValidator.cpp b/src/validator/GetSubgraphValidator.cpp
index 0b0498093c721248582253ba577173bbb068d792..3c06731c5f49b478f156b0d27e7a83b42859929a 100644
--- a/src/validator/GetSubgraphValidator.cpp
+++ b/src/validator/GetSubgraphValidator.cpp
@@ -53,8 +53,8 @@ Status GetSubgraphValidator::validateStep(StepClause* step) {
         return Status::Error("Step clause was not declared.");
     }
 
-    if (step->isUpto()) {
-        return Status::Error("Get Subgraph not support upto.");
+    if (step->isMToN()) {
+        return Status::Error("Get Subgraph not support m to n steps.");
     }
     steps_ = step->steps();
     return Status::OK();
diff --git a/src/validator/GoValidator.cpp b/src/validator/GoValidator.cpp
index 24c290481ccc28ac2b11b0c73d3aebb041255228..f376e92721ef2178acb6c9b81ba9af3f12bda177 100644
--- a/src/validator/GoValidator.cpp
+++ b/src/validator/GoValidator.cpp
@@ -46,11 +46,29 @@ Status GoValidator::validateStep(const StepClause* step) {
     if (step == nullptr) {
         return Status::Error("Step clause nullptr.");
     }
-    auto steps = step->steps();
-    if (steps <= 0) {
-        return Status::Error("Only accpet positive number steps.");
+    if (step->isMToN()) {
+        auto* mToN = qctx_->objPool()->makeAndAdd<StepClause::MToN>();
+        mToN->mSteps = step->mToN()->mSteps;
+        mToN->nSteps = step->mToN()->nSteps;
+        if (mToN->mSteps == 0) {
+            mToN->mSteps = 1;
+        }
+        if (mToN->nSteps < mToN->mSteps) {
+            return Status::Error("`%s', upper bound steps should be greater than lower bound.",
+                                 step->toString().c_str());
+        }
+        if (mToN->mSteps == mToN->nSteps) {
+            steps_ = mToN->mSteps;
+            return Status::OK();
+        }
+        mToN_ = mToN;
+    } else {
+        auto steps = step->steps();
+        if (steps == 0) {
+            return Status::Error("Only accpet positive number steps.");
+        }
+        steps_ = steps;
     }
-    steps_ = steps;
     return Status::OK();
 }
 
@@ -238,10 +256,14 @@ Status GoValidator::validateYield(YieldClause* yield) {
 }
 
 Status GoValidator::toPlan() {
-    if (steps_ > 1) {
-        return buildNStepsPlan();
+    if (mToN_ == nullptr) {
+        if (steps_ > 1) {
+            return buildNStepsPlan();
+        } else {
+            return buildOneStepPlan();
+        }
     } else {
-        return buildOneStepPlan();
+        return buildMToNPlan();
     }
 }
 
@@ -259,12 +281,14 @@ Status GoValidator::oneStep(PlanNode* dependencyForGn,
 
     PlanNode* dependencyForProjectResult = gn;
 
+    // Get the src props and edge props if $-.prop, $var.prop, $$.tag.prop were declared.
     PlanNode* projectSrcEdgeProps = nullptr;
     if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty() ||
         !exprProps_.dstTagProps().empty()) {
-        projectSrcEdgeProps = buildProjectSrcEdgePropsForGN(gn);
+        projectSrcEdgeProps = buildProjectSrcEdgePropsForGN(gn->varName(), gn);
     }
 
+    // Join the dst props if $$.tag.prop was declared.
     PlanNode* joinDstProps = nullptr;
     if (!exprProps_.dstTagProps().empty() && projectSrcEdgeProps != nullptr) {
         joinDstProps = buildJoinDstProps(projectSrcEdgeProps);
@@ -273,11 +297,11 @@ Status GoValidator::oneStep(PlanNode* dependencyForGn,
         dependencyForProjectResult = joinDstProps;
     }
 
+    // Join input props if $-.prop declared.
     PlanNode* joinInput = nullptr;
     if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
         joinInput = buildJoinPipeOrVariableInput(
-            projectFromJoin,
-            joinDstProps == nullptr ? projectSrcEdgeProps : joinDstProps);
+            projectFromJoin, joinDstProps == nullptr ? projectSrcEdgeProps : joinDstProps);
     }
     if (joinInput != nullptr) {
         dependencyForProjectResult = joinInput;
@@ -334,6 +358,7 @@ Status GoValidator::buildNStepsPlan() {
 
     Project* projectDstFromGN = projectDstVidsFromGN(gn, startVidsVar);
 
+    // Trace to the start vid if $-.prop was declared.
     Project* projectFromJoin = nullptr;
     if ((!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) &&
         projectLeftVarForJoin != nullptr && projectDstFromGN != nullptr) {
@@ -365,30 +390,152 @@ Status GoValidator::buildNStepsPlan() {
     return Status::OK();
 }
 
-PlanNode* GoValidator::buildProjectSrcEdgePropsForGN(PlanNode* gn) {
-    DCHECK(gn != nullptr);
+Status GoValidator::buildMToNPlan() {
+    auto* plan = qctx_->plan();
+
+    auto* bodyStart = StartNode::make(plan);
+
+    std::string startVidsVar;
+    PlanNode* projectStartVid = nullptr;
+    if (!starts_.empty() && srcRef_ == nullptr) {
+        startVidsVar = buildConstantInput();
+    } else {
+        projectStartVid = buildRuntimeInput();
+        startVidsVar = projectStartVid->varName();
+    }
+
+    Project* projectLeftVarForJoin = nullptr;
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
+        projectLeftVarForJoin = buildLeftVarForTraceJoin(projectStartVid);
+    }
+
+    auto* gn = GetNeighbors::make(plan, bodyStart, space_.id);
+    gn->setSrc(src_);
+    gn->setVertexProps(buildSrcVertexProps());
+    gn->setEdgeProps(buildEdgeProps());
+    gn->setInputVar(startVidsVar);
+    VLOG(1) << gn->varName();
+
+    Project* projectDstFromGN = projectDstVidsFromGN(gn, startVidsVar);
+    PlanNode* dependencyForProjectResult = projectDstFromGN;
+
+    // Trace to the start vid if $-.prop was declared.
+    Project* projectFromJoin = nullptr;
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
+        if ((!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) &&
+            projectLeftVarForJoin != nullptr && projectDstFromGN != nullptr) {
+            projectFromJoin = traceToStartVid(projectLeftVarForJoin, projectDstFromGN);
+        }
+    }
+
+    // Get the src props and edge props if $-.prop, $var.prop, $$.tag.prop were declared.
+    PlanNode* projectSrcEdgeProps = nullptr;
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty() ||
+        !exprProps_.dstTagProps().empty()) {
+        PlanNode* depForProject = projectDstFromGN;
+        if (projectFromJoin != nullptr) {
+            depForProject = projectFromJoin;
+        }
+        projectSrcEdgeProps = buildProjectSrcEdgePropsForGN(gn->varName(), depForProject);
+    }
+
+    // Join the dst props if $$.tag.prop was declared.
+    PlanNode* joinDstProps = nullptr;
+    if (!exprProps_.dstTagProps().empty() && projectSrcEdgeProps != nullptr) {
+        joinDstProps = buildJoinDstProps(projectSrcEdgeProps);
+    }
+    if (joinDstProps != nullptr) {
+        dependencyForProjectResult = joinDstProps;
+    }
+
+    // Join input props if $-.prop declared.
+    PlanNode* joinInput = nullptr;
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
+        joinInput = buildJoinPipeOrVariableInput(
+            projectFromJoin, joinDstProps == nullptr ? projectSrcEdgeProps : joinDstProps);
+    }
+    if (joinInput != nullptr) {
+        dependencyForProjectResult = joinInput;
+    }
+
+    if (filter_ != nullptr) {
+        auto* filterNode = Filter::make(plan, dependencyForProjectResult,
+                    newFilter_ != nullptr ? newFilter_ : filter_);
+        filterNode->setInputVar(
+            dependencyForProjectResult == projectDstFromGN ?
+                gn->varName() : dependencyForProjectResult->varName());
+        filterNode->setColNames(dependencyForProjectResult->colNames());
+        dependencyForProjectResult = filterNode;
+    }
+
+    SingleInputNode* projectResult =
+        Project::make(plan, dependencyForProjectResult,
+        newYieldCols_ != nullptr ? newYieldCols_ : yields_);
+    projectResult->setInputVar(
+            dependencyForProjectResult == projectDstFromGN ?
+                gn->varName() : dependencyForProjectResult->varName());
+    projectResult->setColNames(std::vector<std::string>(colNames_));
+
+    SingleInputNode* dedupNode = nullptr;
+    if (distinct_) {
+        dedupNode = Dedup::make(plan, projectResult);
+        dedupNode->setInputVar(projectResult->varName());
+        dedupNode->setColNames(std::move(colNames_));
+    }
+
+    auto* loop = Loop::make(
+        plan,
+        projectLeftVarForJoin == nullptr ? projectStartVid
+                                         : projectLeftVarForJoin,  // dep
+        dedupNode == nullptr ? projectResult : dedupNode,  // body
+        buildNStepLoopCondition(mToN_->nSteps));
+
+    if (projectStartVid != nullptr) {
+        tail_ = projectStartVid;
+    } else {
+        tail_ = loop;
+    }
+
+    std::vector<std::string> collectVars;
+    if (dedupNode == nullptr) {
+        collectVars = {projectResult->varName()};
+    } else {
+        collectVars = {dedupNode->varName()};
+    }
+    auto* dataCollect =
+        DataCollect::make(plan, loop, DataCollect::CollectKind::kMToN, collectVars);
+    dataCollect->setMToN(mToN_);
+    dataCollect->setDistinct(distinct_);
+    dataCollect->setColNames(projectResult->colNames());
+    root_ = dataCollect;
+    return Status::OK();
+}
+
+PlanNode* GoValidator::buildProjectSrcEdgePropsForGN(std::string gnVar, PlanNode* dependency) {
+    DCHECK(dependency != nullptr);
 
     auto* plan = qctx_->plan();
+
+    // Get _vid for join if $-/$var were declared.
     if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
         auto* srcVidCol = new YieldColumn(
-            new VariablePropertyExpression(new std::string(gn->varName()),
-                                        new std::string(kVid)),
+            new VariablePropertyExpression(new std::string(gnVar), new std::string(kVid)),
             new std::string(kVid));
         srcAndEdgePropCols_->addColumn(srcVidCol);
     }
 
     VLOG(1) << "build dst cols";
+    // Get all _dst to a single column.
     if (!exprProps_.dstTagProps().empty()) {
         joinDstVidColName_ = vctx_->anonColGen()->getCol();
-        auto* dstVidCol = new YieldColumn(
-            new EdgePropertyExpression(new std::string("*"),
-                                        new std::string(kDst)),
-            new std::string(joinDstVidColName_));
+        auto* dstVidCol =
+            new YieldColumn(new EdgePropertyExpression(new std::string("*"), new std::string(kDst)),
+                            new std::string(joinDstVidColName_));
         srcAndEdgePropCols_->addColumn(dstVidCol);
     }
 
-    auto* project = Project::make(plan, gn, srcAndEdgePropCols_);
-    project->setInputVar(gn->varName());
+    auto* project = Project::make(plan, dependency, srcAndEdgePropCols_);
+    project->setInputVar(gnVar);
     project->setColNames(deduceColNames(srcAndEdgePropCols_));
     VLOG(1) << project->varName();
 
@@ -448,11 +595,11 @@ PlanNode* GoValidator::buildJoinDstProps(PlanNode* projectSrcDstProps) {
     return joinDst;
 }
 
-PlanNode* GoValidator::buildJoinPipeOrVariableInput(
-    PlanNode* projectFromJoin, PlanNode* dependencyForJoinInput) {
+PlanNode* GoValidator::buildJoinPipeOrVariableInput(PlanNode* projectFromJoin,
+                                                    PlanNode* dependencyForJoinInput) {
     auto* plan = qctx_->plan();
 
-    if (steps_ > 1) {
+    if (steps_ > 1 || mToN_ != nullptr) {
         DCHECK(projectFromJoin != nullptr);
         auto* joinHashKey = new VariablePropertyExpression(
                 new std::string(dependencyForJoinInput->varName()), new std::string(kVid));
@@ -460,11 +607,15 @@ PlanNode* GoValidator::buildJoinPipeOrVariableInput(
         auto* probeKey = new VariablePropertyExpression(
                 new std::string(projectFromJoin->varName()), new std::string(dstVidColName_));
         plan->saveObject(probeKey);
-        auto* join = DataJoin::make(
-            plan, dependencyForJoinInput,
-            {dependencyForJoinInput->varName(), ExecutionContext::kLatestVersion},
-            {projectFromJoin->varName(), ExecutionContext::kLatestVersion},
-            {joinHashKey}, {probeKey});
+        auto* join =
+            DataJoin::make(plan,
+                           dependencyForJoinInput,
+                           {dependencyForJoinInput->varName(), ExecutionContext::kLatestVersion},
+                           {projectFromJoin->varName(),
+                            mToN_ != nullptr ? ExecutionContext::kPreviousOneVersion
+                                             : ExecutionContext::kLatestVersion},
+                           {joinHashKey},
+                           {probeKey});
         std::vector<std::string> colNames = dependencyForJoinInput->colNames();
         for (auto& col : projectFromJoin->colNames()) {
             colNames.emplace_back(col);
@@ -477,7 +628,7 @@ PlanNode* GoValidator::buildJoinPipeOrVariableInput(
     DCHECK(dependencyForJoinInput != nullptr);
     auto* joinHashKey = new VariablePropertyExpression(
         new std::string(dependencyForJoinInput->varName()),
-        new std::string(steps_ > 1 ? firstBeginningSrcVidColName_ : kVid));
+        new std::string((steps_ > 1 || mToN_ != nullptr) ? firstBeginningSrcVidColName_ : kVid));
     plan->saveObject(joinHashKey);
     auto* joinInput =
         DataJoin::make(plan, dependencyForJoinInput,
@@ -485,7 +636,7 @@ PlanNode* GoValidator::buildJoinPipeOrVariableInput(
                         ExecutionContext::kLatestVersion},
                         {fromType_ == kPipe ? inputVarName_ : userDefinedVarName_,
                         ExecutionContext::kLatestVersion},
-                        {joinHashKey}, {steps_ > 1 ? srcRef_ : src_});
+                        {joinHashKey}, {(steps_ > 1 || mToN_ != nullptr) ? srcRef_ : src_});
     std::vector<std::string> colNames = dependencyForJoinInput->colNames();
     for (auto& col : outputs_) {
         colNames.emplace_back(col.first);
@@ -534,6 +685,7 @@ Project* GoValidator::traceToStartVid(Project* projectLeftVarForJoin,
     columns->addColumn(column);
     auto* projectJoin = Project::make(plan, join, plan->saveObject(columns));
     projectJoin->setInputVar(join->varName());
+    projectJoin->setOutputVar(projectLeftVarForJoin->varName());
     projectJoin->setColNames(deduceColNames(columns));
     VLOG(1) << projectJoin->varName();
 
diff --git a/src/validator/GoValidator.h b/src/validator/GoValidator.h
index feaf8b8a657cef415949f8f22e67b77325daa31f..8317843aaa1880fcf4e6c379a4b9ffb02dd450bf 100644
--- a/src/validator/GoValidator.h
+++ b/src/validator/GoValidator.h
@@ -43,6 +43,8 @@ private:
 
     Status buildNStepsPlan();
 
+    Status buildMToNPlan();
+
     Status oneStep(PlanNode* dependencyForGn, const std::string& inputVarNameForGN,
                    PlanNode* projectFromJoin);
 
@@ -69,10 +71,10 @@ private:
     Project* traceToStartVid(Project* projectLeftVarForJoin,
                              Project* projectDstFromGN);
 
-    PlanNode* buildJoinPipeOrVariableInput(PlanNode* gn,
-                                           PlanNode* projectFromJoin);
+    PlanNode* buildJoinPipeOrVariableInput(PlanNode* projectFromJoin,
+                                           PlanNode* dependencyForJoinInput);
 
-    PlanNode* buildProjectSrcEdgePropsForGN(PlanNode* gn);
+    PlanNode* buildProjectSrcEdgePropsForGN(std::string gnVar, PlanNode* dependency);
 
     PlanNode* buildJoinDstProps(PlanNode* projectSrcDstProps);
 
@@ -84,6 +86,7 @@ private:
 
 private:
     int64_t                                                 steps_;
+    StepClause::MToN*                                       mToN_{nullptr};
     FromType                                                fromType_{kConstantExpr};
     Expression*                                             srcRef_{nullptr};
     Expression*                                             src_{nullptr};
diff --git a/src/validator/test/QueryValidatorTest.cpp b/src/validator/test/QueryValidatorTest.cpp
index c943df8babe97900e3bb7d03edabafcf72c8fe40..de5099d8f10812e63cfe82505265d6dd4311e82b 100644
--- a/src/validator/test/QueryValidatorTest.cpp
+++ b/src/validator/test/QueryValidatorTest.cpp
@@ -787,6 +787,151 @@ TEST_F(QueryValidatorTest, OutputToAPipe) {
     }
 }
 
+TEST_F(QueryValidatorTest, GoMToN) {
+    {
+        std::string query  =
+            "GO 1 TO 2 STEPS FROM '1' OVER like YIELD DISTINCT like._dst";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kDataCollect,
+            PK::kLoop,
+            PK::kStart,
+            PK::kDedup,
+            PK::kProject,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart,
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        std::string query  =
+            "GO 0 TO 2 STEPS FROM '1' OVER like YIELD DISTINCT like._dst";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kDataCollect,
+            PK::kLoop,
+            PK::kStart,
+            PK::kDedup,
+            PK::kProject,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart,
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        std::string query  =
+            "GO 1 TO 2 STEPS FROM '1' OVER like "
+            "YIELD DISTINCT like._dst, like.likeness, $$.person.name";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kDataCollect,
+            PK::kLoop,
+            PK::kStart,
+            PK::kDedup,
+            PK::kProject,
+            PK::kDataJoin,
+            PK::kProject,
+            PK::kGetVertices,
+            PK::kDedup,
+            PK::kProject,
+            PK::kProject,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart,
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        std::string query  =
+            "GO 1 TO 2 STEPS FROM '1' OVER like REVERSELY YIELD DISTINCT like._dst";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kDataCollect,
+            PK::kLoop,
+            PK::kStart,
+            PK::kDedup,
+            PK::kProject,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart,
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        std::string query  =
+            "GO 1 TO 2 STEPS FROM '1' OVER like BIDIRECT YIELD DISTINCT like._dst";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kDataCollect,
+            PK::kLoop,
+            PK::kStart,
+            PK::kDedup,
+            PK::kProject,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart,
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        std::string query  =
+            "GO 1 TO 2 STEPS FROM '1' OVER * YIELD serve._dst, like._dst";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kDataCollect,
+            PK::kLoop,
+            PK::kStart,
+            PK::kProject,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart,
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        std::string query  =
+            "GO 1 TO 2 STEPS FROM '1' OVER * "
+            "YIELD serve._dst, like._dst, serve.start, like.likeness, $$.person.name";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kDataCollect,
+            PK::kLoop,
+            PK::kStart,
+            PK::kProject,
+            PK::kDataJoin,
+            PK::kProject,
+            PK::kGetVertices,
+            PK::kDedup,
+            PK::kProject,
+            PK::kProject,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart,
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        std::string query  =
+            "GO FROM 'Tim Duncan' OVER like YIELD like._src as src, like._dst as dst "
+            "| GO 1 TO 2 STEPS FROM $-.src OVER like YIELD $-.src as src, like._dst as dst";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kDataCollect,
+            PK::kLoop,
+            PK::kProject,
+            PK::kProject,
+            PK::kProject,
+            PK::kDataJoin,
+            PK::kProject,
+            PK::kDataJoin,
+            PK::kGetNeighbors,
+            PK::kProject,
+            PK::kStart,
+            PK::kProject,
+            PK::kDataJoin,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart,
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+}
+
+
 TEST_F(QueryValidatorTest, GoInvalid) {
     {
         // friend not exist.
diff --git a/tests/query/stateless/test_new_go.py b/tests/query/v1/test_new_go.py
similarity index 96%
rename from tests/query/stateless/test_new_go.py
rename to tests/query/v1/test_new_go.py
index fc60b187fc2629db4dcb7658e67ca503c1f91c43..e422fcf120ab056a1cc4ccb3cb9f2b5f626fb50a 100644
--- a/tests/query/stateless/test_new_go.py
+++ b/tests/query/v1/test_new_go.py
@@ -1259,7 +1259,6 @@ class TestGoQuery(NebulaTestSuite):
         self.check_out_of_order_result(resp, expected_data["rows"])
         """
 
-
         stmt = '''GO FROM 'Boris Diaw' OVER serve WHERE $$.team.name CONTAINS \"Haw\"\
             YIELD $^.player.name, serve.start_year, serve.end_year, $$.team.name'''
         resp = self.execute_query(stmt)
@@ -1316,13 +1315,14 @@ class TestGoQuery(NebulaTestSuite):
         self.check_resp_succeeded(resp)
         self.check_empty_result(resp)
 
-    @pytest.mark.skip(reason = 'm to n not implement')
     def test_with_intermediate_data(self):
         # zero to zero
+        """
         stmt = "GO 0 TO 0 STEPS FROM 'Tony Parker' OVER like YIELD DISTINCT like._dst"
         resp = self.execute_query(stmt)
         self.check_resp_succeeded(resp)
         self.check_empty_result(resp)
+        """
 
         # simple
         stmt = "GO 1 TO 2 STEPS FROM 'Tony Parker' OVER like YIELD DISTINCT like._dst"
@@ -1497,7 +1497,7 @@ class TestGoQuery(NebulaTestSuite):
         resp = self.execute_query(stmt)
         self.check_resp_succeeded(resp)
         expected_data = {
-            "column_names" : ["serve._src"],
+            "column_names" : ["serve._dst"],
             "rows" : [
                 ["Tim Duncan"],
                 ["Tony Parker"],
@@ -1525,7 +1525,7 @@ class TestGoQuery(NebulaTestSuite):
         resp = self.execute_query(stmt)
         self.check_resp_succeeded(resp)
         expected_data = {
-            "column_names" : ["serve._src"],
+            "column_names" : ["serve._dst"],
             "rows" : [
                 ["Tim Duncan"],
                 ["Tony Parker"],
@@ -1550,7 +1550,7 @@ class TestGoQuery(NebulaTestSuite):
         self.check_out_of_order_result(resp, expected_data["rows"])
 
         # bidirectionally
-        stmt = "GO 1 TO 2 STEPS FROM 'Spurs' OVER like BIDIRECT YIELD DISTINCT like._dst"
+        stmt = "GO 1 TO 2 STEPS FROM 'Tony Parker' OVER like BIDIRECT YIELD DISTINCT like._dst"
         resp = self.execute_query(stmt)
         self.check_resp_succeeded(resp)
         expected_data = {
@@ -1611,42 +1611,42 @@ class TestGoQuery(NebulaTestSuite):
         self.check_column_names(resp, expected_data["column_names"])
         self.check_out_of_order_result(resp, expected_data["rows"])
 
-        # over
-        stmt = "GO 1 TO 2 STEPS FROM 'Russell Westbrook' OVER * YIELD DISTINCT serve._dst, like._dst"
+        # over *
+        stmt = "GO 1 TO 2 STEPS FROM 'Russell Westbrook' OVER * YIELD serve._dst, like._dst"
         resp = self.execute_query(stmt)
         self.check_resp_succeeded(resp)
         expected_data = {
             "column_names" : ["serve._dst", "like._dst"],
             "rows" : [
-                ["Thunders", 0],
-                [0, "Paul George"],
-                [0, "James Harden"],
-                ["Pacers", 0],
-                ["Thunders", 0],
-                [0, "Russell Westbrook"],
-                ["Thunders", 0],
-                ["Rockets", 0],
-                [0, "Russell Westbrook"]
+                ["Thunders", T_NULL],
+                [T_NULL, "Paul George"],
+                [T_NULL, "James Harden"],
+                ["Pacers", T_NULL],
+                ["Thunders", T_NULL],
+                [T_NULL, "Russell Westbrook"],
+                ["Thunders", T_NULL],
+                ["Rockets", T_NULL],
+                [T_NULL, "Russell Westbrook"]
             ]
         }
         self.check_column_names(resp, expected_data["column_names"])
         self.check_out_of_order_result(resp, expected_data["rows"])
 
-        stmt = "GO 0 TO 2 STEPS FROM 'Russell Westbrook' OVER * YIELD DISTINCT serve._dst, like._dst"
+        stmt = "GO 0 TO 2 STEPS FROM 'Russell Westbrook' OVER * YIELD serve._dst, like._dst"
         resp = self.execute_query(stmt)
         self.check_resp_succeeded(resp)
         expected_data = {
             "column_names" : ["serve._dst", "like._dst"],
             "rows" : [
-                ["Thunders", 0],
-                [0, "Paul George"],
-                [0, "James Harden"],
-                ["Pacers", 0],
-                ["Thunders", 0],
-                [0, "Russell Westbrook"],
-                ["Thunders", 0],
-                ["Rockets", 0],
-                [0, "Russell Westbrook"]
+                ["Thunders", T_NULL],
+                [T_NULL, "Paul George"],
+                [T_NULL, "James Harden"],
+                ["Pacers", T_NULL],
+                ["Thunders", T_NULL],
+                [T_NULL, "Russell Westbrook"],
+                ["Thunders", T_NULL],
+                ["Rockets", T_NULL],
+                [T_NULL, "Russell Westbrook"]
             ]
         }
         self.check_column_names(resp, expected_data["column_names"])
@@ -1660,15 +1660,15 @@ class TestGoQuery(NebulaTestSuite):
         expected_data = {
             "column_names" : ["serve._dst", "like._dst", "serve.start_year", "like.likeness", "$$.player.name"],
             "rows" : [
-                ["Thunders", 0],
-                [0, "Paul George"],
-                [0, "James Harden"],
-                ["Pacers", 0],
-                ["Thunders", 0],
-                [0, "Russell Westbrook"],
-                ["Thunders", 0],
-                ["Rockets", 0],
-                [0, "Russell Westbrook"]
+                ["Thunders", T_NULL, 2008, T_NULL, T_NULL],
+                [T_NULL, "Paul George", T_NULL, 90, "Paul George"],
+                [T_NULL, "James Harden", T_NULL, 90, "James Harden"],
+                ["Pacers", T_NULL, 2010, T_NULL, T_NULL],
+                ["Thunders", T_NULL, 2017, T_NULL, T_NULL],
+                [T_NULL, "Russell Westbrook", T_NULL, 95, "Russell Westbrook"],
+                ["Thunders", T_NULL, 2009, T_NULL, T_NULL],
+                ["Rockets", T_NULL, 2012, T_NULL, T_NULL],
+                [T_NULL, "Russell Westbrook", T_NULL, 80, "Russell Westbrook"]
             ]
         }
         self.check_column_names(resp, expected_data["column_names"])
@@ -1681,15 +1681,15 @@ class TestGoQuery(NebulaTestSuite):
         expected_data = {
             "column_names" : ["serve._dst", "like._dst", "serve.start_year", "like.likeness", "$$.player.name"],
             "rows" : [
-                ["Thunders", 0, 2008, 0, ""],
-                [0, "Paul George", 0, 90, "Paul George"],
-                [0, "James Harden", 0, 90, "James Harden"],
-                ["Pacers", 0, 2010, 0, ""],
-                ["Thunders", 0, 2017, 0, ""],
-                [0, "Russell Westbrook", 0, 95, "Russell Westbrook"],
-                ["Thunders", 0, 2009, 0, ""],
-                ["Rockets", 0, 2012, 0, ""],
-                [0, "Russell Westbrook", 0, 80, "Russell Westbrook"]
+                ["Thunders", T_NULL, 2008, T_NULL, T_NULL],
+                [T_NULL, "Paul George", T_NULL, 90, "Paul George"],
+                [T_NULL, "James Harden", T_NULL, 90, "James Harden"],
+                ["Pacers", T_NULL, 2010, T_NULL, T_NULL],
+                ["Thunders", T_NULL, 2017, T_NULL, T_NULL],
+                [T_NULL, "Russell Westbrook", T_NULL, 95, "Russell Westbrook"],
+                ["Thunders", T_NULL, 2009, T_NULL, T_NULL],
+                ["Rockets", T_NULL, 2012, T_NULL, T_NULL],
+                [T_NULL, "Russell Westbrook", T_NULL, 80, "Russell Westbrook"]
             ]
         }
         self.check_column_names(resp, expected_data["column_names"])
@@ -1702,13 +1702,13 @@ class TestGoQuery(NebulaTestSuite):
         expected_data = {
             "column_names" : ["serve._dst", "like._dst"],
             "rows" : [
-                [0, "Dejounte Murray"],
-                [0, "James Harden"],
-                [0, "Paul George"],
-                [0, "Dejounte Murray"],
-                [0, "Russell Westbrook"],
-                [0, "Luka Doncic"],
-                [0, "Russell Westbrook"]
+                [T_NULL, "Dejounte Murray"],
+                [T_NULL, "James Harden"],
+                [T_NULL, "Paul George"],
+                [T_NULL, "Dejounte Murray"],
+                [T_NULL, "Russell Westbrook"],
+                [T_NULL, "Luka Doncic"],
+                [T_NULL, "Russell Westbrook"]
             ]
         }
         self.check_column_names(resp, expected_data["column_names"])
@@ -1721,13 +1721,13 @@ class TestGoQuery(NebulaTestSuite):
         expected_data = {
             "column_names" : ["serve._dst", "like._dst"],
             "rows" : [
-                [0, "Dejounte Murray"],
-                [0, "James Harden"],
-                [0, "Paul George"],
-                [0, "Dejounte Murray"],
-                [0, "Russell Westbrook"],
-                [0, "Luka Doncic"],
-                [0, "Russell Westbrook"]
+                [T_NULL, "Dejounte Murray"],
+                [T_NULL, "James Harden"],
+                [T_NULL, "Paul George"],
+                [T_NULL, "Dejounte Murray"],
+                [T_NULL, "Russell Westbrook"],
+                [T_NULL, "Luka Doncic"],
+                [T_NULL, "Russell Westbrook"]
             ]
         }
         self.check_column_names(resp, expected_data["column_names"])
diff --git a/tests/query/stateless/test_new_groupby.py b/tests/query/v1/test_new_groupby.py
similarity index 100%
rename from tests/query/stateless/test_new_groupby.py
rename to tests/query/v1/test_new_groupby.py