From 4fc5b325fbc7c01a35bf20dfae19dbfe2a6d0877 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 6 Nov 2022 17:43:04 +0200 Subject: [PATCH] introduce CollectRootFields and CollectSubfields Rather than merging subSelectionSets of a field set using MergeSelectionSets and then calling CollectFields, introducing CollectSubfields allows the field set's groupedSubfieldSet to be calculated directly. This may be helpful if the specification were ever to be altered such that additional state beyond the current selection set were to be required to calculate the response, i.e. if it were to be required to know the originating selectionSet of a given field within the fieldSet for determining when to communicate a reference signal. See https://github.com/graphql/graphql-spec/pull/998#issue-1435084489 --- spec/Section 5 -- Validation.md | 2 +- spec/Section 6 -- Execution.md | 100 +++++++++++++++++--------------- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index aa4c46996..5e470a0b2 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -463,7 +463,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 {CollectFields()}. 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 28862ea89..de5cf5e42 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -131,7 +131,9 @@ 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, +- Let {groupedFieldSet} be the result of {CollectRootFields(queryType, + selectionSet, variableValues)}. +- Let {data} be the result of running {ExecuteFieldSet(groupedFieldSet, queryType, initialValue, variableValues)} _normally_ (allowing parallelization). - Let {errors} be the list of all _field error_ raised while executing the @@ -153,7 +155,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, +- Let {groupedFieldSet} be the result of {CollectRootFields(mutationType, + selectionSet, variableValues)}. +- Let {data} be the result of running {ExecuteFieldSet(groupedFieldSet, mutationType, initialValue, variableValues)} _serially_. - Let {errors} be the list of all _field error_ raised while executing the selection set. @@ -256,7 +260,7 @@ CreateSourceEventStream(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 {groupedFieldSet} be the result of {CollectFields(subscriptionType, +- Let {groupedFieldSet} be the result of {CollectRootFields(subscriptionType, selectionSet, variableValues)}. - If {groupedFieldSet} does not have exactly one entry, raise a _request error_. - Let {fields} be the value of the first entry in {groupedFieldSet}. @@ -301,7 +305,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, +- Let {groupedFieldSet} be the result of {CollectRootFields(subscriptionType, + selectionSet, variableValues)}. +- Let {data} be the result of running {ExecuteFieldSet(groupedFieldSet, subscriptionType, initialValue, variableValues)} _normally_ (allowing parallelization). - Let {errors} be the list of all _field error_ raised while executing the @@ -322,20 +328,17 @@ Unsubscribe(responseStream): - Cancel {responseStream} -## Executing Selection Sets +## Executing Field 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. +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. -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. +Each represented field in the grouped field set produces an entry into a +response map. -ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): +ExecuteFieldSet(groupedFieldSet, objectType, objectValue, variableValues): -- Let {groupedFieldSet} be the result of {CollectFields(objectType, - selectionSet, variableValues)}. - 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 @@ -490,10 +493,12 @@ 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, selectionSet, variableValues, groupedFieldSet, +visitedFragments): +- If {groupedFieldSet} is not provided, initialize it an empty ordered map of + lists. - 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. @@ -508,7 +513,7 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - If {selection} is a {Field}: - Let {responseKey} be the response key of {selection} (the alias if defined, otherwise the field name). - - Let {groupForResponseKey} be the list in {groupedFields} for + - Let {groupForResponseKey} be the list in {groupedFieldSet} for {responseKey}; if no such list exists, create it as an empty list. - Append {selection} to the {groupForResponseKey}. - If {selection} is a {FragmentSpread}: @@ -524,31 +529,16 @@ 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}. - - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments)}. - - 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}. + - Call {CollectFields(objectType, fragmentSelectionSet, variableValues, + groupedFieldSet, 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)}. - - 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}. + - Call {CollectFields(objectType, fragmentSelectionSet, variableValues, + groupedFieldSet, visitedFragments)}. DoesFragmentTypeApply(objectType, fragmentType): @@ -565,6 +555,30 @@ DoesFragmentTypeApply(objectType, fragmentType): Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` directives may be applied in either order since they apply commutatively. +### Root Field Collection + +Root field collection processes the operation's top-level selection set: + +CollectRootFields(rootType, operationSelectionSet, variableValues): + +- Initialize {groupedFieldSet} to an empty ordered map of lists. +- Call {CollectFields(rootType, operationSelectionSet, groupedFieldSet)}. +- Return {groupedFieldSet}. + +### Object Subfield Collection + +Root field collection processes the operation's top-level selection set: + +CollectSubfields(objectType, fields, variableValues): + +- Initialize {groupedSubfieldSet} to an empty ordered map of lists. +- For each {field} in {fields}: + - Let {fieldSelectionSet} be the selection set of {field}. + - If {fieldSelectionSet} is null or empty, continue to the next field. + - Call {CollectFields(objectType, fieldSelectionSet, variableValues, + groupedSubfieldSet)}. +- Return {groupedSubfieldSet}. + ## Executing Fields Each field requested in the grouped field set that is defined on the selected @@ -692,8 +706,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 {groupedSubfieldSet} be the result of calling + {CollectSubfields(objectType, fields, variableValues)}. + - Return the result of evaluating {ExecuteFieldSet(groupedSubfieldSet, objectType, result, variableValues)} _normally_ (allowing for parallelization). @@ -761,15 +776,6 @@ sub-selections. 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 Field Errors A _field error_ is an error raised from a particular field during value