Failure to create a relationship under specific conditions

The context is an HTTP API server which auto-generates the Cypher queries (Equill/restagraph: App that dynamically generates REST-ish APIs for a Neo4j database, using a schema defined within the database. - Codeberg.org), and I promise this approach makes sense in the context of the API's semantics.

I can create the resources just fine:

CREATE (:parent {uid: "foo"})-[:PARENT_CHILD]->(:child {uid: "bar"})-[:CHILD_GRANDCHILD]->(:grandchild {uid: "baz"});

Creating back-links from child to parent, and from grandchild to child, works fine in the traditional manner using a hand-crafted query

MATCH (p:parent {uid: "foo"})-[:PARENT_CHILD]->(c:child {uid: "bar"}) CREATE (c)-[:CHILD_PARENT]->(p);
MATCH (:parent {uid: "foo"})-[:PARENT_CHILD]->(c:child {uid: "bar"})-[:CHILD_GRANDCHILD]->(g:grandchild {uid: "baz"}) CREATE  (g)-[:GRANDCHILD_CHILD]->(c);

If I check these with the following queries, I get the expected output:

MATCH (p:parent {uid: "foo"})-[:PARENT_CHILD]->(c:child {uid: "bar"})-[:CHILD_PARENT]->(z) RETURN z;
MATCH (p:parent {uid: "foo"})-[:PARENT_CHILD]->(c:child {uid: "bar"})-[:CHILD_GRANDCHILD]->(:grandchild {uid: "baz"})-[:GRANDCHILD_CHILD]->(z) RETURN z;

However, that isn't how the server generates this code. It doesn't check whether the target of a new relationship can be reached via a subset of the source's path, so it generates both paths from scratch. Because of this, it tries to create those relationships with suboptimal Cypher queries:

MATCH (p:parent {uid: "foo"}), (:parent {uid: "foo"})-[:PARENT_CHILD]->(c:child {uid: "bar"}) CREATE (c)-[:CHILD_PARENT]->(p);
MATCH (:parent {uid: "foo"})-[:PARENT_CHILD]->(c:child {uid: "bar"}), (:parent {uid: "foo"})-[:PARENT_CHILD]->(:child {uid: "bar"})-[:CHILD_GRANDCHILD]->(g:grandchild {uid: "baz"}) CREATE (g)-[:GRANDCHILD_CHILD]->(c);

The odd thing here is that the first one (depth=2 to depth=1) works fine. The second one (depth=3 to depth=2) fails. It returns the first two of the usual lines:

0 rows
ready to start consuming query after 457 ms, results consumed after another 0 ms

However, the expected third line "Created 1 relationships" doesn't appear. When I test with the previous queries ending in "RETURN z", the first returns the requested property, but the second reports that this relationship doesn't exist.

My question is whether this should succeed and I've found a bug, or whether I'm doing it wrong and should find another approach. I've verified this behaviour with versions 4.4.18 and 5.21.0.

Hello,

This is not a bug, the issue is that within a single MATCH clause, Cypher uses relationship isomorphism: the same relationship cannot be used more than once in a single path.

In particular the :PARENT_CHILD relationship between foo and bar is specified twice in the same MATCH pattern. If there were actually 2 separate :PARENT_CHILD relationships between these same nodes, it would be fine (although you would get two paths as a result, since there would be two distinct paths that could be generated to fulfill the pattern, since you just swap which relationship is used to fulfill each subpattern).

But with only one such :PARENT_CHILD relationship, once it is used by one subpattern, it cannot be used to fulfill the other, that would be traversing the same relationship twice to fulfill the same pattern.

While this excerpt cannot succeed with a single relationship of this type between the two nodes:

MATCH (:parent {uid: "foo"})-[:PARENT_CHILD]->(:child {uid: "bar"}), (:parent {uid: "foo"})-[:PARENT_CHILD]->(:child {uid: "bar"})

The query excerpt below can succeed, because each MATCH generates its own path. The relationship isomorphism only applies within a path, it doesn't apply between the two separate paths.

MATCH (:parent {uid: "foo"})-[:PARENT_CHILD]->(c:child {uid: "bar"})
MATCH (:parent {uid: "foo"})-[:PARENT_CHILD]->(c:child {uid: "bar"})

Also, a note as to your data model...by only using one relationship :PARENT_CHILD, you can find most of the things you want from the graph without needing to specify additional relationships. Getting a node's child, and getting a node's parent, is just traversing the same relationship, but in a different direction. Getting the grandchild of a node is just traversing two :PARENT_CHILD relationships downward. Getting the grandparent of a child is traversing the same two relationships in the opposite direction.

In fact your labels aren't even needed. Just use a :Person label. If it's a parent, then it has an outgoing :PARENT_CHILD relationship. If it's a child, then it has an incoming :PARENT_CHILD relationship. If you keep the data model simple, you don't really need the extra. Additionally, by using a common label, you can index the label and property for lookup of any person in your graph. As it is now, you have separate labels on each, so they each would need to have their own separate indexes, and you'd need to know if the node is a child, grandchild, parent, or other to be able to use indexes at all. The labels you are using should probably not be labels, because what you're specifying is an emergent property of relationship structure.

Ah, that explains why this specific case is the only one that fails!

Thanks very much for that. Looks like I need to hack in some extra logic to detect and handle the case where the source and destination paths share at least one relationship.

Thanks also for the advice about the labels, but I'm already there, and that is in fact how I represent people :slight_smile:

The code I presented here was the simplest, clearest example of how to see and reproduce the problem, so the "parent", "child" and "grandchild" labels were just to be nice and explicit about how each node fits into the scheme of things. The actual use is decidedly general-purpose, and covering that background would just have muddied the waters.

...then I re-read your explanation a little more carefully, and realised I don't need any special-case code at all.
I simply need to break that single MATCH clause into two of them, and no other changes are required.

I just tested this approach, and it works nicely. Thanks again!