neo4j / graphql

A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
https://neo4j.com/docs/graphql-manual/current/
Apache License 2.0
507 stars 149 forks source link

Using "_NOT" on an empty relationship in where clause returns false, leaving out the queried node that doesn't have any relationships #517

Closed jrsperry closed 2 years ago

jrsperry commented 3 years ago

The "_NOT" operator on relationships requires the relationship to exist. Using "_NOT" in a where clause on a relationship therefore leaves out the object you are querying for.

For example, you have a graph with 2 people, 1 of which has a child. When you look for people who don't have a child with a particular name, people who don't have any children are always excluded from the result set. Looking at the cypher executed in the logs this appears to occur because the cypher predicate "NONE" is used, which requires the relationship to exist. Not sure if this is a cypher or graphql bug then.

neo4j version: 4.3.2 neo4j-graphql version: 2.2.0

Steps to reproduce:

Create graph:

create (p:Person {name: "Josh"})-[:HAS_CHILD]->(c:Child {name: "Bob"})
create (p2:Person {name: "Sue"})

Graphql Schema:

type Person {
  name: String
  children: [Child] @relationship(type: "HAS_CHILD", direction: OUT)
}

type Child {
  name: String
}

Graphql query, I would expect to see 2 persons returned, instead only 1 is returned (the one with a child).

{
  people(where: {children_NOT: {name: "Adam"}}){
    name
  }
}

image

But when you query for people, without a filter, 2 people are return, and an empty array on the "children" property.

image

darrellwarde commented 3 years ago

Hey @jrsperry, sorry for the slow reply, interesting one this!

Unsure whether to class this as a bug or not, because when we see a relationship field in the where argument (in this case, children or children_NOT), the first thing we do in the Cypher is check whether that relationship exists or not, which you may have seen in the EXISTS before the NONE:

MATCH (this:Person)
WHERE EXISTS((this)-[:HAS_CHILD]->(:Child)) AND NONE(this_children_NOT IN [(this)-[:HAS_CHILD]->(this_children_NOT:Child) | this_children_NOT] WHERE this_children_NOT.name = $this_children_NOT_name)
RETURN this { .name } as this

There is a workaround here, or perhaps the appropriate solution to this problem, which is to also add the case when the relationship doesn't exist to your filter:

{
  people(
    where: { OR: [{ children: null }, { children_NOT: { name: "Adam" } }] }
  ) {
    name
  }
}

And this will work as you expect. Personally, I think this is working as it should, and I will close this issue if the workaround works for you.

jrsperry commented 2 years ago

as of 3.5.0 the switch to use exists predicate solves this problem.

https://github.com/neo4j/graphql/pull/1629