diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 47d3efb3d..e917375d6 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -757,7 +757,7 @@ type Person { } ``` -Valid operations must supply a nested field set for any field that returns an +Valid operations must supply a selection of fields for any field that returns an object, so this operation is not valid: ```graphql counter-example diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 6cebd7310..af9d7e33d 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -468,7 +468,7 @@ should be 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. diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index f3e080705..018aa533c 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -133,8 +133,8 @@ respectively. ### Query If the operation is a query, the result of the operation is the result of -executing the operation’s top level _selection set_ with the query root -operation type. +executing the operation’s _root selection set_ with the query root operation +type. An initial value may be provided when executing a query operation. @@ -142,19 +142,15 @@ ExecuteQuery(query, schema, variableValues, initialValue): - 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)} _normally_ (allowing - parallelization). -- Let {errors} be the list of all _execution error_ raised while executing the - selection set. -- Return an unordered map containing {data} and {errors}. +- Let {rootSelectionSet} be the _root selection set_ in {query}. +- Return {ExecuteRootSelectionSet(variableValues, initialValue, queryType, + rootSelectionSet, "normal")}. ### Mutation If the operation is a mutation, the result of the operation is the result of -executing the operation’s top level _selection set_ on the mutation root object -type. This selection set should be executed serially. +executing the operation’s _root selection set_ on the mutation root object type. +This selection set should be executed serially. It is expected that the top level fields in a mutation operation perform side-effects on the underlying data system. Serial execution of the provided @@ -164,12 +160,9 @@ ExecuteMutation(mutation, schema, variableValues, initialValue): - Let {mutationType} be the root Mutation type in {schema}. - Assert: {mutationType} is an Object type. -- Let {selectionSet} be the top level selection set in {mutation}. -- Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - mutationType, initialValue, variableValues)} _serially_. -- Let {errors} be the list of all _execution error_ raised while executing the - selection set. -- Return an unordered map containing {data} and {errors}. +- Let {rootSelectionSet} be the _root selection set_ in {mutation}. +- Return {ExecuteRootSelectionSet(variableValues, initialValue, mutationType, + rootSelectionSet, "serial")}. ### Subscription @@ -335,13 +328,9 @@ ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue): - Let {subscriptionType} be the root Subscription type in {schema}. - Assert: {subscriptionType} is an Object type. -- Let {selectionSet} be the top level selection set in {subscription}. -- Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - subscriptionType, initialValue, variableValues)} _normally_ (allowing - parallelization). -- Let {errors} be the list of all _execution error_ raised while executing the - selection set. -- Return an unordered map containing {data} and {errors}. +- Let {rootSelectionSet} be the _root selection set_ in {subscription}. +- Return {ExecuteRootSelectionSet(variableValues, initialValue, + subscriptionType, rootSelectionSet, "normal")}. Note: The {ExecuteSubscriptionEvent()} algorithm is intentionally similar to {ExecuteQuery()} since this is how each event result is produced. @@ -359,17 +348,227 @@ Unsubscribe(responseStream): ## Executing Selection Sets -To execute a _selection set_, the object value being evaluated and the object -type need to be known, as well as whether it must be executed serially, or may -be executed in parallel. +The process of executing a GraphQL operation is to recursively execute every +selected field in the operation. To do this, first all initially selected fields +from the operation's top most _root selection set_ are collected, then each +executed. As each field completes, all its subfields are collected, then each +executed. This process continues until there are no more subfields to collect +and execute. + +### Executing the Root Selection Set + +:: A _root selection set_ is the top level _selection set_ provided by a GraphQL +operation. A root selection set always selects from a _root operation type_. + +To execute the root selection set, the initial value being evaluated and the +root type must be known, as well as whether it must be executed serially, or may +be executed in parallel (see +[Normal and Serial Execution](#sec-Normal-and-Serial-Execution). -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 result map. +Executing the root selection set works similarly for queries (parallel), +mutations (serial), and subscriptions (where it is executed for each event in +the underlying Source Stream). -ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): +First, the _selection set_ is collected into a _grouped field set_ which is then +executed, returning the resulting {data} and {errors}. + +ExecuteRootSelectionSet(variableValues, initialValue, objectType, selectionSet, +executionMode): - Let {groupedFieldSet} be the result of {CollectFields(objectType, selectionSet, variableValues)}. +- Let {data} be the result of running {ExecuteCollectedFields(groupedFieldSet, + objectType, initialValue, variableValues)} _serially_ if {executionMode} is + {"serial"}, otherwise _normally_ (allowing parallelization)). +- Let {errors} be the list of all _execution error_ raised while executing the + selection set. +- Return an unordered map containing {data} and {errors}. + +### Field Collection + +Before execution, the _root selection set_ is converted to a _grouped field set_ +by calling {CollectFields()}. This ensures all fields with the same response +name, including those in referenced fragments, are executed at the same time. + +:: A _grouped field set_ is a map where each entry is a _response name_ and its +associated _field set_. A _grouped field set_ may be produced from a selection +set via {CollectFields()} or from the selection sets of a _field set_ via +{CollectSubfields()}. + +:: A _field set_ is a list of selected fields that share the same _response +name_ (the field alias if defined, otherwise the field's name). + +Note: The order of field selections in a _field set_ is significant, hence the +algorithms in this specification model it as a list. Any later duplicated field +selections in a field set will not impact its interpretation, so using an +ordered set would yield equivalent results. + +As an example, collecting the fields of this query's selection set would result +in a grouped field set with two entries, `"a"` and `"b"`, with two instances of +the field `a` and one of field `b`: + +```graphql example +{ + a { + subfield1 + } + ...ExampleFragment +} + +fragment ExampleFragment on Query { + a { + subfield2 + } + b +} +``` + +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): + +- If {visitedFragments} is not provided, initialize it to the empty set. +- Initialize {groupedFields} to an empty ordered map of lists. +- For each {selection} in {selectionSet}: + - If {selection} provides the directive `@skip`, let {skipDirective} be that + directive. + - If {skipDirective}'s {if} argument is {true} or is a variable in + {variableValues} with the value {true}, continue with the next {selection} + in {selectionSet}. + - If {selection} provides the directive `@include`, let {includeDirective} be + that directive. + - If {includeDirective}'s {if} argument is not {true} and is not a variable + in {variableValues} with the value {true}, continue with the next + {selection} in {selectionSet}. + - If {selection} is a {Field}: + - Let {responseName} be the _response name_ of {selection} (the alias if + defined, otherwise the field name). + - Let {groupForResponseName} be the list in {groupedFields} for + {responseName}; if no such list exists, create it as an empty list. + - Append {selection} to the {groupForResponseName}. + - If {selection} is a {FragmentSpread}: + - 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 + {selectionSet}. + - Let {fragmentType} be the type condition on {fragment}. + - If {DoesFragmentTypeApply(objectType, fragmentType)} is {false}, continue + with the next {selection} in {selectionSet}. + - Let {fragmentSelectionSet} be the top-level selection set of {fragment}. + - Let {fragmentGroupedFieldSet} be the result of calling + {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments)}. + - For each {fragmentGroup} in {fragmentGroupedFieldSet}: + - Let {responseName} be the response name shared by all fields in + {fragmentGroup}. + - Let {groupForResponseName} be the list in {groupedFields} for + {responseName}; if no such list exists, create it as an empty list. + - Append all items in {fragmentGroup} to {groupForResponseName}. + - 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)}. + - For each {fragmentGroup} in {fragmentGroupedFieldSet}: + - Let {responseName} be the response name shared by all fields in + {fragmentGroup}. + - Let {groupForResponseName} be the list in {groupedFields} for + {responseName}; if no such list exists, create it as an empty list. + - Append all items in {fragmentGroup} to {groupForResponseName}. +- Return {groupedFields}. + +DoesFragmentTypeApply(objectType, fragmentType): + +- If {fragmentType} is an Object Type: + - If {objectType} and {fragmentType} are the same type, return {true}, + otherwise return {false}. +- If {fragmentType} is an Interface Type: + - 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. + +**Merging Selection Sets** + +When a field is executed, during value completion the _selection set_ of each of +the related field selections with the same response name are collected together +to produce a single _grouped field set_ in order to continue execution of the +sub-selection sets. + +An example operation illustrating parallel fields with the same name with +sub-selections. + +Continuing the example above, + +```graphql example +{ + a { + subfield1 + } + ...ExampleFragment +} + +fragment ExampleFragment on Query { + a { + subfield2 + } + b +} +``` + +After resolving the value for field `"a"`, the following multiple selection sets +are merged together so `"subfield1"` and `"subfield2"` are resolved in the same +phase with the same value. + +CollectSubfields(objectType, fields, variableValues): + +- Let {groupedFieldSet} be an empty map. +- For each {field} in {fields}: + - Let {fieldSelectionSet} be the selection set of {field}. + - If {fieldSelectionSet} is null or empty, continue to the next field. + - Let {fieldGroupedFieldSet} be the result of {CollectFields(objectType, + fieldSelectionSet, variableValues)}. + - For each {fieldGroupedFieldSet} as {responseName} and {subfields}: + - Let {groupForResponseName} be the list in {groupedFieldSet} for + {responseName}; if no such list exists, create it as an empty list. + - Append all fields in {subfields} to {groupForResponseName}. +- Return {groupedFieldSet}. + +Note: All the {fields} passed to {CollectSubfields()} share the same _response +name_. + +### Executing Collected Fields + +The {CollectFields()} and {CollectSubfields()} algorithms transitively collect +the field selections from a _selection set_ or the associated selection sets of +a _field set_ respectively, and split them into groups by their _response name_ +to produce a _grouped field set_. + +To execute a _grouped field set_, the object value being evaluated and the +object type need to be known, as well as whether it must be executed serially, +or may be executed in parallel (see +[Normal and Serial Execution](#sec-Normal-and-Serial-Execution). + +Each entry in the grouped field set represents a _response name_ which produces +an entry into a result map. + +ExecuteCollectedFields(groupedFieldSet, objectType, objectValue, +variableValues): + - Initialize {resultMap} to an empty ordered map. - For each {groupedFieldSet} as {responseName} and {fields}: - Let {fieldName} be the name of the first entry in {fields}. Note: This value @@ -383,7 +582,8 @@ ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): - Return {resultMap}. Note: {resultMap} is ordered by which fields appear first in the operation. This -is explained in greater detail in the Field Collection section below. +is explained in greater detail in the [Field Collection](#sec-Field-Collection) +section. **Errors and Non-Null Types** @@ -391,7 +591,7 @@ is explained in greater detail in the Field Collection section below. -If during {ExecuteSelectionSet()} a _response position_ with a non-null type +If during {ExecuteCollectedFields()} a _response position_ with a non-null type raises an _execution error_ then that error must propagate to the parent response position (the entire selection set in the case of a field, or the entire list in the case of a list position), either resolving to {null} if @@ -501,112 +701,6 @@ A correct executor must generate the following result for that _selection set_: } ``` -### Field Collection - -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 name_ (the alias if defined, otherwise the field -name). This ensures all fields with the same response name (including those in -referenced fragments) are executed at the same time. - -As an example, collecting the fields of this selection set would collect two -instances of the field `a` and one of field `b`: - -```graphql example -{ - a { - subfield1 - } - ...ExampleFragment -} - -fragment ExampleFragment on Query { - a { - subfield2 - } - b -} -``` - -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): - -- If {visitedFragments} is not provided, initialize it to the empty set. -- Initialize {groupedFields} to an empty ordered map of lists. -- For each {selection} in {selectionSet}: - - If {selection} provides the directive `@skip`, let {skipDirective} be that - directive. - - If {skipDirective}'s {if} argument is {true} or is a variable in - {variableValues} with the value {true}, continue with the next {selection} - in {selectionSet}. - - If {selection} provides the directive `@include`, let {includeDirective} be - that directive. - - If {includeDirective}'s {if} argument is not {true} and is not a variable - in {variableValues} with the value {true}, continue with the next - {selection} in {selectionSet}. - - If {selection} is a {Field}: - - Let {responseName} be the _response name_ of {selection} (the alias if - defined, otherwise the field name). - - Let {groupForResponseName} be the list in {groupedFields} for - {responseName}; if no such list exists, create it as an empty list. - - Append {selection} to the {groupForResponseName}. - - If {selection} is a {FragmentSpread}: - - 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 - {selectionSet}. - - Let {fragmentType} be the type condition on {fragment}. - - If {DoesFragmentTypeApply(objectType, fragmentType)} is {false}, continue - with the next {selection} in {selectionSet}. - - Let {fragmentSelectionSet} be the top-level selection set of {fragment}. - - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments)}. - - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - - Let {responseName} be the response name shared by all fields in - {fragmentGroup}. - - Let {groupForResponseName} be the list in {groupedFields} for - {responseName}; if no such list exists, create it as an empty list. - - Append all items in {fragmentGroup} to {groupForResponseName}. - - 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)}. - - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - - Let {responseName} be the response name shared by all fields in - {fragmentGroup}. - - Let {groupForResponseName} be the list in {groupedFields} for - {responseName}; if no such list exists, create it as an empty list. - - Append all items in {fragmentGroup} to {groupForResponseName}. -- Return {groupedFields}. - -DoesFragmentTypeApply(objectType, fragmentType): - -- If {fragmentType} is an Object Type: - - If {objectType} and {fragmentType} are the same type, return {true}, - otherwise return {false}. -- If {fragmentType} is an Interface Type: - - 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. - ## Executing Fields Each field requested in the grouped field set that is defined on the selected @@ -738,8 +832,9 @@ CompleteValue(fieldType, fields, result, variableValues): - Let {objectType} be {fieldType}. - 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, + - Let {groupedFieldSet} be the result of calling {CollectSubfields(objectType, + fields, variableValues)}. + - Return the result of evaluating {ExecuteCollectedFields(groupedFieldSet, objectType, result, variableValues)} _normally_ (allowing for parallelization). @@ -784,38 +879,6 @@ ResolveAbstractType(abstractType, objectValue): for determining the Object type of {abstractType} given the value {objectValue}. -**Merging Selection Sets** - -When more than one field of the same name is executed in parallel, the -_selection set_ for each of the fields are merged together when completing the -value in order to continue execution of the sub-selection sets. - -An example operation illustrating parallel fields with the same name with -sub-selections. - -```graphql example -{ - me { - firstName - } - me { - lastName - } -} -``` - -After resolving the value for `me`, the selection sets are merged together so -`firstName` and `lastName` can be resolved for one value. - -MergeSelectionSets(fields): - -- Let {selectionSet} be an empty list. -- For each {field} in {fields}: - - Let {fieldSelectionSet} be the selection set of {field}. - - If {fieldSelectionSet} is null or empty, continue to the next field. - - Append all selections in {fieldSelectionSet} to {selectionSet}. -- Return {selectionSet}. - ### Handling Execution Errors