From 0770844b4f69b9a1a6cb09163752611ec0229e3c Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 15 Jan 2023 15:52:41 +0200 Subject: [PATCH] add edits for merging defers at the same level --- spec/Section 6 -- Execution.md | 248 ++++++++++++++++++++------------- 1 file changed, 151 insertions(+), 97 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 3110c1e5d..10b219363 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -133,16 +133,23 @@ An initial value may be provided when executing a query operation. ExecuteQuery(query, schema, variableValues, initialValue): - Let {subsequentPayloads} be an empty list. +- Initialize {branches} to a weakly held map between grouped field sets and sets + of paths. - 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 {groupedFieldSet} and {deferredGroupedFieldsList} be the result of +- Let {groupedFieldSet} and {newDeferDepth} be the result of {CollectRootFields(queryType, selectionSet, variableValues)}. - Let {data} be the result of running {ExecuteGroupedFieldSet(groupedFieldSet, deferredGroupedFieldsList, queryType, initialValue, variableValues, - subsequentPayloads)} _normally_ (allowing parallelization). + subsequentPayloads, branches)} _normally_ (allowing parallelization). - Let {errors} be the list of all _field error_ raised while executing the selection set. +- If {newDeferDepth} is defined and {ShouldBranch(branches, groupedFieldSet, + path)} is {true}: + - Call {ExecuteDeferredFragment(objectType, objectValue, groupedFieldSet, + path, newDeferDepth, variableValues, asyncRecord, subsequentPayloads, + branches)} - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: @@ -170,6 +177,8 @@ mutations ensures against race conditions during these side-effects. ExecuteMutation(mutation, schema, variableValues, initialValue): - Let {subsequentPayloads} be an empty list. +- Initialize {branches} to a weakly held map between grouped field sets and sets + of paths. - 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}. @@ -177,9 +186,14 @@ ExecuteMutation(mutation, schema, variableValues, initialValue): {CollectRootFields(mutationType, selectionSet, variableValues)}. - Let {data} be the result of running {ExecuteGroupedFieldSet(groupedFieldSet, deferredGroupedFieldsList mutationType, initialValue, variableValues, - subsequentPayloads)} _serially_. + subsequentPayloads, branches)} _serially_. - Let {errors} be the list of all _field error_ raised while executing the selection set. +- If {newDeferDepth} is defined and {ShouldBranch(branches, groupedFieldSet, + path)} is {true}: + - Call {ExecuteDeferredFragment(objectType, objectValue, + deferredGroupFieldSet, path, newDeferDepth, variableValues, asyncRecord, + subsequentPayloads, branches)} - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: @@ -416,25 +430,24 @@ be executed in parallel. Each represented field in the grouped field set produces an entry into a response map. -ExecuteGroupedFieldSet(groupedFieldSet, deferredGroupedFieldsList, objectType, -objectValue, variableValues, path, subsequentPayloads, asyncRecord): +ExecuteGroupedFieldSet(groupedFieldSet, objectType, objectValue, variableValues, +path, subsequentPayloads, branches, asyncRecord): -- If {path} is not provided, initialize it to an empty list. +- If {path} is not provided, initialize it to an empty list, unique for this + execution. - If {subsequentPayloads} is not provided, initialize it to the empty set. - Initialize {resultMap} to an empty ordered map. -- For each {groupedFieldSet} as {responseKey} and {fields}: +- For each {groupedFieldSet} as {responseKey} and {fieldGroup}: - 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, path, subsequentPayloads, asyncRecord)}. - - Set {responseValue} as the value for {responseKey} in {resultMap}. -- For each {deferredGroupFieldSet} in {deferredGroupedFieldsList} - - Call {ExecuteDeferredFragment(objectType, objectValue, - deferredGroupFieldSet, path, variableValues, asyncRecord, - subsequentPayloads)} + - If {ShouldExecute(fields, fieldType, asyncRecord)} is {true}: + - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, + fields, variableValues, path, subsequentPayloads, branches, + asyncRecord)}. + - Set {responseValue} as the value for {responseKey} in {resultMap}. - Return {resultMap}. Note: {resultMap} is ordered by which fields appear first in the operation. This @@ -615,19 +628,31 @@ are no longer required to execute serially. Execution of the deferred or streamed sections of the subsection may be executed in parallel, as defined in {ExecuteStreamField} and {ExecuteDeferredFragment}. +#### Tagged Field Node + +A Tagged Field Record is a structure containing: + +- {field}: the underlying field from an operation. +- {depth}: the depth of the field. Root fields have a depth of 0, their + subfields have a depth of 1, and so on. The depth is used by the + {CollectFields} algorithm to assign a {deferDepth}. +- {deferDepth}: the depth of the closest enclosing fragment with a defer + directive, or undefined if the field is not deferred. + ### 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 key (the alias if defined, otherwise the field -name). This ensures all fields with the same response key (including those in -referenced fragments) are executed at the same time. A deferred selection set's -fields will not be included in the grouped field set. Rather, a record -representing the deferred fragment and additional context will be stored in a -list. The executor revisits and resumes execution for the list of deferred +tagged field records that share a response key (the alias if defined, otherwise +the field name). This ensures all fields with the same response key (including +those in referenced fragments) are executed at the same time. + +A deferred selection set's fields will not be included in the initial response. +Rather, the executor revisits and resumes execution for the list of deferred fragment records after the initial execution is initiated. This deferred -execution would ‘re-execute’ fields with the same response key that were present -in the grouped field set. +execution will not ‘re-execute’ fields with the same response key that were +present in the initial payload, but may ‘re-execute’ fields present in other +deferred payloads. As an example, collecting the fields of this selection set would collect two instances of the field `a` and one of field `b`: @@ -652,10 +677,10 @@ 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, visitedFragments, depth, +deferDepth): - Initialize {groupedFieldSet} to an empty ordered map of lists. -- Initialize {deferredGroupedFieldsList to an empty list. - For each {selection} in {selectionSet}: - If {selection} provides the directive `@skip`, let {skipDirective} be that directive. @@ -668,11 +693,13 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): in {variableValues} with the value {true}, continue with the next {selection} in {selectionSet}. - If {selection} is a {Field}: + - Let {taggedField} be an empty tagged field record created from + {selection}, {depth} and {deferDepth}. - Let {responseKey} be the response key of {selection} (the alias if defined, otherwise the field name). - 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}. + - Append {taggedField} to the {groupForResponseKey}. - If {selection} is a {FragmentSpread}: - Let {fragmentSpreadName} be the name of {selection}. - If {fragmentSpreadName} provides the directive `@defer` and its {if} @@ -692,26 +719,23 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - 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}. - If {deferDirective} is defined: - - Let {fragmentGroupedFieldSet} and {fragmentDeferredGroupedFieldsList} be - the result of calling {CollectFields(objectType, fragmentSelectionSet, - variableValues, visitedFragments)}. - - Append all items in {fragmentDeferredGroupedFieldsList} to - {deferredGroupedFieldsList}. - - Append {deferredGroupedFields} to {deferredGroupedFieldsList}. - - Continue with the next {selection} in {selectionSet}. - - Let {fragmentGroupedFieldSet} and {fragmentDeferredGroupedFieldsList} be - the result of calling {CollectFields(objectType, fragmentSelectionSet, - variableValues, visitedFragments, deferredGroupedFieldsList)}. + - Let {newDeferDepth} be depth. + - Let {fragmentDeferDepth} be {depth}. + - Otherwise: + - Let {fragmentDeferDepth} be {depthDepth}. + - Let {fragmentSelectionSet} be the top-level selection set of {fragment}. + - Let {fragmentGroupedFieldSet} and {fragmentNewDeferDepth} be the result of + calling {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments, depth, fragmentDeferDepth)}. + - If {fragmentNewDeferDepth} is defined: + - Let {newDeferDepth} be depth. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. - Let {groupForResponseKey} be the list in {groupedFieldSet} for {responseKey}; if no such list exists, create it as an empty list. - Append all items in {fragmentGroup} to {groupForResponseKey}. - - Append all items in {fragmentDeferredGroupedFieldsList} to - {deferredGroupedFieldsList}. - If {selection} is an {InlineFragment}: - Let {fragmentType} be the type condition on {selection}. - If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, @@ -725,25 +749,22 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - If this execution is for a subscription operation, raise a _field error_. - If {deferDirective} is defined: - - Let {fragmentGroupedFieldSet} and {fragmentDeferredGroupedFieldList} be - the result of calling {CollectFields(objectType, fragmentSelectionSet, - variableValues, visitedFragments)}. - - Append all items in {fragmentDeferredGroupedFieldsList} to - {deferredGroupedFieldsList}. - - Append {deferredGroupedFields} to {deferredGroupedFieldsList}. - - Continue with the next {selection} in {selectionSet}. - - Let {fragmentGroupedFieldSet} and {fragmentDeferredGroupedFieldList} be - the result of calling {CollectFields(objectType, fragmentSelectionSet, - variableValues, visitedFragments)}. + - Let {newDeferDepth} be depth. + - Let {fragmentDeferDepth} be {depth}. + - Otherwise: + - Let {fragmentDeferDepth} be {depthDepth}. + - Let {fragmentGroupedFieldSet} and {fragmentNewDeferDepth} be the result of + calling {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments, depth, fragmentDeferDepth)}. + - If {fragmentNewDeferDepth} is defined: + - Let {newDeferDepth} be depth. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. - Let {groupForResponseKey} be the list in {groupedFieldSet} for {responseKey}; if no such list exists, create it as an empty list. - Append all items in {fragmentGroup} to {groupForResponseKey}. - - Append all items in {fragmentDeferredGroupedFieldsList} to - {deferredGroupedFieldsList}. -- Return {groupedFieldSet}, {deferredGroupedFieldsList} and {visitedFragments}. +- Return {groupedFieldSet}, {newDeferDepth} and {visitedFragments}. Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` directives may be applied in either order since they apply commutatively. @@ -767,6 +788,9 @@ All Async Payload Records are structures containing: - {path}: a list of field names and indices from root to the location of the corresponding `@defer` or `@stream` directive. +- {deferDepth}: a number corresponding to the depth along the path of any + enclosing fragment containing the defer directive, or undefined, if this + payload is a Stream Record not contained by a defer directive. - {iterator}: The underlying iterator if created from a `@stream` directive. - {isCompletedIterator}: a boolean indicating the payload record was generated from an iterator that has completed. @@ -777,9 +801,10 @@ All Async Payload Records are structures containing: #### Execute Deferred Fragment ExecuteDeferredFragment(objectType, objectValue, groupedFieldSet, path, -variableValues, parentRecord, subsequentPayloads): +deferDepth, variableValues, parentRecord, subsequentPayloads, branches): -- Let {deferRecord} be an async payload record created from {path}. +- Let {deferRecord} be an async payload record created from {path} and + {deferDepth}. - Initialize {errors} on {deferRecord} to an empty list. - Let {dataExecution} be the asynchronous future value of: - Let {payload} be an unordered map. @@ -791,7 +816,8 @@ variableValues, parentRecord, subsequentPayloads): {objectType}. - If {fieldType} is defined: - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, - fields, variableValues, path, subsequentPayloads, asyncRecord)}. + fields, variableValues, path, subsequentPayloads, asyncRecord, + branches)}. - Set {responseValue} as the value for {responseKey} in {resultMap}. - Append any encountered field errors to {errors}. - If {parentRecord} is defined: @@ -815,10 +841,10 @@ Root field collection processes the operation's top-level selection set: CollectRootFields(rootType, operationSelectionSet, variableValues): - Initialize {visitedFragments} to the empty set. -- Let {groupedFieldSet} and {deferredGroupedFieldsList} be the result of calling +- Let {groupedFieldSet} and {newDeferDepth} be the result of calling {CollectFields(rootType, operationSelectionSet, variableValues, visitedFragments)}. -- Return {groupedFieldSet} and {deferredGroupedFieldsList}. +- Return {groupedFieldSet} and {newDeferDepth}. ### Object Subfield Collection @@ -828,22 +854,21 @@ CollectSubfields(objectType, fields, variableValues): - Initialize {visitedFragments} to the empty set. - Initialize {groupedSubfieldSet} to an empty ordered map of lists. -- Initialize {deferredGroupedSubfieldsList} to 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. - - Let {fieldGroupedFieldSet} and {fieldDeferredGroupedFieldsList} be the - result of calling {CollectFields(objectType, fragmentSelectionSet, - variableValues, visitedFragments)}. + - Let {fieldGroupedFieldSet} and {fieldNewDeferDepth} be the result of calling + {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments)}. - For each {fieldGroup} in {fieldGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. - Let {groupForResponseKey} be the list in {groupedFieldSet} for {responseKey}; if no such list exists, create it as an empty list. - Append all items in {fieldGroup} to {groupForResponseKey}. - - Append all items in {fieldDeferredGroupedFieldsList} to - {deferredGroupedSubfieldsList}. -- Return {groupedSubfieldSet} and {deferredGroupedSubfieldsList}. + - If {fieldNewDeferDepth} is defined: + - Let {deferDepth} be {fieldNewDeferDepth}. +- Return {groupedSubfieldSet} and {deferDepth}. ## Executing Fields @@ -854,17 +879,19 @@ finally completes that value either by recursively executing another selection set or coercing a scalar value. ExecuteField(objectType, objectValue, fieldType, fields, variableValues, path, -subsequentPayloads, asyncRecord): +subsequentPayloads, branches, asyncRecord): - Let {field} be the first entry in {fields}. - Let {fieldName} be the field name of {field}. -- Append {fieldName} to {path}. +- Let {fieldPath} be a unique list for for this execution equal to {path} with + {field} appended. - Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} - Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. - Let {result} be the result of calling {CompleteValue(fieldType, fields, - resolvedValue, variableValues, path, subsequentPayloads, asyncRecord)}. + resolvedValue, variableValues, fieldPath, subsequentPayloads, branches, + asyncRecord)}. - Return {result}. ### Coercing Field Arguments @@ -967,13 +994,16 @@ yielded items satisfies `initialCount` specified on the `@stream` directive. #### Execute Stream Field -ExecuteStreamField(iterator, index, fields, innerType, path, parentRecord, -variableValues, subsequentPayloads): +ExecuteStreamField(iterator, index, fields, innerType, path, deferDepth, +parentRecord, variableValues, subsequentPayloads, branches): -- Let {streamRecord} be an async payload record created from {path}, and - {iterator}. +- If {parentRecord} is defined: + - Let {deferDepth} be equal to the corresponding entry on {asyncRecord}. +- Let {streamRecord} be an async payload record created from {path}, + {deferDepth}, and {iterator}. - Initialize {errors} on {streamRecord} to an empty list. -- Let {itemPath} be {path} with {index} appended. +- Let {itemPath} be a unique list for this execution equal to {path} with + {index} appended. - Let {dataExecution} be the asynchronous future value of: - Wait for the next item from {iterator}. - If an item is not retrieved because {iterator} has completed: @@ -986,11 +1016,12 @@ variableValues, subsequentPayloads): - Otherwise: - Let {item} be the item retrieved from {iterator}. - Let {data} be the result of calling {CompleteValue(innerType, fields, - item, variableValues, itemPath, subsequentPayloads, parentRecord)}. + item, variableValues, itemPath, subsequentPayloads, branches, + parentRecord)}. - Append any encountered field errors to {errors}. - Increment {index}. - Call {ExecuteStreamField(iterator, index, fields, innerType, path, - streamRecord, variableValues, subsequentPayloads)}. + streamRecord, variableValues, subsequentPayloads, branches)}. - If a field error was raised, causing a {null} to be propagated to {data}, and {innerType} is a Non-Nullable type: - Add an entry to {payload} named `items` with the value {null}. @@ -1007,7 +1038,7 @@ variableValues, subsequentPayloads): - Append {streamRecord} to {subsequentPayloads}. CompleteValue(fieldType, fields, result, variableValues, path, -subsequentPayloads, asyncRecord): +subsequentPayloads, asyncRecord, branches): - If the {fieldType} is a Non-Null type: - Let {innerType} be the inner type of {fieldType}. @@ -1037,16 +1068,17 @@ subsequentPayloads, asyncRecord): - If {streamDirective} is defined and {index} is greater than or equal to {initialCount}: - Call {ExecuteStreamField(iterator, index, fields, innerType, path, - asyncRecord, subsequentPayloads)}. + asyncRecord, subsequentPayloads, branches)}. - Return {items}. - Otherwise: - Wait for the next item from {result} via the {iterator}. - If an item is not retrieved because of an error, raise a _field error_. - Let {resultItem} be the item retrieved from {result}. - - Let {itemPath} be {path} with {index} appended. + - Let {itemPath} be a unique list for this execution equal to {path} with + {index} appended. - Let {resolvedItem} be the result of calling {CompleteValue(innerType, fields, resultItem, variableValues, itemPath, subsequentPayloads, - asyncRecord)}. + branches, asyncRecord)}. - Append {resolvedItem} to {items}. - Increment {index}. - Return {items}. @@ -1057,11 +1089,48 @@ subsequentPayloads, asyncRecord): - Let {objectType} be {fieldType}. - Otherwise if {fieldType} is an Interface or Union type. - Let {objectType} be {ResolveAbstractType(fieldType, result)}. - - Let {groupedSubfieldSet} and {deferredGroupedSubfieldsList} be the result of - calling {CollectSubfields(objectType, fields, variableValues)}. - - Return the result of evaluating {ExecuteGroupedFieldSet(groupedSubfieldSet, - deferredGroupedSubfieldsList objectType, result, variableValues, path, - subsequentPayloads, asyncRecord)} _normally_ (allowing for parallelization). + - Let {groupedSubfieldSet} and {newDeferDepth} be the result of calling + {CollectSubfields(objectType, fields, variableValues)}. + - Let {resultMap} be the result of evaluating + {ExecuteGroupedFieldSet(groupedSubfieldSet, deferredGroupedSubfieldsList + objectType, result, variableValues, path, subsequentPayloads, branches, + asyncRecord)} _normally_ (allowing for parallelization). + - If {newDeferDepth} is defined and {ShouldBranch(branches, groupedFieldSet, + path)} is {true}: + - Call {ExecuteDeferredFragment(objectType, objectValue, groupedFieldSet, + path, newDeferDepth, variableValues, asyncRecord, subsequentPayloads, + branches)}, + - Return {resultMap}. + +ShouldBranch(branches, groupedFieldSet, path): + +- Let {paths} be the entry in {branches} for {groupedFieldSet}. +- If {paths} is not defined: + - Let {paths} be a new set containing {path}. + - Set the entry for {groupedFieldSet} in {branches} to {paths}. + - Return {true}. +- If {paths} contains {path}, return {false}. +- Add {path} to {paths}. +- Return {true}. + +ShouldExecute(fieldGroup, fieldType, asyncPayloadRecord): + +- Let {hasDepth} equal {false}. +- If {asyncPayloadRecord} is defined: + - Let {deferDepth} be the corresponding entry on {asyncPayloadRecord}. +- If {deferDepth} is not defined or if the underlying named type of {fieldType} + is not a leaf type: + - For each {taggedField} in {fieldGroup}: + - Let {fieldDeferDepth} be the entry for {deferDepth} on {taggedField}. + - If {fieldDeferDepth} is equal to {deferDepth}: + - Set {hasDepth} equal to {true}. + - Return {hasDepth}. +- For each {taggedField} in {fieldGroup}: + - Let {fieldDeferDepth} be the entry for {deferDepth} on {taggedField}. + - If {fieldDeferDepth} is not defined, return {false}. + - If {fieldDeferDepth} is equal to {deferDepth}: + - Set {hasDepth} equal to {true}. +- Return {hasDepth}. **Coercing Results** @@ -1211,21 +1280,6 @@ this {null} as propagated as high as the error boundary will allow. } ``` -Response 3, another defer payload is sent. The data in this payload is -unaffected by the previous null error. - -```json example -{ - "incremental": [ - { - "path": ["birthday"], - "data": { "year": "2022" } - } - ], - "hasNext": false -} -``` - If the `stream` directive is present on a list field with a Non-Nullable inner type, and a field error has caused a {null} to propagate to the list item, the {null} should not propagate any further, and the associated Stream Payload's