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

Edge and Constraint Validation implementations and queries in strange transactions #91

Open
epii-1 opened this issue Jul 27, 2020 · 0 comments
Labels
question Further information is requested

Comments

@epii-1
Copy link
Collaborator

epii-1 commented Jul 27, 2020

Edge Validation and Constraint Validation of the @uniqueForTarget directive have very similar roles. Theoretically the only difference between them is that the edge validation checks that the source has one one such edge (when not a list), while the constraint makes sure the target only has one such edge. But they are implemented very differently:
The Edge validation is enforced during the transaction, while the constraint is not enforced until finishing the transaction.
This behavior might need to be standardized to avoid some confusion.

But it also leads to the question of how to standardized it. Do we want to enforce them both during or just before finishing the transaction? If not done carefully both alternatives might actually lead to some very strange results while mixing queries with transactions.
Take the following example schema:

type Source {
    id: ID!
    edgeA: Target
    edgeB: [Target] @uniqueForTarget 
}
type Target {
    id: ID!
   _edgeAEdgeFromSource: [Source]
   _edgeBEdgeFromSource: Source
}

Notice the differences between the edges here.

We also assume we have some (custom)query that takes an id of a Source object and returns the total number of edges originating from the Source with that id: NumberOfEdgesFromSource(ID!): Int.

As I am not really sure how the exact syntax for reusing results during transactions will look like, I will just use the following to represent this:

createObject(data: SomeData)
{
    id bind X
}

Where X is now a variabel we can reuse in further steps of the transaction.

Now take the following two, similar but different, transactions which demonstrates the differences between the two methods of enforcement following how they are current implemented:
1.

mutation {
    // This might actually fail as I might missuse how to create objects from empty data, but you get the point.
    createTarget(data: {}){
        id bind targetID
    }
    createSource(data: {}){
        id bind sourceID
    }
    createEdgeAEdgeFromSource(data: {
        sourceID: sourceID
        targetID: targetID
    }){
        id bind edgeA1ID
    }
   createEdgeAEdgeFromSource(data: {
        sourceID: sourceID
        targetID: targetID
    }){
        id bind edgeA2ID
    }
}
query {
    NumberOfEdgesFromSource(sourceID) bind Result
}

mutation {
    deleteEdgeAEdgeFromSource(edgeA2ID)
}
mutation {
    // This might actually fail as I might missuse how to create objects from empty data, but you get the point.
    createTarget(data: {}){
        id bind targetID
    }
    createSource(data: {}){
        id bind sourceID
    }
    createEdgeBEdgeFromSource(data: {
        sourceID: sourceID
        targetID: targetID
    }){
        id bind edgeB1ID
    }
   createEdgeBEdgeFromSource(data: {
        sourceID: sourceID
        targetID: targetID
    }){
        id bind edgeB2ID
    }
}
query {
    NumberOfEdgesFromSource(sourceID) bind Result
}

mutation {
    deleteEdgeBEdgeFromSource(edgeB2ID)
}

Transaction 1 will fail early as the edge validation will throw an error as we are trying to put two edges into a field that takes only one edge. Even if the results of the complete transaction would actually be correct according to the schema.

Transaction 2 will not fail as the end results is correct according to the schema. But the value returned from the intermediate query would be two, which does not fit with what one might expect, as there is only one edge after the transaction has finished.

This is of course quite an harmless example. There might be worse cases where the results from the intermediate query is then reused in further mutations and where these non-correct states basically leads to data loss or data corruption. Depending in how schemas are built, this might even turn out to be a security flaw.

A solution would be to implement intermediate sub-transactions where correct state is enforced before each intermediate query in an transaction.
But the question is: do we really care? Will this actually be doable in practice, or will we not allow queries during transactions?

@hartig @keski @rcapshaw

@epii-1 epii-1 added the question Further information is requested label Jul 27, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

1 participant