Daniel Frees
Daniel Frees

Reputation: 53

neo4j unique constraint violation when using MERGE?

I have code to add nodes to a neo4j instance via the python driver. Nodes are matched based on label and 'name', and there is a unique constraint imposed on label and 'name'. I use MERGE so that I either MATCH or CREATE a node depending whether it exists. I then add new properties if I matched an existing node but have new properties to add from my graph in python. This code has been working consistently, even when I edit nodes with new properties. However, it recently broke when I changed the 'count' property on node(17) with name 'My' and label 'Person'. Is there a reason why changing (rather than adding) a property would cause a problem with this code?

node_label and node_name are properties of nodes in a networkx graph, as are other_props (other_props contains the 'count').

Here is my code:

query = (
                f"MERGE (n: {node_label} {{name: \"{node_name}\"}})\n"
                f"ON CREATE\n"
                f"    SET n.created = timestamp()\n"
                f"SET n += {{{other_props}}}\n"
                f"RETURN n, n.created"
            )

My understanding is that with MERGE it should be impossible to violate my unique constraint if I am MERGEing based on the same properties involved in the unique constraint.

But I am somehow getting the following error:

constraint error

If anyone has ideas on how I could possibly be violating the unique constraint, do let me know. I am still fairly new to Cypher and am looking to get better.

In neo4j browser, the exact same query has no issue...

neo4j runs query fine

Upvotes: 0

Views: 373

Answers (1)

Robsdedude
Robsdedude

Reputation: 1403

Here is a short Python bit of how I imagine the stripped down version of what you're doing looks like

import neo4j
import neo4j.exceptions


def main():
    uri = "neo4j://localhost:7687"
    user = "neo4j"
    password = "pass"
    auth = neo4j.basic_auth(user, password)
    with neo4j.GraphDatabase.driver(uri, auth=auth) as driver:
        with driver.session() as session:
            create_query = "CREATE (:Person {name: $name})"
            match_query = "MATCH (p:Person {name: $name}) RETURN p"

            merge_query = ("MERGE (p:Person {name: $name}) "
                           "ON CREATE "
                           "  SET p.name=$name2")
            wrong_merge = ("MERGE (p:Person {name: $name}) "
                           "ON CREATE "
                           "  SET p.name='created' "
                           "SET p.name=$name2")

            session.run("CREATE CONSTRAINT person_name IF NOT EXISTS "
                        "FOR (p:Person) REQUIRE p.name IS UNIQUE")
            session.run("MATCH (n) DETACH DELETE n")
            session.run(create_query, name="Alice")
            session.run(create_query, name="Bob")
            try:
                session.run(create_query, name="Alice")
                assert False  # should not get here
            except neo4j.exceptions.Neo4jError as e:
                print(f"Fails as expected: {e}")

            # noop because Alice already exists
            session.run(merge_query, name="Alice", name2="Alice2")
            assert len(list(session.run(match_query, name="Alice"))) == 1
            assert not list(session.run(match_query, name="Alice2"))

            # finds Alice and always updates
            session.run(wrong_merge, name="Alice", name2="Alice2")
            assert not list(session.run(match_query, name="Alice"))
            assert len(list(session.run(match_query, name="Alice2"))) == 1

            # You can't rename Alice to Bob, bcs. Bob already exists
            try:
                session.run(wrong_merge, name="Alice2", name2="Bob")
                assert False  # should not get here
            except neo4j.exceptions.Neo4jError as e:
                print(f"Fails as expected: {e}")


if __name__ == "__main__":
    main()

Have a look at the wrong_merge query. The second SET always gets executed, regardless if MERGE creates a new node or matches an existing one. I strongly suspect name to be present in your other_props hence trying to change the name to something that already exists.

Lastly, regardless whether this is the case or not, I recommend you to get used to never building any database queries (no matter which db) by doing string concatenation or interpolation. Almost all DBs offer a way to parametrize your query. This has two benefits:

  1. The database can optimize your query that only differs in the parameters
  2. You never end up in a situation where you make yourself vulnerable to query injections.

Upvotes: 0

Related Questions