From 216c642a568326ded687ec71a309d75b3dd4cbe1 Mon Sep 17 00:00:00 2001 From: rachelwelchio Date: Sun, 16 Aug 2026 14:08:52 +0800 Subject: [PATCH 1/2] fix: keep orphan nodes after removeEdge (empty erase guard) Signed-off-by: rachelwelchio --- include/CXXGraph/Graph/Graph_impl.hpp | 40 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/include/CXXGraph/Graph/Graph_impl.hpp b/include/CXXGraph/Graph/Graph_impl.hpp index 85b10b60..56fc3acb 100644 --- a/include/CXXGraph/Graph/Graph_impl.hpp +++ b/include/CXXGraph/Graph/Graph_impl.hpp @@ -123,6 +123,10 @@ std::optional Graph::addEdge(shared> edge) { auto& [from, to] = edge->getNodePair(); + // Connected nodes are no longer isolated + isolatedNodesSet.erase(from); + isolatedNodesSet.erase(to); + (*cachedAdjListOut)[from].emplace_back(to, edge); (*cachedAdjListIn)[to].emplace_back(from, edge); if (!edge.get()->isDirected().value_or(true)) { @@ -180,7 +184,10 @@ void Graph::removeEdge(const std::string& edgeUserId) { edgeSet.erase(std::find_if(this->edgeSet.begin(), this->edgeSet.end(), [edgeOpt](const Edge *edge) { return (*(edgeOpt.value()) == *edge); })); */ - edgeSet.erase(edgeSet.find(edgeOpt.value())); + auto edgeIt = edgeSet.find(edgeOpt.value()); + if (edgeIt != edgeSet.end()) { + edgeSet.erase(edgeIt); + } auto& [from, to] = edgeOpt.value().get()->getNodePair(); @@ -255,6 +262,19 @@ void Graph::removeEdge(const std::string& edgeUserId) { } } } + + // Endpoints that no longer appear in any adjacency list become isolated + // nodes (same approach as Hypergraph::unregisterEdgeFromIncidence_). + auto stillConnected = [this](const shared>& node) { + return cachedAdjListOut->find(node) != cachedAdjListOut->end() || + cachedAdjListIn->find(node) != cachedAdjListIn->end(); + }; + if (!stillConnected(from)) { + isolatedNodesSet.insert(from); + } + if (!stillConnected(to)) { + isolatedNodesSet.insert(to); + } } } @@ -266,7 +286,10 @@ void Graph::removeEdge(const CXXGraph::id_t edgeId) { edgeSet.erase(std::find_if(this->edgeSet.begin(), this->edgeSet.end(), [edgeOpt](const Edge *edge) { return (*(edgeOpt.value()) == *edge); })); */ - edgeSet.erase(edgeSet.find(edgeOpt.value())); + auto edgeIt = edgeSet.find(edgeOpt.value()); + if (edgeIt != edgeSet.end()) { + edgeSet.erase(edgeIt); + } auto& [from, to] = edgeOpt.value().get()->getNodePair(); @@ -341,6 +364,19 @@ void Graph::removeEdge(const CXXGraph::id_t edgeId) { } } } + + // Endpoints that no longer appear in any adjacency list become isolated + // nodes (same approach as Hypergraph::unregisterEdgeFromIncidence_). + auto stillConnected = [this](const shared>& node) { + return cachedAdjListOut->find(node) != cachedAdjListOut->end() || + cachedAdjListIn->find(node) != cachedAdjListIn->end(); + }; + if (!stillConnected(from)) { + isolatedNodesSet.insert(from); + } + if (!stillConnected(to)) { + isolatedNodesSet.insert(to); + } } } From 013993b8926a8fde11c65239cad843c90e0fd9a7 Mon Sep 17 00:00:00 2001 From: rachelwelchio Date: Sun, 16 Aug 2026 14:08:56 +0800 Subject: [PATCH 2/2] fix: keep orphan nodes after removeEdge (empty erase guard) Signed-off-by: rachelwelchio --- test/GraphTest.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/GraphTest.cpp b/test/GraphTest.cpp index d3e1fb90..d4722670 100644 --- a/test/GraphTest.cpp +++ b/test/GraphTest.cpp @@ -1411,6 +1411,30 @@ TEST(GraphTest, remove_node_connectedNode2) { ASSERT_EQ(graph.getEdgeSet().size(), 1); } +TEST(GraphTest, removeEdge_keepsOrphanNodesIsolated) { + // Regression for #497: removeEdge must not drop endpoints that were only + // introduced via that edge; they should remain as isolated nodes. + CXXGraph::Node a("A", 1); + CXXGraph::Node b("B", 2); + CXXGraph::DirectedWeightedEdge ab("ab", a, b, 1.0); + + CXXGraph::Graph graph; + graph.addEdge(&ab); + + ASSERT_EQ(graph.getNodeSet().size(), 2); + ASSERT_EQ(graph.getEdgeSet().size(), 1); + ASSERT_TRUE(graph.getNode("A").has_value()); + ASSERT_TRUE(graph.getNode("B").has_value()); + + graph.removeEdge("ab"); + + ASSERT_EQ(graph.getEdgeSet().size(), 0); + ASSERT_EQ(graph.getNodeSet().size(), 2); + ASSERT_EQ(graph.getIsolatedNodeSet().size(), 2); + ASSERT_TRUE(graph.getNode("A").has_value()); + ASSERT_TRUE(graph.getNode("B").has_value()); +} + TEST(GraphTest, remove_node_removeInvalidNode) { /** Test to call the remove_node function on a node that was never added. In * this case getNode will return an optional that is nullptr*/