diff --git a/cpp/merge_module/algorithm/merge.cpp b/cpp/merge_module/algorithm/merge.cpp index 3402a47b3..f5c001e14 100644 --- a/cpp/merge_module/algorithm/merge.cpp +++ b/cpp/merge_module/algorithm/merge.cpp @@ -1,11 +1,103 @@ #include "merge.hpp" - #include #include #include #include #include "mgp.hpp" +bool Merge::LabelsContained(const std::unordered_set &labels, const mgp::Node &node) { + bool contained = false; + auto size = labels.size(); // this works if labels are unique, which they are + size_t counter = 0; + + for (const auto label : node.Labels()) { + if (labels.find(label) != labels.end()) { + counter++; + } + } + + if (counter == size) { + contained = true; + } + + return contained; +} + +bool Merge::IdentProp(const mgp::Map &ident_prop, const mgp::Node &node) { + bool match = true; + for (const auto &[key, value] : ident_prop) { + if (value != node.GetProperty(std::string(key))) { + match = false; + } + } + return match; +} + +void Merge::Node(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) { + mgp::MemoryDispatcherGuard guard{memory}; + const auto arguments = mgp::List(args); + const auto record_factory = mgp::RecordFactory(result); + try { + auto graph = mgp::Graph(memgraph_graph); + auto labels = arguments[0].ValueList(); + auto ident_prop = arguments[1].ValueMap(); + auto create_prop = arguments[2].ValueMap(); + auto match_prop = arguments[3].ValueMap(); + std::unordered_set label_set; + std::unordered_map ident_prop_map; + std::unordered_map create_prop_map; + std::unordered_map match_prop_map; + /*conversion of mgp::Maps to unordered_map for easier use of SetProperties(it expects an unordered map as + * argument)*/ + auto convert_to_map = [](const auto& source, auto& destination) { + for (const auto& [key, value] : source) { + destination.emplace(key, value); + } + }; + convert_to_map(ident_prop, ident_prop_map); + + convert_to_map(create_prop, create_prop_map); + + convert_to_map(match_prop, match_prop_map); + + + /*creating a set of labels for O(1) check of labels*/ + for (const auto elem : labels) { + const auto label = elem.ValueString(); + if (label.empty()) { + throw mgp::ValueException("List of labels cannot contain empty string!"); + } + label_set.insert(elem.ValueString()); + } + + bool matched = false; + for (auto node : graph.Nodes()) { + //check if node already exists, if true, merge, if not, create + if (LabelsContained(label_set, node) && IdentProp(ident_prop, node)) { + matched = true; + node.SetProperties(match_prop_map); + auto record = record_factory.NewRecord(); + record.Insert(std::string(kNodeRes).c_str(), node); + } + } + if (!matched) { + auto node = graph.CreateNode(); + for (const auto label : label_set) { + node.AddLabel(label); + } + // when merge creates it creates identyfing props also + node.SetProperties(ident_prop_map); + node.SetProperties(create_prop_map); + auto record = record_factory.NewRecord(); + record.Insert(std::string(kNodeRes).c_str(), node); + } + + } catch (const std::exception &e) { + record_factory.SetErrorMessage(e.what()); + return; + } +} + namespace { template diff --git a/cpp/merge_module/algorithm/merge.hpp b/cpp/merge_module/algorithm/merge.hpp index f0ac91680..7306ddd21 100644 --- a/cpp/merge_module/algorithm/merge.hpp +++ b/cpp/merge_module/algorithm/merge.hpp @@ -2,6 +2,9 @@ #include +#include +#include + namespace Merge { /* relationship constants */ @@ -14,6 +17,20 @@ constexpr const std::string_view kRelationshipArg5 = "endNode"; constexpr const std::string_view kRelationshipArg6 = "onMatchProps"; constexpr const std::string_view kRelationshipResult = "rel"; +/* node constants */ +constexpr std::string_view kProcedureNode = "node"; +constexpr std::string_view kNodeArg1 = "labels"; +constexpr std::string_view kNodeArg2 = "identProps"; +constexpr std::string_view kNodeArg3 = "createProps"; +constexpr std::string_view kNodeArg4 = "matchProps"; +constexpr std::string_view kNodeRes = "node"; + void Relationship(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); + + +void Node(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory); +bool IdentProp(const mgp::Map &identProp, const mgp::Node &node); +bool LabelsContained(const std::unordered_set &labels, const mgp::Node &node); + } // namespace Merge diff --git a/cpp/merge_module/merge_module.cpp b/cpp/merge_module/merge_module.cpp index 51a943480..db097d754 100644 --- a/cpp/merge_module/merge_module.cpp +++ b/cpp/merge_module/merge_module.cpp @@ -5,6 +5,12 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) { try { mgp::MemoryDispatcherGuard guard{memory}; + AddProcedure(Merge::Node, std::string(Merge::kProcedureNode).c_str(), mgp::ProcedureType::Write, + {mgp::Parameter(std::string(Merge::kNodeArg1).c_str(), {mgp::Type::List, mgp::Type::String}), + mgp::Parameter(std::string(Merge::kNodeArg2).c_str(), {mgp::Type::Map, mgp::Type::Any}), + mgp::Parameter(std::string(Merge::kNodeArg3).c_str(), {mgp::Type::Map, mgp::Type::Any}), + mgp::Parameter(std::string(Merge::kNodeArg4).c_str(), {mgp::Type::Map, mgp::Type::Any})}, + {mgp::Return(std::string(Merge::kNodeRes).c_str(), mgp::Type::Node)}, module, memory); AddProcedure(Merge::Relationship, Merge::kProcedureRelationship, mgp::ProcedureType::Write, {mgp::Parameter(Merge::kRelationshipArg1, mgp::Type::Node), diff --git a/e2e_correctness/merge_test/test_node1/input.cyp b/e2e_correctness/merge_test/test_node1/input.cyp new file mode 100644 index 000000000..af94f4ec9 --- /dev/null +++ b/e2e_correctness/merge_test/test_node1/input.cyp @@ -0,0 +1 @@ +CREATE (:Dog:Dog {name: "Rex", id:0}), (:Human:Employee {name: "Junior", id:1}),(:Human:Boss {name: "Senior", id:2}) diff --git a/e2e_correctness/merge_test/test_node1/test.yml b/e2e_correctness/merge_test/test_node1/test.yml new file mode 100644 index 000000000..f7ec773f0 --- /dev/null +++ b/e2e_correctness/merge_test/test_node1/test.yml @@ -0,0 +1,5 @@ +memgraph_query: > + CALL merge.node(["Human"],{name: "Senior"},{},{name:"Boss"}) YIELD node RETURN node; + +neo4j_query: > + CALL apoc.merge.node(["Human"],{name: "Senior"},{},{name: "Boss"}) YIELD node; diff --git a/e2e_correctness/merge_test/test_node2/input.cyp b/e2e_correctness/merge_test/test_node2/input.cyp new file mode 100644 index 000000000..f07f12eb2 --- /dev/null +++ b/e2e_correctness/merge_test/test_node2/input.cyp @@ -0,0 +1 @@ +CREATE (:Dog:Dog {name: "Rex", id:0}), (:Human:Employee {name: "Rex", id:1}),(:Human:Boss {name: "Senior", id:2}) diff --git a/e2e_correctness/merge_test/test_node2/test.yml b/e2e_correctness/merge_test/test_node2/test.yml new file mode 100644 index 000000000..fa23306f9 --- /dev/null +++ b/e2e_correctness/merge_test/test_node2/test.yml @@ -0,0 +1,5 @@ +memgraph_query: > + CALL merge.node([],{name: "Rex"},{},{name:"Carlito"}) YIELD node RETURN node; + +neo4j_query: > + MERGE (n {name: "Rex" }) ON CREATE SET n += {} ON MATCH SET n += {name: "Carlito"} RETURN n; diff --git a/e2e_correctness/merge_test/test_node3/input.cyp b/e2e_correctness/merge_test/test_node3/input.cyp new file mode 100644 index 000000000..af94f4ec9 --- /dev/null +++ b/e2e_correctness/merge_test/test_node3/input.cyp @@ -0,0 +1 @@ +CREATE (:Dog:Dog {name: "Rex", id:0}), (:Human:Employee {name: "Junior", id:1}),(:Human:Boss {name: "Senior", id:2}) diff --git a/e2e_correctness/merge_test/test_node3/test.yml b/e2e_correctness/merge_test/test_node3/test.yml new file mode 100644 index 000000000..8a17d4a15 --- /dev/null +++ b/e2e_correctness/merge_test/test_node3/test.yml @@ -0,0 +1,5 @@ +memgraph_query: > + CALL merge.node([],{},{},{graph:"this_one"}) YIELD node RETURN node; + +neo4j_query: > + MERGE (n) ON CREATE SET n += {} ON MATCH SET n += {graph:"this_one"} RETURN n; diff --git a/e2e_correctness/merge_test/test_node4/input.cyp b/e2e_correctness/merge_test/test_node4/input.cyp new file mode 100644 index 000000000..af94f4ec9 --- /dev/null +++ b/e2e_correctness/merge_test/test_node4/input.cyp @@ -0,0 +1 @@ +CREATE (:Dog:Dog {name: "Rex", id:0}), (:Human:Employee {name: "Junior", id:1}),(:Human:Boss {name: "Senior", id:2}) diff --git a/e2e_correctness/merge_test/test_node4/test.yml b/e2e_correctness/merge_test/test_node4/test.yml new file mode 100644 index 000000000..9ac5bfdc6 --- /dev/null +++ b/e2e_correctness/merge_test/test_node4/test.yml @@ -0,0 +1,5 @@ +memgraph_query: > + CALL merge.node(["Dog"],{name: "Rico"},{height:199, id:9},{}) YIELD node RETURN node; + +neo4j_query: > + CALL apoc.merge.node(["Dog"],{name: "Rico"},{height:199, id:9},{}) YIELD node; diff --git a/e2e_correctness/merge_test/test_node5/input.cyp b/e2e_correctness/merge_test/test_node5/input.cyp new file mode 100644 index 000000000..af94f4ec9 --- /dev/null +++ b/e2e_correctness/merge_test/test_node5/input.cyp @@ -0,0 +1 @@ +CREATE (:Dog:Dog {name: "Rex", id:0}), (:Human:Employee {name: "Junior", id:1}),(:Human:Boss {name: "Senior", id:2}) diff --git a/e2e_correctness/merge_test/test_node5/test.yml b/e2e_correctness/merge_test/test_node5/test.yml new file mode 100644 index 000000000..dc58c4ed8 --- /dev/null +++ b/e2e_correctness/merge_test/test_node5/test.yml @@ -0,0 +1,5 @@ +memgraph_query: > + CALL merge.node(["Dog"],{name: "Rex"},{height:199},{graph:"this_one"}) YIELD node RETURN node; + +neo4j_query: > + CALL apoc.merge.node(["Dog"],{name: "Rex"},{height:124727},{graph:"this_one"}) YIELD node; diff --git a/e2e_correctness/merge_test/test_node6/input.cyp b/e2e_correctness/merge_test/test_node6/input.cyp new file mode 100644 index 000000000..79e194d22 --- /dev/null +++ b/e2e_correctness/merge_test/test_node6/input.cyp @@ -0,0 +1 @@ +CREATE (:Dog:Dog {name: "Rex", id:0}), (:Human:Boss {name: "", id:1}),(:Human:Boss {name: "", id:2}) diff --git a/e2e_correctness/merge_test/test_node6/test.yml b/e2e_correctness/merge_test/test_node6/test.yml new file mode 100644 index 000000000..0cb0f11b0 --- /dev/null +++ b/e2e_correctness/merge_test/test_node6/test.yml @@ -0,0 +1,5 @@ +memgraph_query: > + CALL merge.node(["Human","Boss"],{name: ""},{height:199},{name:"Le Boss"}) YIELD node RETURN node; + +neo4j_query: > + CALL apoc.merge.node(["Human","Boss"],{name: ""},{height:124727},{name:"Le Boss"}) YIELD node;