nebula icon indicating copy to clipboard operation
nebula copied to clipboard

Enable MULTIUPDATE and LOOKUP | UPDATE

Open JackChuengQAQ opened this issue 1 year ago • 11 comments

What type of PR is this?

  • [ ] bug
  • [x] 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
  • [x] N/A

Affects:

  • [x] 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.

JackChuengQAQ avatar Sep 25 '24 09:09 JackChuengQAQ

CLA assistant check
All committers have signed the CLA.

CLAassistant avatar Sep 25 '24 09:09 CLAassistant

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

cc @Salieri-004 @dutor @codesigner

MuYiYong avatar Oct 08 '24 06:10 MuYiYong

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

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

cc @Salieri-004 @dutor @codesigner @JackChuengQAQ

MuYiYong avatar Oct 08 '24 06:10 MuYiYong

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

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

Salieri-004 avatar Oct 08 '24 07:10 Salieri-004

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

JackChuengQAQ avatar Oct 09 '24 03:10 JackChuengQAQ

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

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

Salieri-004 avatar Oct 09 '24 10:10 Salieri-004

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

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

JackChuengQAQ avatar Oct 12 '24 07:10 JackChuengQAQ

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

Salieri-004 avatar Oct 13 '24 10:10 Salieri-004

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

Salieri-004 avatar Oct 13 '24 14:10 Salieri-004

@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 算子的做法)。但我感觉这个构建数据集的过程其实是不必要的。

JackChuengQAQ avatar Oct 15 '24 08:10 JackChuengQAQ

@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)
}

Salieri-004 avatar Oct 15 '24 09:10 Salieri-004

补充了两个 commit

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

JackChuengQAQ avatar Oct 25 '24 17:10 JackChuengQAQ

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

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

JackChuengQAQ avatar Oct 27 '24 14:10 JackChuengQAQ

按照 @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,不知道是否存在影响

JackChuengQAQ avatar Oct 27 '24 14:10 JackChuengQAQ

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

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

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

Salieri-004 avatar Oct 27 '24 15:10 Salieri-004

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

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

JackChuengQAQ avatar Oct 28 '24 07:10 JackChuengQAQ

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

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

好的,我认为没问题

Salieri-004 avatar Oct 28 '24 07:10 Salieri-004

LGTM. 感谢您的贡献!

Salieri-004 avatar Oct 29 '24 06:10 Salieri-004