We have a plugin that perform tasks like "cascade" deletion, where it deletes outgoing relationship endNodes that have no other parents. It also fetches subgraphs using a similar "downstream" pattern.
In one cypher query, I call both of these in succession. Works fine if there is only one deleted rel. But if multiple outgoing relationships would be deleted, the subsequent fetch fails with Neo.ClientError.Statement.EntityNotFound: org.neo4j.internal.kernel.api.exceptions.EntityNotFoundException: Unable to load RELATIONSHIP with id 1071.
Indeed, 1071 is one of the (in this case 2) rels that would have been deleted. But why does the subsequent fetch try to load a rel deleted earlier in the transaction? The fetch in uvs.cascade.getNode is implemented with a TraversalDescription. I've also tried enclosing both the delete and fetch in their own transactions in the plugin – same result. This can be reproduced using only the Neo4j Browser. I've even tried dropping and recreating indexes, as I've read it can happen with corrupt indexes. Thanks in advance!
MATCH (qt:QueryTemplate {uuid: {uuid} })-[:RESPECTS]->(ar:AlignmentRule)-[A:ALIGNS]->(a:Axis)
CALL uvs.cascade.deleteRel(A, true) YIELD out
WITH distinct qt AS qt
CALL uvs.cascade.getNode(qt) YIELD path
RETURN path
This is a fairly specific question, but I'm not sure much help can be given without seeing the code that implements uvs.cascade.getNode(qt). You say that this is implemented with a TraversalDescription. I'm guessing that TraversalDescription must be touching (somehow) one of the relationships that was deleted by uvs.cascade.deleteRel.
It sounds too like the answer might hinge on deletion semantics details in Neo4j and how uvs.cascade.deleteRel is handling its transaction.
Can I ask why you're deleting a relationship with a specialized stored procedure in the first place and not just deleting it with cypher? I feel like there's a lot of extra logic hiding behind that second "true" argument, and something else is going on in this scenario.
Thanks David, and agreed it's quite specific. I could share some code, but the delete code is long and in dire need of cleanup. Here is the get, though (Kotlin):
@Procedure(name = "uvs.cascade.getNode", mode = Mode.WRITE)
fun getNode(@Name("node") node: Node, @Name(value = "depth", defaultValue = "-1") depth: Long): Stream<PathResult> {
if (node == null || db == null) {
return Stream.empty()
}
var td = db!!.traversalDescription()
td = td.breadthFirst()
for (type in Global.UVSRelationshipType.values()) {
td = td.relationships(type, Direction.OUTGOING)
}
if (depth >= 0) td = td.evaluator(Evaluators.toDepth<Any>(depth.toInt()))
td = td.uniqueness(Uniqueness.RELATIONSHIP_LEVEL)
return td.traverse(node).stream().map { PathResult(it) }
}
Separately the delete and get functions work quite reliably; only seems to be an issue when calling both in same transaction. The TraversalDescription would indeed capture rels deleted in the delete operation, were those rels still around. Based on looking at examples in APOC, I've been removing all explicit transactions within the plugin functions, but not sure if that's correct. In any case, doesn't seem to make a difference here, whether both or neither of the delete or get calls use explicit transactions.
We're deleting relationships in the plugin following a specific pattern that guarantees any "orphaned" nodes (ones left with no incoming relationship) are automatically deleted. The cypher for this (over 100+ queries) was getting out of hand, so pattern-izing it made more sense. I also didn't find anything in APOC specific enough for our needs, at the time this pattern was developed.