Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions cpp/refactor_module/algorithm/refactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -863,3 +863,127 @@ void Refactor::RenameType(mgp_list *args, mgp_graph *memgraph_graph, mgp_result
return;
}
}

void Refactor::MergeNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
mgp::MemoryDispatcherGuard guard{memory};
const auto arguments = mgp::List(args);
auto graph = mgp::Graph(memgraph_graph);
const auto record_factory = mgp::RecordFactory(result);

try {
const auto nodes = arguments[0].ValueList();
if (nodes.Empty()) {
throw mgp::ValueException(kMergeNodesEmptyListError.data());
}

// Verify all elements are nodes
for (const auto &node_value : nodes) {
if (!node_value.IsNode()) {
throw mgp::ValueException(kMergeNodesInvalidTypeError.data());
}
}

const auto config = arguments[1].ValueMap();
const bool mergeRels = [&config, &kMergeNodesRelationshipsStrategy]() -> bool {
if (config.KeyExists(kMergeNodesRelationshipsStrategy)) {
if (!config.At(kMergeNodesRelationshipsStrategy).IsBool()) {
throw mgp::ValueException(kMergeRelationshipsInvalidValueError.data());
}

return config.At(kMergeNodesRelationshipsStrategy).ValueBool();
}

return false;
}();

// Get properties strategy from config
std::string prop_strategy = [&config, &kMergeNodesPropertiesStrategy, &kMergeNodesPropertiesStrategyAlternative,
&kMergeNodesPropertiesCombine]() -> std::string {
if (config.KeyExists(kMergeNodesPropertiesStrategy)) {
return std::string(config.At(kMergeNodesPropertiesStrategy).ValueString());
}
if (config.KeyExists(kMergeNodesPropertiesStrategyAlternative)) {
return std::string(config.At(kMergeNodesPropertiesStrategyAlternative).ValueString());
}
return std::string(kMergeNodesPropertiesCombine);
}();

// Convert to lowercase for case-insensitive comparison
std::transform(prop_strategy.begin(), prop_strategy.end(), prop_strategy.begin(),
[](unsigned char c) { return std::tolower(c); });

// Validate property strategy
if (prop_strategy != kMergeNodesPropertiesCombine && prop_strategy != kMergeNodesPropertiesDiscard &&
prop_strategy != kMergeNodesPropertiesOverride && prop_strategy != kMergeNodesPropertiesOverwrite) {
throw mgp::ValueException(kMergeNodesInvalidPropertyStrategyError.data());
}

// Get the first node as the target node
auto target_node = nodes[0].ValueNode();

// Process remaining nodes
for (size_t i = 1; i < nodes.Size(); ++i) {
auto source_node = nodes[i].ValueNode();

// Handle properties based on strategy
// Discard properties keeps the target properties so it's not handled in if-else
if (prop_strategy == kMergeNodesPropertiesCombine) {
// Combine properties from both nodes
auto source_props = source_node.Properties();
for (const auto &[key, value] : source_props) {
auto target_property = target_node.GetProperty(key);
if (target_property.IsList()) {
auto target_list = target_property.ValueList();
target_list.AppendExtend(source_props[key]);
target_node.SetProperty(key, mgp::Value(std::move(target_list)));
} else if (!target_property.IsNull()) {
auto combined_properties = mgp::List();
combined_properties.AppendExtend(target_property);
combined_properties.AppendExtend(source_props[key]);
target_node.SetProperty(key, mgp::Value(std::move(combined_properties)));
} else {
target_node.SetProperty(key, source_props[key]);
}
}
} else if (prop_strategy == kMergeNodesPropertiesOverride ||
prop_strategy == kMergeNodesPropertiesOverwrite) {
// Override/overwrite target properties with source properties
auto source_props = source_node.Properties();
for (const auto &[key, value] : source_props) {
target_node.SetProperty(key, value);
}
}

// Copy labels from source to target
auto source_labels = source_node.Labels();
for (size_t j = 0; j < source_labels.Size(); ++j) {
target_node.AddLabel(std::move(source_labels[j]));
}

// Handle relationships
// Copy all relationships from source to target
if (mergeRels) {
auto in_rels = source_node.InRelationships();
for (const auto &rel : in_rels) {
graph.CreateRelationship(rel.From(), target_node, rel.Type());
}

auto out_rels = source_node.OutRelationships();
for (const auto &rel : out_rels) {
graph.CreateRelationship(target_node, rel.To(), rel.Type());
}
}

// Delete the source node
graph.DetachDeleteNode(source_node);
}

// Return the merged node
auto record = record_factory.NewRecord();
record.Insert(kMergeNodesResult.data(), target_node);

} catch (const std::exception &e) {
record_factory.SetErrorMessage(e.what());
return;
}
}
19 changes: 19 additions & 0 deletions cpp/refactor_module/algorithm/refactor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,23 @@ constexpr std::string_view kRenameTypePropertyArg2 = "new_property";
constexpr std::string_view kRenameTypePropertyArg3 = "rels";
constexpr std::string_view kRenameTypePropertyResult = "relationships_changed";

/* merge_nodes constants */
constexpr std::string_view kProcedureMergeNodes = "merge_nodes";
constexpr std::string_view kMergeNodesArgNodes = "nodes";
constexpr std::string_view kMergeNodesArgConfig = "config";
constexpr std::string_view kMergeNodesResult = "node";
constexpr std::string_view kMergeNodesPropertiesStrategy = "properties";
constexpr std::string_view kMergeNodesPropertiesStrategyAlternative = ".*";
constexpr std::string_view kMergeNodesRelationshipsStrategy = "mergeRels";
constexpr std::string_view kMergeNodesPropertiesCombine = "combine";
constexpr std::string_view kMergeNodesPropertiesDiscard = "discard";
constexpr std::string_view kMergeNodesPropertiesOverride = "override";
constexpr std::string_view kMergeNodesPropertiesOverwrite = "overwrite";
constexpr std::string_view kMergeNodesEmptyListError = "The nodes list cannot be empty";
constexpr std::string_view kMergeNodesInvalidTypeError = "All elements in the nodes list must be of type Node";
constexpr std::string_view kMergeRelationshipsInvalidValueError = "mergeRels parameter must either be true or false";
constexpr std::string_view kMergeNodesInvalidPropertyStrategyError = "Property strategy must be one of: combine, discard, override/overwrite";

void From(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory);

void To(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory);
Expand Down Expand Up @@ -171,4 +188,6 @@ void NormalizeAsBoolean(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *r
void ExtractNode(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory);

void RenameType(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory);

void MergeNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory);
} // namespace Refactor
5 changes: 5 additions & 0 deletions cpp/refactor_module/refactor_module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *mem
{mgp::Return(std::string(Refactor::kRenameTypePropertyResult).c_str(), mgp::Type::Int)}, module,
memory);

AddProcedure(Refactor::MergeNodes, Refactor::kProcedureMergeNodes, mgp::ProcedureType::Write,
{mgp::Parameter(Refactor::kMergeNodesArgNodes, {mgp::Type::List, mgp::Type::Node}),
mgp::Parameter(Refactor::kMergeNodesArgConfig, mgp::Type::Map, mgp::Value(mgp::Map{}))},
{mgp::Return(Refactor::kMergeNodesResult, mgp::Type::Node)}, module, memory);

} catch (const std::exception &e) {
return 1;
}
Expand Down
1 change: 1 addition & 0 deletions e2e/refactor_test/test_merge_nodes_asterisk/input.cyp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'}) CREATE (n3:Person {name: 'Charlie', age: 35, city: 'London'}) CREATE (n1)-[:KNOWS {since: 2020}]->(n2) CREATE (n2)-[:WORKS_WITH {project: 'Project X'}]->(n3) CREATE (n3)-[:FRIENDS_WITH {since: 2019}]->(n1);
8 changes: 8 additions & 0 deletions e2e/refactor_test/test_merge_nodes_asterisk/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query: >
MATCH (n1:Person {age: 30}), (n2:Person {name: 'Bob'}), (n3:Person {name: 'Charlie'})
CALL refactor.merge_nodes([n1, n2, n3], {`.*`: 'combine', mergeRels: true}) YIELD node
RETURN node.name as name, outDegree(node) as out_degree, inDegree(node) as in_degree;
output:
- name: [Bob, Charlie]
out_degree: 3
in_degree: 3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {name: 'Alice', age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'});
9 changes: 9 additions & 0 deletions e2e/refactor_test/test_merge_nodes_case_insensitive/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query: >
MATCH (n1:Person {name: 'Alice'}), (n2:Person {name: 'Bob'})
CALL refactor.merge_nodes([n1, n2], {properties: 'COMBINE'}) YIELD node
RETURN node.name as name, node.age as age, node.city as city, node.country as country;
output:
- name: [Alice, Bob]
age: [30, 25]
city: New York
country: USA
1 change: 1 addition & 0 deletions e2e/refactor_test/test_merge_nodes_combine_basic/input.cyp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {name: 'Alice', age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'});
9 changes: 9 additions & 0 deletions e2e/refactor_test/test_merge_nodes_combine_basic/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query: >
MATCH (n1:Person {name: 'Alice'}), (n2:Person {name: 'Bob'})
CALL refactor.merge_nodes([n1, n2], {properties: 'combine'}) YIELD node
RETURN node.name as name, node.age as age, node.city as city, node.country as country;
output:
- name: [Alice, Bob]
age: [30, 25]
city: New York
country: USA
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n4:Employee {name: 'David', salary: 50000}) CREATE (n5:Manager {name: 'Eve', department: 'IT'});
9 changes: 9 additions & 0 deletions e2e/refactor_test/test_merge_nodes_combine_basic_2/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query: >
MATCH (n4:Employee {name: 'David'}), (n5:Manager {name: 'Eve'})
CALL refactor.merge_nodes([n4, n5], {properties: 'combine'}) YIELD node
RETURN node.name as name, node.salary as salary, node.department as department, labels(node) as labels;
output:
- name: [David, Eve]
salary: 50000
department: IT
labels: [Employee, Manager]
1 change: 1 addition & 0 deletions e2e/refactor_test/test_merge_nodes_combine_merge/input.cyp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'}) CREATE (n3:Person {name: 'Charlie', age: 35, city: 'London'}) CREATE (n1)-[:KNOWS {since: 2020}]->(n2) CREATE (n2)-[:WORKS_WITH {project: 'Project X'}]->(n3) CREATE (n3)-[:FRIENDS_WITH {since: 2019}]->(n1);
8 changes: 8 additions & 0 deletions e2e/refactor_test/test_merge_nodes_combine_merge/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query: >
MATCH (n1:Person {age: 30}), (n2:Person {name: 'Bob'}), (n3:Person {name: 'Charlie'})
CALL refactor.merge_nodes([n1, n2, n3], {properties: 'combine', mergeRels: true}) YIELD node
RETURN node.name as name, outDegree(node) as out_degree, inDegree(node) as in_degree;
output:
- name: [Bob, Charlie]
out_degree: 3
in_degree: 3
1 change: 1 addition & 0 deletions e2e/refactor_test/test_merge_nodes_discard_basic/input.cyp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {name: 'Alice', age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'});
9 changes: 9 additions & 0 deletions e2e/refactor_test/test_merge_nodes_discard_basic/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query: >
MATCH (n1:Person {name: 'Alice'}), (n2:Person {name: 'Bob'})
CALL refactor.merge_nodes([n1, n2], {properties: 'discard'}) YIELD node
RETURN node.name as name, node.age as age, node.city as city, node.country as country;
output:
- name: Alice
age: 30
city: New York
country: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query: >
CALL refactor.merge_nodes([], {properties: 'combine'}) YIELD node
RETURN node;
exception: >-
"The nodes list cannot be empty"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'}) CREATE (n3:Person {name: 'Charlie', age: 35, city: 'London'}) CREATE (n1)-[:KNOWS {since: 2020}]->(n2) CREATE (n2)-[:WORKS_WITH {project: 'Project X'}]->(n3) CREATE (n3)-[:FRIENDS_WITH {since: 2019}]->(n1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query: >
MATCH (n1:Person {age: 30}), (n2:Person {name: 'Bob'}), (n3:Person {name: 'Charlie'})
CALL refactor.merge_nodes([n1, n2, n3], {`.*`: 'combine', mergeRels: "nothing"}) YIELD node
RETURN node.name as name, outDegree(node) as out_degree, inDegree(node) as in_degree;
exception: >
mergeRels parameter must either be true or false

1 change: 1 addition & 0 deletions e2e/refactor_test/test_merge_nodes_merge_false/input.cyp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'}) CREATE (n3:Person {name: 'Charlie', age: 35, city: 'London'}) CREATE (n1)-[:KNOWS {since: 2020}]->(n2) CREATE (n2)-[:WORKS_WITH {project: 'Project X'}]->(n3) CREATE (n3)-[:FRIENDS_WITH {since: 2019}]->(n1);
8 changes: 8 additions & 0 deletions e2e/refactor_test/test_merge_nodes_merge_false/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query: >
MATCH (n1:Person {age: 30}), (n2:Person {name: 'Bob'}), (n3:Person {name: 'Charlie'})
CALL refactor.merge_nodes([n1, n2, n3], {`.*`: 'combine', mergeRels: false}) YIELD node
RETURN node.name as name, outDegree(node) as out_degree, inDegree(node) as in_degree;
output:
- name: [Bob, Charlie]
out_degree: 0
in_degree: 0
1 change: 1 addition & 0 deletions e2e/refactor_test/test_merge_nodes_merge_rels/input.cyp
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {name: 'Alice', age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'}) CREATE (n3:Person {name: 'Charlie', age: 35, city: 'London'}) CREATE (n1)-[:KNOWS {since: 2020}]->(n2) CREATE (n2)-[:WORKS_WITH {project: 'Project X'}]->(n3) CREATE (n3)-[:FRIENDS_WITH {since: 2019}]->(n1);
8 changes: 8 additions & 0 deletions e2e/refactor_test/test_merge_nodes_merge_rels/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query: >
MATCH (n1:Person {name: 'Alice'}), (n2:Person {name: 'Bob'}), (n3:Person {name: 'Charlie'})
CALL refactor.merge_nodes([n1, n2, n3], {properties: 'combine', mergeRels: true}) YIELD node
RETURN node.name as name, outDegree(node) as out_degree, inDegree(node) as in_degree;
output:
- name: [Alice, Bob, Charlie]
out_degree: 3
in_degree: 3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {name: 'Alice', age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'});
9 changes: 9 additions & 0 deletions e2e/refactor_test/test_merge_nodes_override_basic/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query: >
MATCH (n1:Person {name: 'Alice'}), (n2:Person {name: 'Bob'})
CALL refactor.merge_nodes([n1, n2], {properties: 'override'}) YIELD node
RETURN node.name as name, node.age as age, node.city as city, node.country as country;
output:
- name: Bob
age: 25
city: New York
country: USA
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE (n1:Person {name: 'Alice', age: 30, city: 'New York'}) CREATE (n2:Person {name: 'Bob', age: 25, country: 'USA'});
9 changes: 9 additions & 0 deletions e2e/refactor_test/test_merge_nodes_overwrite_basic/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query: >
MATCH (n1:Person {name: 'Alice'}), (n2:Person {name: 'Bob'})
CALL refactor.merge_nodes([n1, n2], {properties: 'overwrite'}) YIELD node
RETURN node.name as name, node.age as age, node.city as city, node.country as country;
output:
- name: Bob
age: 25
city: New York
country: USA