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

Incremental Delivery spec draft #1110

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7ff9d0b
Extract common logic from ExecuteQuery, ExecuteMutation and ExecuteSu…
benjie Apr 28, 2023
8f4168b
Change ExecuteSelectionSet to ExecuteGroupedFieldSet
benjie Apr 28, 2023
69de3fd
Correct reference to MergeSelectionSets
benjie Aug 21, 2023
667364a
moves Field Collection section earlier
yaacovCR Feb 15, 2024
bb23a1b
Introduce `@defer` directive
yaacovCR Feb 15, 2024
65be49a
refactor a few lines out of YieldSubsequentResults
yaacovCR Jun 13, 2024
12513e1
add a word or two about which child nodes are being promoted
yaacovCR Jun 18, 2024
7284e2d
be more graphy
yaacovCR Jun 18, 2024
3fd7b90
fix timing
yaacovCR Jun 19, 2024
d3ab7a3
reuse function
yaacovCR Jun 19, 2024
853b031
fix
yaacovCR Jun 19, 2024
57d6193
rename BuildGraph to GraphFromRecords
yaacovCR Jun 19, 2024
fe42e8c
reword recursive abort case
yaacovCR Jun 19, 2024
d8966dc
bring BuildFieldPlan in line with implementation
yaacovCR Jul 17, 2024
136afea
rename "deferred grouped field set record" to "execution group"
yaacovCR Jul 17, 2024
f11f956
rename ExecuteExecutionGroup to CollectExecutionGroup
yaacovCR Jul 17, 2024
5e0a10a
properly initialize deferUsages with their parents
yaacovCR Jul 18, 2024
5490ed1
move Field Collection back to where it was
yaacovCR Jul 18, 2024
22dfbb8
f
yaacovCR Jul 18, 2024
b80d53b
use fieldDetailsList consistently
yaacovCR Jul 18, 2024
66536f4
add info re: data structures
yaacovCR Jul 18, 2024
a88da21
rename FieldPlan to ExecutionPlan
yaacovCR Jul 20, 2024
741605b
path => label
yaacovCR Jul 24, 2024
7251c7b
add missing arguments
yaacovCR Jul 25, 2024
2d121f1
add missing return value
yaacovCR Jul 25, 2024
c2d83a0
fix some renaming around CollectExecutionGroups and ExecuteExecutionG…
yaacovCR Jul 25, 2024
ccc26f2
Correct argument name
yaacovCR Aug 26, 2024
879818f
clarify errors from ExecuteExecutionPlan
yaacovCR Aug 26, 2024
a020ea1
add initial versions of explanations for the algorithms in the "Execu…
yaacovCR Aug 26, 2024
a6c164d
add subheadings
yaacovCR Sep 5, 2024
f15235a
adjust heading
yaacovCR Sep 6, 2024
50f644d
Initialize graph
yaacovCR Sep 6, 2024
afe40cd
adjust YieldSubsequentResults algorithm per review
yaacovCR Sep 6, 2024
606a6f1
reuse GetIncrementalResult() for the error case
yaacovCR Sep 6, 2024
f44deb5
add descriptions and fix bug within GetNewRootNodes, it needs the old…
yaacovCR Sep 6, 2024
f8f6f35
finish addressing review comments
yaacovCR Sep 6, 2024
4f8e668
add missing word
yaacovCR Sep 6, 2024
89983ce
Add Response Section for defer/stream (#4)
robrichard Sep 18, 2024
d40130d
Add directives and validation sections (#5)
robrichard Sep 18, 2024
f7e9124
Add examples to Response section
robrichard Nov 1, 2024
8e9cbcb
skip deferred fragment spread when already in visited fragments
robrichard Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ignoreRegExpList:
- /[a-z]{2,}'s/
words:
# Terms of art
- deprioritization
- endianness
- interoperation
- monospace
Expand Down
106 changes: 104 additions & 2 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -794,8 +794,9 @@ And will yield the subset of each object type queried:
When querying an Object, the resulting mapping of fields are conceptually
ordered in the same order in which they were encountered during execution,
excluding fragments for which the type does not apply and fields or fragments
that are skipped via `@skip` or `@include` directives. This ordering is
correctly produced when using the {CollectFields()} algorithm.
that are skipped via `@skip` or `@include` directives or temporarily skipped via
`@defer`. This ordering is correctly produced when using the {CollectFields()}
algorithm.

Response serialization formats capable of representing ordered maps should
maintain this ordering. Serialization formats which can only represent unordered
Expand Down Expand Up @@ -1942,6 +1943,11 @@ by a validator, executor, or client tool such as a code generator.

GraphQL implementations should provide the `@skip` and `@include` directives.

GraphQL implementations are not required to implement the `@defer` and `@stream`
directives. If either or both of these directives are implemented, they must be
implemented according to this specification. GraphQL implementations that do not
support these directives must not make them available via introspection.

GraphQL implementations that support the type system definition language must
provide the `@deprecated` directive if representing deprecated portions of the
schema.
Expand Down Expand Up @@ -2162,3 +2168,99 @@ to the relevant IETF specification.
```graphql example
scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")
```

### @defer

```graphql
directive @defer(
label: String
if: Boolean! = true
) on FRAGMENT_SPREAD | INLINE_FRAGMENT
```

The `@defer` directive may be provided for fragment spreads and inline fragments
to inform the executor to delay the execution of the current fragment to
indicate deprioritization of the current fragment. A query with `@defer`
directive will cause the request to potentially return multiple responses, where
non-deferred data is delivered in the initial response and data deferred is
delivered in a subsequent response. `@include` and `@skip` take precedence over
`@defer`.

```graphql example
query myQuery($shouldDefer: Boolean) {
user {
name
...someFragment @defer(label: "someLabel", if: $shouldDefer)
}
}
fragment someFragment on User {
id
profile_picture {
uri
}
}
```

#### @defer Arguments

- `if: Boolean! = true` - When `true`, fragment _should_ be deferred (see
related note below). When `false`, fragment will not be deferred and data will
be included in the initial response. Defaults to `true` when omitted.
- `label: String` - May be used by GraphQL clients to identify the data from
responses and associate it with the corresponding defer directive. If
provided, the GraphQL service must add it to the corresponding pending object
in the response. `label` must be unique label across all `@defer` and
`@stream` directives in a document. `label` must not be provided as a
variable.

### @stream

```graphql
directive @stream(
label: String
if: Boolean! = true
initialCount: Int = 0
) on FIELD
```

The `@stream` directive may be provided for a field of `List` type so that the
backend can leverage technology such as asynchronous iterators to provide a
partial list in the initial response, and additional list items in subsequent
responses. `@include` and `@skip` take precedence over `@stream`.

```graphql example
query myQuery($shouldStream: Boolean) {
user {
friends(first: 10) {
nodes @stream(label: "friendsStream", initialCount: 5, if: $shouldStream)
}
}
}
```

#### @stream Arguments

- `if: Boolean! = true` - When `true`, field _should_ be streamed (see related
note below). When `false`, the field will not be streamed and all list items
will be included in the initial response. Defaults to `true` when omitted.
- `label: String` - May be used by GraphQL clients to identify the data from
responses and associate it with the corresponding stream directive. If
provided, the GraphQL service must add it to the corresponding pending object
in the response. `label` must be unique label across all `@defer` and
`@stream` directives in a document. `label` must not be provided as a
variable.
- `initialCount: Int` - The number of list items the service should return as
part of the initial response. If omitted, defaults to `0`. A field error will
be raised if the value of this argument is less than `0`.

Note: The ability to defer and/or stream parts of a response can have a
potentially significant impact on application performance. Developers generally
need clear, predictable control over their application's performance. It is
highly recommended that GraphQL services honor the `@defer` and `@stream`
directives on each execution. However, the specification allows advanced use
cases where the service can determine that it is more performant to not defer
and/or stream. Therefore, GraphQL clients _must_ be able to process a response
that ignores the `@defer` and/or `@stream` directives. This also applies to the
`initialCount` argument on the `@stream` directive. Clients _must_ be able to
process a streamed response that contains a different number of initial list
items than what was specified in the `initialCount` argument.
181 changes: 180 additions & 1 deletion spec/Section 5 -- Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ FieldsInSetCanMerge(set):
{set} including visiting fragments and inline fragments.
- Given each pair of members {fieldA} and {fieldB} in {fieldsForName}:
- {SameResponseShape(fieldA, fieldB)} must be true.
- {SameStreamDirective(fieldA, fieldB)} must be true.
- If the parent types of {fieldA} and {fieldB} are equal or if either is not
an Object Type:
- {fieldA} and {fieldB} must have identical field names.
Expand Down Expand Up @@ -456,6 +457,16 @@ SameResponseShape(fieldA, fieldB):
- If {SameResponseShape(subfieldA, subfieldB)} is {false}, return {false}.
- Return {true}.

SameStreamDirective(fieldA, fieldB):

- If neither {fieldA} nor {fieldB} has a directive named `stream`.
- Return {true}.
- If both {fieldA} and {fieldB} have a directive named `stream`.
- Let {streamA} be the directive named `stream` on {fieldA}.
- Let {streamB} be the directive named `stream` on {fieldB}.
- If {streamA} and {streamB} have identical sets of arguments, return {true}.
- Return {false}.

Note: In prior versions of the spec the term "composite" was used to signal a
type that is either an Object, Interface or Union type.

Expand All @@ -467,7 +478,7 @@ unambiguous. Therefore any two field selections which might both be encountered
for the same object are only valid if they are equivalent.

During execution, the simultaneous execution of fields with the same response
name is accomplished by {MergeSelectionSets()} and {CollectFields()}.
name is accomplished by {CollectSubfields()}.

For simple hand-written GraphQL, this rule is obviously a clear developer error,
however nested fragments can make this difficult to detect manually.
Expand Down Expand Up @@ -1521,6 +1532,174 @@ query ($foo: Boolean = true, $bar: Boolean = false) {
}
```

### Defer And Stream Directives Are Used On Valid Root Field

** Formal Specification **

- For every {directive} in a document.
- Let {directiveName} be the name of {directive}.
- Let {mutationType} be the root Mutation type in {schema}.
- Let {subscriptionType} be the root Subscription type in {schema}.
- If {directiveName} is "defer" or "stream":
- The parent type of {directive} must not be {mutationType} or
{subscriptionType}.

**Explanatory Text**

The defer and stream directives are not allowed to be used on root fields of the
mutation or subscription type.

For example, the following document will not pass validation because `@defer`
has been used on a root mutation field:

```raw graphql counter-example
mutation {
... @defer {
mutationField
}
}
```

### Defer And Stream Directives Are Used On Valid Operations

** Formal Specification **

- Let {subscriptionFragments} be the empty set.
- For each {operation} in a document:
- If {operation} is a subscription operation:
- Let {fragments} be every fragment referenced by that {operation}
transitively.
- For each {fragment} in {fragments}:
- Let {fragmentName} be the name of {fragment}.
- Add {fragmentName} to {subscriptionFragments}.
- For every {directive} in a document:
- If {directiveName} is not "defer" or "stream":
- Continue to the next {directive}.
- Let {ancestor} be the ancestor operation or fragment definition of
{directive}.
- If {ancestor} is a fragment definition:
- If the fragment name of {ancestor} is not present in
{subscriptionFragments}:
- Continue to the next {directive}.
- If {ancestor} is not a subscription operation:
- Continue to the next {directive}.
- Let {if} be the argument named "if" on {directive}.
- {if} must be defined.
- Let {argumentValue} be the value passed to {if}.
- {argumentValue} must be a variable, or the boolean value "false".

**Explanatory Text**

The defer and stream directives can not be used to defer or stream data in
subscription operations. If these directives appear in a subscription operation
they must be disabled using the "if" argument. This rule will not permit any
defer or stream directives on a subscription operation that cannot be disabled
using the "if" argument.

For example, the following document will not pass validation because `@defer`
has been used in a subscription operation with no "if" argument defined:

```raw graphql counter-example
subscription sub {
newMessage {
... @defer {
body
}
}
}
```

### Defer And Stream Directive Labels Are Unique

** Formal Specification **

- Let {labelValues} be an empty set.
- For every {directive} in the document:
- Let {directiveName} be the name of {directive}.
- If {directiveName} is "defer" or "stream":
- For every {argument} in {directive}:
- Let {argumentName} be the name of {argument}.
- Let {argumentValue} be the value passed to {argument}.
- If {argumentName} is "label":
- {argumentValue} must not be a variable.
- {argumentValue} must not be present in {labelValues}.
- Append {argumentValue} to {labelValues}.

**Explanatory Text**

The `@defer` and `@stream` directives each accept an argument "label". This
label may be used by GraphQL clients to uniquely identify response payloads. If
a label is passed, it must not be a variable and it must be unique within all
other `@defer` and `@stream` directives in the document.

For example the following document is valid:

```graphql example
{
dog {
...fragmentOne
...fragmentTwo @defer(label: "dogDefer")
}
pets @stream(label: "petStream") {
name
}
}

fragment fragmentOne on Dog {
name
}

fragment fragmentTwo on Dog {
owner {
name
}
}
```

For example, the following document will not pass validation because the same
label is used in different `@defer` and `@stream` directives.:

```raw graphql counter-example
{
dog {
...fragmentOne @defer(label: "MyLabel")
}
pets @stream(label: "MyLabel") {
name
}
}

fragment fragmentOne on Dog {
name
}
```

### Stream Directives Are Used On List Fields

**Formal Specification**

- For every {directive} in a document.
- Let {directiveName} be the name of {directive}.
- If {directiveName} is "stream":
- Let {adjacent} be the AST node the directive affects.
- {adjacent} must be a List type.

**Explanatory Text**

GraphQL directive locations do not provide enough granularity to distinguish the
type of fields used in a GraphQL document. Since the stream directive is only
valid on list fields, an additional validation rule must be used to ensure it is
used correctly.

For example, the following document will only pass validation if `field` is
defined as a List type in the associated schema.

```graphql counter-example
query {
field @stream(initialCount: 0)
}
```

## Variables

### Variable Uniqueness
Expand Down
Loading
Loading