diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 9a87d6a5b..29990a9c2 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -697,8 +697,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 query 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. +fragments 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 @@ -1752,7 +1753,7 @@ A GraphQL schema describes directives which are used to annotate various parts of a GraphQL document as an indicator that they should be evaluated differently by a validator, executor, or client tool such as a code generator. -GraphQL implementations should provide the `@skip` and `@include` directives. +GraphQL implementations should provide the `@skip`, `@include`, `@defer` and `@stream` directives. GraphQL implementations that support the type system definition language must provide the `@deprecated` directive if representing deprecated portions of @@ -1920,3 +1921,48 @@ type ExampleType { oldField: String @deprecated(reason: "Use `newField`.") } ``` + +### @defer +```graphql +directive @defer(label: String!, if: Boolean) 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 delivered in a subsequent response. +`@include` and `@skip` take presedence over `@defer`. + +```graphql example +query myQuery($shouldDefer: Boolean) { + user { + name + ...someFragment @defer(label: 'someLabel', if: $shouldDefer) + } +} +fragment someFragment on User { + id + profile_picture { + uri + } +} +``` + +### @stream +```graphql +directive @stream(label: String!, initial_count: Int!, if: Boolean) on FIELD +``` +The `@stream` directive may be provided for a field of `List` type so that the +backend can leverage technology such asynchronous iterators to provide a partial +list in the initial response, and additional list items in subsequent responses. +`@include` and `@skip` take presedence over `@stream`. +```graphql example +query myQuery($shouldDefer: Boolean) { + user { + friends(first: 10) { + nodes @stream(label: "friendsStream", initial_count: 5) + } + } +} + +``` diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 4247e2e8e..4ea1f0282 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -26,11 +26,15 @@ request is determined by the result of executing this operation according to the "Executing Operations” section below. ExecuteRequest(schema, document, operationName, variableValues, initialValue): - + Note: the execution assumes implementing language support coroutines. + Alternatively, the socket can provide a write buffer pointer to allow {ExecuteRequest()} + to directly write payloads into the buffer. * Let {operation} be the result of {GetOperation(document, operationName)}. * Let {coercedVariableValues} be the result of {CoerceVariableValues(schema, operation, variableValues)}. * If {operation} is a query operation: - * Return {ExecuteQuery(operation, schema, coercedVariableValues, initialValue)}. + * Let {payloads} be the iterator obtained from calling {ExecuteQuery(operation, schema, coercedVariableValues, initialValue, subsequentPayloads)}. + * For reach {payload} in {payloads}: + * Yield {payload}. * Otherwise if {operation} is a mutation operation: * Return {ExecuteMutation(operation, schema, coercedVariableValues, initialValue)}. * Otherwise if {operation} is a subscription operation: @@ -124,15 +128,21 @@ An initial value may be provided when executing a query. ExecuteQuery(query, schema, variableValues, initialValue): + * Let {subsequentPayloads} be an empty list. * Let {queryType} be the root Query type in {schema}. * Assert: {queryType} is an Object type. * Let {selectionSet} be the top level Selection Set in {query}. * Let {data} be the result of running - {ExecuteSelectionSet(selectionSet, queryType, initialValue, variableValues)} + {ExecuteSelectionSet(selectionSet, queryType, initialValue, variableValues, subsequentPayloads)} *normally* (allowing parallelization). * Let {errors} be any *field errors* produced while executing the selection set. - * Return an unordered map containing {data} and {errors}. + * Yield an unordered map containing {data} and {errors}. + * For each {payload} in {subsequentPayloads}: + * If {payload} is a Deferred Fragment Record: + * Yield the value from calling {ResolveDeferredFragmentRecord(payload, variableValues, subsequentPayloads)}. + * If {payload} is a Stream Record: + * Yield ResolveStreamRecord(payload, variableValues, subsequentPayloads). ### Mutation @@ -320,17 +330,19 @@ First, the selection set is turned into a grouped field set; then, each represented field in the grouped field set produces an entry into a response map. -ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): +ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues, subsequentPayloads, parentPath): + * If {subsequentPayloads} is not provided, initialize it to the empty set. + * If {parentPath} is not provided, initialize it to an emtpy list. * Let {groupedFieldSet} be the result of - {CollectFields(objectType, selectionSet, variableValues)}. + {CollectFields(objectType, objectValue, selectionSet, variableValues, subsequentPayloads, parentPath)}. * Initialize {resultMap} to an empty ordered map. * For each {groupedFieldSet} as {responseKey} and {fields}: * Let {fieldName} be the name of the first entry in {fields}. Note: This value is unaffected if an alias is used. * Let {fieldType} be the return type defined for the field {fieldName} of {objectType}. * If {fieldType} is defined: - * Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, fields, variableValues)}. + * Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, fields, variableValues, subsequentPayloads, parentPath)}. * Set {responseValue} as the value for {responseKey} in {resultMap}. * Return {resultMap}. @@ -452,7 +464,12 @@ Before execution, the selection set is converted to a grouped field set by calling {CollectFields()}. Each entry in the grouped field set is a list of fields that share a response key (the alias if defined, otherwise the field name). This ensures all fields with the same response key included via -referenced fragments are executed at the same time. +referenced fragments are executed at the same time. A deferred seclection set's +fields will not be included in the grouped field set. Rather, a record +representing the deferred fragment and addition context will be stored in a +list. The executor revisits and resume execution for the list of deferred +fragment records after the initial execution finishes. + As an example, collecting the fields of this selection set would collect two instances of the field `a` and one of field `b`: @@ -477,7 +494,7 @@ The depth-first-search order of the field groups produced by {CollectFields()} is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. -CollectFields(objectType, selectionSet, variableValues, visitedFragments): +CollectFields(objectType, objectValue, selectionSet, variableValues, deferredFragments, parentPath, visitedFragments): * If {visitedFragments} is not provided, initialize it to the empty set. * Initialize {groupedFields} to an empty ordered map of lists. @@ -497,7 +514,6 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): * Let {fragmentSpreadName} be the name of {selection}. * If {fragmentSpreadName} is in {visitedFragments}, continue with the next {selection} in {selectionSet}. - * Add {fragmentSpreadName} to {visitedFragments}. * Let {fragment} be the Fragment in the current Document whose name is {fragmentSpreadName}. * If no such {fragment} exists, continue with the next {selection} in @@ -506,6 +522,11 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): * If {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue with the next {selection} in {selectionSet}. * Let {fragmentSelectionSet} be the top-level selection set of {fragment}. + * If {fragmentSpreadName} provides the directive `@defer`, let {deferDirective} be that directive. + * If {deferDirective}'s {if} argument is {true} or is a variable in {variableValues} with the value {true}: + * Let {deferredFragment} be the result of calling {DeferFragment(objectType, objectValue, fragmentSelectionSet, parentPath)}. + * Append {deferredFragment} to {deferredFragments}. + * Continue with the next {selection} in {selectionSet}. * Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. * For each {fragmentGroup} in {fragmentGroupedFieldSet}: @@ -513,18 +534,27 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): * Let {groupForResponseKey} be the list in {groupedFields} for {responseKey}; if no such list exists, create it as an empty list. * Append all items in {fragmentGroup} to {groupForResponseKey}. + * Add {fragmentSpreadName} to {visitedFragments}. * If {selection} is an {InlineFragment}: * Let {fragmentType} be the type condition on {selection}. * If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue with the next {selection} in {selectionSet}. * Let {fragmentSelectionSet} be the top-level selection set of {selection}. - * Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. + * If {InlineFragment} provides the directive `@defer`, let {deferDirective} be that directive. + * If {deferDirective}'s {if} argument is {true} or is a variable in {variableValues} with the value {true}: + * Let {deferredFragment} be the result of calling {DeferFragment(objectType, objectValue, fragmentSelectionSet, parentPath)}. + * Append {deferredFragment} to {deferredFragments}. + * Continue with the next {selection} in {selectionSet}. + * Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments, parentPath)}. * For each {fragmentGroup} in {fragmentGroupedFieldSet}: * Let {responseKey} be the response key shared by all fields in {fragmentGroup}. * Let {groupForResponseKey} be the list in {groupedFields} for {responseKey}; if no such list exists, create it as an empty list. * Append all items in {fragmentGroup} to {groupForResponseKey}. * Return {groupedFields}. + +Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` +directives may be applied in either order since they apply commutatively. DoesFragmentTypeApply(objectType, fragmentType): @@ -534,9 +564,34 @@ DoesFragmentTypeApply(objectType, fragmentType): * if {objectType} is an implementation of {fragmentType}, return {true} otherwise return {false}. * If {fragmentType} is a Union: * if {objectType} is a possible type of {fragmentType}, return {true} otherwise return {false}. - -Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` -directives may be applied in either order since they apply commutatively. + +DeferFragment(objectType, objectValue, fragmentSelectionSet, parentPath): + * Let {label} be the value or the variable to {deferDirective}'s {label} argument. + * Let {deferredFragmentRecord} be the result of calling {CreateDeferredFragmentRecord(label, objectType, objectValue, fragmentSelectionSet, parentPath)}. + * return {deferredFragmentRecord}. + + +#### Deferred Fragment Record +**Formal Specification** +Let {deferredFragment} be an inline fragment or fragment spread with `@defer` provided. +Deferred Fragment Record is a structure containing: +* {label} value derived from the `@defer` directive. +* {objectType} of the {deferredFragment}. +* {objectValue} of the {deferredFragment}. +* {fragmentSelectionSet}: the top level selection set of {deferredFragment}. +* {path} a list of field names and indices from root to {deferredFragment}. + +CreateDeferredFragmentRecord(label, objectType, objectValue, fragmentSelectionSet, path): + * If {path} is not provided, initialize it to an empty list. + * Construct a deferred fragment record based on the parameters passed in. + +ResolveDeferredFragmentRecord(deferredFragmentRecord, variableValues, subsequentPayloads): + * Let {label, objectType, objectValue, fragmentSelectionSet, path} be the corresponding fields + in the deferred fragment record structure. + * Let {payload} be the result of calling {ExecuteSelectionSet(fragmentSelectionSet, objectType, objectValue, variableValues, subsequentPayloads, path)}. + * Add an entry to {payload} named `label` with the value {label}. + * Add an entry to {payload} named `path` with the value {path}. + * Return {payload}. ## Executing Fields @@ -547,12 +602,20 @@ coerces any provided argument values, then resolves a value for the field, and finally completes that value either by recursively executing another selection set or coercing a scalar value. -ExecuteField(objectType, objectValue, fieldType, fields, variableValues): +ExecuteField(objectType, objectValue, fieldType, fields, variableValues, subsequentPayloads, parentPath): * Let {field} be the first entry in {fields}. * Let {fieldName} be the field name of {field}. * Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} + * If {field} provides the directive `@stream`, let {streamDirective} be that directive. + * Let {initialCount} be the value or variable provided to {streamDirective}'s {initial_count} argument. + * Let {resolvedValue} be {ResolvedFieldGenerator(objectType, objectValue, fieldName, argumentValues, initialCount)}. + * Let {result} be the result of calling {CompleteValue(fieldType, fields, resolvedValue, variableValues, subsequentPayloads, parentPath)}. + * Append {fieldName} to the {path} field of every {subsequentPayloads}. + * Return {result}. * Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. - * Return the result of {CompleteValue(fieldType, fields, resolvedValue, variableValues)}. + * Let {result} be the result of calling {CompleteValue(fieldType, fields, resolvedValue, variableValues, subsequentPayloads)}. + * Append {fieldName} to the {path} of for every {subsequentPayloads}. + * Return {result}. ### Coercing Field Arguments @@ -616,30 +679,83 @@ must only allow usage of variables of appropriate types. While nearly all of GraphQL execution can be described generically, ultimately the internal system exposing the GraphQL interface must provide values. This is exposed via {ResolveFieldValue}, which produces a value for a given -field on a type for a real value. +field on a type for a real value. In addition, {ResolveFieldGenerator} will be +exposed to produce an iterator for a field with `List` return type. +The internal system may optionally define a generator function. In the case +where the generator is not defined, the GraphQL executor provide a default generator. +For example, a trivial generator that yield the entire list upon the first iteration. -As an example, this might accept the {objectType} `Person`, the {field} +As an example, a {ResolveFieldValue} might accept the {objectType} `Person`, the {field} {"soulMate"}, and the {objectValue} representing John Lennon. It would be expected to yield the value representing Yoko Ono. +A {ResolveFieldGenerator} might accept the {objectType} `MusicBand`, the {field} +{"members"}, and the {objectValue} representing Beatles. It would be expected to yield +a iterator of values representing, John Lennon, Paul, McCartney, Ringo Starr and +George Harrison. + ResolveFieldValue(objectType, objectValue, fieldName, argumentValues): * Let {resolver} be the internal function provided by {objectType} for determining the resolved value of a field named {fieldName}. * Return the result of calling {resolver}, providing {objectValue} and {argumentValues}. + +ResolveFieldGenerator(objectType, objectValue, fieldName, argumentValues, initialCount): + * If {objectType} provide an internal function {generatorResolver} for + generating partitially resolved valueof a list field named {fieldName}: + * Let {generatorResolver} be the internal function. + * Return the iterator from calling {generatorResolver}, providing + {objectValue}, {argumentValues} and {initialCount}. + * Create {generator} from {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. + * Return {generator}. Note: It is common for {resolver} to be asynchronous due to relying on reading an underlying database or networked service to produce a value. This necessitates the rest of a GraphQL executor to handle an asynchronous -execution flow. - +execution flow. In addition, a commom implementation of {generator} is to leverage +asynchronos iterators or asynchronos generators provided by many programing languages. ### Value Completion After resolving the value for a field, it is completed by ensuring it adheres to the expected return type. If the return type is another Object type, then -the field execution process continues recursively. - -CompleteValue(fieldType, fields, result, variableValues): +the field execution process continues recursively. In the case where a value +returned for a list type field is an iterator due to `@stream` specified on the +field, value completition iterates over the iterator until the number of items +yield by the iterator satisfies `initial_count` specified on the `@stream` directive. +Unresolved items in the iterator will be stored in a stream record which the executor +resumes to execute after the initial execution finishes. + +#### Stream Record +**Formal Specification** +Let {streamField} be a list field with a `@stream` directive provided. +A Stream Record is a structure containing: + {label}: value derived from the `@stream` directive's `label` argument. + {iterator}: created by {ResolveFieldGenerator}. + {resolvedItems}: items resolved from the {iterator} but not yet delivered. + {index}: indicating the position of the item in the complete list. + {path}: a list of field names and indices from root to {streamField}. + {fields}: the group of fields grouped by CollectFields() for {streamField}. + {innerType}: inner type of {streamField}'s type. + +CreateStreamRecord(label, initialCount, iterator, resolvedItems, index, fields, innerType): +* Construct a stream record based on the parameters passed in. + +ResolveStreamRecord(streamRecord, variableValues, subsequentPayloads): +* Let {label, iterator, resolvedItems, index, path, fields, innerType} be the correspondent fields on + the Stream Record structure. +* Remove the first entry from {resolvedItem}, let the entry be {item}. If {resolvedItem} is empty, + retrieve more items from {iterator}: + * Append {index} to {path}. + * Increment {index}. + * Let {payload} be the result of calling CompleteValue(innerType, fields, item, variableValues, subsequentPayloads, path)}. + * Add an entry to {payload} named `label` with the value {label}. + * Add an entry to {payload} named `path` with the value {path}. + * If {resolveItem} is not empty or {iterator} does not reach the end: + * Append {streamRecord} to {subsequentPayloads}. + * Return {payload}. + + +CompleteValue(fieldType, fields, result, variableValues, subsequentPayloads, parentPath): * If the {fieldType} is a Non-Null type: * Let {innerType} be the inner type of {fieldType}. * Let {completedResult} be the result of calling @@ -649,10 +765,26 @@ CompleteValue(fieldType, fields, result, variableValues): * If {result} is {null} (or another internal value similar to {null} such as {undefined} or {NaN}), return {null}. * If {fieldType} is a List type: + * If {result} is a iterator: + * Let {field} be thte first entry in {fields}. + * Let {innerType} be the inner type of {fieldType}. + * Let {streamDirective} be the `@stream` directived provided on {field}. + * Let {initialCount} be the value or variable provided to {streamDirective}'s {initial_count} argument. + * Let {label} be the value or variable provided to {streamDirective}'s {label} argument. + * Let {resolvedItems} be an empty list + * For each {members} in {result}: + * Append all items from {members} to {resolvedItems}. + * If the length of {resolvedItems} is greater or equal to {initialCount}: + * Let {initialItems} be the sublist from the beginning to the {initialCount}-th item. + * Let {remainingItems} be the sublist from {initialCount}-th + 1 item to the end. + * Let {streamRecord} be the result of calling {CreateStreamRecord(label, initialCount, result, remainingItems, initialCount, fields, innerType, parentPath)} + * Append {streamRecord} to {subsequentPayloads}. + * Let {result} be {initialItems}. + * Exit For each loop. * If {result} is not a collection of values, throw a field error. * Let {innerType} be the inner type of {fieldType}. * Return a list where each list item is the result of calling - {CompleteValue(innerType, fields, resultItem, variableValues)}, where + {CompleteValue(innerType, fields, resultItem, variableValues, subsequentPayloads, parentPath)}, where {resultItem} is each item in {result}. * If {fieldType} is a Scalar or Enum type: * Return the result of "coercing" {result}, ensuring it is a legal value of @@ -663,7 +795,7 @@ CompleteValue(fieldType, fields, result, variableValues): * Otherwise if {fieldType} is an Interface or Union type. * Let {objectType} be {ResolveAbstractType(fieldType, result)}. * Let {subSelectionSet} be the result of calling {MergeSelectionSets(fields)}. - * Return the result of evaluating {ExecuteSelectionSet(subSelectionSet, objectType, result, variableValues)} *normally* (allowing for parallelization). + * Return the result of evaluating {ExecuteSelectionSet(subSelectionSet, objectType, result, variableValues, subsequentPayloads, parentPath)} *normally* (allowing for parallelization). **Resolving Abstract Types**