Handle partial data and error paths - #926
Conversation
78fc852 to
9557888
Compare
|
I think the spec is open to interpretation here. The relevant sections are 6.3.3, 6.4.4, 7.1, esp. 7.1.6 and example 209. I can't see explicit text there which says that a conforming implementation MUST return a partial result, I see more of an assumption that it typically will. I think that fits with the execution model of the Javascript reference implementation, where every field is backed by it's own effectful "resolver" each of which could fail independently. That said, I read the spec as saying that if a partial result is returned, then it MUST propagate nulls up to nullable positions in the way described. I think on balance I'm comfortable with this. Grackle's execution model is different, in as much as typically if the base mapping fails (eg. due to DB query failure) that will typically be sufficiently catastrophic that a partial result won't be possible. But I think it should be possible to have the failure of an isolated effect handler nested at the leaf of a query result in a partial result being returned. In fact, I think I had assumed that this was already at least partly possible because a failing effect handler can already return a null result with problems attached ... the missing part would be the upwards null propagation. I'm not too concerned about "users that expect data to be complete if there is data at the root" ... thanks to the null propagation to a nullable field, the result is always schema-valid, so the client should be able to handle results of that form. Would it be practical to make it possible to opt in to a "no partial failures" mode which matches the current behaviour? |
|
I agree part of the spec seems written from a somewhat implementation-oriented perspective. Which doesn't help. But I don't know how open to interpretation it is. From 6.3.3:
In 6.4.4:
From 7.1.6:
So execution errors (AKA during effect resolution) have to result in partial data, without halting sibling continuation ( For Grackle, if the base mapping is a Skunk mapping and the database is down then the execution error appears at the base, so the base of the result is null ( With this PR, if the base mapping has a Skunk field
That's exactly the only thing this PR does 😁 (plus
It might be. The JavaScript GraphQL library has recently introduced an experimental directive I know this is a long comment, for an already complicated PR, so I apologize 😅. But I think it is important to understand the impact and actually agree on if this is what Grackle is intended to do. |
This is a complicated change, and open to discussion if it is actually wanted.
Failed fields now propagate their `null` to the nearest nullable position, rather than a `Result.Failure` which discards all data. Example:
Before:
```graphql
query {
ping
viaEffect {
name
}
}
{
"errors": [
{ "message": "boom" }
]
}
```
After:
```graphql
query {
ping
viaEffect {
name
}
}
{
"data": {
"ping": "pong",
"viaEffect": null
},
"errors": [
{ "message": "boom", "path": ["viaEffect"] }
]
}
```
This is a big breaking change for users that expect data to be complete if there is `data` at the root. But it is in line with the GraphQL specification, which says that a failed field should not discard its siblings' data. This partially closes a few conformance suites, except for `location`s in errors, which are not (yet) implemented.
---
The interpreter now tracks the response position of each value:
- A failure at a nullable position completes as null and keeps the data of its siblings.
- A failure at a non-null position propagates its null to the nearest enclosing nullable position.
- A null which reaches the root leaves `data` as null.
Each problem carries the response path of its own position, with the alias of the field and the index of the list entry.
Both error policies now complete the deferred positions of a failed batch as null and keep the rest of the response. An internal error stays a request error and aborts the completion.
BREAKING: `Problem.path` changes type from `List[String]` to `List[Problem.PathSegment]`, so that a path can hold list indexes. A segment is a `Name(String)` or an `Index(Int)`.
9557888 to
b56c3ee
Compare
|
Yes, I think you're right. As you point out, there's ongoing discussion around controlling this behaviour. Even in we don't expose an experimental directive, would it make sense to include the plumbing to enable the choice now, rather than wait and retrofit it later? FWIW, the discussions seem to be focussed on client controls on null propagation. It seems at least as reasonable to think about server controls (which I could imaging being implemented via a schema directive). I must admit, my intuitions would be to favour a default "fail all, fail fast" semantics determined by the server, but then I'm a server implementor, not a client 😉 |
|
I'm not sure what you mean by introducing the plumbing, if there's no way to control it. I must be missing something because it sounds like dead code 😅. I think the error propagation comes from the idea of wanting GraphQL to have a "mega-query" that gets everything you need to render a page, rather than a million little requests. If you have that, different parts of a query aren't necessarily related to each other, so if one service is down most of the query will still work. For me most queries in GraphQL have been a single thing per query, so maybe I'm doing something wrong. Maybe I just need more microservices or something 🤷 |
I'm imagining something along the lines of an additional argument to But that isn't a blocker. I'm happy to approve this without it. |
This is a complicated change, and open to discussion if it is actually wanted.
Failed fields now propagate their
nullto the nearest nullable position, rather than aResult.Failurewhich discards all data. Example:Before:
After:
More examples are in the tests, which show the difference clearly. This is a big breaking change for users that expect data to be complete if there is
dataat the root. Release notes should mention this very clearly. But it is in line with the GraphQL specification, which says that a failed field should not discard its siblings' data. This partially closes a few conformance suites, except forlocations in errors, which are not (yet) implemented.The interpreter now tracks the response position of each value:
dataas null.Each problem carries the response path of its own position, with the alias of the field and the index of the list entry.
Both error policies now complete the deferred positions of a failed batch as null and keep the rest of the response. An internal error stays a request error and aborts the completion.
BREAKING:
Problem.pathchanges type fromList[String]toList[Problem.PathSegment], so that a path can hold list indexes. A segment is aName(String)or anIndex(Int).