Skip to content

Commit 5aa7072

Browse files
committed
Add graph.UpdateVertex func
This PR is a fix for #173 With this PR a Vertex can be replaced allowing callers to first retrieve an existing Vertex, make all updates, and then call the new function `graph.UpdateVertex` with the old hash and the new vertex.
1 parent b8919a8 commit 5aa7072

File tree

5 files changed

+413
-0
lines changed

5 files changed

+413
-0
lines changed

directed.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,70 @@ func (d *directed[K, T]) AddVertex(value T, options ...func(*VertexProperties))
3737
return d.store.AddVertex(hash, value, properties)
3838
}
3939

40+
func (d *directed[K, T]) UpdateVertex(existingHash K, value T, options ...func(*VertexProperties)) error {
41+
_, _, err := d.store.Vertex(existingHash)
42+
if err != nil {
43+
return err
44+
}
45+
46+
newHash := d.hash(value)
47+
_, _, err = d.store.Vertex(newHash)
48+
if !errors.Is(err, ErrVertexNotFound) {
49+
return ErrVertexAlreadyExists
50+
}
51+
52+
adjacencyMap, err := d.AdjacencyMap()
53+
if err != nil {
54+
return err
55+
}
56+
57+
existingEdges := adjacencyMap[existingHash]
58+
59+
for _, edge := range existingEdges {
60+
err = d.RemoveEdge(edge.Source, edge.Target)
61+
if err != nil {
62+
return err
63+
}
64+
}
65+
66+
properties := VertexProperties{
67+
Weight: 0,
68+
Attributes: make(map[string]string),
69+
}
70+
71+
for _, option := range options {
72+
option(&properties)
73+
}
74+
75+
err = d.store.AddVertex(newHash, value, properties)
76+
if err != nil {
77+
return err
78+
}
79+
80+
for _, existingEdge := range existingEdges {
81+
src := existingEdge.Source
82+
if existingEdge.Source == existingHash {
83+
src = newHash
84+
}
85+
tgt := existingEdge.Target
86+
if existingEdge.Target == existingHash {
87+
tgt = newHash
88+
}
89+
90+
edge := Edge[K]{
91+
Source: src,
92+
Target: tgt,
93+
Properties: existingEdge.Properties,
94+
}
95+
96+
if err := d.addEdge(src, tgt, edge); err != nil {
97+
return err
98+
}
99+
}
100+
101+
return nil
102+
}
103+
40104
func (d *directed[K, T]) AddVerticesFrom(g Graph[K, T]) error {
41105
adjacencyMap, err := g.AdjacencyMap()
42106
if err != nil {

directed_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,144 @@ func TestDirected_RemoveVertex(t *testing.T) {
301301
}
302302
}
303303

304+
func TestDirected_UpdateVertex(t *testing.T) {
305+
tests := map[string]struct {
306+
vertices []int
307+
properties *VertexProperties
308+
edges []Edge[int]
309+
updateVertex struct {
310+
existingHash int
311+
vertex int
312+
properties *VertexProperties
313+
}
314+
expectedVertices []int
315+
expectedProperties *VertexProperties
316+
expectedEdges []Edge[int]
317+
expectedErr error
318+
}{
319+
"update a vertex": {
320+
vertices: []int{1, 2},
321+
edges: []Edge[int]{
322+
{
323+
Source: 1,
324+
Target: 2,
325+
Properties: EdgeProperties{
326+
Weight: 10,
327+
Attributes: map[string]string{
328+
"color": "red",
329+
},
330+
Data: "my-edge",
331+
},
332+
},
333+
},
334+
updateVertex: struct {
335+
existingHash int
336+
vertex int
337+
properties *VertexProperties
338+
}{
339+
existingHash: 1,
340+
vertex: 100,
341+
properties: &VertexProperties{
342+
Weight: 20,
343+
Attributes: map[string]string{
344+
"color": "blue",
345+
"label": "a blue edge",
346+
},
347+
},
348+
},
349+
expectedVertices: []int{100, 2},
350+
expectedEdges: []Edge[int]{
351+
{
352+
Source: 100,
353+
Target: 2,
354+
Properties: EdgeProperties{
355+
Weight: 10,
356+
Attributes: map[string]string{
357+
"color": "red",
358+
},
359+
Data: "my-edge",
360+
},
361+
},
362+
},
363+
},
364+
}
365+
366+
for name, test := range tests {
367+
t.Run(name, func(t *testing.T) {
368+
graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]())
369+
370+
var err error
371+
372+
for _, vertex := range test.vertices {
373+
if test.properties == nil {
374+
err = graph.AddVertex(vertex)
375+
continue
376+
}
377+
// If there are vertex attributes, iterate over them and call the
378+
// VertexAttribute functional option for each entry. A vertex should
379+
// only have one attribute so that AddVertex is invoked once.
380+
for key, value := range test.properties.Attributes {
381+
err = graph.AddVertex(vertex, VertexWeight(test.properties.Weight), VertexAttribute(key, value))
382+
}
383+
}
384+
385+
for _, edge := range test.edges {
386+
_ = graph.AddEdge(copyEdge(edge))
387+
}
388+
389+
if test.updateVertex.properties == nil {
390+
err = graph.UpdateVertex(test.updateVertex.existingHash, test.updateVertex.vertex)
391+
} else {
392+
options := []func(*VertexProperties){VertexWeight(test.updateVertex.properties.Weight)}
393+
for key, value := range test.updateVertex.properties.Attributes {
394+
options = append(options, VertexAttribute(key, value))
395+
}
396+
err = graph.UpdateVertex(test.updateVertex.existingHash, test.updateVertex.vertex, options...)
397+
}
398+
if !errors.Is(err, test.expectedErr) {
399+
t.Fatalf("expected error %v, got %v", test.expectedErr, err)
400+
}
401+
402+
graphStore := graph.store.(*memoryStore[int, int])
403+
404+
for _, expectedVertex := range test.expectedVertices {
405+
if len(graphStore.vertices) != len(test.expectedVertices) {
406+
t.Errorf("%s: vertex count doesn't match: expected %v, got %v", name, len(test.expectedVertices), len(graphStore.vertices))
407+
}
408+
409+
hash := graph.hash(expectedVertex)
410+
vertices := graph.store.(*memoryStore[int, int]).vertices
411+
if _, ok := vertices[hash]; !ok {
412+
t.Errorf("%s: vertex %v not found in graph: %v", name, expectedVertex, vertices)
413+
}
414+
415+
if test.properties == nil {
416+
continue
417+
}
418+
419+
if graphStore.vertexProperties[hash].Weight != test.expectedProperties.Weight {
420+
t.Errorf("%s: edge weights don't match: expected weight %v, got %v", name, test.expectedProperties.Weight, graphStore.vertexProperties[hash].Weight)
421+
}
422+
423+
if len(graphStore.vertexProperties[hash].Attributes) != len(test.expectedProperties.Attributes) {
424+
t.Fatalf("%s: attributes lengths don't match: expcted %v, got %v", name, len(test.expectedProperties.Attributes), len(graphStore.vertexProperties[hash].Attributes))
425+
}
426+
}
427+
428+
for _, expectedEdge := range test.expectedEdges {
429+
actualEdge, err := graph.Edge(expectedEdge.Source, expectedEdge.Target)
430+
if err != nil {
431+
t.Fatalf("unexpected error: %v", err.Error())
432+
}
433+
434+
if !edgesAreEqual(expectedEdge, actualEdge, false) {
435+
t.Errorf("expected edge %v, got %v", expectedEdge, actualEdge)
436+
}
437+
}
438+
})
439+
}
440+
}
441+
304442
func TestDirected_AddEdge(t *testing.T) {
305443
tests := map[string]struct {
306444
vertices []int

graph.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ type Graph[K comparable, T any] interface {
7777
//
7878
AddVertex(value T, options ...func(*VertexProperties)) error
7979

80+
// UpdateVertex updates an existing vertex in the graph. If the vertex doesn't
81+
// exist in the graph, ErrVertexNotFound will be returned.
82+
UpdateVertex(hash K, value T, options ...func(*VertexProperties)) error
83+
8084
// AddVerticesFrom adds all vertices along with their properties from the
8185
// given graph to the receiving graph.
8286
//

undirected.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,75 @@ func (u *undirected[K, T]) AddVertex(value T, options ...func(*VertexProperties)
3838
return u.store.AddVertex(hash, value, prop)
3939
}
4040

41+
func (u *undirected[K, T]) UpdateVertex(existingHash K, value T, options ...func(*VertexProperties)) error {
42+
_, _, err := u.store.Vertex(existingHash)
43+
if err != nil {
44+
return err
45+
}
46+
47+
newHash := u.hash(value)
48+
_, _, err = u.store.Vertex(newHash)
49+
if !errors.Is(err, ErrVertexNotFound) {
50+
return ErrVertexAlreadyExists
51+
}
52+
53+
adjacencyMap, err := u.AdjacencyMap()
54+
if err != nil {
55+
return err
56+
}
57+
58+
existingEdges := adjacencyMap[existingHash]
59+
60+
for _, edge := range existingEdges {
61+
err = u.RemoveEdge(edge.Source, edge.Target)
62+
if err != nil {
63+
return err
64+
}
65+
}
66+
67+
err = u.store.RemoveVertex(existingHash)
68+
if err != nil {
69+
return err
70+
}
71+
72+
properties := VertexProperties{
73+
Weight: 0,
74+
Attributes: make(map[string]string),
75+
}
76+
77+
for _, option := range options {
78+
option(&properties)
79+
}
80+
81+
err = u.store.AddVertex(newHash, value, properties)
82+
if err != nil {
83+
return err
84+
}
85+
86+
for _, existingEdge := range existingEdges {
87+
src := existingEdge.Source
88+
if existingEdge.Source == existingHash {
89+
src = newHash
90+
}
91+
tgt := existingEdge.Target
92+
if existingEdge.Target == existingHash {
93+
tgt = newHash
94+
}
95+
96+
edge := Edge[K]{
97+
Source: src,
98+
Target: tgt,
99+
Properties: existingEdge.Properties,
100+
}
101+
102+
if err := u.addEdge(src, tgt, edge); err != nil {
103+
return err
104+
}
105+
}
106+
107+
return nil
108+
}
109+
41110
func (u *undirected[K, T]) Vertex(hash K) (T, error) {
42111
vertex, _, err := u.store.Vertex(hash)
43112
return vertex, err

0 commit comments

Comments
 (0)