graphql icon indicating copy to clipboard operation
graphql copied to clipboard

Nested delete mutations with relationships to unions and subscriptions enabled fail to delete all targeted nodes depending on the union definition in the schema

Open a-alle opened this issue 9 months ago • 3 comments

Describe the bug Given a relationship to a Union type and with subscriptions enabled, the following nested delete mutation does not delete all targeted nodes. Notice that this is related to the order of graphql arguments. If the order of the member object types is switched in the definition of the union Director, then all nodes are being deleted.

Type definitions

  type Movie {
    title: String!
    actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN)
    directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN)
    reviewers: [Reviewer!]! @relationship(type: "REVIEWED", properties: "Review", direction: IN)
  }
            
  type Actor {
    name: String!
    movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT)
  }
            
  interface ActedIn @relationshipProperties {
    screenTime: Int!
  }
            
  interface Directed @relationshipProperties {
    year: Int!
  }
            
  interface Review @relationshipProperties {
    score: Int!
  }
        
  type Person implements Reviewer {
    name: String!
    reputation: Int!
    movies: [Movie!]! @relationship(type: "REVIEWED", direction: OUT, properties: "Review")
  }
            
  type Influencer implements Reviewer {
    reputation: Int!
    url: String!
  }
            
  # this union definition results in failure of deletion of all targeted nodes
  union Director = Person | Actor
  # this union definition does indeed work and all targeted nodes are getting deleted
  # union Director =  Actor | Person
            
  interface Reviewer {
    reputation: Int!
  }

To Reproduce Steps to reproduce the behavior:

  1. Run a server with subscriptions enabled
  2. Execute the following set-up Cypher query
CREATE (ana:Person {name: "Ana", reputation: 10})
CREATE (bob:Influencer {url: "/bob", reputation: 10})
CREATE (julia:Person {name: "Julia", reputation: 10})
CREATE (const:Movie {title: "Constantine"})
MERGE (ana)-[:REVIEWED {score: 100}]->(const)
MERGE (bob)-[:REVIEWED {score: 100}]->(const)
MERGE (julia)-[:REVIEWED {score: 100}]->(const)

CREATE (jill:Person {name: "Jill", reputation: 10})
CREATE (jim:Person {name: "Jim", reputation: 10})
CREATE (keanu:Actor {name: "Keanu Reeves"})
CREATE (keanu2:Actor {name: "Keanu Reeves"})
CREATE (jw:Movie {title: "John Wick"})
MERGE (jim)-[:DIRECTED {year: 2020}]->(jw)
MERGE (jill)-[:DIRECTED {year: 2020}]->(jw)
MERGE (keanu)-[:DIRECTED {year: 2019}]->(jw)
MERGE (keanu2)-[:DIRECTED {year: 2019}]->(jw)
MERGE (jim)-[:REVIEWED {score: 10}]->(const)
MERGE (keanu)-[:ACTED_IN {screenTime: 420}]->(const)
  1. Execute the following delete mutation:
mutation {
  deleteMovies(
    where: {
      title: "John Wick"
    },
    delete: {
      directors: {
        Actor: [
          {
            where: {
              node: {
                name: "Keanu Reeves"
               }
            },
           delete: {
             movies: [
               {
                 where: {
                   node: {
                     title_STARTS_WITH: "Constantine"
                    }
                  },
                delete: {
                  reviewers: [
                    {
                      where: {
                        node: {
                          reputation: 10
                        }
                      }
                    }
                  ]
                }
              }
            ]
          }
        }
      ],
      Person: [
        {
          where: {
            node: {
              reputation: 10
            }
          },
          delete: {
            movies: [
              {
                where: {
                  node: {
                    title_STARTS_WITH: "Constantine"
                  }
                },
               delete: {
                 reviewers: [
                   {
                     where: {
                       node: {  
                         _on: {
                           Person: {
                             name: "Ana"
                           },
                           Influencer: {
                             url: "/bob"
                           }
                         }
                       }
                     }
                   }
                 ]
               }
             }
           ]
         }
       }
     ]
   }
 }
 ) {
    nodesDeleted
    relationshipsDeleted
  }
 }
  1. Generated Cypher should look something like:
WITH [] AS meta
MATCH (this:Movie)
WHERE this.title = "John Wick"
WITH this, meta + { event: "delete", id: id(this), properties: { old: this { .* }, new: null }, timestamp: timestamp(), typename: "Movie" } AS meta
WITH *
CALL {
WITH *
WITH *, []  AS meta
OPTIONAL MATCH (this)<-[this_directors_Person0_relationship:DIRECTED]-(this_directors_Person0:Person)
WHERE this_directors_Person0.reputation = 10
WITH *
CALL {
WITH *
WITH *, []  AS meta
OPTIONAL MATCH (this_directors_Person0)-[this_directors_Person0_movies0_relationship:REVIEWED]->(this_directors_Person0_movies0:Movie)
WHERE this_directors_Person0_movies0.title STARTS WITH "Constantine"
WITH *
CALL {
WITH *
WITH *, []  AS meta
OPTIONAL MATCH (this_directors_Person0_movies0)<-[this_directors_Person0_movies0_reviewers_Person0_relationship:REVIEWED]-(this_directors_Person0_movies0_reviewers_Person0:Person)
WHERE this_directors_Person0_movies0_reviewers_Person0.name = "Ana"
WITH this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship, meta, this_directors_Person0_movies0_reviewers_Person0_relationship, collect(DISTINCT this_directors_Person0_movies0_reviewers_Person0) AS this_directors_Person0_movies0_reviewers_Person0_to_delete
CALL {
        WITH this_directors_Person0_movies0_reviewers_Person0_relationship, this_directors_Person0_movies0_reviewers_Person0_to_delete, this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship
        UNWIND this_directors_Person0_movies0_reviewers_Person0_to_delete AS x
        WITH [] + { event: "delete", id: id(x), properties: { old: x { .* }, new: null }, timestamp: timestamp(), typename: "Person" } + { event: "delete_relationship", timestamp: timestamp(), id_from: id(x), id_to: id(this_directors_Person0_movies0), id: id(this_directors_Person0_movies0_reviewers_Person0_relationship), relationshipName: "REVIEWED", fromTypename: "Person", toTypename: "Movie", properties: { from: x { .* }, to: this_directors_Person0_movies0 { .* }, relationship: this_directors_Person0_movies0_reviewers_Person0_relationship { .* } } } AS meta, x, this_directors_Person0_movies0_reviewers_Person0_relationship, this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship
        DETACH DELETE x
        RETURN collect(meta) AS delete_meta
}
WITH delete_meta, meta
RETURN reduce(m=meta, n IN delete_meta | m + n) AS delete_meta
}
WITH this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship, meta, collect(delete_meta) as delete_meta
WITH this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship, reduce(m=meta, n IN delete_meta | m + n) AS meta
WITH *
CALL {
WITH *
WITH *, []  AS meta
OPTIONAL MATCH (this_directors_Person0_movies0)<-[this_directors_Person0_movies0_reviewers_Influencer0_relationship:REVIEWED]-(this_directors_Person0_movies0_reviewers_Influencer0:Influencer)
WHERE this_directors_Person0_movies0_reviewers_Influencer0.url = "/bob"
WITH this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship, meta, this_directors_Person0_movies0_reviewers_Influencer0_relationship, collect(DISTINCT this_directors_Person0_movies0_reviewers_Influencer0) AS this_directors_Person0_movies0_reviewers_Influencer0_to_delete
CALL {
        WITH this_directors_Person0_movies0_reviewers_Influencer0_relationship, this_directors_Person0_movies0_reviewers_Influencer0_to_delete, this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship
        UNWIND this_directors_Person0_movies0_reviewers_Influencer0_to_delete AS x
        WITH [] + { event: "delete", id: id(x), properties: { old: x { .* }, new: null }, timestamp: timestamp(), typename: "Influencer" } + { event: "delete_relationship", timestamp: timestamp(), id_from: id(x), id_to: id(this_directors_Person0_movies0), id: id(this_directors_Person0_movies0_reviewers_Influencer0_relationship), relationshipName: "REVIEWED", fromTypename: "Influencer", toTypename: "Movie", properties: { from: x { .* }, to: this_directors_Person0_movies0 { .* }, relationship: this_directors_Person0_movies0_reviewers_Influencer0_relationship { .* } } } AS meta, x, this_directors_Person0_movies0_reviewers_Influencer0_relationship, this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship
        DETACH DELETE x
        RETURN collect(meta) AS delete_meta
}
WITH delete_meta, meta
RETURN reduce(m=meta, n IN delete_meta | m + n) AS delete_meta
}
WITH this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship, meta, collect(delete_meta) as delete_meta
WITH this, this_directors_Person0, this_directors_Person0_relationship, this_directors_Person0_movies0, this_directors_Person0_movies0_relationship, reduce(m=meta, n IN delete_meta | m + n) AS meta
WITH this, this_directors_Person0, this_directors_Person0_relationship, meta, this_directors_Person0_movies0_relationship, collect(DISTINCT this_directors_Person0_movies0) AS this_directors_Person0_movies0_to_delete
CALL {
        WITH this_directors_Person0_movies0_relationship, this_directors_Person0_movies0_to_delete, this, this_directors_Person0, this_directors_Person0_relationship
        UNWIND this_directors_Person0_movies0_to_delete AS x
        WITH [] + { event: "delete", id: id(x), properties: { old: x { .* }, new: null }, timestamp: timestamp(), typename: "Movie" } + { event: "delete_relationship", timestamp: timestamp(), id_from: id(this_directors_Person0), id_to: id(x), id: id(this_directors_Person0_movies0_relationship), relationshipName: "REVIEWED", fromTypename: "Person", toTypename: "Movie", properties: { from: this_directors_Person0 { .* }, to: x { .* }, relationship: this_directors_Person0_movies0_relationship { .* } } } AS meta, x, this_directors_Person0_movies0_relationship, this, this_directors_Person0, this_directors_Person0_relationship
        DETACH DELETE x
        RETURN collect(meta) AS delete_meta
}
WITH delete_meta, meta
RETURN reduce(m=meta, n IN delete_meta | m + n) AS delete_meta
}
WITH this, this_directors_Person0, this_directors_Person0_relationship, meta, collect(delete_meta) as delete_meta
WITH this, this_directors_Person0, this_directors_Person0_relationship, reduce(m=meta, n IN delete_meta | m + n) AS meta
WITH this, meta, this_directors_Person0_relationship, collect(DISTINCT this_directors_Person0) AS this_directors_Person0_to_delete
CALL {
        WITH this_directors_Person0_relationship, this_directors_Person0_to_delete, this
        UNWIND this_directors_Person0_to_delete AS x
        WITH [] + { event: "delete", id: id(x), properties: { old: x { .* }, new: null }, timestamp: timestamp(), typename: "Person" } + { event: "delete_relationship", timestamp: timestamp(), id_from: id(x), id_to: id(this), id: id(this_directors_Person0_relationship), relationshipName: "DIRECTED", fromTypename: "Person", toTypename: "Movie", properties: { from: x { .* }, to: this { .* }, relationship: this_directors_Person0_relationship { .* } } } AS meta, x, this_directors_Person0_relationship, this
        DETACH DELETE x
        RETURN collect(meta) AS delete_meta
}
WITH delete_meta, meta
RETURN reduce(m=meta, n IN delete_meta | m + n) AS delete_meta
}
WITH this, meta, collect(delete_meta) as delete_meta
WITH this, reduce(m=meta, n IN delete_meta | m + n) AS meta
WITH *
CALL {
WITH *
WITH *, []  AS meta
OPTIONAL MATCH (this)<-[this_directors_Actor0_relationship:DIRECTED]-(this_directors_Actor0:Actor)
WHERE this_directors_Actor0.name = "Keanu Reeves"
WITH *
CALL {
WITH *
WITH *, []  AS meta
OPTIONAL MATCH (this_directors_Actor0)-[this_directors_Actor0_movies0_relationship:ACTED_IN]->(this_directors_Actor0_movies0:Movie)
WHERE this_directors_Actor0_movies0.title STARTS WITH "Constantine"
WITH *
CALL {
WITH *
WITH *, []  AS meta
OPTIONAL MATCH (this_directors_Actor0_movies0)<-[this_directors_Actor0_movies0_reviewers_Person0_relationship:REVIEWED]-(this_directors_Actor0_movies0_reviewers_Person0:Person)
WHERE this_directors_Actor0_movies0_reviewers_Person0.reputation = 10
WITH this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship, meta, this_directors_Actor0_movies0_reviewers_Person0_relationship, collect(DISTINCT this_directors_Actor0_movies0_reviewers_Person0) AS this_directors_Actor0_movies0_reviewers_Person0_to_delete
CALL {
        WITH this_directors_Actor0_movies0_reviewers_Person0_relationship, this_directors_Actor0_movies0_reviewers_Person0_to_delete, this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship
        UNWIND this_directors_Actor0_movies0_reviewers_Person0_to_delete AS x
        WITH [] + { event: "delete", id: id(x), properties: { old: x { .* }, new: null }, timestamp: timestamp(), typename: "Person" } + { event: "delete_relationship", timestamp: timestamp(), id_from: id(x), id_to: id(this_directors_Actor0_movies0), id: id(this_directors_Actor0_movies0_reviewers_Person0_relationship), relationshipName: "REVIEWED", fromTypename: "Person", toTypename: "Movie", properties: { from: x { .* }, to: this_directors_Actor0_movies0 { .* }, relationship: this_directors_Actor0_movies0_reviewers_Person0_relationship { .* } } } AS meta, x, this_directors_Actor0_movies0_reviewers_Person0_relationship, this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship
        DETACH DELETE x
        RETURN collect(meta) AS delete_meta
}
WITH delete_meta, meta
RETURN reduce(m=meta, n IN delete_meta | m + n) AS delete_meta
}
WITH this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship, meta, collect(delete_meta) as delete_meta
WITH this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship, reduce(m=meta, n IN delete_meta | m + n) AS meta
WITH *
CALL {
WITH *
WITH *, []  AS meta
OPTIONAL MATCH (this_directors_Actor0_movies0)<-[this_directors_Actor0_movies0_reviewers_Influencer0_relationship:REVIEWED]-(this_directors_Actor0_movies0_reviewers_Influencer0:Influencer)
WHERE this_directors_Actor0_movies0_reviewers_Influencer0.reputation = 10
WITH this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship, meta, this_directors_Actor0_movies0_reviewers_Influencer0_relationship, collect(DISTINCT this_directors_Actor0_movies0_reviewers_Influencer0) AS this_directors_Actor0_movies0_reviewers_Influencer0_to_delete
CALL {
        WITH this_directors_Actor0_movies0_reviewers_Influencer0_relationship, this_directors_Actor0_movies0_reviewers_Influencer0_to_delete, this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship
        UNWIND this_directors_Actor0_movies0_reviewers_Influencer0_to_delete AS x
        WITH [] + { event: "delete", id: id(x), properties: { old: x { .* }, new: null }, timestamp: timestamp(), typename: "Influencer" } + { event: "delete_relationship", timestamp: timestamp(), id_from: id(x), id_to: id(this_directors_Actor0_movies0), id: id(this_directors_Actor0_movies0_reviewers_Influencer0_relationship), relationshipName: "REVIEWED", fromTypename: "Influencer", toTypename: "Movie", properties: { from: x { .* }, to: this_directors_Actor0_movies0 { .* }, relationship: this_directors_Actor0_movies0_reviewers_Influencer0_relationship { .* } } } AS meta, x, this_directors_Actor0_movies0_reviewers_Influencer0_relationship, this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship
        DETACH DELETE x
        RETURN collect(meta) AS delete_meta
}
WITH delete_meta, meta
RETURN reduce(m=meta, n IN delete_meta | m + n) AS delete_meta
}
WITH this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship, meta, collect(delete_meta) as delete_meta
WITH this, this_directors_Actor0, this_directors_Actor0_relationship, this_directors_Actor0_movies0, this_directors_Actor0_movies0_relationship, reduce(m=meta, n IN delete_meta | m + n) AS meta
WITH this, this_directors_Actor0, this_directors_Actor0_relationship, meta, this_directors_Actor0_movies0_relationship, collect(DISTINCT this_directors_Actor0_movies0) AS this_directors_Actor0_movies0_to_delete
CALL {
        WITH this_directors_Actor0_movies0_relationship, this_directors_Actor0_movies0_to_delete, this, this_directors_Actor0, this_directors_Actor0_relationship
        UNWIND this_directors_Actor0_movies0_to_delete AS x
        WITH [] + { event: "delete", id: id(x), properties: { old: x { .* }, new: null }, timestamp: timestamp(), typename: "Movie" } + { event: "delete_relationship", timestamp: timestamp(), id_from: id(this_directors_Actor0), id_to: id(x), id: id(this_directors_Actor0_movies0_relationship), relationshipName: "ACTED_IN", fromTypename: "Actor", toTypename: "Movie", properties: { from: this_directors_Actor0 { .* }, to: x { .* }, relationship: this_directors_Actor0_movies0_relationship { .* } } } AS meta, x, this_directors_Actor0_movies0_relationship, this, this_directors_Actor0, this_directors_Actor0_relationship
        DETACH DELETE x
        RETURN collect(meta) AS delete_meta
}
WITH delete_meta, meta
RETURN reduce(m=meta, n IN delete_meta | m + n) AS delete_meta
}
WITH this, this_directors_Actor0, this_directors_Actor0_relationship, meta, collect(delete_meta) as delete_meta
WITH this, this_directors_Actor0, this_directors_Actor0_relationship, reduce(m=meta, n IN delete_meta | m + n) AS meta
WITH this, meta, this_directors_Actor0_relationship, collect(DISTINCT this_directors_Actor0) AS this_directors_Actor0_to_delete
CALL {
        WITH this_directors_Actor0_relationship, this_directors_Actor0_to_delete, this
        UNWIND this_directors_Actor0_to_delete AS x
        WITH [] + { event: "delete", id: id(x), properties: { old: x { .* }, new: null }, timestamp: timestamp(), typename: "Actor" } + { event: "delete_relationship", timestamp: timestamp(), id_from: id(x), id_to: id(this), id: id(this_directors_Actor0_relationship), relationshipName: "DIRECTED", fromTypename: "Actor", toTypename: "Movie", properties: { from: x { .* }, to: this { .* }, relationship: this_directors_Actor0_relationship { .* } } } AS meta, x, this_directors_Actor0_relationship, this
        DETACH DELETE x
        RETURN collect(meta) AS delete_meta
}
WITH delete_meta, meta
RETURN reduce(m=meta, n IN delete_meta | m + n) AS delete_meta
}
WITH this, meta, collect(delete_meta) as delete_meta
WITH this, reduce(m=meta, n IN delete_meta | m + n) AS meta
DETACH DELETE this
WITH collect(meta) AS meta
WITH REDUCE(m=[], n IN meta | m + n) AS meta
RETURN meta
  1. See that the node (:Person {name: "Julia", reputation:10}) does get disconnected but it does not get deleted even though it was targeted for deletion.
  2. Comment-out the union Director definition and uncomment the second one
  3. Run set-up cypher again
  4. Run delete mutation again
  5. Notice the node (:Person {name: "Julia", reputation:10}) does get deleted.

Expected behavior The node should have been deleted no matter which order the member types are defined in the union definition. More than that, any graphql definition order in the schema should not have an impact on the behaviour of the generated Cypher.

a-alle avatar Sep 27 '23 14:09 a-alle