Skip to content

feat: add initial defer support#1365

Draft
devsergiy wants to merge 79 commits intomasterfrom
feat/eng-7770-add-defer-support
Draft

feat: add initial defer support#1365
devsergiy wants to merge 79 commits intomasterfrom
feat/eng-7770-add-defer-support

Conversation

@devsergiy
Copy link
Copy Markdown
Member

@devsergiy devsergiy commented Jan 20, 2026

Defer Support Implementation

This branch adds end-to-end support for the @defer directive in GraphQL execution.

pkg/astnormalization

new rules

  • inlineFragmentExpandDefer - expands @defer into per-field inline internal defer directives
  • deferEnsureTypename - injects typename selection in case all fields in a selection set are deferred

updated rules

  • directive_include_skip - shares a helper to add an internal typename placeholder
  • field_deduplication - now merges defer directives during merging duplicated fields
  • inline_fragment_selection_merging - same as field_deduplication - takes defer into account

operation normalizer

The order of the rules is slightly changed

pkg/ast

Added defer-related helpers to ast_field.go, ast_directive.go, ast_argument.go, ast_inline_fragment.go

pkg/asttransform

Added internal defer directive definitions to the base schema and updated fixtures

pkg/engine/plan

File Change What
plan.go added New DeferResponsePlan type and DeferResponsePlanKind constant
planner_configuration.go modified pathConfiguration gets deferID; path index stores full config instead of empty struct; new DeferID() / PathWithFieldRef() accessors
path_builder_visitor.go major Fields now could be planned multiple times per single ds suggestion (deferred path, defer-parent path, normal path); refactor to use currentFieldInfo struct instead of passing separate arguments; objectFetchConfiguration now have deferID
node_selection_visitor.go modified Requirement tracking structs gain deferInfo / parentFieldDeferID; deduplication key is now per defer scope
required_fields_visitor.go major Required fields updates alias logic and handles applying @__defer_internal annotations to the fields to put them into the correct scope; new resolveDeferredAlias() helper is added to create proper defer alias
datasource_filter_node_suggestions.go modified NodeSuggestion gets DeferInfo; new ProcessDefer() propagates defer IDs to nodes parents up to the root query node or root entity nodes which requires a key - e.g. to nearest query entry point
datasource_filter_collect_nodes_visitor.go modified Reads @__defer_internal during tree building to populate DeferInfo on suggestions
abstract_selection_rewriter*.go modified typeNameSelection() takes a deferID to give an injected __typename a proper scope
node_selection_builder.go modified Calls ProcessDefer() after process if node selection is finished - to propagate defer ids to parents
visitor.go refactored Emits DeferResponsePlan; misc cleanup
analyze_plan_kind.go deleted Removed; we now determining if the is deffered based on fetches having defer id

pkg/postprocess

postprocess.go

  • Refactored the Processor struct: processors are now grouped into FetchTreeProcessors and ResponseTreeProcessors named structs instead of anonymous slices.
  • Processing is split into two explicit phases: processFlatFetchTree (dedupe → fetchID → templates → deps → concrete types) and organizeFetchTree (order by deps → parallelize).
  • A new DeferResponsePlan case was added — it processes the initial response tree normally, calls extractDeferFetches to split out deferred fetch groups, then runs organizeFetchTree on the initial response and on each deferred group separately.
  • New DisableExtractDeferFetches() option added.

new processor extract_defer_fetches.go

Splits a flat DeferResponsePlan fetch tree into two buckets:

  • fetches without a deferID stay in the root sequence;
  • fetches with a deferID are grouped by that ID into DeferFetchGroup entries on Response.Defers, sorted in natural numeric order.

pkg/engine/postprocess

postprocess.go

  • Refactored the Processor struct: processors are now grouped into FetchTreeProcessors and ResponseTreeProcessors named structs instead of anonymous slices.
  • Processing is split into two explicit phases: processFlatFetchTree (dedupe → fetchID → templates → deps → concrete types) and organizeFetchTree (order by deps → parallelize).
  • A new DeferResponsePlan case was added — it processes the initial response tree normally, calls extractDeferFetches to split out deferred fetch groups, then runs organizeFetchTree on the initial response and on each deferred group separately.
  • New DisableExtractDeferFetches() option added.

new processor extract_defer_fetches.go

Splits a flat DeferResponsePlan fetch tree into two buckets: - fetches without a deferID stay in the root sequence; - fetches with a deferID are grouped by that ID into DeferFetchGroup entries on Response.Defers, sorted in natural numeric order.

pkg/engine/resolve

response.go, fetch.go, node_object.go, const.go

  • Added supporting data structures for defer: GraphQLDeferResponse, DeferFetchGroup, DeferResponseWriter interface
  • DeferID added to FetchDependencies and Field via DeferField struct

loader.go

  • Init() extracted as a public method;
  • resolveFetchNode made public as ResolveFetchNode so the resolver can call it separately for the initial and each deferred fetch group.

resolvable.go

  • New method ResolveDefer() is added to render a single incremental response chunk ({"incremental":[...],"hasNext":...}) for one defer group. Two-pass approach: dry-run first to catch auth errors, then real render.
  • New state fields deferMode, deferID, enableDeferRender are added to gate which fields get rendered per resolve pass.

resolve.go

New entry point ResolveGraphQLDeferResponse() is added:

  • It orchestrates full defer execution: loads and flushes the initial response
  • Iterates over DeferFetchGroups. Each group executes fetches, resolves data for the give group and flushing a chunk of data

Integration tests

  • execution/engine/execution_engine_defer_test.go — large end-to-end test suite
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go — datasource-level defer tests
  • Introspection fixtures updated to include @defer in schema

closes ENG-8799
closes ENG-7978
closes ENG-7976

Checklist

  • I have discussed my proposed changes in an issue and have received approval to proceed.
  • I have followed the coding standards of the project.
  • Tests or benchmarks have been added or updated.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

This pull request implements comprehensive support for the GraphQL @defer directive, enabling incremental response delivery. It adds AST normalization for deferred inline fragments, execution engine support for streaming deferred responses, federation-aware defer planning, and defer-aware field merging. Schema artifacts and test fixtures are updated to reflect the new @defer and @__defer_internal directives.

Changes

Cohort / File(s) Summary
AST & Directive Utilities
v2/pkg/ast/ast_argument.go, v2/pkg/ast/ast_directive.go, v2/pkg/ast/ast_field.go, v2/pkg/ast/ast_inline_fragment.go
Added helper methods for directive and argument manipulation: AddStringArgument, HasDirectiveByNameBytes, RemoveDirectiveByRef, MergeFieldsDefer, AddDeferInternalDirectiveToField, FieldInternalDeferID, and InlineFragmentDirectiveByName. Updated DirectiveSetsAreEqual to treat @__defer_internal-only sets as equivalent to empty sets and perform order-independent directive matching.
AST Normalization Passes
v2/pkg/astnormalization/astnormalization.go, v2/pkg/astnormalization/inline_fragment_expand_defer.go, v2/pkg/astnormalization/defer_ensure_typename.go
Introduced WithInlineDefer() option and two new normalization passes: inlineFragmentExpandDefer expands @defer directives on inline fragments into explicit selections with @__defer_internal annotations; deferEnsureTypename adds placeholder __typename fields to preserve non-empty selection sets when all siblings are deferred.
Normalization Tests & Helpers
v2/pkg/astnormalization/astnormalization_test.go, v2/pkg/astnormalization/inline_fragment_expand_defer_test.go, v2/pkg/astnormalization/defer_ensure_typename_test.go, v2/pkg/astnormalization/field_deduplication.go, v2/pkg/astnormalization/field_deduplication_test.go
Extended test coverage for defer functionality with new test cases validating inline defer expansion, typename placeholder insertion, and field deduplication with defer directives. Updated field deduplication to call MergeFieldsDefer when merging equivalent leaf fields. Refactored test helpers to use options structs instead of variadic booleans.
Additional Normalization Updates
v2/pkg/astnormalization/directive_include_skip.go, v2/pkg/astnormalization/directive_include_skip_test.go, v2/pkg/astnormalization/fragment_spread_inlining_test.go, v2/pkg/astnormalization/inline_fragment_selection_merging.go, v2/pkg/astnormalization/inline_fragment_selection_merging_test.go, v2/pkg/astnormalization/inline_selections_from_inline_fragments_test.go
Refactored typename placeholder insertion (renamed alias from __internal__typename_placeholder to ___typename), added defer-aware field merging in MergeFieldsDefer calls, and extended test cases for defer interaction scenarios.
Base Schema & Fixtures
v2/pkg/asttransform/base.graphql, v2/pkg/asttransform/internal.graphql, v2/pkg/asttransform/baseschema.go, v2/pkg/asttransform/baseschema_test.go
Introduced embedded GraphQL schema files defining standard scalars, directives (@defer), and introspection types. Updated MergeDefinitionWithBaseSchema to delegate to new MergeDefinitionWithBaseSchemaWithInternal function for conditional internal-directive inclusion.
Golden Fixtures
v2/pkg/asttransform/fixtures/*.golden, v2/pkg/asttransform/stream.graphql
Updated all golden schema fixtures with backtick-formatted scalar descriptions, enhanced @specifiedBy and new @defer/@__defer_internal directive definitions. Added stream.graphql with @stream directive support.
Introspection & Federation
v2/pkg/introspection/generator.go, v2/pkg/introspection/generator_test.go, v2/pkg/introspection/testdata/starwars.schema.graphql, v2/pkg/introspection/fixtures/starwars_introspected.golden, v2/pkg/federation/schema.go, v2/pkg/federation/fixtures/federated_schema.golden
Updated introspection generator to filter out __-prefixed directives, removed redundant introspection types from test schema (now sourced from base schema), and updated federation schema to use MergeDefinitionWithBaseSchemaWithInternal. Updated golden fixtures with @defer, @oneOf, @specifiedBy directives and scalar description formatting.
Engine Introspection Fixtures
execution/engine/testdata/full_introspection*.json, v2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection*.golden
Updated introspection JSON/golden outputs to include @defer directive metadata, backtick-formatted scalar descriptions, and enhanced @specifiedBy argument descriptions.
Execution Engine Core
execution/engine/execution_engine.go, execution/engine/config_factory_proxy_test.go, execution/engine/engine_config_test.go
Added inline-defer normalization in operation pipeline. Updated execution dispatch to handle *plan.DeferResponsePlan via ResolveGraphQLDeferResponse. Enhanced schema introspection with @defer and @__defer_internal directives in embedded test data.
Execution Engine Tests
execution/engine/execution_engine_test.go, execution/engine/execution_engine_helpers_test.go, execution/engine/execution_engine_defer_test.go
Added streaming response support to test framework with withStreamingResponse() option. Enhanced mock-response helpers to track request usage and support assertion logging. Introduced comprehensive execution_engine_defer_test.go (2308 lines) validating defer across scalar/nested fields, federation scenarios, and error handling.
Plan Types & Configuration
v2/pkg/engine/plan/plan.go, v2/pkg/engine/plan/configuration.go, v2/pkg/engine/plan/datasource_configuration.go
Added DeferResponsePlanKind and DeferResponsePlan type implementing Plan interface. Added DisableCalculateFieldDependencies configuration flag. Removed FetchID from DataSourcePlannerConfiguration and deprecated OverrideFieldPathFromAlias.
Node Selection & Filtering
v2/pkg/engine/plan/datasource_filter_collect_nodes_visitor.go, v2/pkg/engine/plan/datasource_filter_node_suggestions.go, v2/pkg/engine/plan/node_selection_builder.go, v2/pkg/engine/plan/node_selection_visitor.go
Added defer metadata extraction through DeferInfo type and propagation to node suggestions. Implemented ProcessDefer to propagate defer parent information up node trees. Added defer-aware field requirement context with deferInfo and parentFieldDeferID tracking. Extended node visitor with defer-ID awareness for pending requirements.
Path & Planner Configuration
v2/pkg/engine/plan/abstract_selection_rewriter.go, v2/pkg/engine/plan/abstract_selection_rewriter_helpers.go, v2/pkg/engine/plan/abstract_selection_rewriter_info.go, v2/pkg/engine/plan/planner_configuration.go, v2/pkg/engine/plan/path_builder.go, v2/pkg/engine/plan/path_builder_visitor.go
Added DeferID() string method to PlannerConfiguration. Extended selectionSetInfo with typenameFieldDeferId. Updated typeNameSelection to accept and apply deferred directives. Refactored selection rewriting to track typename defer IDs and attach appropriate directives. Enhanced debug output for field dependencies.
Required Fields & Visitor
v2/pkg/engine/plan/required_fields_visitor.go, v2/pkg/engine/plan/required_fields_visitor_test.go, v2/pkg/engine/plan/visitor.go
Introduced comprehensive defer-aware required-fields handling with deferred aliasing (__internal_* aliases with @__defer_internal directives). Added resolveDeferredAlias for reuse vs new-alias decisions. Extended test coverage to 400+ lines for defer scenarios including cross-scope conflicts and directive propagation. Refactored visitor field-stack tracking and plan-kind determination to replace AnalyzePlanKind with planner-based defer/subscription detection.
Removed Analysis
v2/pkg/engine/plan/analyze_plan_kind.go, v2/pkg/engine/plan/analyze_plan_kind_test.go
Removed AnalyzePlanKind function and corresponding test file (186 lines), consolidating plan-kind logic into visitor-based detection.
Resolution & Response Types
v2/pkg/engine/resolve/fetch.go, v2/pkg/engine/resolve/node_object.go, v2/pkg/engine/resolve/response.go, v2/pkg/engine/resolve/loader.go
Added DeferID field to FetchDependencies and updated DeferField struct to hold DeferID. Introduced GraphQLDeferResponse and DeferFetchGroup types with DeferResponseWriter interface. Exported ResolveFetchNode method in Loader.
Deferred Response Resolution
v2/pkg/engine/resolve/resolvable.go, v2/pkg/engine/resolve/resolve.go, v2/pkg/engine/resolve/const.go
Added two-pass defer rendering in ResolveDefer with envelope/path generation via helper methods. Introduced defer-mode flags and field filtering logic for initial vs incremental responses. Implemented ResolveGraphQLDeferResponse in Resolver to orchestrate defer-group processing. Added incrementalHasNext and hasNext literals.
Post-Processing
v2/pkg/engine/postprocess/extract_defer_fetches.go, v2/pkg/engine/postprocess/postprocess.go
Added extractDeferFetches processor to extract and group fetches by DeferID. Refactored Processor.Process with grouped processor structs and new DeferResponsePlan branch handling fetch tree processing and defer extraction.
Datasource & Testing
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go, v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.go, v2/pkg/engine/datasourcetesting/datasourcetesting.go
Added guard in planner to skip DEFER_INTERNAL directive propagation. Introduced comprehensive defer-planning tests (690 lines) validating fetch ordering and federation scenarios. Refactored datasource testing to support single post-processor and added WithDefer option with WithCalculateFieldDependencies split from field-dependencies flag.
Lexer & Literals
v2/pkg/lexer/literal/literal.go
Added DEFER_INTERNAL and LABEL byte literals.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Engine as Execution Engine
    participant Normalizer as AST Normalizer
    participant Planner as Query Planner
    participant Resolver as Response Resolver
    participant Writer as Response Writer

    Client->>Engine: Execute(operation with `@defer`)
    Engine->>Normalizer: Normalize with WithInlineDefer()
    Normalizer->>Normalizer: inlineFragmentExpandDefer<br/>(expand `@defer` to `@__defer_internal`)
    Normalizer->>Normalizer: deferEnsureTypename<br/>(add __typename placeholders)
    Normalizer-->>Engine: Normalized operation
    
    Engine->>Planner: Plan deferred operation
    Planner->>Planner: collectNodesDSVisitor<br/>(extract defer metadata)
    Planner->>Planner: ProcessDefer<br/>(propagate defer parents)
    Planner-->>Engine: DeferResponsePlan<br/>(root fetch + deferred groups)
    
    Engine->>Resolver: ResolveGraphQLDeferResponse
    Resolver->>Resolver: Resolve initial fetches
    Resolver->>Writer: Resolve(initial data)
    Writer-->>Client: Initial response
    
    loop For each deferred group
        Resolver->>Resolver: Fetch deferred nodes
        Resolver->>Resolver: ResolveDefer(group data)
        Resolver->>Writer: Flush(incremental chunk)
        Writer-->>Client: Incremental response
    end
    
    Resolver->>Writer: Complete()
    Writer-->>Client: hasNext: false
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eng-7770-add-defer-support

@devsergiy devsergiy changed the title Feat/eng 7770 add defer support feat: add initial defer support Jan 26, 2026
@devsergiy devsergiy mentioned this pull request Feb 16, 2026
@devsergiy devsergiy force-pushed the feat/eng-7770-add-defer-support branch from ee840d7 to 49ce82b Compare February 17, 2026 18:47
*v.currentFields[len(v.currentFields)-1].fields = append(*v.currentFields[len(v.currentFields)-1].fields, v.currentField)
*v.currentObjectFields[len(v.currentObjectFields)-1].fields = append(*v.currentObjectFields[len(v.currentObjectFields)-1].fields, v.currentField)

// append the current field to the list of current fields
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and previous comments are like redundant.

Comment on lines +849 to +854
// When the current field has an object type, we need to push its fields slice to the stack.
// However, we can do that only after the field, which we are currently creating, will be added to the parent object fields.
// So we defer this action to be executed right after the current field is added to the parent object fields slice.
// This is more simple than analyzing resolve.Node, because this object could be nested in a list.
v.Walker.DefferOnEnterField(func() {
v.currentFields = append(v.currentFields, objectFields{
v.currentObjectFields = append(v.currentObjectFields, objectFields{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DefferOnEnterField name is not the best. Not mentioning the "deffer" unrelated to the "Defer" feature, maybe better name would be PostEnterField or RunAfterEnterField.

@@ -329,6 +345,7 @@ func (c *pathBuilderVisitor) EnterDocument(operation, definition *ast.Document)

c.fieldDependenciesForPlanners = make(map[int][]int)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this field is not used anymore

return false
}

return slices.ContainsFunc(treeNodeChildren(node), func(child int) bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a field with many children and many defer groups, this could scan the same children list multiple times per field. Is it possible for some user to have many defer groups in a very big query? Will this linear search affect the performance of such queries?

@@ -675,19 +749,23 @@ func (c *pathBuilderVisitor) hasFieldsWaitingForDependency() bool {
// in case current field has @requires directive, and we were able to plan it - it means that all fields from requires selection set was planned before that.
// So we need to notify planner of current fieldRef about dependencies on those other fields
// we know where fields were planned, because we record planner id of each planned field
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 // addFieldDependencies adds dependencies between planners based on the @requires directive.
  // If the current field has a @requires directive and we were able to plan it, it means that all fields
  // from the requires selection set were planned before it.
  // Hence, we need to notify the planner of the current fieldRef about dependencies on those other fields.
  // We know where fields were planned because we record the planner ID of each planned field.

Comment on lines 790 to 799
notified := slices.Contains(fetchConfiguration.dependsOnFetchIDs, plannerIdx)
if !notified {

fetchConfiguration.dependsOnFetchIDs = append(fetchConfiguration.dependsOnFetchIDs, plannerIdx)
// sort
slices.Sort(fetchConfiguration.dependsOnFetchIDs)
// remove consecutive duplicates
fetchConfiguration.dependsOnFetchIDs = slices.Compact(fetchConfiguration.dependsOnFetchIDs)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notified check should already prevent duplicates being added. Or if you want to sort the result then you should sort at the exit.

// resolveDeferredAlias decides how to alias a deferred required field.
// Precondition: v.config.deferInfo != nil && v.isRootLevel().
//
// Decision table:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slight reformat

//   - __internal_{fieldName} absent                                                        → addAlias
//   - __internal_{fieldName} present, same scope                                           → reuseFieldRef
//   - __internal_{fieldName} present, diff scope, __internal_{deferID}_{fieldName} absent  → addAlias, includeDeferID
//   - __internal_{fieldName} present, diff scope, __internal_{deferID}_{fieldName} present → reuseFieldRef

return v.config.operation.StringValueContentString(val.Ref)
}
return ""
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the function below (from ast_field.go) doing the same?

func (d *Document) FieldInternalDeferID(fieldRef int) (id string, exists bool) {
	directiveRef, exists := d.Fields[fieldRef].Directives.HasDirectiveByNameBytes(d, literal.DEFER_INTERNAL)
	if !exists {
		return "", false
	}
	idValue, exists := d.DirectiveArgumentValueByName(directiveRef, []byte("id"))
	if !exists {
		return "", false
	}
	return d.StringValueContentString(idValue.Ref), true
}

v.OperationNodes = append(v.OperationNodes, selectionSetNode)
}

func (v *requiredFieldsVisitor) fieldHasDeferInternal(fieldRef int) bool {
Copy link
Copy Markdown
Contributor

@ysmolski ysmolski Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use Document.FieldInternalDeferID from ast instead introducing this method?

}

func (v *requiredFieldsVisitor) isRootLevel() bool {
return len(v.OperationNodes) == 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i cannot grasp it... does it mean that we do not handle deeply nested @requires fields in deferred context?


// we are skipping adding __typename field to the required fields,
// because we want to depend only on the regular key fields, not the __typename field
if !bytes.Equal(fieldName, typeNameFieldBytes) || (bytes.Equal(fieldName, typeNameFieldBytes) && v.config.isTypeNameForEntityInterface) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noticed, that this check can be simplified to

if !bytes.Equal(fieldName, typeNameFieldBytes) || v.config.isTypeNameForEntityInterface {


// DisableCalculateFieldDependencies controls whether the planner calculates
// field dependencies at all.
DisableCalculateFieldDependencies bool
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you add it?

Comment on lines +277 to 285
c.handleFieldRequiredByRequires(fieldCtx)
// skip to the next suggestion as we only handle requires here
continue
}

if suggestion.requiresKey != nil {
// add @key requirements for the field
c.handleFieldsRequiredByKey(fieldRef, parentPath, typeName, fieldName, currentPath, ds, *suggestion.requiresKey)
c.handleFieldsRequiredByKey(fieldCtx, *suggestion.requiresKey)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: handleFieldRequiredByRequires and handleFieldsRequiredByKey - naming could be better. I suggest following: queueRequiresDependencies and queueKeyFieldsForJump

Comment on lines +525 to +527
if requirements.requirementConfigs[i].selectionSet == fieldConfiguration.SelectionSet && requirements.requirementConfigs[i].dsHash == fieldCtx.dsConfig.Hash() && requirements.requirementConfigs[i].isTypenameForEntityInterface == isTypenameForEntityInterface {
if slices.IndexFunc(requirements.requirementConfigs[i].requestedByFieldRefs, func(fieldRef int) bool {
return fieldRef == requestedByFieldRef
return fieldRef == fieldCtx.fieldRef
Copy link
Copy Markdown
Contributor

@ysmolski ysmolski Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this long if is not for humans, can you break it down into individual named conditions? and see my other comment

if fieldCtx.deferInfo != nil {
deferID = fieldCtx.deferInfo.ID
}
existsKey := pendingFieldRequirementExistsKey{fieldCtx.dsConfig.Hash(), fieldConfiguration.SelectionSet, isTypenameForEntityInterface, deferID}
Copy link
Copy Markdown
Contributor

@ysmolski ysmolski Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please do not format code like this one unreadable line

} else {
for i := range requirements.requirementConfigs {
if requirements.requirementConfigs[i].selectionSet == fieldConfiguration.SelectionSet && requirements.requirementConfigs[i].dsHash == dsHash && requirements.requirementConfigs[i].isTypenameForEntityInterface == isTypenameForEntityInterface {
if requirements.requirementConfigs[i].selectionSet == fieldConfiguration.SelectionSet && requirements.requirementConfigs[i].dsHash == fieldCtx.dsConfig.Hash() && requirements.requirementConfigs[i].isTypenameForEntityInterface == isTypenameForEntityInterface {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the code above (line 510) we already checked if we have a new field requirement. Why do we do another scan on line 524?

requirements.existsTracker[existsKey] = struct{}{}
requirements.requirementConfigs = append(requirements.requirementConfigs, config)
} else {
for i := range requirements.requirementConfigs {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, why do we iterate again even if we already could found the existing requirementConfigs?


// handleRequiredField is the EnterField entry point for @requires fields.
// It builds requiredFieldInfo and dispatches to the deferred or non-deferred path.
func (v *requiredFieldsVisitor) handleRequiredField(ref int) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many handle* methods in this visitor. I am not sure that the comment to this function is useful to the reader. It just names things happening in the code without telling the story of the method. How about this high level description?

"It ensures that this fragment field has a representation in the operation"

And I would suggest this rename:

  • handleRequiredField to applyRequiredField
  • handleRequiredFieldDeferred to applyRequiresFieldDeferred
  • handleRequiredFieldNonDeferred to applyRequiresFieldDirect

The same thought can be applied to the "key" methods.

Comment on lines +50 to +51
func (d *extractDeferFetches) fetchGroups(deferPlan *plan.DeferResponsePlan) (root []*resolve.FetchTreeNode, deffered map[string][]*resolve.FetchTreeNode) {
fetchGroups := make(map[string][]*resolve.FetchTreeNode)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uhm, you named it in the return and then used completely different variable in the body. I suggest to use fetchGroups everywhere.


for _, fetch := range deferPlan.Response.Response.Fetches.ChildNodes {
deferID := fetch.Item.Fetch.Dependencies().DeferID
if deferID == "" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain what is the meaning of empty string in deferID?

})

if deferID != "" {
r.operation.AddDeferInternalDirectiveToField(field.Ref, deferID, "", "")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please explain empty magic values.

}
}

writer.Complete()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why we Complete only here and not ever when errors are returned?

Comment on lines +478 to +481
if t.resolvable.hasErrors() {
return resolveInfo, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the error is not returned here and no .Complete too?

r.typeNames = r.typeNames[:len(r.typeNames)-1]
}()

if r.deferMode {
Copy link
Copy Markdown
Contributor

@ysmolski ysmolski Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please inverse and bail early to flatten the if.

As I understood the code below, it does two things: collection and rendering at the same time, with mutable state like enableDeferRender, incrementalItemWritten, deferItemDataNull being toggled inside. That means at least 4 paths (initial render, defer render, defer null-envelope, seek/passthrough only). Correct me if I wrong.

All of it makes this code very complicated and error prone. I tried to think of ways to simplify it, but quickly failed.

return false
}

func (r *Resolvable) collectDeferFields(obj *Object) (deferFields map[int]struct{}, seekFields map[int]struct{}) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO deferFields should be named as renderFields - fields we should render now, in the currect pass,

and seekFields are transitFields or passThroughFields - fields we have to go through to get renderable fields.

The callee site could reflect that. seek is not the right word the meaning, unless I have not grasped some other meanting.

if r.deferMode {
deferFields, seekFiels := r.collectDeferFields(obj)

if len(deferFields) > 0 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, inverse the len(deferFields) > 0 and bail early. The if after this one will lose 2nd comparison too.

return false
}

// skip array if it's item do not have an object kind
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Skip array if its item type is not an object kind.


for i := range obj.Fields {
if filter.enabled {
// if mode is seek
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As my comment suggested above, it might be more readable to name it "passThrough" instead of "seek"

return false
}

func (r *Resolvable) shoulSkipObjectFieldByTypenames(field *Field) bool {
Copy link
Copy Markdown
Contributor

@ysmolski ysmolski Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldSkipObjectFieldByTypenames or even shouldSkipFieldByTypeCondition

Copy link
Copy Markdown
Contributor

@ysmolski ysmolski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1st pass is done. Overall - well done job! I expected it to be more complicated. Although I have not understood in full some partos of the planner, especially those visitors with big state. But hey, I tried! :) This PR adds some more tech debt in the resolver. Maybe we could avoid it? See the relevant comment. And those bools to records the state are making the code hard to decipher.

As for other items, judge from the comments if it can be improved or just needs some clarification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants