nebula
nebula copied to clipboard
Enable MULTIUPDATE and LOOKUP | UPDATE
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 vid、vid 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, $-.rank的edge_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计划节点对接收到的引用属性内容进行去重,再将去重后的结果传递给 UpdateRefVertex 或 UpdateRefEdge 计划节点。
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.
看了下新增的MULTIUPDATE,和原有的 UPDATE 基本上语法是一致的,只是 vid 支持多个。 因此,是否可以在原有 UPDATE 基础上增加支持,而不是新增MULTIUPDATE。我理解应该是语法层面能够兼容的。 因为至少从用户层面,期望应该是统一的
cc @Salieri-004 @dutor @codesigner
另外,关于 LOOKUP | UPDATE 语法,确实简化了很多操作,但个人不是很建议,几个原因是:
- 改变了
|原有的作用。 - 从未来 GQL 的角度来讲,用的 match + set,也是分开来的
- 复杂度/工作量/测试角度等原因 故建议直接用lookup + update,通过管道符链接即可;
cc @Salieri-004 @dutor @codesigner @JackChuengQAQ
感谢@JackChuengQAQ 的贡献。对于上述的feature,我同意 @MuYiYong 的观点。
- MULTIUPDATE和UPDATE在用户看来行为是一致的,我们是否可以直接扩展UPDATE而不是新增语法。
- LOOKUP|UPDATE这种语法和现有的语法设计差别比较大,我个人建议将LOOKUP语句和UPDATE语句通过|连接即可,直接使用|连接LOOKUP和UPDATE关键词更像是一种语法糖,我觉得需要再做斟酌。
- 应该是可以在直接在 UPDATE 语句上进行扩展的 ,最初区分更多是为了保证新增功能的独立性,在 parser 中简单改动即可
- OK,那后续在 pull request 修改中注释掉 LOOKUP | UPDATE 的语法解析部分?只保留当前使用管道符连接 LOOKUP 语句和 UPDATE 语句的部分
- 应该是可以在直接在 UPDATE 语句上进行扩展的 ,最初区分更多是为了保证新增功能的独立性,在 parser 中简单改动即可
- OK,那后续在 pull request 修改中注释掉 LOOKUP | UPDATE 的语法解析部分?只保留当前使用管道符连接 LOOKUP 语句和 UPDATE 语句的部分
👍 好的。先删掉LOOKUP|UPDATE的语法吧。
我已经补充了一个简单的修改
- 删除了LOOKUP|UPDATE的语法
- 同时删除了 MULTIUPDATE 关键字,将对用户的语法与原生的 UPDATE 统一。
@JackChuengQAQ 需要补充相关feature的tck测试,可参见 https://discuss.nebula-graph.com.cn/t/topic/4172 https://discuss.nebula-graph.com.cn/t/topic/4594
@JackChuengQAQ 我注意到您为MultiUpdate和UpdateRef重新实现了新的parser,validator,executor,考虑到MultiUpdate和UpdateRef只是在为原有的Update扩充功能,且大部分的逻辑实际上是可以公用的,我想在原有Update的parser,validator,executor上进行扩充会更好,这样可以省去很多冗余的重复代码,也方便后续的修改和维护。从我的角度看,似乎在把更新单个vertex和更新多个vertex的语义区分两套实现是很奇怪的,后者应该是前者的超集。
@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 需要补充相关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)
}
补充了两个 commit
- 增加了 MultiUpdate.feature 和 LookupUpdate.feature, 对批量更新和搜索后更新进行了测试
- 将新增的 UpdateMulti- 和 UpdateRef- 算子都整合到原有的 Update 流程中,避免了代码的冗余。如您所说,构建数据集的时间相对较短,因此,实现方案的风格依旧与Fetch或Delete的实现一致,即在本地构建数据集后,统一后续的流程。 对于冗余的RPC操作,目前我正在熟悉 Nebula 的通信协议,我会在后续的 PR 中进一步修改。
按照 @Salieri-004 的意见进行了第一次修改。 目前待修改的内容:
- checkInput() 的合并。我会尝试定义一个MutateUtils类
- srcId.isNull() || srcId.empty() 的检查;inputVar.empty() 的检查 - 这部分检查其实是参考 delete 实现的,是否需要保留呢
按照 @Salieri-004 的意见进行了第一次修改。 目前待修改的内容:
- checkInput() 的合并。我会尝试定义一个MutateUtils类
- 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 的意见进行了第一次修改。 目前待修改的内容:
- checkInput() 的合并。我会尝试定义一个MutateUtils类
- srcId.isNull() || srcId.empty() 的检查;inputVar.empty() 的检查 - 这部分检查其实是参考 delete 实现的,是否需要保留呢
srcId.isNull() || srcId.empty()的检查,我理解已经被isValidVid所覆盖了,您可以查看其实现。inputVar.empty()的问题,我review您的实现没有看到inputVar可能为空的情况。
第三次提交修改。 修改了 comment 以及 UT failed,删去了 srcId.isNull() || srcId.empty(), InputVar() 以及冗余的 handleResult。
对于 MutateEdgeUtils 的友类实现。考虑到 checkInput 中会调用 DeleteEdgeValidator 和 UpdateEdgeValidator 的成员变量以及调用 Validator 基类的成员函数,现 MutateEdgeUtils 拟实现如下: 令其与 Validator 互为友类,并将 DeleteEdgeValidator / UpdateEdgeValidator 本身指针与成员变量作为参数输入给 MutateEdgeUtils::checkInput。 这是一个可接受的方案吗。
第三次提交修改。 修改了 comment 以及 UT failed,删去了 srcId.isNull() || srcId.empty(), InputVar() 以及冗余的 handleResult。
对于 MutateEdgeUtils 的友类实现。考虑到 checkInput 中会调用 DeleteEdgeValidator 和 UpdateEdgeValidator 的成员变量以及调用 Validator 基类的成员函数,现 MutateEdgeUtils 拟实现如下: 令其与 Validator 互为友类,并将 DeleteEdgeValidator / UpdateEdgeValidator 本身指针与成员变量作为参数输入给 MutateEdgeUtils::checkInput。 这是一个可接受的方案吗。
好的,我认为没问题
LGTM. 感谢您的贡献!