diff --git a/include/lgraph/lgraph_txn.h b/include/lgraph/lgraph_txn.h index 4e42ac93d9..d425abed27 100644 --- a/include/lgraph/lgraph_txn.h +++ b/include/lgraph/lgraph_txn.h @@ -423,7 +423,8 @@ class Transaction { int UpsertEdge(int64_t src, int64_t dst, size_t label_id, const std::vector& unique_pos, const std::vector& field_ids, - const std::vector& field_values); + const std::vector& field_values, + std::optional pair_unique_pos); /** * @brief List indexes @@ -482,6 +483,11 @@ class Transaction { EdgeIndexIterator GetEdgeIndexIterator(size_t label_id, size_t field_id, const FieldData& key_start, const FieldData& key_end); + EdgeIndexIterator GetEdgePairUniqueIndexIterator(size_t label_id, size_t field_id, + int64_t src_vid, int64_t dst_vid, + const FieldData& key_start, + const FieldData& key_end); + /** * @brief Gets vertex index iterator. The iterator has field value [key_start, key_end]. So * key_start=key_end=v returns an iterator pointing to all vertexes that has field diff --git a/src/core/transaction.cpp b/src/core/transaction.cpp index e948fc3a4a..7a157b8ca3 100644 --- a/src/core/transaction.cpp +++ b/src/core/transaction.cpp @@ -680,6 +680,23 @@ EdgeIndexIterator Transaction::GetEdgeIndexIterator(size_t label_id, size_t fiel return index->GetIterator(this, std::move(ks), std::move(ke), 0); } +EdgeIndexIterator Transaction::GetEdgePairUniqueIndexIterator( + size_t label_id, size_t field_id, VertexId src_vid, VertexId dst_vid, + const FieldData& key_start, const FieldData& key_end) { + EdgeIndex* index = GetEdgeIndex(label_id, field_id); + if (!index || !index->IsReady() || index->GetType() != IndexType::PairUniqueIndex) { + THROW_CODE(InputError, "Edge pair unique index is not created for this field"); + } + Value ks, ke; + if (!key_start.IsNull()) { + ks = field_data_helper::FieldDataToValueOfFieldType(key_start, index->KeyType()); + } + if (!key_end.IsNull()) { + ke = field_data_helper::FieldDataToValueOfFieldType(key_end, index->KeyType()); + } + return index->GetIterator(this, std::move(ks), std::move(ke), src_vid, dst_vid); +} + EdgeIndexIterator Transaction::GetEdgeIndexIterator(const std::string& label, const std::string& field, const std::string& key_start = "", diff --git a/src/core/transaction.h b/src/core/transaction.h index b3406c2914..3b994043a5 100644 --- a/src/core/transaction.h +++ b/src/core/transaction.h @@ -937,6 +937,10 @@ class Transaction { const FieldData& key_start = FieldData(), const FieldData& key_end = FieldData()); + EdgeIndexIterator GetEdgePairUniqueIndexIterator( + size_t label_id, size_t field_id, VertexId src_vid, VertexId dst_vid, + const FieldData& key_start, const FieldData& key_end); + EdgeIndexIterator GetEdgeIndexIterator(const std::string& label, const std::string& field, const std::string& key_start, const std::string& key_end); diff --git a/src/cypher/procedure/procedure.cpp b/src/cypher/procedure/procedure.cpp index dd86249e72..5cc1349110 100644 --- a/src/cypher/procedure/procedure.cpp +++ b/src/cypher/procedure/procedure.cpp @@ -679,13 +679,17 @@ void BuiltinProcedure::DbUpsertVertexByJson(RTContext *ctx, const Record *record void BuiltinProcedure::DbUpsertEdge(RTContext *ctx, const Record *record, const VEC_EXPR &args, const VEC_STR &yield_items, std::vector *records) { - CYPHER_ARG_CHECK(args.size() == 4, - "need 4 parameters, " - "e.g. db.upsertEdge(label_name, start_spec, end_spec, list_data)") + CYPHER_ARG_CHECK(args.size() == 4 || args.size() == 5, + "need 4 or 5 parameters, " + "e.g. db.upsertEdge(label_name, start_spec, end_spec, list_data) or " + "db.upsertEdge(label_name, start_spec, end_spec, list_data, pair_unique_field)") CYPHER_ARG_CHECK(args[0].IsString(), "label_name type should be string") CYPHER_ARG_CHECK(args[1].IsMap(), "start_spec type should be map") CYPHER_ARG_CHECK(args[2].IsMap(), "end_spec type should be map") CYPHER_ARG_CHECK(args[3].IsArray(), "list_data type should be list") + if (args.size() == 5) { + CYPHER_ARG_CHECK(args[4].IsString(), "pair_unique_field type should be string") + } CYPHER_DB_PROCEDURE_GRAPH_CHECK(); if (ctx->txn_) ctx->txn_->Abort(); const auto& start = *args[1].constant.map; @@ -696,6 +700,10 @@ void BuiltinProcedure::DbUpsertEdge(RTContext *ctx, const Record *record, if (!end.count("type") || !end.count("key")) { THROW_CODE(InputError, "end_spec missing 'type' or 'key'"); } + std::string pair_unique_field; + if (args.size() == 5) { + pair_unique_field = args[4].constant.AsString(); + } std::string start_type = start.at("type").AsString(); std::string start_json_key = start.at("key").AsString(); std::string end_type = end.at("type").AsString(); @@ -736,11 +744,20 @@ void BuiltinProcedure::DbUpsertEdge(RTContext *ctx, const Record *record, auto index_fds = txn.GetTxn()->ListEdgeIndexByLabel(args[0].constant.AsString()); std::unordered_map unique_indexs; + bool pair_unique_configured = false; for (auto& index : index_fds) { if (index.type == lgraph_api::IndexType::GlobalUniqueIndex) { unique_indexs[txn.GetEdgeFieldId(label_id, index.field)] = true; + } else if (index.type == lgraph_api::IndexType::PairUniqueIndex) { + if (!pair_unique_field.empty() && index.field == pair_unique_field) { + pair_unique_configured = true; + } } } + if (!pair_unique_field.empty() && !pair_unique_configured) { + THROW_CODE(InputError, "No edge pair unique index is configured for this field: {}", + pair_unique_field); + } const auto& list = *args[3].constant.array; int64_t json_total = list.size(); @@ -749,17 +766,24 @@ void BuiltinProcedure::DbUpsertEdge(RTContext *ctx, const Record *record, int64_t insert = 0; int64_t update = 0; std::vector, std::vector, - std::vector>> lines; + std::vector, std::optional >> lines; for (auto& line : list) { int64_t start_vid = -1; int64_t end_vid = -1; std::vector unique_pos; + std::optional pair_unique_pos; std::vector field_ids; std::vector fds; bool success = true; if (!line.IsMap()) { THROW_CODE(InputError, "The type of the elements in the list must be map"); } + if (!pair_unique_field.empty()) { + if (!line.map->count(pair_unique_field)) { + json_error++; + continue; + } + } for (auto& item : *line.map) { if (item.first == start_json_key) { auto fd = item.second.scalar; @@ -801,13 +825,17 @@ void BuiltinProcedure::DbUpsertEdge(RTContext *ctx, const Record *record, field_ids.push_back(iter->second.first); if (unique_indexs.count(iter->second.first)) { unique_pos.push_back(field_ids.size() - 1); + } else { + if (!pair_unique_field.empty() && pair_unique_field == item.first) { + pair_unique_pos = field_ids.size() - 1; + } } } } } if (success && start_vid >= 0 && end_vid >= 0) { lines.emplace_back(start_vid, end_vid, std::move(unique_pos), - std::move(field_ids), std::move(fds)); + std::move(field_ids), std::move(fds), pair_unique_pos); } else { json_error++; } @@ -816,7 +844,8 @@ void BuiltinProcedure::DbUpsertEdge(RTContext *ctx, const Record *record, txn = db.CreateWriteTxn(); for (auto& l : lines) { int ret = txn.UpsertEdge(std::get<0>(l), std::get<1>(l), - label_id, std::get<2>(l), std::get<3>(l), std::get<4>(l)); + label_id, std::get<2>(l), std::get<3>(l), + std::get<4>(l), std::get<5>(l)); if (ret == 0) { index_conflict++; } else if (ret == 1) { @@ -838,13 +867,17 @@ void BuiltinProcedure::DbUpsertEdge(RTContext *ctx, const Record *record, void BuiltinProcedure::DbUpsertEdgeByJson(RTContext *ctx, const Record *record, const VEC_EXPR &args, const VEC_STR &yield_items, std::vector *records) { - CYPHER_ARG_CHECK(args.size() == 4, - "need 4 parameters, " - "e.g. db.upsertEdgeByJson(label_name, start_spec, end_spec, list_data)") + CYPHER_ARG_CHECK(args.size() == 4 || args.size() == 5, + "need 4 or 5 parameters, " + "e.g. db.upsertEdgeByJson(label_name, start_spec, end_spec, list_data) or " + "db.upsertEdgeByJson(label_name, start_spec, end_spec, list_data, pair_unique_field)") CYPHER_ARG_CHECK(args[0].IsString(), "label_name type should be string") CYPHER_ARG_CHECK(args[1].IsString(), "start_spec type should be json string") CYPHER_ARG_CHECK(args[2].IsString(), "end_spec type should be json string") CYPHER_ARG_CHECK(args[3].IsString(), "list_data type should be json string") + if (args.size() == 5) { + CYPHER_ARG_CHECK(args[4].IsString(), "pair_unique_field type should be json string") + } CYPHER_DB_PROCEDURE_GRAPH_CHECK(); if (ctx->txn_) ctx->txn_->Abort(); nlohmann::json json_data = nlohmann::json::parse(args[3].constant.AsString()); @@ -860,6 +893,11 @@ void BuiltinProcedure::DbUpsertEdgeByJson(RTContext *ctx, const Record *record, THROW_CODE(InputError, "end_spec missing 'type' or 'key'"); } + std::string pair_unique_field; + if (args.size() == 5) { + pair_unique_field = args[4].constant.AsString(); + } + std::string start_type = start["type"].get(); std::string start_json_key = start["key"].get(); std::string end_type = end["type"].get(); @@ -900,11 +938,20 @@ void BuiltinProcedure::DbUpsertEdgeByJson(RTContext *ctx, const Record *record, auto index_fds = txn.GetTxn()->ListEdgeIndexByLabel(args[0].constant.AsString()); std::unordered_map unique_indexs; + bool pair_unique_configured = false; for (auto& index : index_fds) { if (index.type == lgraph_api::IndexType::GlobalUniqueIndex) { unique_indexs[txn.GetEdgeFieldId(label_id, index.field)] = true; + } else if (index.type == lgraph_api::IndexType::PairUniqueIndex) { + if (!pair_unique_field.empty() && index.field == pair_unique_field) { + pair_unique_configured = true; + } } } + if (!pair_unique_field.empty() && !pair_unique_configured) { + THROW_CODE(InputError, "No edge pair unique index is configured for this field: {}", + pair_unique_field); + } int64_t json_total = json_data.size(); int64_t json_error = 0; @@ -912,14 +959,21 @@ void BuiltinProcedure::DbUpsertEdgeByJson(RTContext *ctx, const Record *record, int64_t insert = 0; int64_t update = 0; std::vector, std::vector, - std::vector>> lines; + std::vector, std::optional>> lines; for (auto& line : json_data) { int64_t start_vid = -1; int64_t end_vid = -1; + std::optional pair_unique_pos; std::vector unique_pos; std::vector field_ids; std::vector fds; bool success = true; + if (!pair_unique_field.empty()) { + if (!line.count(pair_unique_field)) { + json_error++; + continue; + } + } for (auto& item : line.items()) { if (item.key() == start_json_key) { auto fd = JsonToFieldData(item.value(), start_pf_fs); @@ -961,13 +1015,17 @@ void BuiltinProcedure::DbUpsertEdgeByJson(RTContext *ctx, const Record *record, field_ids.push_back(iter->second.first); if (unique_indexs.count(iter->second.first)) { unique_pos.push_back(field_ids.size() - 1); + } else { + if (!pair_unique_field.empty() && pair_unique_field == item.key()) { + pair_unique_pos = field_ids.size() - 1; + } } } } } if (success && start_vid >= 0 && end_vid >= 0) { lines.emplace_back(start_vid, end_vid, std::move(unique_pos), - std::move(field_ids), std::move(fds)); + std::move(field_ids), std::move(fds), pair_unique_pos); } else { json_error++; } @@ -975,8 +1033,10 @@ void BuiltinProcedure::DbUpsertEdgeByJson(RTContext *ctx, const Record *record, txn.Abort(); txn = db.CreateWriteTxn(); for (auto& l : lines) { - int ret = txn.UpsertEdge(std::get<0>(l), std::get<1>(l), - label_id, std::get<2>(l), std::get<3>(l), std::get<4>(l)); + int ret = txn.UpsertEdge( + std::get<0>(l), std::get<1>(l), + label_id, std::get<2>(l), + std::get<3>(l), std::get<4>(l), std::get<5>(l)); if (ret == 0) { index_conflict++; } else if (ret == 1) { diff --git a/src/lgraph_api/lgraph_txn.cpp b/src/lgraph_api/lgraph_txn.cpp index da00ab21d8..c7cfec2c32 100644 --- a/src/lgraph_api/lgraph_txn.cpp +++ b/src/lgraph_api/lgraph_txn.cpp @@ -269,23 +269,45 @@ bool Transaction::UpsertEdge(int64_t src, int64_t dst, size_t label_id, int Transaction::UpsertEdge(int64_t src, int64_t dst, size_t label_id, const std::vector& unique_pos, const std::vector& field_ids, - const std::vector& field_values) { + const std::vector& field_values, + std::optional pair_unique_pos) { ThrowIfInvalid(); for (auto pos : unique_pos) { if (pos >= field_ids.size()) { THROW_CODE(InputError, "unique_pos is out of the field_ids's range"); } } - auto iter = txn_->GetOutEdgeIterator(EdgeUid(src, dst, label_id, 0, 0), false); - if (iter.IsValid()) { + std::optional euid; + if (pair_unique_pos.has_value()) { + if (pair_unique_pos.value() > field_ids.size()) { + THROW_CODE(InputError, "pair_unique_pos is out of the field_ids's range"); + } + auto iter = txn_->GetEdgePairUniqueIndexIterator( + label_id, field_ids[pair_unique_pos.value()], + src, dst, + field_values[pair_unique_pos.value()], + field_values[pair_unique_pos.value()]); + if (iter.IsValid()) { + auto uid = iter.GetUid(); + if (uid.src == src && uid.dst == dst && uid.lid == label_id) { + euid = uid; + } + } + } else { + auto iter = txn_->GetOutEdgeIterator(EdgeUid(src, dst, label_id, 0, 0), false); + if (iter.IsValid()) { + euid = iter.GetUid(); + } + } + if (euid.has_value()) { for (auto pos : unique_pos) { auto tmp = txn_->GetEdgeIndexIterator( label_id, field_ids[pos], field_values[pos], field_values[pos]); - if (tmp.IsValid() && (tmp.GetUid() != iter.GetUid())) { + if (tmp.IsValid() && (tmp.GetUid() != euid.value())) { return 0; } } - txn_->SetEdgeProperty(iter, field_ids, field_values); + txn_->SetEdgeProperty(euid.value(), field_ids, field_values); return 2; } else { for (auto pos : unique_pos) { @@ -339,6 +361,16 @@ EdgeIndexIterator Transaction::GetEdgeIndexIterator(size_t label_id, size_t fiel txn_); } +EdgeIndexIterator Transaction::GetEdgePairUniqueIndexIterator(size_t label_id, size_t field_id, + int64_t src_vid, int64_t dst_vid, + const FieldData& key_start, + const FieldData& key_end) { + ThrowIfInvalid(); + return EdgeIndexIterator(txn_->GetEdgePairUniqueIndexIterator( + label_id, field_id, src_vid, dst_vid, key_start, key_end), + txn_); +} + VertexIndexIterator Transaction::GetVertexIndexIterator(const std::string& label, const std::string& field, const FieldData& key_start, @@ -501,6 +533,7 @@ OutEdgeIterator Transaction::GetEdgeByUniqueIndex(size_t label_id, size_t field_ euid = eit.GetUid(); return GetOutEdgeIterator(euid, false); } + size_t Transaction::GetNumVertices() { ThrowIfInvalid(); return txn_->graph_->GetLooseNumVertex(txn_->GetTxn()); diff --git a/test/test_lgraph_api.cpp b/test/test_lgraph_api.cpp index 7432ecf275..9bf38e3296 100644 --- a/test/test_lgraph_api.cpp +++ b/test/test_lgraph_api.cpp @@ -1184,3 +1184,87 @@ TEST_F(TestLGraphApi, deleteAllVertex) { UT_EXPECT_EQ(expect, detail); txn.Abort(); } + +TEST_F(TestLGraphApi, pairUniqueIndex) { + std::string path = "./testdb"; + auto ADMIN = lgraph::_detail::DEFAULT_ADMIN_NAME; + auto ADMIN_PASS = lgraph::_detail::DEFAULT_ADMIN_PASS; + lgraph::AutoCleanDir cleaner(path); + Galaxy galaxy(path); + std::string db_path; + galaxy.SetCurrentUser(ADMIN, ADMIN_PASS); + GraphDB db = galaxy.OpenGraph("default"); + VertexOptions vo("id"); + vo.detach_property = true; + UT_EXPECT_TRUE(db.AddVertexLabel("Person", + std::vector({{"id", FieldType::INT32, false}}), + vo)); + EdgeOptions eo; + eo.detach_property = true; + UT_EXPECT_TRUE(db.AddEdgeLabel("like", + std::vector({{"id", FieldType::INT32, false}}), + eo)); + UT_EXPECT_TRUE(db.AddEdgeLabel("know", + std::vector({{"id", FieldType::INT32, false}}), + eo)); + UT_EXPECT_TRUE(db.AddEdgeIndex("like", "id", IndexType::PairUniqueIndex)); + + std::vector vp{"id"}; + std::vector ep{"id"}; + auto txn = db.CreateWriteTxn(); + std::vector vids; + for (int i = 0; i < 100; i++) { + auto vid = txn.AddVertex( + std::string("Person"), vp, + {FieldData::Int32(i)}); + vids.push_back(vid); + } + for (int i = 0; i < vids.size()-1; i++) { + txn.AddEdge(vids[i], vids[i+1], + std::string("like"), vp, + {FieldData::Int32(i)}); + txn.AddEdge(vids[i], vids[i+1], + std::string("know"), vp, + {FieldData::Int32(i)}); + } + txn.Commit(); + for (int i = 0; i < vids.size()-1; i++) { + txn = db.CreateWriteTxn(); + UT_EXPECT_THROW_MSG(txn.AddEdge(vids[i], vids[i+1], + std::string("like"), vp, + {FieldData::Int32(i)}), "index value already exists"); + txn.Abort(); + } + txn = db.CreateWriteTxn(); + for (int i = 0; i < vids.size()-1; i++) { + txn.AddEdge(vids[i], vids[i+1], std::string("know"), vp, {FieldData::Int32(i)}); + } + txn.Commit(); + for (int i = 0; i < vids.size()-1; i++) { + txn = db.CreateWriteTxn(); + auto like_lid = txn.GetEdgeLabelId("like"); + auto fid = txn.GetEdgeFieldId(like_lid, "id"); + { + auto iter = txn.GetEdgePairUniqueIndexIterator( + like_lid, fid, vids[i], vids[i + 1], FieldData::Int32(i), FieldData::Int32(i)); + UT_EXPECT_TRUE(iter.IsValid()); + auto euid = iter.GetUid(); + UT_EXPECT_EQ(euid.src, vids[i]); + UT_EXPECT_EQ(euid.dst, vids[i+1]); + UT_EXPECT_EQ(euid.lid, like_lid); + auto iter2 = txn.GetOutEdgeIterator(euid); + UT_EXPECT_EQ(iter2.GetField("id"), FieldData::Int32(i)); + } + { + auto iter = txn.GetEdgePairUniqueIndexIterator(like_lid, fid, vids[i], vids[i + 1], + FieldData::Int32(i + 1), + FieldData::Int32(i + 1)); + if (iter.IsValid()) { + auto euid = iter.GetUid(); + UT_EXPECT_FALSE(euid.src == vids[i] && euid.dst == vids[i + 1] + && euid.lid == like_lid); + } + } + txn.Abort(); + } +} diff --git a/test/test_upsert.cpp b/test/test_upsert.cpp index 84aa3db0e9..ee9cb6de59 100644 --- a/test/test_upsert.cpp +++ b/test/test_upsert.cpp @@ -20,6 +20,255 @@ class TestUpsert : public TuGraphTest {}; +TEST_F(TestUpsert, upsertWithPairUniqueError) { + lgraph::GlobalConfig conf; + conf.db_dir = "./testdb"; + conf.http_port = 7774; + conf.enable_rpc = true; + conf.use_pthread = true; + conf.rpc_port = 19999; + conf.bind_host = "127.0.0.1"; + lgraph::AutoCleanDir cleaner(conf.db_dir); + auto server = StartLGraphServer(conf); + + lgraph::RpcClient client("127.0.0.1:19999", "admin", "73@TuGraph"); + std::string str; + + bool ret = client.CallCypher(str, "CALL db.createVertexLabel('node1', 'id' , " + "'id' ,'INT32', false, " + "'name' ,'STRING', false, " + "'num', 'INT32', false, 'desc', 'STRING', true)"); + UT_EXPECT_TRUE(ret); + ret = client.CallCypher(str, "CALL db.createVertexLabel('node2', 'id' , " + "'id' ,'INT32', false, " + "'name' ,'STRING', false, " + "'num', 'INT32', false, 'desc', 'STRING', true)"); + UT_EXPECT_TRUE(ret); + ret = client.CallCypher(str, + R"(CALL db.createEdgeLabel('edge1','[["node1","node2"]]', + 'id' ,'INT32', false, + 'name' ,'STRING', false, + 'num', 'INT32', false, 'desc', 'STRING', true))"); + UT_EXPECT_TRUE(ret); + // node1 + for (int i = 1; i <= 10; i++) { + auto prop = FMA_FMT("id:{}, name:'{}', num:{}", i, "name" + std::to_string(i), i); + prop = "{ " + prop + " }"; + ret = client.CallCypher(str, FMA_FMT("create (n:node1 {} )", prop)); + UT_EXPECT_TRUE(ret); + } + // node2 + for (int i = 1; i <= 10; i++) { + auto prop = FMA_FMT("id:{}, name:'{}', num:{}", i, "name" + std::to_string(i), i); + prop = "{ " + prop + " }"; + ret = client.CallCypher(str, FMA_FMT("create (n:node2 {} )", prop)); + UT_EXPECT_TRUE(ret); + } + // edge1 + { + nlohmann::json array = nlohmann::json::array(); + for (int i = 1; i <= 10; i++) { + nlohmann::json prop; + prop["node1_id"] = i; + prop["node2_id"] = i; + + prop["id"] = i; + prop["name"] = "name" + std::to_string(i); + prop["num"] = i; + prop["desc"] = "desc " + std::to_string(i); + array.push_back(prop); + } + nlohmann::json start; + start["type"] = "node1"; + start["key"] = "node1_id"; + nlohmann::json end; + end["type"] = "node2"; + end["key"] = "node2_id"; + std::string cypher = FMA_FMT("CALL db.upsertEdgeByJson('edge1','{}', '{}', '{}', 'num')", + start.dump(), + end.dump(), array.dump()); + ret = client.CallCypher(str, cypher); + UT_EXPECT_FALSE(ret); + UT_EXPECT_EQ(str, R"(No edge pair unique index is configured for this field: num)"); + } +} + +TEST_F(TestUpsert, upsertWithPairUnique) { + lgraph::GlobalConfig conf; + conf.db_dir = "./testdb"; + conf.http_port = 7774; + conf.enable_rpc = true; + conf.use_pthread = true; + conf.rpc_port = 19999; + conf.bind_host = "127.0.0.1"; + lgraph::AutoCleanDir cleaner(conf.db_dir); + auto server = StartLGraphServer(conf); + + lgraph::RpcClient client("127.0.0.1:19999", "admin", "73@TuGraph"); + std::string str; + + bool ret = client.CallCypher(str, "CALL db.createVertexLabel('node1', 'id' , " + "'id' ,'INT32', false, " + "'name' ,'STRING', false, " + "'num', 'INT32', false, 'desc', 'STRING', true)"); + UT_EXPECT_TRUE(ret); + ret = client.CallCypher(str, "CALL db.createVertexLabel('node2', 'id' , " + "'id' ,'INT32', false, " + "'name' ,'STRING', false, " + "'num', 'INT32', false, 'desc', 'STRING', true)"); + UT_EXPECT_TRUE(ret); + ret = client.CallCypher(str, + R"(CALL db.createEdgeLabel('edge1','[["node1","node2"]]', + 'id' ,'INT32', false, + 'name' ,'STRING', false, + 'num', 'INT32', false, 'desc', 'STRING', true))"); + UT_EXPECT_TRUE(ret); + ret = client.CallCypher(str, "CALL db.addEdgeIndex('edge1', 'num', false, true)"); + UT_EXPECT_TRUE(ret); + + // node1 + for (int i = 1; i <= 10; i++) { + auto prop = FMA_FMT("id:{}, name:'{}', num:{}", i, "name" + std::to_string(i), i); + prop = "{ " + prop + " }"; + ret = client.CallCypher(str, FMA_FMT("create (n:node1 {} )", prop)); + UT_EXPECT_TRUE(ret); + } + // node2 + for (int i = 1; i <= 10; i++) { + auto prop = FMA_FMT("id:{}, name:'{}', num:{}", i, "name" + std::to_string(i), i); + prop = "{ " + prop + " }"; + ret = client.CallCypher(str, FMA_FMT("create (n:node2 {} )", prop)); + UT_EXPECT_TRUE(ret); + } + // edge1 + { + nlohmann::json array = nlohmann::json::array(); + for (int i = 1; i <= 10; i++) { + nlohmann::json prop; + prop["node1_id"] = i; + prop["node2_id"] = i; + + prop["id"] = i; + prop["name"] = "name" + std::to_string(i); + prop["num"] = i; + prop["desc"] = "desc " + std::to_string(i); + array.push_back(prop); + } + nlohmann::json start; + start["type"] = "node1"; + start["key"] = "node1_id"; + nlohmann::json end; + end["type"] = "node2"; + end["key"] = "node2_id"; + std::string cypher = FMA_FMT("CALL db.upsertEdgeByJson('edge1','{}', '{}', '{}', 'num')", + start.dump(), end.dump(), array.dump()); + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"([{"data_error":0,"index_conflict":0,"insert":10,"total":10,"update":0}])"); + } + { + nlohmann::json array = nlohmann::json::array(); + for (int i = 1; i <= 10; i++) { + nlohmann::json prop; + prop["node1_id"] = i; + prop["node2_id"] = i; + + prop["id"] = i; + prop["name"] = "name" + std::to_string(i); + prop["num"] = i; + prop["desc"] = "desc " + std::to_string(i + 10); + array.push_back(prop); + } + nlohmann::json start; + start["type"] = "node1"; + start["key"] = "node1_id"; + nlohmann::json end; + end["type"] = "node2"; + end["key"] = "node2_id"; + std::string cypher = FMA_FMT("CALL db.upsertEdgeByJson('edge1','{}', '{}', '{}', 'num')", + start.dump(), end.dump(), array.dump()); + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"([{"data_error":0,"index_conflict":0,"insert":0,"total":10,"update":10}])"); + + cypher = "match(n)-[r]->(m) return r.desc limit 1"; + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"([{"r.desc":"desc 11"}])"); + + cypher = "match(n)-[r]->(m) return count(r)"; + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"xx([{"count(r)":10}])xx"); + } + { + nlohmann::json array = nlohmann::json::array(); + for (int i = 1; i <= 10; i++) { + nlohmann::json prop; + prop["node1_id"] = i; + prop["node2_id"] = i; + + prop["id"] = i; + prop["name"] = "name" + std::to_string(i); + prop["num"] = i+1; + prop["desc"] = "desc " + std::to_string(i); + array.push_back(prop); + } + nlohmann::json start; + start["type"] = "node1"; + start["key"] = "node1_id"; + nlohmann::json end; + end["type"] = "node2"; + end["key"] = "node2_id"; + std::string cypher = FMA_FMT("CALL db.upsertEdgeByJson('edge1','{}', '{}', '{}', 'num')", + start.dump(), end.dump(), array.dump()); + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"([{"data_error":0,"index_conflict":0,"insert":10,"total":10,"update":0}])"); + cypher = "match(n)-[r]->(m) return count(r)"; + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"xx([{"count(r)":20}])xx"); + cypher = "match(n)-[r]->(m) where n.id = 1 and m.id = 1 return r.desc"; + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"xx([{"r.desc":"desc 11"},{"r.desc":"desc 1"}])xx"); + } + { + nlohmann::json array = nlohmann::json::array(); + for (int i = 1; i <= 10; i++) { + nlohmann::json prop; + prop["node1_id"] = i; + prop["node2_id"] = i; + + prop["id"] = i; + prop["name"] = "name" + std::to_string(i); + prop["num"] = i+1; + prop["desc"] = "desc " + std::to_string(i+1); + array.push_back(prop); + } + nlohmann::json start; + start["type"] = "node1"; + start["key"] = "node1_id"; + nlohmann::json end; + end["type"] = "node2"; + end["key"] = "node2_id"; + std::string cypher = FMA_FMT("CALL db.upsertEdgeByJson('edge1','{}', '{}', '{}', 'num')", + start.dump(), end.dump(), array.dump()); + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"([{"data_error":0,"index_conflict":0,"insert":0,"total":10,"update":10}])"); + cypher = "match(n)-[r]->(m) return count(r)"; + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"xx([{"count(r)":20}])xx"); + cypher = "match(n)-[r]->(m) where n.id = 1 and m.id = 1 return r.desc"; + ret = client.CallCypher(str, cypher); + UT_EXPECT_TRUE(ret); + UT_EXPECT_EQ(str, R"xx([{"r.desc":"desc 11"},{"r.desc":"desc 2"}])xx"); + } +} + TEST_F(TestUpsert, upsert) { lgraph::GlobalConfig conf; conf.db_dir = "./testdb";