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 8 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
2 changes: 1 addition & 1 deletion cpp/memgraph
Submodule memgraph updated 440 files
130 changes: 130 additions & 0 deletions cpp/refactor_module/algorithm/refactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -863,3 +863,133 @@ 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(std::string(kMergeNodesEmptyListError).c_str());
Comment thread
Josipmrden marked this conversation as resolved.
Outdated
}

// Verify all elements are nodes
for (const auto &node_value : nodes) {
if (!node_value.IsNode()) {
throw mgp::ValueException(std::string(kMergeNodesInvalidTypeError).c_str());
Comment thread
Josipmrden marked this conversation as resolved.
Outdated
}
}

const auto config = arguments[1].ValueMap();
bool mergeRels = false;
if (config.KeyExists(kMergeNodesRelationshipsStrategy)) {
if (!config.At(kMergeNodesRelationshipsStrategy).IsBool()) {
throw mgp::ValueException(std::string(kMergeRelationshipsInvalidValueError).c_str());
}

mergeRels = config.At(kMergeNodesRelationshipsStrategy).ValueBool();
}
Comment thread
Josipmrden marked this conversation as resolved.
Outdated

// Get properties strategy from config
std::string prop_strategy = std::string(kMergeNodesPropertiesCombine);
if (config.KeyExists(kMergeNodesPropertiesStrategy)) {
prop_strategy = config.At(kMergeNodesPropertiesStrategy).ValueString();
}
if (config.KeyExists(kMergeNodesPropertiesStrategyAlternative)) {
prop_strategy = config.At(kMergeNodesPropertiesStrategyAlternative).ValueString();
}
Comment thread
Josipmrden marked this conversation as resolved.
Outdated

const auto mergeNodesPropertiesCombineString = std::string(kMergeNodesPropertiesCombine);
const auto mergeNodesPropertiesDiscardString = std::string(kMergeNodesPropertiesDiscard);
const auto mergeNodesPropertiesOverrideString = std::string(kMergeNodesPropertiesOverride);
const auto mergeNodesPropertiesOverwriteString = std::string(kMergeNodesPropertiesOverwrite);

// 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 != mergeNodesPropertiesCombineString && prop_strategy != mergeNodesPropertiesDiscardString &&
Comment thread
Josipmrden marked this conversation as resolved.
Outdated
prop_strategy != mergeNodesPropertiesOverrideString && prop_strategy != mergeNodesPropertiesOverwriteString) {
throw mgp::ValueException(std::string(kMergeNodesInvalidPropertyStrategyError).c_str());
}

// 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
if (prop_strategy == mergeNodesPropertiesCombineString) {
// Combine properties from both nodes
auto source_props = source_node.Properties();
for (const auto &[key, value] : source_props) {
if (!source_props.contains(key)) {
// nothing to combine
continue;
}
Comment thread
Josipmrden marked this conversation as resolved.
Outdated

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 == mergeNodesPropertiesDiscardString) {
// Keep only target node properties
// No action needed
Comment thread
Josipmrden marked this conversation as resolved.
Outdated
} else if (prop_strategy == mergeNodesPropertiesOverrideString ||
prop_strategy == mergeNodesPropertiesOverwriteString) {
// 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(source_labels[j]);
Comment thread
Josipmrden marked this conversation as resolved.
Outdated
}

// 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(std::string(kMergeNodesResult).c_str(), 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
Loading