Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable MULTIUPDATE and LOOKUP | UPDATE #5953

Merged
merged 9 commits into from
Oct 29, 2024

Conversation

JackChuengQAQ
Copy link
Contributor

@JackChuengQAQ JackChuengQAQ commented Sep 25, 2024

What type of PR is this?

  • bug
  • feature
  • enhancement

What problem(s) does this PR solve?

Issue(s) number:

#5934

Description:

  • Added MULTIUPDATE syntax for batch processing.
  • Introduced LOOKUP | UPDATE syntax for seamless query and update operations.

批量更新语句 (MULTIUPDATE)

在原生 Nebula 中,UPDATE 语句的操作限制为一次仅更新一个节点或一条边。为实现 LOOKUP | UPDATE 语句的功能,首先必须扩展 UPDATE 的能力,以支持批量更新操作。因此,新定义了 MULTIUPDATE 语句,以支持批量更新操作。这一新增功能允许同时更新多个节点或边,并为复杂查询和更新需求提供了基本支持。

批量节点更新

更新后 UPDATE VERTEX 语句可以更新点,相较于原生 UPDATE 语句,可以一次更新一个点或者多个点。

  • 语法

批量节点更新的语法如下,与原生 UPDATE 保持一致

UPDATE VERTEX ON <tag_name> <vid> [ , <vid> ... ]
SET <update_prop>
[WHEN <condition>]
[YIELD <output>]
  • 示例
// 查看年龄为 26 的球员
[nba]> lookup on player where player.age==26 yield id(vertex), player.age
+-------------------+------------+
| id(VERTEX)        | player.age |
+-------------------+------------+
| "Carmelo Anthony" | 26         |
| "Cory Joseph"     | 26         |
+-------------------+------------+

// 同时将两位球员的年龄更新 age=age-1
[nba]> update vertex on player "Carmelo Anthony", "Cory Joseph" \
       SET age=age-1 yield name, age
+-------------------+-----+
| name              | age |
+-------------------+-----+
| "Carmelo Anthony" | 25  |
| "Cory Joseph"     | 25  |
+-------------------+-----+

批量边更新

UPDATE EDGE 语句可以更新边,相较于原生 UPDATE 语句,可以一次更新一条边或者多个边。

  • 语法

批量边更新的语法如下,与原生 UPDATE 保持一致

UPDATE EDGE ON <edge_type> 
<src_vid> -> <dst_vid>[@<rank>] [, <src_vid> -> <dst_vid>[@<rank>] ...]
SET <update_prop>
[WHEN <condition>]
[YIELD <output>]
  • 示例
// 查看 likeness 超过 100 的边
[nba]> lookup on like where like.likeness > 100 yield edge as e
+---------------------------------------------------------------+
| e                                                             |
+---------------------------------------------------------------+
| [:like "Tim Duncan"->"Manu Ginobili" @0 {likeness: 104}]      |
| [:like "Shaquille O'Neal"->"JaVale McGee" @0 {likeness: 101}] |
+---------------------------------------------------------------+

// 同时将两条边的 likeness 设置为 100
[nba]> multiupdate edge on like \
       "Tim Duncan" -> "Manu Ginobili", "Shaquille O'Neal"->"JaVale McGee" \  
       SET likeness=100 YIELD likeness
+----------+
| likeness |
+----------+
| 100      |
| 100      |
+----------+

查询 & 更新语句 (LOOKUP | UPDATE)

查询 & 更新点

为了更新满足特定条件的部分数据的需求,设计使用管道符,连接 LOOKUP 和 UPDATE 语句

使用管道符

UPDATE VERTEX 语句可以结合管道符使用,将 LOOKUP 语句的查询结果传递给 UPDATE VERTEX 语句,实现查询 & 更新的同时进行。

  • 示例
[nba]> lookup on player where player.age==25 yield id(vertex), player.age
+-------------------+------------+
| id(VERTEX)        | player.age |
+-------------------+------------+
| "Carmelo Anthony" | 25         |
| "Cory Joseph"     | 25         |
+-------------------+------------+

// 通过管道符对 tag 属性进行更新,令 age=26
[nba]> lookup on player where player.age==25 yield id(vertex) as id \
       | update vertex on player $-.id set age=26 yield age
+-----+
| age |
+-----+
| 26  |
| 26  |
+-----+

查询 & 更新边

类似地,对于更新边,提供了管道符的方式。

使用管道符

UPDATE VERTEX 语句可以结合管道符使用,将 LOOKUP 语句的查询结果传递给 UPDATE VERTEX 语句,实现查询 & 更新的同时进行。

  • 示例
// 查看 likeness=99 的边
[nba]> lookup on like where like.likeness == 99 \
       yield src(edge), dst(edge), like.likeness
+--------------+-----------------+---------------+
| src(EDGE)    | dst(EDGE)       | like.likeness |
+--------------+-----------------+---------------+
| "Tim Duncan" | "Manu Ginobili" | 99            |
| "Tim Duncan" | "Tony Parker"   | 99            |
+--------------+-----------------+---------------+

// 将 likeness=99 的边的 likness 更新 50
[nba]> lookup on like where like.likeness == 99 \
       yield src(edge) as src, dst(edge) as dst \
       | update edge on like $-.src -> $-.dst \
         SET likeness=50 yield likeness
+----------+
| likeness |
+----------+
| 50       |
| 50       |
+----------+

How do you solve it?

技术实现主要分为以下两个部分:

  • UpdateMulti- 算子实现
  • UpdateRef- 算子实现

对于新增算子,根据 Nebula 数据库的架构,Parser->Validator->Planner->Executor,新增算子至少需要在四个模块中增加对应的代码。

UpdateMulti- 算子实现

UpdateMulti- 算子包括 UpdateMultiVertex 算子和 UpdateMultiEdge 算子,分别对应批量节点更新和批量边更新。通过增加 UpdateMulti- 算子,实现了节点属性和边属性的批量更新。

Parser

在定义了新关键字 KW_MULTIUPDATE ("multiupdate") 后,update_multi_vertex_sentence语句和 update_multi_edge_sentence语句定义如下。

  • update_multi_vertex_sentence语句
update_multi_vertex_sentence 
    : KW_MULTIUPDATE KW_VERTEX KW_ON name_label vid_list 
      KW_SET update_list when_clause yield_clause {
        auto sentence = new UpdateMultiVertexSentence($5, $4, $7, $8, $9);
        $$ = sentence;
    }
    ;
  • update_multi_edge_sentence语句
update_multi_edge_sentence
    : KW_MULTIUPDATE KW_EDGE KW_ON name_label edge_keys 
      KW_SET update_list when_clause yield_clause {
        auto sentence = new UpdateMultiEdgeSentence($5, $4, $7, $8, $9);
        $$ = sentence; 
    }
    ;

相比于原生 UPDATE 语句,MULTIUPDATE VERTEX 语句将vid替换为 vid_lists; MULTIUPDATE EDGE 语句将 vid R_ARROW vidvid R_ARROW vid AT rank 等多种表示一条边的方式,替换为可以包含多个边的edge_keys

在 Mutate Sentence 中,新增 UpdateMultiVertexSentence 类和 UpdateMultiEdgeSentence 类,对相应的输入信息进行存储,使用 Kind::kMultiVertex 和 Kind::kMultiEdge 进行标识,并在下一阶段进行使用。

Validator

Validator 类主要由三个函数组成。首先,构造函数接受得到的 Sentence,对 Validator 类进行初始化;其次,调用 Validator 类的 ValidateImpl函数,对语句内容进行校验;最后,调用 toPlan函数,构造查询节点和对应的查询计划。对应地,在 MutateValidator 中新增了 UpdateMultiVertexValidator 类和 UpdateMultiEdgeValidator 类。

相比于原生的 Update 语句,UpdateMulti-算子对应的 Validator 需要对节点(边)列表中的每一个节点(边)进行校验。

  • UpdateMultiVertexValidator::validateImpl() 节点校验部分

移植原生的校验流程,对 vidList 中的每一个 vid 进行校验。

for (auto &vid : sentence->getVertices()->vidList()) {
    auto idRet = SchemaUtil::toVertexID(vid, vidType_);
    if (!idRet.ok()) {
        LOG(ERROR) << idRet.status();
        return std::move(idRet).status();
    }
    vertices_.emplace_back(std::move(idRet).value());
}
  • UpdateMultiEdgeValidator::validateImpl() 边校验部分

在原生的 UpdateEdgeValidator中,srcid,dstid,rank 作为直接的成员变量。由于 UpdateEdgeValidator 需要一次性处理多条边,使用 EdgeKeys() 对边信息进行管理。为了提高代码的可读性,新建了 EdgeId 结构体,用于存储每条边的srcid,dstid,rank信息。

for (auto &edgekey : sentence->getEdgeKeys()->keys()) {
    auto srcIdRet = SchemaUtil::toVertexID(edgekey->srcid(), vidType_);
    if (!srcIdRet.ok()) {
        // ERROR
    }
    auto dstIdRet = SchemaUtil::toVertexID(edgekey->dstid(), vidType_);
    if (!dstIdRet.ok()) {
        // ERROR
    }
    auto rank = edgekey->rank();
    EdgeId edgeId = EdgeId(std::move(srcIdRet).value(), std::move(dstIdRet).value(), rank);

    edgeIds_.emplace_back(std::move(edgeId));
}

对于 toPlan函数,则是构造算子对应的计划节点。

值得注意的是,在图数据库 Nebula 中,一条边会以入边和出边的形式分别存储在两个端点中,即正向边src->dst 和反向边 dst->src。在更新边时,需要构建两个 UpdateEdge 计划节点,进行两次更新。UpdateMultiEdgeValidator::toPlan()采取了类似的方式。 其中,反向边的 edgeType 是 -edgeType_。函数最后的布尔值参数表示更新的是正向边 (false) 还是反向边 (true)。

auto *outNode = UpdateMultiEdge::make(qctx_,
                                  nullptr,
                                  spaceId_,
                                  name_,
                                  edgeIds_,
                                  edgeType_,
                                  insertable_,
                                  updatedProps_,
                                  {},
                                  condition_,
                                  {},
                                  false);
auto *inNode = UpdateMultiEdge::make(qctx_,
                                  outNode,
                                  spaceId_,
                                  std::move(name_),
                                  std::move(edgeIds_),
                                  -edgeType_,
                                  insertable_,
                                  std::move(updatedProps_),
                                  std::move(returnProps_),
                                  std::move(condition_),
                                  std::move(yieldColNames_),
                                  true);
root_ = inNode;
tail_ = outNode;

Planner

在 Planner 的 Mutate 计划节点中,新增了 UpdateMultiVertex 类 和 UpdateMultiEdge 类。UpdateMultiVertex 类与原生的 UpdateVertex 类相似,将 vid 替换为了 vid_list。UpdateMultiEdge 类采用相似的方法,在 UpdateEdge 类中,由于每次只更新一条边,可以在构建计划节点时直接交换 src 和 dst 节点的位置实现对反向边的更新。在 UpdateMultiEdge 类中,新增了布尔值随机变量 isRerverse_, 以表示更新的是否为反向边。

Executor

在 Executor 中,Executor 类接收计划节点,并向 storage 层发送 Request 执行,在执行完成后,接收 Responce 并对结果进行汇总。对应地,在 UpdateExecutor 中新增了 UpdateMultiVertexExecutor 类和 UpdateMultiEdgeExecutor 类。

相比于原生的 UpdateVertexExecutor 和 UpdateEdgeExecutor,UpdateMultiVertexExecutor 类和 UpdateMultiEdgeExecutor 类在执行过程中一次异步地向存储层发送多个请求,并对结果进行统一地处理。以 UpdateMultiVertexExecutor 类的 execute函数为例。

  • UpdateMultiVertexExecutor::execute() 请求发送和回复处理部分

观察代码,首先,对应 vid_list 中的每一个节点,构建并向 storage 层发送 updateVertex 请求 Request;其次,使用folly::collectAll对每个请求得到的 Responce 进行汇总 (包括错误码检查和结果处理);最后,如果存在 yield 子句 (即value.props_ref().has_value()为真),还需要将结果汇总到数据集中进行返回。

// update request for each vertex
for (auto &vId : VIds) {
    futures.emplace_back(
        qctx()
        ->getStorageClient()
        ->updateVertex(param,
        vId,
        umvNode->getTagId(),
        umvNode->getUpdatedProps(),
        umvNode->getInsertable(),
        umvNode->getReturnProps(),
        umvNode->getCondition())
        .via(runner()));
}

// collect all responses
return folly::collectAll(futures)
    .via(runner())
    .ensure([updateMultiVertTime]() {
        VLOG(1) << "updateMultiVertTime: " << updateMultiVertTime.elapsedInUSec() << "us";
    })
    .thenValue([this](
    std::vector<folly::Try<StatusOr<storage::cpp2::UpdateResponse>>> results) {
        memory::MemoryCheckGuard guard;
        SCOPED_TIMER(&execTime_);
        DataSet finalResult;
        bool propsExist = false;
        for (auto& result : results) {
            // EXCEPTION CHECK
            // ...
            auto value = std::move(result.value()).value();

            if (value.props_ref().has_value()) {
                propsExist = true;
                auto status = handleMultiResult(finalResult, std::move(*value.props_ref()));
                if (!status.ok()) {
                    return status;
                }
            }
        }

        // print the final result
        if (!propsExist) {
            return Status::OK();
        } else {
            return finish(ResultBuilder()
                .value(std::move(finalResult))
                .iter(Iterator::Kind::kDefault)
                .build());
        }
    });

为了在 Executor 中能对多个 Responce 进行处理和汇总,在 Update 基类中增加了成员函数 handleMultiResult能够将多个 Responce 汇总到一个数据集中。在新增的四个算子中,该成员函数均被共享和使用。

  • handleMultiResult函数实现
Status UpdateBaseExecutor::handleMultiResult(DataSet &result, DataSet &&data) {
    // Exception Check
    // ...
    result.colNames = yieldNames_;
    // result.colNames = std::move(yieldNames_);
    for (auto &row : data.rows) {
      std::vector<Value> columns;
      for (auto i = 1u; i < row.values.size(); i++) {
        columns.emplace_back(std::move(row.values[i]));
      }
      result.rows.emplace_back(std::move(columns));
    }
    return Status::OK();
}

UpdateRef- 算子实现

UpdateRef- 算子包括 UpdateRefVertex 算子和 UpdateRefEdge 算子,分别对应节点更新和边更新。UpdateRef- 算子允许 Update 算子使用引用属性,接受其他语句的结果并进行处理,是 lookup | update 功能实现的基础。通过增加 RefMulti- 算子,允许 UPDATE 语句引用属性,实现 lookup | update 功能。

Parser

仍然使用 MULTIUPDATE 关键字,将引用属性作为需要更新的对象。update_ref_vertex_sentence语句和 update_ref_edge_sentence语句定义如下。

  • update_ref_vertex_sentence语句

在构建 UpdateRefVertexSentence,可以提前对引用属性进行校验。

update_ref_vertex_sentence
    : KW_MULTIUPDATE KW_VERTEX KW_ON name_label vid_ref_expression KW_SET update_list when_clause yield_clause {
        if(graph::ExpressionUtils::findAny($5,{Expression::Kind::kVar})) {
            throw nebula::GraphParser::syntax_error(@5, "Parameter is not supported in update clause");
        }
        auto sentence = new UpdateRefVertexSentence($5, $4, $7, $8, $9);
        $$ = sentence;
    }
    ;
  • update_ref_edge_sentence语句
update_ref_edge_sentence
    : KW_MULTIUPDATE KW_EDGE KW_ON name_label edge_key_ref 
      KW_SET update_list when_clause yield_clause {
        auto sentence = new UpdateRefEdgeSentence($5, $4, $7, $8, $9);
        $$ = sentence; 
    }
    ;

相比于原生 UPDATE 语句,支持 UpdateRef- 算子的语句将vid替换为引用属性 vid_ref; MULTIUPDATE EDGE 语句表示一条边的方式,替换为可以包含$-.src, $-.dst, $-.rankedge_key_ref

在 Mutate Sentence 中,对应地新增 UpdateRefVertexSentence 类和 UpdateRefVertexSentence 类,对相应的输入信息进行存储,使用 Kind::kRefVertex 和 Kind::kRefEdge 进行标识,并在下一阶段进行使用。

Validator

对应地,在 MutateValidator 中新增了 UpdateRefVertexValidator 类和 UpdateRefEdgeValidator 类。

相比于原生的 Update 语句,UpdateRef-算子对应的 Validator 需要对引用属性进行校验。以 UpdateRefVertexValidator 类中的校验部分为例。

  • UpdateRefVertexValidator::validateImpl() 引用属性校验部分

首先从 sentence 中提取 vidRef_,然后调用 deduceExprType 对引用属性进行解析,最后进行类型校验。

vidRef_ = sentence->getVertices()->ref();
auto type = deduceExprType(vidRef_);
NG_RETURN_IF_ERROR(type);
if (type.value() != vidType_) {
    std::stringstream ss;
    ss << "The vid `" << vidRef_->toString() << "' should be type of `" << vidType_
       << "', but was`" << type.value() << "'";
    return Status::SemanticError(ss.str());
  }

类似地,在 UpdateRefEdgeValidator 类中,需要对 $-.src, $-.dst, $-.rank分别进行引用属性的校验。

在调用 toPlan函数时,使用一个 dedup计划节点对接收到的引用属性内容进行去重,再将去重后的结果传递给 UpdateRefVertexUpdateRefEdge 计划节点。

  • UpdateRefVertexValidator::toPlan() 计划构建部分
auto *dedupVid = Dedup::make(qctx_, nullptr);
dedupVid->setInputVar(vidVar);
auto urvNode = UpdateRefVertex::make(qctx_,
                                 dedupVid,
                                 spaceId_,
                                 std::move(name_),
                                 vidRef_,
                                 tagId_,
                                 insertable_,
                                 std::move(updatedProps_),
                                 std::move(returnProps_),
                                 std::move(condition_),
                                 std::move(yieldColNames_));
root_ = urvNode;
tail_ = dedupVid;
  • UpdateRefEdgeValidator::toPlan() 计划构建部分
auto *ureNode = UpdateRefEdge::make(qctx_,
                                    dedup,
                                    spaceId_,
                                    std::move(name_),
                                    edgeKeyRefs_.front(),
                                    edgeType_,
                                    insertable_,
                                    updatedProps_,
                                    returnProps_,
                                    condition_,
                                    yieldColNames_);
  root_ = ureNode;
  tail_ = dedup;
  return Status::OK();

Planer

在 Planner 的 Mutate 计划节点中,新增了 UpdateRefVertex 类 和 UpdateRefEdge 类。UpdateRef- 计划节点的实现难点主要在于,原生 Nebula 中的 Update 基类的基类是 SingleDependencyNode,本身不具备接收输入信息的接口。因此,为了使得新增 UpdateRef- 算子能够正确地接收引用属性的输入,新建了 UpdateRef 基类,继承 SingleInputNode 类型,然后在 UpdateRef 基类的基础上,对 UpdateRefVertex 类 和 UpdateRefEdge 类进行了实现。

  • UpdateRef基类的定义
class UpdateRef : public SingleInputNode {
public:
    // ...
protected:
  friend ObjectPool;
  UpdateRef(QueryContext* qctx,
         Kind kind,
         PlanNode* input,
         GraphSpaceID spaceId,
         std::string name,
         bool insertable,
         std::vector<storage::cpp2::UpdatedProp> updatedProps,
         std::vector<std::string> returnProps,
         std::string condition,
         std::vector<std::string> yieldNames)
      : SingleInputNode(qctx, kind, input),
        spaceId_(spaceId),
        schemaName_(std::move(name)),
        insertable_(insertable),
        updatedProps_(std::move(updatedProps)),
        returnProps_(std::move(returnProps)),
        condition_(std::move(condition)),
        yieldNames_(std::move(yieldNames)) {}
protected:
    // ...

Executor

对应地,在 UpdateExecutor 中新增了 UpdateRefVertexExecutor 类和 UpdateRefEdgeExecutor 类。与 UpdateMultiVertexExecutor 类似,在执行过程中一次异步地向存储层发送多个请求,并对结果进行统一地处理。UpdateRef- 算子在构建请求之前,需要根据引用属性,提取出接收的结果。以 UpdateRefVertexExecutor 类为例。

  • UpdateRefVertexExecutor::execute() 属性提取部分。

首先,根据 vidRef,从计划节点的祖先节点中获取 inputVar;再使用 getResult()函数根据inputVar获取inputResult;最后遍历iter,将需要更新的节点 id 置入 vertices 中。

    if (vidRef != nullptr) {
      auto inputVar = urvNode->inputVar();
      if (inputVar.empty()) {
        DCHECK(urvNode->dep() != nullptr);
        auto* gn = static_cast<const SingleInputNode*>(urvNode->dep())->dep();
        DCHECK(gn != nullptr);
        inputVar = static_cast<const SingleInputNode*>(gn)->inputVar();
      }
      DCHECK(!inputVar.empty());
      auto& inputResult = ectx_->getResult(inputVar);
      auto iter = inputResult.iter();
      vertices.reserve(iter->size());
      QueryExpressionContext ctx(ectx_);
      for (; iter->valid(); iter->next()) {
        auto val = Expression::eval(vidRef, ctx(iter.get()));
        if (val.isNull() || val.empty()) {
          continue;
        }
        if (!SchemaUtil::isValidVid(val, *spaceInfo.spaceDesc.vid_type_ref())) {
            // ...
        }
        vertices.emplace_back(std::move(val));
      }
    }

提取出需要更新的节点(边)后,即可以使用类似于 UpdateMulti- 算子的方式,对这些节点(边)进行批量更新。

由于 UpdateRefEdgeExecutor 类将正向边和反向边的处理设计在一个计划节点中,因此,UpdateRefEdgeExecutor 需要同时构造正向和反向边,即:

    storage::cpp2::EdgeKey edgeKey;
    edgeKey.src_ref() = srcId;
    edgeKey.dst_ref() = dstId;
    edgeKey.ranking_ref() = rank.getInt();
    edgeKey.edge_type_ref() = edgeType;

    storage::cpp2::EdgeKey reverse_edgeKey;
    reverse_edgeKey.src_ref() = std::move(dstId);
    reverse_edgeKey.dst_ref() = std::move(srcId);
    reverse_edgeKey.ranking_ref() = rank.getInt();
    reverse_edgeKey.edge_type_ref() = -edgeType;

###LOOKUP | UPDATE 的语句改写

  • 使用管道符的语句

在使用管道符的语句中,通过管道符 | 对 LOOKUP 语句和 UPDATE 语句进行连接,构建 PipedSentence,在 LOOKUP 语句执行完毕后,将结果输送给 UPDATE 语句进行执行。

lookup_pipe_update_sentence
    : lookup_sentence PIPE update_ref_vertex_sentence {
        $$ = new PipedSentence($1, $3);
    } 
    | lookup_sentence PIPE update_ref_edge_sentence {
        $$ = new PipedSentence($1, $3);
    }
  • LOOKUP | UPDATE VERTEX 语句

LOOKUP | UPDATE VERTEX 语句本质上等价于 通过管道符 | 对 LOOKUP 语句和 UPDATE 语句进行连接。但希望通过语法解析器,避免用户在使用过程中对于节点信息和引用属性的繁琐输入。对于两个语句,tag name 应当是相同的。对于第一个执行的 LOOKUP 语句,其 YIELD 子句应当返回id(vertex) as id;对于第二个执行的 UPDATE 语句,其接受的引用属性应当为 $-.id。因此,在语法解析器中,需要手动添加 idExpr, "id" 作为 LOOKUP 语句的 YieldColumn;同时,$-.id 作为 UPDATE 语句的引用属性。值得注意的是,在语法解析器中,需要对字符串 tag_name 进行深拷贝复制。 最后使用构造的 lookup_sentence 和 update_ref_vertex_sentence 构建 lookup_pipe_update_sentence。

lookup_pipe_update_sentence
    : KW_LOOKUP PIPE KW_UPDATE KW_VERTEX KW_ON name_label lookup_where_clause KW_SET update_list when_clause yield_clause {
        // yield_clause for lookup_sentence is only `id(vertex) as id`
        ArgumentList* optArgList = ArgumentList::make(qctx->objPool());
        Expression* arg = VertexExpression::make(qctx->objPool());
        optArgList->addArgument(arg);
        Expression* idExpr = FunctionCallExpression::make(qctx->objPool(), "id", optArgList);
        YieldColumn* idYieldColumn = new YieldColumn(idExpr, "id");
        
        auto fields = new YieldColumns();
        fields->addColumn(idYieldColumn);
        auto vid_yield_clause = new YieldClause(fields, true);
        
        std::string* name_label_copy = new std::string(*$6);

        // look up
        auto lookup_sentence = new LookupSentence($6, $7, vid_yield_clause);

        // input_ref `$-.id`
        auto vid_input_ref = InputPropertyExpression::make(qctx->objPool(), "id");

        // multiupdate
        auto ref_update_sentence = new UpdateRefVertexSentence(vid_input_ref, name_label_copy, $9, $10, $11);

        $$ = new PipedSentence(lookup_sentence, ref_update_sentence);
    }
  • LOOKUP | UPDATE EDGE 语句

类似的,对于LOOKUP | UPDATE EDGE 语句,进行类似的改写。对于两个语句,edge type 应当是相同的。对于第一个执行的 LOOKUP 语句,其 YIELD 子句应当返回src(edge) as src, dst(edge) as dst, rank(edge) as rank;对于第二个执行的 UPDATE 语句,其接受的引用属性应当为 $-.src -> $-.dst @ $-.rank。因此,在语法解析器中,需要手动添加 "src", "dst", "rank" 作为 LOOKUP 语句的 YieldColumn;同时,利用$-.src, $-.dst, $-.rank构建 EdgeKeyRef 作为 UPDATE EDGE 语句的引用属性。最后使用构造的 lookup_sentence 和 update_ref_edge_sentence 构建 lookup_pipe_update_sentence。

KW_LOOKUP PIPE KW_UPDATE KW_EDGE KW_ON name_label lookup_where_clause KW_SET update_list when_clause yield_clause {
        // yield_clause for lookup_sentence is `src(edge) as src, dst(edge) as dst, rank(edge) as rank`
        ArgumentList* arg_list_src = ArgumentList::make(qctx->objPool());
        Expression *arg_src = EdgeExpression::make(qctx->objPool());
        arg_list_src->addArgument(arg_src);
        Expression* srcExpr = FunctionCallExpression::make(qctx->objPool(), "src", arg_list_src);
        YieldColumn* srcYieldColumn = new YieldColumn(srcExpr, "src");

        ArgumentList* arg_list_dst = ArgumentList::make(qctx->objPool());
        Expression *arg_dst = EdgeExpression::make(qctx->objPool());
        arg_list_dst->addArgument(arg_dst);
        Expression* dstExpr = FunctionCallExpression::make(qctx->objPool(), "dst", arg_list_dst);
        YieldColumn* dstYieldColumn = new YieldColumn(dstExpr, "dst");

        ArgumentList* arg_list_rank = ArgumentList::make(qctx->objPool());
        Expression *arg_rank = EdgeExpression::make(qctx->objPool());
        arg_list_rank->addArgument(arg_rank);
        Expression* rankExpr = FunctionCallExpression::make(qctx->objPool(), "rank", arg_list_rank);
        YieldColumn* rankYieldColumn = new YieldColumn(rankExpr, "rank");

        auto fields = new YieldColumns();
        fields->addColumn(srcYieldColumn);
        fields->addColumn(dstYieldColumn);
        fields->addColumn(rankYieldColumn);
        auto edge_yield_clause = new YieldClause(fields, true);

        std::string* name_label_copy = new std::string(*$6);
        auto lookup_sentence = new LookupSentence($6, $7, edge_yield_clause);

        // input_ref `$-.src -> $-.dst @ $-.rank`
        auto src_input_ref = InputPropertyExpression::make(qctx->objPool(), "src");
        auto dst_input_ref = InputPropertyExpression::make(qctx->objPool(), "dst");
        auto rank_input_ref = InputPropertyExpression::make(qctx->objPool(), "rank");
        
        auto edge_key_ref = new EdgeKeyRef(src_input_ref, dst_input_ref, rank_input_ref);

        auto update_ref_edge_sentence = new UpdateRefEdgeSentence(edge_key_ref, name_label_copy, $9, $10, $11);

        $$ = new PipedSentence(lookup_sentence, update_ref_edge_sentence);
    }

Special notes for your reviewer, ex. impact of this fix, design document, etc:

Checklist:

Tests:

  • Unit test(positive and negative cases)
  • Function test
  • Performance test
  • N/A

Affects:

  • Documentation affected (Please add the label if documentation needs to be modified.)
  • Incompatibility (If it breaks the compatibility, please describe it and add the label.)
  • If it's needed to cherry-pick (If cherry-pick to some branches is required, please label the destination version(s).)
  • Performance impacted: Consumes more CPU/Memory

Release notes:

Please confirm whether to be reflected in release notes and how to describe:

  • Added MULTIUPDATE syntax for batch processing.
  • Introduced LOOKUP | UPDATE syntax for seamless query and update operations.

@CLAassistant
Copy link

CLAassistant commented Sep 25, 2024

CLA assistant check
All committers have signed the CLA.

@Salieri-004 Salieri-004 added the ready-for-testing PR: ready for the CI test label Sep 27, 2024
@MuYiYong
Copy link

MuYiYong commented Oct 8, 2024

看了下新增的MULTIUPDATE,和原有的 UPDATE 基本上语法是一致的,只是 vid 支持多个。
因此,是否可以在原有 UPDATE 基础上增加支持,而不是新增MULTIUPDATE。我理解应该是语法层面能够兼容的。
因为至少从用户层面,期望应该是统一的

cc @Salieri-004 @dutor @codesigner

@MuYiYong
Copy link

MuYiYong commented Oct 8, 2024

另外,关于 LOOKUP | UPDATE 语法,确实简化了很多操作,但个人不是很建议,几个原因是:

  1. 改变了 | 原有的作用。
  2. 从未来 GQL 的角度来讲,用的 match + set,也是分开来的
  3. 复杂度/工作量/测试角度等原因
    故建议直接用lookup + update,通过管道符链接即可;

cc @Salieri-004 @dutor @codesigner @JackChuengQAQ

@Salieri-004
Copy link
Contributor

感谢@JackChuengQAQ 的贡献。对于上述的feature,我同意 @MuYiYong 的观点。

  1. MULTIUPDATE和UPDATE在用户看来行为是一致的,我们是否可以直接扩展UPDATE而不是新增语法。
  2. LOOKUP|UPDATE这种语法和现有的语法设计差别比较大,我个人建议将LOOKUP语句和UPDATE语句通过|连接即可,直接使用|连接LOOKUP和UPDATE关键词更像是一种语法糖,我觉得需要再做斟酌。

@JackChuengQAQ
Copy link
Contributor Author

  1. 应该是可以在直接在 UPDATE 语句上进行扩展的 ,最初区分更多是为了保证新增功能的独立性,在 parser 中简单改动即可
  2. OK,那后续在 pull request 修改中注释掉 LOOKUP | UPDATE 的语法解析部分?只保留当前使用管道符连接 LOOKUP 语句和 UPDATE 语句的部分

@Salieri-004
Copy link
Contributor

  1. 应该是可以在直接在 UPDATE 语句上进行扩展的 ,最初区分更多是为了保证新增功能的独立性,在 parser 中简单改动即可
  2. OK,那后续在 pull request 修改中注释掉 LOOKUP | UPDATE 的语法解析部分?只保留当前使用管道符连接 LOOKUP 语句和 UPDATE 语句的部分

👍 好的。先删掉LOOKUP|UPDATE的语法吧。

@JackChuengQAQ
Copy link
Contributor Author

我已经补充了一个简单的修改

  1. 删除了LOOKUP|UPDATE的语法
  2. 同时删除了 MULTIUPDATE 关键字,将对用户的语法与原生的 UPDATE 统一。

@Salieri-004
Copy link
Contributor

@Salieri-004
Copy link
Contributor

Salieri-004 commented Oct 13, 2024

@JackChuengQAQ 我注意到您为MultiUpdate和UpdateRef重新实现了新的parser,validator,executor,考虑到MultiUpdate和UpdateRef只是在为原有的Update扩充功能,且大部分的逻辑实际上是可以公用的,我想在原有Update的parser,validator,executor上进行扩充会更好,这样可以省去很多冗余的重复代码,也方便后续的修改和维护。从我的角度看,似乎在把更新单个vertex和更新多个vertex的语义区分两套实现是很奇怪的,后者应该是前者的超集。

@JackChuengQAQ
Copy link
Contributor Author

@JackChuengQAQ 需要补充相关feature的tck测试,可参见 https://discuss.nebula-graph.com.cn/t/topic/4172 https://discuss.nebula-graph.com.cn/t/topic/4594

我正在学习使用 nebula 的 tests,但在部署测试环境时遇到了困难。详见 https://discuss.nebula-graph.com.cn/t/topic/16129

@JackChuengQAQ 我注意到您为MultiUpdate和UpdateRef重新实现了新的parser,validator,executor,考虑到MultiUpdate和UpdateRef只是在为原有的Update扩充功能,且大部分的逻辑实际上是可以公用的,我想在原有Update的parser,validator,executor上进行扩充会更好,这样可以省去很多冗余的重复代码,也方便后续的修改和维护。从我的角度看,似乎在把更新单个vertex和更新多个vertex的语义区分两套实现是很奇怪的,后者应该是前者的超集。

更新多个节点的方法(MultiUpdate)可以直接合成到原有的 Update,相当于原有的 Update 直接接收 vid_list 而不是 v_id。

对于 UpdateRef,您看是否需要合并(还是单独拆分出两个算子比较好呢)?如果要合并 MultiUpdate 和 UpdateRef 算子的话。能想到的方法是将 Multi 的多个节点首先构建成 dataset,然后统一使用 UpdateRef 算子 (目前 delete 算子的做法)。但我感觉这个构建数据集的过程其实是不必要的。

@Salieri-004
Copy link
Contributor

Salieri-004 commented Oct 15, 2024

@JackChuengQAQ 需要补充相关feature的tck测试,可参见 https://discuss.nebula-graph.com.cn/t/topic/4172 https://discuss.nebula-graph.com.cn/t/topic/4594

我正在学习使用 nebula 的 tests,但在部署测试环境时遇到了困难。详见 https://discuss.nebula-graph.com.cn/t/topic/16129

@JackChuengQAQ 我注意到您为MultiUpdate和UpdateRef重新实现了新的parser,validator,executor,考虑到MultiUpdate和UpdateRef只是在为原有的Update扩充功能,且大部分的逻辑实际上是可以公用的,我想在原有Update的parser,validator,executor上进行扩充会更好,这样可以省去很多冗余的重复代码,也方便后续的修改和维护。从我的角度看,似乎在把更新单个vertex和更新多个vertex的语义区分两套实现是很奇怪的,后者应该是前者的超集。

更新多个节点的方法(MultiUpdate)可以直接合成到原有的 Update,相当于原有的 Update 直接接收 vid_list 而不是 v_id。

对于 UpdateRef,您看是否需要合并(还是单独拆分出两个算子比较好呢)?如果要合并 MultiUpdate 和 UpdateRef 算子的话。能想到的方法是将 Multi 的多个节点首先构建成 dataset,然后统一使用 UpdateRef 算子 (目前 delete 算子的做法)。但我感觉这个构建数据集的过程其实是不必要的。

您好,我认为参考Fetch或Delete的实现即可,这也和我们往常的设计风格相统一。我认为“构建数据集”的过程相比于网络和存储的开销来说或许是微不足道的。如果您认为没必要先构建一个dataset,我们或许可以仅在executor做一些输入源的判断。比如

void updateVertices(const std::vector<Value>& vec) {
...
}

if (fromConstInput) {
   updateVertices(umvNode->getVIds());
}
else {

    std::vector<Value> vec;
    // ... getNodes from dataset
    updateVertices(vec)
}

@JackChuengQAQ
Copy link
Contributor Author

补充了两个 commit

  1. 增加了 MultiUpdate.feature 和 LookupUpdate.feature, 对批量更新和搜索后更新进行了测试
  2. 将新增的 UpdateMulti- 和 UpdateRef- 算子都整合到原有的 Update 流程中,避免了代码的冗余。如您所说,构建数据集的时间相对较短,因此,实现方案的风格依旧与Fetch或Delete的实现一致,即在本地构建数据集后,统一后续的流程。
    对于冗余的RPC操作,目前我正在熟悉 Nebula 的通信协议,我会在后续的 PR 中进一步修改。

Copy link
Contributor

@Salieri-004 Salieri-004 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同学您好,请使用clang-format格式化代码并通过CI的format check。请根据comment修改您的PR。

src/graph/executor/mutate/DeleteExecutor.cpp Outdated Show resolved Hide resolved
src/graph/executor/mutate/UpdateExecutor.cpp Outdated Show resolved Hide resolved
src/graph/executor/mutate/UpdateExecutor.cpp Outdated Show resolved Hide resolved
src/graph/executor/mutate/UpdateExecutor.cpp Outdated Show resolved Hide resolved
src/graph/executor/mutate/UpdateExecutor.cpp Outdated Show resolved Hide resolved
src/parser/parser.yy Outdated Show resolved Hide resolved
tests/Makefile Outdated Show resolved Hide resolved
@@ -0,0 +1,88 @@
@lookup_update
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

测试用的标记符号不要提交

tests/tck/features/update/MultiUpdate.feature Outdated Show resolved Hide resolved
tests/tck/features/update/MultiUpdate.feature Outdated Show resolved Hide resolved
@JackChuengQAQ
Copy link
Contributor Author

按照 @Salieri-004 的意见进行了第一次修改。
目前待修改的内容:

  1. checkInput() 的合并。我会尝试定义一个MutateUtils类
  2. srcId.isNull() || srcId.empty() 的检查;inputVar.empty() 的检查 - 这部分检查其实是参考 delete 实现的,是否需要保留呢

@JackChuengQAQ
Copy link
Contributor Author

按照 @Salieri-004 的意见进行了第一次修改。 目前待修改的内容:

  1. checkInput() 的合并。我会尝试定义一个MutateUtils类
  2. srcId.isNull() || srcId.empty() 的检查;inputVar.empty() 的检查 - 这部分检查其实是参考 delete 实现的,是否需要保留呢

isValidVid还会检查该类型和schema中的vid type类型相同,在value.type() == propTypeToValueType(type);我理解srcId.isNull() || srcId.empty() 的检查在此时是不必要的,删去即可。

bool SchemaUtil::isValidVid(const Value &value, nebula::cpp2::PropertyType type) {
  return isValidVid(value) && value.type() == propTypeToValueType(type);
}

我的理解在于,两者检查的后续处理似乎并不相同。srcId.isNull() || srcId.empty() 会被忽略(continue),而 isValidVid return false 会触发 error,不知道是否存在影响

@Salieri-004
Copy link
Contributor

按照 @Salieri-004 的意见进行了第一次修改。 目前待修改的内容:

  1. checkInput() 的合并。我会尝试定义一个MutateUtils类
  2. srcId.isNull() || srcId.empty() 的检查;inputVar.empty() 的检查 - 这部分检查其实是参考 delete 实现的,是否需要保留呢

srcId.isNull() || srcId.empty()的检查,我理解已经被isValidVid所覆盖了,您可以查看其实现。inputVar.empty()的问题,我review您的实现没有看到inputVar可能为空的情况。

Copy link
Contributor

@Salieri-004 Salieri-004 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

麻烦修复CI的UT failed并重新提交

src/graph/executor/mutate/UpdateExecutor.cpp Show resolved Hide resolved
src/graph/executor/mutate/UpdateExecutor.cpp Outdated Show resolved Hide resolved
@JackChuengQAQ
Copy link
Contributor Author

第三次提交修改。
修改了 comment 以及 UT failed,删去了 srcId.isNull() || srcId.empty(), InputVar() 以及冗余的 handleResult。

对于 MutateEdgeUtils 的友类实现。考虑到 checkInput 中会调用 DeleteEdgeValidator 和 UpdateEdgeValidator 的成员变量以及调用 Validator 基类的成员函数,现 MutateEdgeUtils 拟实现如下:
令其与 Validator 互为友类,并将 DeleteEdgeValidator / UpdateEdgeValidator 本身指针与成员变量作为参数输入给 MutateEdgeUtils::checkInput。
这是一个可接受的方案吗。

@Salieri-004
Copy link
Contributor

第三次提交修改。 修改了 comment 以及 UT failed,删去了 srcId.isNull() || srcId.empty(), InputVar() 以及冗余的 handleResult。

对于 MutateEdgeUtils 的友类实现。考虑到 checkInput 中会调用 DeleteEdgeValidator 和 UpdateEdgeValidator 的成员变量以及调用 Validator 基类的成员函数,现 MutateEdgeUtils 拟实现如下: 令其与 Validator 互为友类,并将 DeleteEdgeValidator / UpdateEdgeValidator 本身指针与成员变量作为参数输入给 MutateEdgeUtils::checkInput。 这是一个可接受的方案吗。

好的,我认为没问题

codesigner
codesigner previously approved these changes Oct 28, 2024
@codesigner codesigner enabled auto-merge (squash) October 28, 2024 08:33
auto-merge was automatically disabled October 28, 2024 11:26

Head branch was pushed to by a user without write access

@Salieri-004 Salieri-004 added ready-for-testing PR: ready for the CI test and removed ready-for-testing PR: ready for the CI test labels Oct 28, 2024
@Salieri-004
Copy link
Contributor

LGTM. 感谢您的贡献!

Copy link
Contributor

@codesigner codesigner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Job.

@codesigner codesigner merged commit b4f83eb into vesoft-inc:master Oct 29, 2024
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready-for-testing PR: ready for the CI test
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants