Skip to content

Commit b9e4798

Browse files
authored
DiffGraph: allow simultaneous edge and node removal (#347)
Prior to this, if a user tried to delete a node and an adjacent edge at the same time, we would run into this: ``` scala.MatchError: flatgraph.DiffGraphBuilder$RemoveEdge@7bcbf3a8 (of class flatgraph.DiffGraphBuilder$RemoveEdge) at flatgraph.DiffGraphApplier.splitUpdate$$anonfun$2(DiffGraphApplier.scala:228) ~[io.joern.flatgraph-core_3-0.1.25.jar:0.1.25] ``` Which was because the condition was on the `case RemoveEdge` filtered out that case and there's no fallback `case _` or similar. This makes it explicit and adds a test.
1 parent 7a092cf commit b9e4798

File tree

2 files changed

+86
-16
lines changed

2 files changed

+86
-16
lines changed

core/src/main/scala/flatgraph/DiffGraphApplier.scala

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -196,24 +196,28 @@ private[flatgraph] class DiffGraphApplier(
196196
new EdgeRepr(inR.src, inR.dst, inR.edgeKind, inR.subSeq, setEdgeProperty.property),
197197
graph.schema.neighborOffsetArrayIndex(inR.src.nodeKind, Incoming, inR.edgeKind)
198198
)
199-
case edgeDeletion: RemoveEdge
200-
if !AccessHelpers.isDeleted(edgeDeletion.edge.src) && !AccessHelpers.isDeleted(edgeDeletion.edge.dst) =>
201-
ndiff += 1
199+
case edgeDeletion: RemoveEdge =>
200+
if (AccessHelpers.isDeleted(edgeDeletion.edge.src) || AccessHelpers.isDeleted(edgeDeletion.edge.dst)) {
201+
// the adjacent nodes have already been deleted - nothing more for us to do here
202+
// and, importantly, we do not want to fail if that's the case, but simply relax and enjoy
203+
} else {
202204

203-
/** This is the delEdge case. It is massively annoying.
204-
*
205-
* In order to support edge properties, we need to grab the right edge from e.src->e.dst. If we assume that our graph was built
206-
* normally, i.e. edges were sequentially/batched added without the unsafe unidirectional edges, then our graph has the following
207-
* invariant: The kth edge connecting A->B corresponds to the kth edge connecting B<-A This sucks big time, because edge removal
208-
* is potentially O(N**2). The degenerate behavior occurs when we have ~k edges of the same type starting in src = X or ending in
209-
* the same dst = X. Each deletion then costs us ~k, and if we delete all ~k edges we pay ~ k*k.
210-
*
211-
* But k~N is possible where N is the graph size!
212-
*/
213-
val (outR, inR) = normalizeRepresentation(edgeDeletion.edge)
214-
insert(delEdges, outR, graph.schema.neighborOffsetArrayIndex(outR.src.nodeKind, Outgoing, outR.edgeKind))
215-
insert(delEdges, inR, graph.schema.neighborOffsetArrayIndex(inR.src.nodeKind, Incoming, inR.edgeKind))
205+
ndiff += 1
216206

207+
/** This is the delEdge case. It is massively annoying.
208+
*
209+
* In order to support edge properties, we need to grab the right edge from e.src->e.dst. If we assume that our graph was built
210+
* normally, i.e. edges were sequentially/batched added without the unsafe unidirectional edges, then our graph has the
211+
* following invariant: The kth edge connecting A->B corresponds to the kth edge connecting B<-A This sucks big time, because
212+
* edge removal is potentially O(N**2). The degenerate behavior occurs when we have ~k edges of the same type starting in src =
213+
* X or ending in the same dst = X. Each deletion then costs us ~k, and if we delete all ~k edges we pay ~ k*k.
214+
*
215+
* But k~N is possible where N is the graph size!
216+
*/
217+
val (outR, inR) = normalizeRepresentation(edgeDeletion.edge)
218+
insert(delEdges, outR, graph.schema.neighborOffsetArrayIndex(outR.src.nodeKind, Outgoing, outR.edgeKind))
219+
insert(delEdges, inR, graph.schema.neighborOffsetArrayIndex(inR.src.nodeKind, Incoming, inR.edgeKind))
220+
}
217221
case setNodeProperty: SetNodeProperty if !AccessHelpers.isDeleted(setNodeProperty.node) =>
218222
ndiff += 1
219223
val iter = setNodeProperty.property match {

core/src/test/scala/flatgraph/GraphTests.scala

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,72 @@ class GraphTests extends AnyWordSpec with Matchers {
524524
testSerialization(g)
525525
}
526526

527+
"permit deleting an edge together with it's node" in {
528+
// if a node get's deleted we automatically delete the adjacent edges
529+
// but if a user explicitly removes the edge (additionally) we do not want to fail hard for that...
530+
531+
var g = mkGraph()
532+
debugDump(g) shouldBe
533+
"""#Node numbers (kindId, nnodes) (0: 4), total 4
534+
|Node kind 0. (eid, nEdgesOut, nEdgesIn): (0, 7 [dense], 7 [dense]),
535+
| V0_0 [0] -> V0_2, V0_1, V0_3, V0_2, V0_1
536+
| V0_1 [0] <- V0_0, V0_3, V0_0
537+
| V0_2 [0] <- V0_0, V0_0, V0_3
538+
| V0_3 [0] -> V0_1, V0_2
539+
| V0_3 [0] <- V0_0
540+
|""".stripMargin
541+
542+
val nodeToDelete = g.node(0, 0)
543+
val edgeToDelete = Accessors.getEdgesOut(nodeToDelete, 0)(0)
544+
DiffGraphApplier.applyDiff(
545+
g,
546+
new DiffGraphBuilder(schema)
547+
.removeNode(nodeToDelete)
548+
.removeEdge(edgeToDelete)
549+
)
550+
551+
debugDump(g) shouldBe
552+
"""#Node numbers (kindId, nnodes) (0: 4), total 4
553+
|Node kind 0. (eid, nEdgesOut, nEdgesIn): (0, 2 [dense], 2 [dense]),
554+
| V0_1 [0] <- V0_3
555+
| V0_2 [0] <- V0_3
556+
| V0_3 [0] -> V0_1, V0_2
557+
|""".stripMargin
558+
559+
g = mkGraph()
560+
DiffGraphApplier.applyDiff(
561+
g,
562+
new DiffGraphBuilder(schema)
563+
.removeNode(g.node(0, 1))
564+
)
565+
debugDump(g) shouldBe
566+
"""#Node numbers (kindId, nnodes) (0: 4), total 4
567+
|Node kind 0. (eid, nEdgesOut, nEdgesIn): (0, 4 [dense], 4 [dense]),
568+
| V0_0 [0] -> V0_2, V0_3, V0_2
569+
| V0_2 [0] <- V0_0, V0_0, V0_3
570+
| V0_3 [0] -> V0_2
571+
| V0_3 [0] <- V0_0
572+
|""".stripMargin
573+
574+
testSerialization(g)
575+
576+
g = mkGraph()
577+
DiffGraphApplier.applyDiff(
578+
g,
579+
new DiffGraphBuilder(schema)
580+
.removeNode(g.node(0, 2))
581+
.removeNode(g.node(0, 3))
582+
)
583+
debugDump(g) shouldBe
584+
"""#Node numbers (kindId, nnodes) (0: 4), total 4
585+
|Node kind 0. (eid, nEdgesOut, nEdgesIn): (0, 2 [dense], 2 [dense]),
586+
| V0_0 [0] -> V0_1, V0_1
587+
| V0_1 [0] <- V0_0, V0_0
588+
|""".stripMargin
589+
590+
testSerialization(g)
591+
}
592+
527593
"dont mess up after node deletion" in {
528594
val schema = TestSchema.make(1, 0, 1, nodePropertyPrototypes = Array(Array[String]("")), edgePropertyPrototypes = null)
529595
val g = new Graph(schema)

0 commit comments

Comments
 (0)