Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
a-alle opened this issue Sep 27, 2023 · 3 comments
Labels
bug report Something isn't working confirmed Confirmed bug
Projects

Comments

@a-alle
Copy link
Contributor

a-alle commented Sep 27, 2023

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 a-alle added confirmed Confirmed bug bug report Something isn't working labels Sep 27, 2023
@neo4j-team-graphql neo4j-team-graphql added this to Bug reports in Bug Triage Sep 27, 2023
@neo4j-team-graphql
Copy link
Collaborator

Many thanks for raising this bug report @a-alle. 🐛 We will now attempt to reproduce the bug based on the steps you have provided.

Please ensure that you've provided the necessary information for a minimal reproduction, including but not limited to:

  • Type definitions
  • Resolvers
  • Query and/or Mutation (or multiple) needed to reproduce

If you have a support agreement with Neo4j, please link this GitHub issue to a new or existing Zendesk ticket.

Thanks again! 🙏

1 similar comment
@neo4j-team-graphql
Copy link
Collaborator

Many thanks for raising this bug report @a-alle. 🐛 We will now attempt to reproduce the bug based on the steps you have provided.

Please ensure that you've provided the necessary information for a minimal reproduction, including but not limited to:

  • Type definitions
  • Resolvers
  • Query and/or Mutation (or multiple) needed to reproduce

If you have a support agreement with Neo4j, please link this GitHub issue to a new or existing Zendesk ticket.

Thanks again! 🙏

@neo4j-team-graphql
Copy link
Collaborator

We've been able to confirm this bug using the steps to reproduce that you provided - many thanks @a-alle! 🙏 We will now prioritise the bug and address it appropriately.

@neo4j-team-graphql neo4j-team-graphql moved this from Bug reports to Confirmed in Bug Triage Sep 27, 2023
@darrellwarde darrellwarde moved this from Confirmed to Low priority in Bug Triage Jan 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug report Something isn't working confirmed Confirmed bug
Projects
Bug Triage
Low priority
Development

No branches or pull requests

2 participants