Conversation
📝 WalkthroughWalkthroughThis pull request implements comprehensive support for the GraphQL Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
ee840d7 to
49ce82b
Compare
…th same directives in different order
add todos
add defer plan kind
fix test expectation
| if len(options) > 0 { | ||
| opts = options[0] | ||
| } | ||
|
|
There was a problem hiding this comment.
why do we drop everything except first?
|
@claude review |
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
v2/pkg/engine/plan/planner_configuration.go (1)
140-156:⚠️ Potential issue | 🟡 MinorPointer to function parameter may not reference the intended slice element.
Line 154 stores
&configurationin the map, butconfigurationis the function parameter (a copy). AfterAddPathreturns, this pointer references a value that was copied from the caller, not the element stored inp.pathsat line 142.If you intend to reference the actual path stored in the slice, you should point to the slice element instead:
🔧 Suggested fix
func (p *plannerPathsConfiguration) AddPath(configuration pathConfiguration) { // fmt.Println("[plannerConfiguration.AddPath] parentPath:", p.parentPath, "path:", configuration.String()) p.paths = append(p.paths, configuration) idx := len(p.paths) - 1 p.index[configuration.path] = append(p.index[configuration.path], idx) if configuration.parentPath != "" { p.nonLeafPaths[configuration.parentPath] = struct{}{} } if configuration.pathType == PathTypeFragment { p.fragmentPaths[configuration] = struct{}{} } if configuration.pathType == PathTypeField { - p.indexByFieldRef[configuration.fieldRef] = &configuration + p.indexByFieldRef[configuration.fieldRef] = &p.paths[idx] } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@v2/pkg/engine/plan/planner_configuration.go` around lines 140 - 156, The AddPath method of plannerPathsConfiguration stores &configuration into p.indexByFieldRef but configuration is the method parameter (a copy) so the pointer will not reference the element actually stored in p.paths; instead, after appending, take the address of the slice element (e.g. &p.paths[idx]) and store that in p.indexByFieldRef[configuration.fieldRef], ensuring the map points to the actual element in p.paths; update plannerPathsConfiguration.AddPath to use that slice-element address and keep all other logic (p.paths, p.index, p.nonLeafPaths, p.fragmentPaths) unchanged.execution/engine/execution_engine_test.go (1)
110-155:⚠️ Potential issue | 🟡 MinorReject mixed streaming/non-streaming assertion modes.
When
streamingResponseis enabled, onlyexpectedResponseusesstreamingBuf;indentJSON,expectedFixture, andexpectedJSONResponsestill validateresultWriter.String(). That makes those combinations assert a different artifact than the streamed payload.Suggested guard
streamingBuf := bytes.NewBuffer(nil) if opts.streamingResponse { resultWriter.SetFlushCallback(func(data []byte) { streamingBuf.Write(data) streamingBuf.Write([]byte{'\n'}) }) } + if opts.streamingResponse && (testCase.indentJSON || testCase.expectedFixture != "" || testCase.expectedJSONResponse != "") { + t.Fatalf("streamingResponse cannot be combined with indentJSON, expectedFixture, or expectedJSONResponse") + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@execution/engine/execution_engine_test.go` around lines 110 - 155, When opts.streamingResponse is true the test currently mixes streamed output (streamingBuf populated in the SetFlushCallback) with non-streamed assertions (using resultWriter.String(), json.Indent, goldie.Assert(), and assert.JSONEq on actualResponse), so ensure the assertions use the same artifact: if opts.streamingResponse (or testCase.streamingResponse) is true, read and assert against streamingBuf.String() for expectedResponse, expectedFixture, expectedJSONResponse and indentJSON flows (and skip or fail fast for assertions that are incompatible), otherwise continue using resultWriter.String(); update the test's branches around streamingBuf, actualResponse, indentJSON, expectedFixture, and expectedJSONResponse to consistently use the streamed value when streamingResponse is enabled and call engine.Execute as before.v2/pkg/engine/plan/visitor.go (1)
220-235:⚠️ Potential issue | 🟠 MajorAlways populate
fieldPlannersfor deferred fields.
assignDeferdepends onv.fieldPlanners[fieldRef], but this block stops filling that map whenDisableCalculateFieldDependenciesis enabled. In that mode deferred fields never getcurrentField.Defer, so the resolver treats them as part of the initial payload.🐛 Proposed fix
- if !v.Config.DisableCalculateFieldDependencies && kind == astvisitor.LeaveField { - // we don't need to do this twice, so we only do it on leave - - // store which fields are planned on which planners - if v.plannerFields[visitorID] == nil { - v.plannerFields[visitorID] = []int{ref} - } else { - v.plannerFields[visitorID] = append(v.plannerFields[visitorID], ref) - } - - // store which planners a field was planned on - if v.fieldPlanners[ref] == nil { - v.fieldPlanners[ref] = []int{visitorID} - } else { - v.fieldPlanners[ref] = append(v.fieldPlanners[ref], visitorID) - } + if kind == astvisitor.LeaveField { + if !v.Config.DisableCalculateFieldDependencies { + // store which fields are planned on which planners + if v.plannerFields[visitorID] == nil { + v.plannerFields[visitorID] = []int{ref} + } else { + v.plannerFields[visitorID] = append(v.plannerFields[visitorID], ref) + } + } + + // assignDefer also depends on this map + if v.fieldPlanners[ref] == nil { + v.fieldPlanners[ref] = []int{visitorID} + } else { + v.fieldPlanners[ref] = append(v.fieldPlanners[ref], visitorID) + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@v2/pkg/engine/plan/visitor.go` around lines 220 - 235, The code currently skips populating v.fieldPlanners when v.Config.DisableCalculateFieldDependencies is true, causing assignDefer to not see planners for deferred fields (so currentField.Defer is never set); update the logic in the visitor leave handling (the block guarded by v.Config.DisableCalculateFieldDependencies and kind == astvisitor.LeaveField) to always append to v.fieldPlanners[ref] for the current visitorID even when DisableCalculateFieldDependencies is true (you may still skip planner-specific dependency calculations but must still perform the v.fieldPlanners[ref] insertion), ensuring v.plannerFields[visitorID] and v.fieldPlanners[ref] remain consistent for use by assignDefer and currentField.Defer.v2/pkg/engine/plan/path_builder_visitor.go (1)
1301-1333:⚠️ Potential issue | 🟠 MajorMake missing-path tracking defer-aware.
Lines 1321-1325 only match by
currentPathand datasource hash. Oncequery.useris planned on the non-deferred planner, the same path on a deferred planner is treated as already satisfied, so the second walk can skip the missing defer-parent/deferred path entirely. The missing-path trackers need the samedeferIDdimension as the planners now do.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@v2/pkg/engine/plan/path_builder_visitor.go` around lines 1301 - 1333, The missing-path tracking in handleMissingPath currently keys only by field.currentPath and matches planned suggestions by DataSourceHash, causing deferred and non-deferred planners to collide; update the logic to include defer ID when recording and checking missing paths (use the same defer identifier used by planners/suggestions), i.e., change potentiallyMissingPathTracker to use a composite key of field.currentPath + deferID (or a struct key) and when checking planners (the loop using c.planners and HasPath) ensure you compare both DataSourceHash and the planner's defer ID to suggestion.deferID so a path planned on a non-deferred planner does not incorrectly mark the deferred planner’s path as satisfied. Ensure suggestions lookup (nodeSuggestions.SuggestionsForPath) and any break/return conditions account for the defer dimension consistently.
🧹 Nitpick comments (5)
v2/pkg/engine/resolve/loader.go (1)
575-577: Consider clarifying the TODO comment.The TODO comment
"unclear why we doing this"highlights a code path that could use documentation. This branch handles the case when there are no parent items to merge into (len(items) == 0), so it setsresponseDataas the root data directly. Consider documenting this intent:// When no parent items exist (e.g., root query response), // set the response as the top-level data.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@v2/pkg/engine/resolve/loader.go` around lines 575 - 577, The TODO comment "unclear why we doing this" should be replaced with a clear explanatory comment: document that when there are no parent items to merge into (i.e., the branch where len(items) == 0), the loader assigns responseData as the top-level/root response by setting l.resolvable.data = responseData; update the comment at that location near l.resolvable.data and responseData to reflect that intent (e.g., "When no parent items exist (root query response), set responseData as the top-level data").v2/pkg/engine/resolve/response.go (1)
66-88: Inconsistent indentation in query plan formatting.The
indentfunction adds 4 spaces for indentation, but the format string literal uses tabs. This may result in misaligned output depending on tab width settings.💡 Suggested fix for consistent indentation
func (r *GraphQLDeferResponse) QueryPlanString() string { indent := func(s string) string { - return strings.ReplaceAll(s, "\n", "\n ") + return strings.ReplaceAll(s, "\n", "\n\t") } primary := indent(r.Response.Fetches.QueryPlan().PrettyPrint()) var secondary []string for _, g := range r.Defers { - secondary = append(secondary, strings.ReplaceAll(g.Fetches.QueryPlan().PrettyPrint(), "\n", "\n ")) + secondary = append(secondary, strings.ReplaceAll(g.Fetches.QueryPlan().PrettyPrint(), "\n", "\n\t")) } return fmt.Sprintf(` QueryPlan { Primary { %s } Deferred [ %s ] } `, primary, strings.Join(secondary, "\n")) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@v2/pkg/engine/resolve/response.go` around lines 66 - 88, The QueryPlanString method has inconsistent indentation: the indent helper injects four spaces but the multi-line format string uses tabs, causing misalignment; update GraphQLDeferResponse.QueryPlanString to use spaces consistently (replace tabs in the format literal with the same four-space indentation) and ensure the secondary entries are indented via the same indent() helper (or by applying strings.ReplaceAll with "\n" -> "\n ") so both primary and deferred query plans align uniformly.v2/pkg/engine/postprocess/extract_defer_fetches.go (2)
50-50: Minor typo:deffered→deferred.🔧 Suggested fix
-func (d *extractDeferFetches) fetchGroups(deferPlan *plan.DeferResponsePlan) (root []*resolve.FetchTreeNode, deffered map[string][]*resolve.FetchTreeNode) { +func (d *extractDeferFetches) fetchGroups(deferPlan *plan.DeferResponsePlan) (root []*resolve.FetchTreeNode, deferred map[string][]*resolve.FetchTreeNode) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@v2/pkg/engine/postprocess/extract_defer_fetches.go` at line 50, The parameter and return variable name "deffered" in function fetchGroups (in type extractDeferFetches) is misspelled; rename it to "deferred" across the function signature and all usages (including the return map type and any references inside fetchGroups and its callers) so the signature becomes fetchGroups(... ) (root []*resolve.FetchTreeNode, deferred map[string][]*resolve.FetchTreeNode) and update every reference to deffered -> deferred to keep names consistent.
29-34: Sorting assumes numeric defer IDs.If
strconv.Atoifails for non-numeric defer IDs, both values default to0, causing all non-numeric IDs to compare equal. This could result in unstable ordering if defer IDs aren't guaranteed to be numeric strings.If defer IDs may be non-numeric, consider falling back to lexicographic comparison when parsing fails:
🔧 Alternative with fallback
deferIds := slices.SortedFunc(maps.Keys(fetchGroups), func(a, b string) int { - an, _ := strconv.Atoi(a) - bn, _ := strconv.Atoi(b) - return cmp.Compare(an, bn) + an, errA := strconv.Atoi(a) + bn, errB := strconv.Atoi(b) + if errA == nil && errB == nil { + return cmp.Compare(an, bn) + } + return cmp.Compare(a, b) // fallback to lexicographic })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@v2/pkg/engine/postprocess/extract_defer_fetches.go` around lines 29 - 34, The current sorting of deferIds using slices.SortedFunc with strconv.Atoi and cmp.Compare assumes numeric IDs and treats parse errors as zero, which makes different non-numeric IDs compare equal; update the comparison inside the slices.SortedFunc (where deferIds is computed from maps.Keys(fetchGroups)) to attempt strconv.Atoi for both a and b and if either parse fails fall back to a deterministic lexicographic compare (e.g., strings.Compare) so non-numeric defer IDs are ordered stably instead of being treated as equal.v2/pkg/astnormalization/defer_ensure_typename.go (1)
59-67: Add a nested-inline-fragment regression here.
SelectionSetFieldSelections(ref)only sees direct fields. A case like...@defer{ ... on User { title } }would lock down that the placeholder still gets injected at the intended depth when the defer-only subtree is wrapped in inline fragments.Based on learnings, validation runs after normalization here, so fragment spreads have already been expanded into inline fragments or fields.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@v2/pkg/astnormalization/defer_ensure_typename.go` around lines 59 - 67, SelectionSetFieldSelections(ref) only returns direct child fields, so nested inline fragments like "... `@defer` { ... on User { title } }" are missed and the placeholder injection logic fails; update the logic that builds fieldSelectionRefs (used with SelectionSetFieldSelections(ref) and parentDeferID()) to recursively traverse the selection set and include fields found inside nested inline fragments (expanded fragment spreads), i.e., implement or call a helper that walks SelectionSet nodes depth-first, collects Field selections within InlineFragment nodes as well as direct fields, and use that collected list in place of the current SelectionSetFieldSelections(ref) result so parentDeferID() and the defer-intersection checks operate on fields inside inline fragments too.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@execution/engine/execution_engine_helpers_test.go`:
- Around line 77-85: The shared map used must be synchronized because RoundTrip
may run concurrently: add "sync" to imports, create a mutex (e.g., usedMu :=
&sync.Mutex{} or var usedMu sync.Mutex) next to the used map, wrap any writes to
used (the handler that marks used[key] = true) with usedMu.Lock()/Unlock(), and
also lock usedMu while reading/iterating used in the t.Cleanup block that checks
testCase.responses so the read is protected; keep the same map and keys (used,
testCase.responses) and only add the mutex and locking/unlocking around
accesses.
In `@v2/pkg/ast/ast_field.go`:
- Around line 203-206: The code currently ignores strconv.Atoi errors when
converting leftDeferIdValue.Ref and rightDeferIdValue.Ref into leftId and
rightId (variables created in the snippet) and leaves a TODO about parent id
handling; update the conversion to check and handle the errors from strconv.Atoi
(e.g., propagate an error up from the containing function or use a clear
fallback and log) instead of silently treating failures as 0, and implement or
call existing logic to consider parent IDs when comparing/merging defer IDs
(update the comparison logic that uses leftId/rightId to also incorporate parent
ID fields or return an explicit error if parent IDs are required but missing).
Ensure you modify the function that contains leftId/rightId so its signature and
callers handle the new error/behavior.
In `@v2/pkg/asttransform/internal.graphql`:
- Around line 4-5: The GraphQL field description for the "label" field is
incorrect (it currently reads "A unique identifier for the results." which
describes "id"); update the description for the label field in the schema so it
accurately describes its purpose (e.g., a human-readable label or name for the
result) by editing the string above the label: String declaration to a correct
sentence referencing "label" instead of "id".
In `@v2/pkg/engine/datasourcetesting/datasourcetesting.go`:
- Around line 42-45: The withPrintPlan branch currently assumes p.Plan(...)
returns *plan.SynchronousResponsePlan and will panic when WithDefer() is set
because p.Plan may return *plan.DeferResponsePlan; update the branch that prints
the plan (and the similar logic around the other occurrence) to capture the
result of p.Plan(...) into a variable and use a type switch on its concrete type
(handling *plan.SynchronousResponsePlan and *plan.DeferResponsePlan) so both can
be printed safely; refer to WithDefer()/testOptions.withDefer, the p.Plan(...)
call, and the plan.SynchronousResponsePlan and plan.DeferResponsePlan types when
making the change.
In `@v2/pkg/engine/plan/datasource_filter_node_suggestions.go`:
- Around line 215-224: The loop that scans parent items (referencing
f.items[parentIdx], parentIdx, f.items[i].deferInfo.ID, deferInfo, and deferIDs)
incorrectly uses break when a parent is already in the same defer scope or
already contains the ID, which stops scanning further same-source parents and
misses updating deferParentPath/deferIDs; change those break statements so the
loop skips only the current parent and continues scanning remaining parents (use
continue semantics) and ensure when a parent is eligible you still append/update
deferParentPath and deferIDs for that parent rather than exiting the loop early.
In `@v2/pkg/engine/plan/node_selection_visitor.go`:
- Around line 292-310: The function wrappingFieldDeferID currently stops
scanning ancestors and returns "" as soon as it encounters a field ancestor
without the `@__defer_internal` directive; change the control flow in
wrappingFieldDeferID so that when Directives.HasDirectiveByNameBytes on
c.operation.Fields[ancestor.Ref] returns exists==false you continue scanning
earlier ancestors (i.e., replace the early return with continue), and only
return "" after the loop if no wrapping defer directive was found; keep the
existing logic that extracts the id via c.operation.DirectiveArgumentValueByName
and c.operation.StringValueContentString when a directive is found.
In `@v2/pkg/engine/plan/required_fields_visitor.go`:
- Around line 579-599: The applyDeferInternalDirective function currently
returns early when v.config.deferInfo == nil which prevents applying
parentFieldDeferID for key fields; change the logic in
applyDeferInternalDirective (used with v.config.deferInfo, v.config.isKey,
v.config.parentFieldDeferID) so that if deferInfo is nil you still check
v.config.parentFieldDeferID and, when non-empty and v.config.isKey is true, call
v.config.operation.AddDeferInternalDirectiveToField(fieldRef,
v.config.parentFieldDeferID, "", ""); otherwise, if deferInfo exists keep the
existing behavior of using deferInfo.ID/Label/ParentID for non-key fields and
for keys when appropriate.
In `@v2/pkg/engine/plan/visitor.go`:
- Around line 1000-1037: The code sets response.Info.OperationType from
v.Operation.OperationDefinitions[0] and then overwrites it to Query in
defer/default branches, causing wrong root types for non-first operations;
update the logic to read the operation type from the selected operation
reference (use the existing opRef/operationRef selected earlier, not
OperationDefinitions[0]) and only set Info.OperationType based on that actual
opRef value (do not unconditionally set ast.OperationTypeQuery in the
DeferResponsePlan and SynchronousResponsePlan branches); change the three places
where v.response.Info is assigned (initial block, Defer case, default case) to
derive OperationType from the selected operation reference so
mutations/subscriptions preserve the correct root type.
In `@v2/pkg/engine/postprocess/extract_defer_fetches.go`:
- Around line 53-54: The fetchGroups method in extractDeferFetches may iterate a
nil Fetches and panic; add a nil-check for deferPlan.Response.Response.Fetches
at the start of extractDeferFetches.fetchGroups (before the for loop over
Fetches.ChildNodes) and return nil for root and an empty map for fetchGroups
when Fetches is nil; keep the rest of the logic unchanged (use the existing
symbols deferPlan.Response.Response.Fetches, Fetches.ChildNodes, and fetchGroups
map).
In `@v2/pkg/engine/resolve/fetch.go`:
- Around line 110-113: The equality check in EqualSingleFetch must include the
DeferID from FetchDependencies so fetches across different defer boundaries
aren't treated as equivalent; update the EqualSingleFetch implementation (the
method that compares two SingleFetch/FetchItem objects) to compare the DeferID
field (e.g., item.FetchDeps.DeferID vs other.FetchDeps.DeferID), handling
nil/empty dependency structs safely, so that differing DeferID values make the
items unequal and prevent merging initial and deferred fetches.
In `@v2/pkg/engine/resolve/node_object.go`:
- Around line 182-184: Field.Copy() currently assigns the DeferField by
reference (Defer: f.Defer) causing copied trees to share the same DeferField
pointer; change Field.Copy() to allocate a new DeferField and copy its contents
(e.g., create &DeferField{DeferID: f.Defer.DeferID} when f.Defer != nil) and
assign that to the copied Field's Defer so each copy gets an independent
DeferField instance.
In `@v2/pkg/engine/resolve/resolvable.go`:
- Around line 257-259: The deferred delivery termination is incorrectly driven
by r.hasErrors() (which reuses r.errors across chunks) so repeated old errors
can make hasNext false; update ResolveDefer to stop using r.hasErrors() for
hasNext and instead derive the "more deferred payloads exist" signal from the
deferred-work state (e.g., a new per-chunk flag or a check on the deferred
queue/iterator like r.deferQueue/remainingCount or a helper r.hasMoreDeferred())
and call r.printHasNext(true) when that derived condition is true; ensure
r.errors is not used to decide continuation across chunks (or clear per-chunk
errors) and apply the same change to the other occurrences in the 266-306 range.
In `@v2/pkg/engine/resolve/resolve.go`:
- Around line 456-468: Currently all rendering is gated by
ctx.ExecutionOptions.SkipLoader; change control flow so loader initialization
and ResolveFetchNode are only skipped when SkipLoader is true, but
t.resolvable.Resolve is always called to render the initial response shape.
Concretely, keep the t.loader.Init(...) and the ResolveFetchNode(...) calls
inside an if !ctx.ExecutionOptions.SkipLoader branch, but move the lines that
set t.resolvable.deferMode, t.resolvable.deferID and the call to
t.resolvable.Resolve(...) outside that branch so they execute regardless of
SkipLoader; apply the same refactor wherever similar gating appears (the blocks
around t.loader.Init / ResolveFetchNode and t.resolvable.Resolve noted in this
file).
---
Outside diff comments:
In `@execution/engine/execution_engine_test.go`:
- Around line 110-155: When opts.streamingResponse is true the test currently
mixes streamed output (streamingBuf populated in the SetFlushCallback) with
non-streamed assertions (using resultWriter.String(), json.Indent,
goldie.Assert(), and assert.JSONEq on actualResponse), so ensure the assertions
use the same artifact: if opts.streamingResponse (or testCase.streamingResponse)
is true, read and assert against streamingBuf.String() for expectedResponse,
expectedFixture, expectedJSONResponse and indentJSON flows (and skip or fail
fast for assertions that are incompatible), otherwise continue using
resultWriter.String(); update the test's branches around streamingBuf,
actualResponse, indentJSON, expectedFixture, and expectedJSONResponse to
consistently use the streamed value when streamingResponse is enabled and call
engine.Execute as before.
In `@v2/pkg/engine/plan/path_builder_visitor.go`:
- Around line 1301-1333: The missing-path tracking in handleMissingPath
currently keys only by field.currentPath and matches planned suggestions by
DataSourceHash, causing deferred and non-deferred planners to collide; update
the logic to include defer ID when recording and checking missing paths (use the
same defer identifier used by planners/suggestions), i.e., change
potentiallyMissingPathTracker to use a composite key of field.currentPath +
deferID (or a struct key) and when checking planners (the loop using c.planners
and HasPath) ensure you compare both DataSourceHash and the planner's defer ID
to suggestion.deferID so a path planned on a non-deferred planner does not
incorrectly mark the deferred planner’s path as satisfied. Ensure suggestions
lookup (nodeSuggestions.SuggestionsForPath) and any break/return conditions
account for the defer dimension consistently.
In `@v2/pkg/engine/plan/planner_configuration.go`:
- Around line 140-156: The AddPath method of plannerPathsConfiguration stores
&configuration into p.indexByFieldRef but configuration is the method parameter
(a copy) so the pointer will not reference the element actually stored in
p.paths; instead, after appending, take the address of the slice element (e.g.
&p.paths[idx]) and store that in p.indexByFieldRef[configuration.fieldRef],
ensuring the map points to the actual element in p.paths; update
plannerPathsConfiguration.AddPath to use that slice-element address and keep all
other logic (p.paths, p.index, p.nonLeafPaths, p.fragmentPaths) unchanged.
In `@v2/pkg/engine/plan/visitor.go`:
- Around line 220-235: The code currently skips populating v.fieldPlanners when
v.Config.DisableCalculateFieldDependencies is true, causing assignDefer to not
see planners for deferred fields (so currentField.Defer is never set); update
the logic in the visitor leave handling (the block guarded by
v.Config.DisableCalculateFieldDependencies and kind == astvisitor.LeaveField) to
always append to v.fieldPlanners[ref] for the current visitorID even when
DisableCalculateFieldDependencies is true (you may still skip planner-specific
dependency calculations but must still perform the v.fieldPlanners[ref]
insertion), ensuring v.plannerFields[visitorID] and v.fieldPlanners[ref] remain
consistent for use by assignDefer and currentField.Defer.
---
Nitpick comments:
In `@v2/pkg/astnormalization/defer_ensure_typename.go`:
- Around line 59-67: SelectionSetFieldSelections(ref) only returns direct child
fields, so nested inline fragments like "... `@defer` { ... on User { title } }"
are missed and the placeholder injection logic fails; update the logic that
builds fieldSelectionRefs (used with SelectionSetFieldSelections(ref) and
parentDeferID()) to recursively traverse the selection set and include fields
found inside nested inline fragments (expanded fragment spreads), i.e.,
implement or call a helper that walks SelectionSet nodes depth-first, collects
Field selections within InlineFragment nodes as well as direct fields, and use
that collected list in place of the current SelectionSetFieldSelections(ref)
result so parentDeferID() and the defer-intersection checks operate on fields
inside inline fragments too.
In `@v2/pkg/engine/postprocess/extract_defer_fetches.go`:
- Line 50: The parameter and return variable name "deffered" in function
fetchGroups (in type extractDeferFetches) is misspelled; rename it to "deferred"
across the function signature and all usages (including the return map type and
any references inside fetchGroups and its callers) so the signature becomes
fetchGroups(... ) (root []*resolve.FetchTreeNode, deferred
map[string][]*resolve.FetchTreeNode) and update every reference to deffered ->
deferred to keep names consistent.
- Around line 29-34: The current sorting of deferIds using slices.SortedFunc
with strconv.Atoi and cmp.Compare assumes numeric IDs and treats parse errors as
zero, which makes different non-numeric IDs compare equal; update the comparison
inside the slices.SortedFunc (where deferIds is computed from
maps.Keys(fetchGroups)) to attempt strconv.Atoi for both a and b and if either
parse fails fall back to a deterministic lexicographic compare (e.g.,
strings.Compare) so non-numeric defer IDs are ordered stably instead of being
treated as equal.
In `@v2/pkg/engine/resolve/loader.go`:
- Around line 575-577: The TODO comment "unclear why we doing this" should be
replaced with a clear explanatory comment: document that when there are no
parent items to merge into (i.e., the branch where len(items) == 0), the loader
assigns responseData as the top-level/root response by setting l.resolvable.data
= responseData; update the comment at that location near l.resolvable.data and
responseData to reflect that intent (e.g., "When no parent items exist (root
query response), set responseData as the top-level data").
In `@v2/pkg/engine/resolve/response.go`:
- Around line 66-88: The QueryPlanString method has inconsistent indentation:
the indent helper injects four spaces but the multi-line format string uses
tabs, causing misalignment; update GraphQLDeferResponse.QueryPlanString to use
spaces consistently (replace tabs in the format literal with the same four-space
indentation) and ensure the secondary entries are indented via the same indent()
helper (or by applying strings.ReplaceAll with "\n" -> "\n ") so both primary
and deferred query plans align uniformly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0c3f5e5a-342f-48e0-89d5-8c0011309338
📒 Files selected for processing (81)
execution/engine/config_factory_proxy_test.goexecution/engine/engine_config_test.goexecution/engine/execution_engine.goexecution/engine/execution_engine_defer_test.goexecution/engine/execution_engine_helpers_test.goexecution/engine/execution_engine_test.goexecution/engine/testdata/full_introspection.jsonexecution/engine/testdata/full_introspection_with_deprecated.jsonexecution/engine/testdata/full_introspection_with_typenames.jsonv2/pkg/ast/ast_argument.gov2/pkg/ast/ast_directive.gov2/pkg/ast/ast_field.gov2/pkg/ast/ast_inline_fragment.gov2/pkg/astnormalization/astnormalization.gov2/pkg/astnormalization/astnormalization_test.gov2/pkg/astnormalization/defer_ensure_typename.gov2/pkg/astnormalization/defer_ensure_typename_test.gov2/pkg/astnormalization/directive_include_skip.gov2/pkg/astnormalization/directive_include_skip_test.gov2/pkg/astnormalization/field_deduplication.gov2/pkg/astnormalization/field_deduplication_test.gov2/pkg/astnormalization/fragment_spread_inlining_test.gov2/pkg/astnormalization/inline_fragment_expand_defer.gov2/pkg/astnormalization/inline_fragment_expand_defer_test.gov2/pkg/astnormalization/inline_fragment_selection_merging.gov2/pkg/astnormalization/inline_fragment_selection_merging_test.gov2/pkg/astnormalization/inline_selections_from_inline_fragments_test.gov2/pkg/asttransform/base.graphqlv2/pkg/asttransform/baseschema.gov2/pkg/asttransform/baseschema_test.gov2/pkg/asttransform/fixtures/complete.goldenv2/pkg/asttransform/fixtures/custom_query_name.goldenv2/pkg/asttransform/fixtures/mutation_only.goldenv2/pkg/asttransform/fixtures/mutation_only_no_internal.goldenv2/pkg/asttransform/fixtures/schema_missing.goldenv2/pkg/asttransform/fixtures/simple.goldenv2/pkg/asttransform/fixtures/subscription_only.goldenv2/pkg/asttransform/fixtures/subscription_renamed.goldenv2/pkg/asttransform/fixtures/with_mutation_subscription.goldenv2/pkg/asttransform/internal.graphqlv2/pkg/asttransform/stream.graphqlv2/pkg/engine/datasource/graphql_datasource/graphql_datasource.gov2/pkg/engine/datasource/graphql_datasource/graphql_datasource_defer_test.gov2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection.goldenv2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection_with_custom_root_operation_types.goldenv2/pkg/engine/datasourcetesting/datasourcetesting.gov2/pkg/engine/plan/abstract_selection_rewriter.gov2/pkg/engine/plan/abstract_selection_rewriter_helpers.gov2/pkg/engine/plan/abstract_selection_rewriter_info.gov2/pkg/engine/plan/abstract_selection_rewriter_test.gov2/pkg/engine/plan/analyze_plan_kind.gov2/pkg/engine/plan/analyze_plan_kind_test.gov2/pkg/engine/plan/configuration.gov2/pkg/engine/plan/datasource_configuration.gov2/pkg/engine/plan/datasource_filter_collect_nodes_visitor.gov2/pkg/engine/plan/datasource_filter_node_suggestions.gov2/pkg/engine/plan/node_selection_builder.gov2/pkg/engine/plan/node_selection_visitor.gov2/pkg/engine/plan/path_builder.gov2/pkg/engine/plan/path_builder_visitor.gov2/pkg/engine/plan/plan.gov2/pkg/engine/plan/planner_configuration.gov2/pkg/engine/plan/required_fields_visitor.gov2/pkg/engine/plan/required_fields_visitor_test.gov2/pkg/engine/plan/visitor.gov2/pkg/engine/postprocess/extract_defer_fetches.gov2/pkg/engine/postprocess/postprocess.gov2/pkg/engine/resolve/const.gov2/pkg/engine/resolve/fetch.gov2/pkg/engine/resolve/loader.gov2/pkg/engine/resolve/node_object.gov2/pkg/engine/resolve/resolvable.gov2/pkg/engine/resolve/resolve.gov2/pkg/engine/resolve/response.gov2/pkg/federation/fixtures/federated_schema.goldenv2/pkg/federation/schema.gov2/pkg/introspection/fixtures/starwars_introspected.goldenv2/pkg/introspection/generator.gov2/pkg/introspection/generator_test.gov2/pkg/introspection/testdata/starwars.schema.graphqlv2/pkg/lexer/literal/literal.go
💤 Files with no reviewable changes (2)
- v2/pkg/engine/plan/analyze_plan_kind.go
- v2/pkg/engine/plan/analyze_plan_kind_test.go
| used := make(map[string]bool) | ||
| if testCase.reportUnused { | ||
| t.Cleanup(func() { | ||
| for key := range testCase.responses { | ||
| if !used[key] { | ||
| t.Logf("UNUSED MOCK [%s]: %s", testCase.expectedHost, key) | ||
| } | ||
| } | ||
| }) |
There was a problem hiding this comment.
Guard used against concurrent RoundTrip calls.
RoundTrip can be hit from multiple goroutines, and the new used map is written in the handler and read again in t.Cleanup without synchronization. That will trip -race in the new incremental/defer tests and make this helper flaky.
🛠 Proposed fix
+ var usedMu sync.Mutex
used := make(map[string]bool)
if testCase.reportUnused {
t.Cleanup(func() {
+ usedMu.Lock()
+ defer usedMu.Unlock()
for key := range testCase.responses {
if !used[key] {
t.Logf("UNUSED MOCK [%s]: %s", testCase.expectedHost, key)
}
}
@@
if testCase.reportUnused {
+ usedMu.Lock()
used[string(gotBody)] = true
+ usedMu.Unlock()
}Also add sync to the imports.
Also applies to: 113-115
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@execution/engine/execution_engine_helpers_test.go` around lines 77 - 85, The
shared map used must be synchronized because RoundTrip may run concurrently: add
"sync" to imports, create a mutex (e.g., usedMu := &sync.Mutex{} or var usedMu
sync.Mutex) next to the used map, wrap any writes to used (the handler that
marks used[key] = true) with usedMu.Lock()/Unlock(), and also lock usedMu while
reading/iterating used in the t.Cleanup block that checks testCase.responses so
the read is protected; keep the same map and keys (used, testCase.responses) and
only add the mutex and locking/unlocking around accesses.
| leftId, _ := strconv.Atoi(d.StringValueContentString(leftDeferIdValue.Ref)) | ||
| rightId, _ := strconv.Atoi(d.StringValueContentString(rightDeferIdValue.Ref)) | ||
|
|
||
| // TODO: need to handle parent id too |
There was a problem hiding this comment.
Silently ignored strconv.Atoi errors and pending TODO.
The strconv.Atoi errors are discarded. If defer IDs are ever non-numeric strings, both will default to 0, causing incorrect merge behavior (both would be "equal"). Additionally, the TODO indicates parent ID handling is incomplete.
🛡️ Suggested defensive handling
- leftId, _ := strconv.Atoi(d.StringValueContentString(leftDeferIdValue.Ref))
- rightId, _ := strconv.Atoi(d.StringValueContentString(rightDeferIdValue.Ref))
+ leftIdStr := d.StringValueContentString(leftDeferIdValue.Ref)
+ rightIdStr := d.StringValueContentString(rightDeferIdValue.Ref)
+ leftId, leftErr := strconv.Atoi(leftIdStr)
+ rightId, rightErr := strconv.Atoi(rightIdStr)
+ if leftErr != nil || rightErr != nil {
+ // Fall back to string comparison if IDs are not numeric
+ if leftIdStr <= rightIdStr {
+ return // left wins or equal
+ }
+ // right wins
+ d.Fields[left].Directives.RemoveDirectiveByRef(leftDeferDirectiveRef)
+ d.Fields[left].Directives.Refs = append(d.Fields[left].Directives.Refs, rightDeferDirectiveRef)
+ return
+ }Note: The past review comments mentioned converting IDs to integers internally. If IDs are guaranteed to be numeric strings, consider documenting this invariant.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@v2/pkg/ast/ast_field.go` around lines 203 - 206, The code currently ignores
strconv.Atoi errors when converting leftDeferIdValue.Ref and
rightDeferIdValue.Ref into leftId and rightId (variables created in the snippet)
and leaves a TODO about parent id handling; update the conversion to check and
handle the errors from strconv.Atoi (e.g., propagate an error up from the
containing function or use a clear fallback and log) instead of silently
treating failures as 0, and implement or call existing logic to consider parent
IDs when comparing/merging defer IDs (update the comparison logic that uses
leftId/rightId to also incorporate parent ID fields or return an explicit error
if parent IDs are required but missing). Ensure you modify the function that
contains leftId/rightId so its signature and callers handle the new
error/behavior.
| "A unique identifier for the results." | ||
| label: String |
There was a problem hiding this comment.
Fix the label description.
The description on Line 4 describes id, not label. That makes internal schema/introspection output misleading.
🛠️ Proposed fix
- "A unique identifier for the results."
+ "A client-provided label for the deferred payload."
label: String📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "A unique identifier for the results." | |
| label: String | |
| "A client-provided label for the deferred payload." | |
| label: String |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@v2/pkg/asttransform/internal.graphql` around lines 4 - 5, The GraphQL field
description for the "label" field is incorrect (it currently reads "A unique
identifier for the results." which describes "id"); update the description for
the label field in the schema so it accurately describes its purpose (e.g., a
human-readable label or name for the result) by editing the string above the
label: String declaration to a correct sentence referencing "label" instead of
"id".
| func WithDefer() func(*testOptions) { | ||
| return func(o *testOptions) { | ||
| o.postProcessors = postProcessors | ||
| o.withDefer = true | ||
| } |
There was a problem hiding this comment.
Handle WithPrintPlan() for defer responses too.
Once WithDefer() is enabled, p.Plan(...) can return a *plan.DeferResponsePlan, but the withPrintPlan branch below still hard-asserts *plan.SynchronousResponsePlan. Any defer test that enables plan printing will panic before it reaches the assertion; a small type switch there would avoid that.
Also applies to: 213-215
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@v2/pkg/engine/datasourcetesting/datasourcetesting.go` around lines 42 - 45,
The withPrintPlan branch currently assumes p.Plan(...) returns
*plan.SynchronousResponsePlan and will panic when WithDefer() is set because
p.Plan may return *plan.DeferResponsePlan; update the branch that prints the
plan (and the similar logic around the other occurrence) to capture the result
of p.Plan(...) into a variable and use a type switch on its concrete type
(handling *plan.SynchronousResponsePlan and *plan.DeferResponsePlan) so both can
be printed safely; refer to WithDefer()/testOptions.withDefer, the p.Plan(...)
call, and the plan.SynchronousResponsePlan and plan.DeferResponsePlan types when
making the change.
| if f.items[parentIdx].deferInfo != nil && f.items[parentIdx].deferInfo.ID == f.items[i].deferInfo.ID { | ||
| // if parent item is in the same defer - | ||
| // we should not mark it as a defer parent, | ||
| // because defer parents are planned twice - in a deffered planner and regular | ||
| break | ||
| } | ||
|
|
||
| if slices.Contains(f.items[parentIdx].deferIDs, f.items[i].deferInfo.ID) { | ||
| // no need to update already contains this defer id | ||
| break |
There was a problem hiding this comment.
Don't stop the parent scan on the first non-updatable match.
treeNode.GetParent().GetData() can legitimately contain multiple duplicate suggestions for the same response node. If the first same-source parent is already in this defer scope or already has the ID, these breaks skip later same-source parents, so the actual ancestor path can miss deferParentPath / deferIDs.
💡 Minimal fix
if f.items[parentIdx].deferInfo != nil && f.items[parentIdx].deferInfo.ID == f.items[i].deferInfo.ID {
// if parent item is in the same defer -
// we should not mark it as a defer parent,
// because defer parents are planned twice - in a deffered planner and regular
- break
+ continue
}
if slices.Contains(f.items[parentIdx].deferIDs, f.items[i].deferInfo.ID) {
// no need to update already contains this defer id
- break
+ continue
} else {
parentIdToUpdate = parentIdx
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@v2/pkg/engine/plan/datasource_filter_node_suggestions.go` around lines 215 -
224, The loop that scans parent items (referencing f.items[parentIdx],
parentIdx, f.items[i].deferInfo.ID, deferInfo, and deferIDs) incorrectly uses
break when a parent is already in the same defer scope or already contains the
ID, which stops scanning further same-source parents and misses updating
deferParentPath/deferIDs; change those break statements so the loop skips only
the current parent and continues scanning remaining parents (use continue
semantics) and ensure when a parent is eligible you still append/update
deferParentPath and deferIDs for that parent rather than exiting the loop early.
| for _, fetch := range deferPlan.Response.Response.Fetches.ChildNodes { | ||
| deferID := fetch.Item.Fetch.Dependencies().DeferID |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how Process is called and if Fetches could be nil
rg -n "extractDeferFetches" --type go -A 5 -B 5Repository: wundergraph/graphql-go-tools
Length of output: 4781
🏁 Script executed:
#!/bin/bash
# Find the DeferResponsePlan type definition
rg -n "type DeferResponsePlan" --type go -A 20Repository: wundergraph/graphql-go-tools
Length of output: 1194
🏁 Script executed:
#!/bin/bash
# Check the Response type and Fetches field
rg -n "Response.*Response" v2/pkg/engine/postprocess/extract_defer_fetches.go --type go -A 2 -B 2Repository: wundergraph/graphql-go-tools
Length of output: 601
🏁 Script executed:
#!/bin/bash
# Look at the entire fetchGroups method to see if there are nil checks
sed -n '50,80p' v2/pkg/engine/postprocess/extract_defer_fetches.goRepository: wundergraph/graphql-go-tools
Length of output: 573
🏁 Script executed:
#!/bin/bash
# Find the plan package location and look for Response type
fd "plan" v2/pkg/engine/ --type d | head -1Repository: wundergraph/graphql-go-tools
Length of output: 91
🏁 Script executed:
#!/bin/bash
# Find GraphQLDeferResponse type definition and its Response field
rg -n "type GraphQLDeferResponse" --type go -A 15Repository: wundergraph/graphql-go-tools
Length of output: 1129
🏁 Script executed:
#!/bin/bash
# Look for where DeferResponsePlan is created or initialized
rg -n "DeferResponsePlan{" --type go -B 5 -A 10Repository: wundergraph/graphql-go-tools
Length of output: 171
🏁 Script executed:
#!/bin/bash
# Check if there's any initialization of Response.Fetches before calling Process
sed -n '17,48p' v2/pkg/engine/postprocess/extract_defer_fetches.goRepository: wundergraph/graphql-go-tools
Length of output: 912
🏁 Script executed:
#!/bin/bash
# Look at the context around line 194 in postprocess.go where Process is called
sed -n '180,210p' v2/pkg/engine/postprocess/postprocess.goRepository: wundergraph/graphql-go-tools
Length of output: 1250
🏁 Script executed:
#!/bin/bash
# Check if there are any nil checks for Fetches in the codebase before accessing it
rg -n "Fetches.*nil\|if.*Fetches\|Fetches.*==" --type go v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Look for where Response.Fetches is first initialized
rg -n "Response.Fetches\s*=" --type go -B 2 -A 2 v2/pkg/engine/postprocess/ v2/pkg/engine/resolve/Repository: wundergraph/graphql-go-tools
Length of output: 521
🏁 Script executed:
#!/bin/bash
# Look at createFetchTree to confirm it initializes Fetches
rg -n "func.*createFetchTree" --type go -A 20 v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 1595
🏁 Script executed:
#!/bin/bash
# Check the GraphQLResponse type to see if Fetches field is ever nullable
rg -n "type GraphQLResponse struct" --type go -A 20Repository: wundergraph/graphql-go-tools
Length of output: 1223
🏁 Script executed:
#!/bin/bash
# Double-check the access pattern - is processFlatFetchTree accessing Fetches safely?
rg -n "processFlatFetchTree" --type go -A 5 v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 2453
🏁 Script executed:
#!/bin/bash
# Get the full createFetchTree implementation to see where Fetches is assigned
sed -n '220,280p' v2/pkg/engine/postprocess/postprocess.goRepository: wundergraph/graphql-go-tools
Length of output: 1589
🏁 Script executed:
#!/bin/bash
# Check if processFlatFetchTree has nil safety checks
rg -n "func.*processFlatFetchTree\|func.*ProcessFetchTree" --type go -A 10 v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check if disableExtractFetches affects whether Fetches gets initialized
rg -n "disableExtractFetches" --type go -B 2 -A 2 v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 1807
🏁 Script executed:
#!/bin/bash
# Check what processFlatFetchTree does with the fetches parameter
rg -n "func.*dedupe.*ProcessFetchTree\|func.*ProcessFetchTree" --type go -A 5 v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Look for nil checks in the fetch tree processors
rg -n "if.*fetches.*nil\|if fetches ==" --type go v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check if there's an early guard when disableExtractFetches is true
sed -n '188,195p' v2/pkg/engine/postprocess/postprocess.goRepository: wundergraph/graphql-go-tools
Length of output: 384
🏁 Script executed:
#!/bin/bash
# Search for the dedupe processor implementation
rg -n "type dedupe struct" --type go -A 20Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Look for ProcessFetchTree implementations in other files
fd "*.go" v2/pkg/engine/postprocess/ | xargs grep -l "ProcessFetchTree" | head -5Repository: wundergraph/graphql-go-tools
Length of output: 355
🏁 Script executed:
#!/bin/bash
# Check the dedupe processor to see if it handles nil
rg -n "func.*dedupe.*ProcessFetchTree" --type go -A 15Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# See if there are any conditional checks before calling processFlatFetchTree
sed -n '180,200p' v2/pkg/engine/postprocess/postprocess.go | cat -nRepository: wundergraph/graphql-go-tools
Length of output: 1061
🏁 Script executed:
#!/bin/bash
# Find the dedupe and other processor implementations
rg -n "type.*Processors struct\|ProcessFetchTree" --type go v2/pkg/engine/postprocess/ | head -30Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Look for the implementation of all processor methods called in processFlatFetchTree
cat v2/pkg/engine/postprocess/postprocess.go | sed -n '39,50p'Repository: wundergraph/graphql-go-tools
Length of output: 797
🏁 Script executed:
#!/bin/bash
# Check if there's a test that shows the behavior with nil Fetches
fd --glob "*.go" -x grep -l "disableExtractFetches.*true\|Fetches.*nil" {} \; v2/pkg/engine/postprocess/ 2>/dev/nullRepository: wundergraph/graphql-go-tools
Length of output: 112
🏁 Script executed:
#!/bin/bash
# Look at what happens when the flag is disabled - search tests
rg -n "disableExtractFetches" --type go -B 3 -A 3Repository: wundergraph/graphql-go-tools
Length of output: 2505
🏁 Script executed:
#!/bin/bash
# Check the actual processors to see if they handle nil Fetches
rg -n "func.*dedupe.*ProcessFetchTree\|func.*appendFetchID.*ProcessFetchTree\|func.*resolveInputTemplates.*ProcessFetchTree" --type go -A 5 v2/pkg/engine/postprocess/ v2/pkg/engine/resolve/Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Look for the dedupe processor type and its ProcessFetchTree method
fd --glob "dedupe*.go" -x cat {} \; 2>/dev/null | head -100Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Search more broadly for ProcessFetchTree implementations
rg -n "ProcessFetchTree" --type go -B 3 -A 10 v2/pkg/engine/postprocess/ | head -80Repository: wundergraph/graphql-go-tools
Length of output: 6663
🏁 Script executed:
#!/bin/bash
# Check the traverseNode implementation to see if it handles nil
rg -n "func.*traverseNode" --type go -A 10 v2/pkg/engine/postprocess/resolve_input_templates.goRepository: wundergraph/graphql-go-tools
Length of output: 451
🏁 Script executed:
#!/bin/bash
# Look for the dedupe processor ProcessFetchTree implementation
rg -n "type dedupe\|func.*dedupe.*ProcessFetchTree" --type go -A 15 v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check if any of these processors have nil checks at the start
rg -n "if.*root.*nil\|if root ==" --type go v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check if there's a test case that passes nil to ProcessFetchTree
rg -n "ProcessFetchTree.*nil\|nil.*ProcessFetchTree" --type go v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check the dedupe processor
rg -n "type dedupe\|ProcessFetchTree" --type go -A 20 v2/pkg/engine/postprocess/ | grep -A 20 "type dedupe"Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Look for all ProcessFetchTree implementations and see which ones have nil checks
rg -n "func.*ProcessFetchTree" --type go -A 3 v2/pkg/engine/postprocess/Repository: wundergraph/graphql-go-tools
Length of output: 4357
🏁 Script executed:
#!/bin/bash
# Check if there's any guard in the main Process loop before calling processFlatFetchTree with nil Fetches
sed -n '183,210p' v2/pkg/engine/postprocess/postprocess.goRepository: wundergraph/graphql-go-tools
Length of output: 1123
🏁 Script executed:
#!/bin/bash
# Check what happens when disableExtractFetches is true - does Fetches remain nil?
# Look at the assign statement in createFetchTree
sed -n '218,260p' v2/pkg/engine/postprocess/postprocess.goRepository: wundergraph/graphql-go-tools
Length of output: 1256
🏁 Script executed:
#!/bin/bash
# Check if there's any nil guard in fetchGroups before accessing Fetches
sed -n '50,70p' v2/pkg/engine/postprocess/extract_defer_fetches.goRepository: wundergraph/graphql-go-tools
Length of output: 573
🏁 Script executed:
#!/bin/bash
# Look for any tests that verify behavior with disableExtractFetches=true
rg -n "disableExtractFetches.*true" --type go -B 5 -A 10Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check the relationship between disableExtractFetches and disableExtractDeferFetches
rg -n "disableExtractFetches\|disableExtractDeferFetches" --type go v2/pkg/engine/postprocess/postprocess.go | head -20Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Check if disableExtractFetches and disableExtractDeferFetches are the same or different flags
rg -n "type processorOptions\|disableExtract" --type go v2/pkg/engine/postprocess/postprocess.go -A 5Repository: wundergraph/graphql-go-tools
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Look at the full Process method to understand the control flow
sed -n '17,25p' v2/pkg/engine/postprocess/extract_defer_fetches.goRepository: wundergraph/graphql-go-tools
Length of output: 335
🏁 Script executed:
#!/bin/bash
# Check what disableExtractFetches controls in the Processor struct and how it relates to extractDeferFetches.disable
sed -n '60,180p' v2/pkg/engine/postprocess/postprocess.go | grep -A 5 -B 5 "disableExtract"Repository: wundergraph/graphql-go-tools
Length of output: 1459
🏁 Script executed:
#!/bin/bash
# Get the full initialization of Processor to see how extractDeferFetches.disable is set
sed -n '130,180p' v2/pkg/engine/postprocess/postprocess.goRepository: wundergraph/graphql-go-tools
Length of output: 1948
🏁 Script executed:
#!/bin/bash
# Check if extractDeferFetches has a nil check for Fetches
sed -n '13,65p' v2/pkg/engine/postprocess/extract_defer_fetches.goRepository: wundergraph/graphql-go-tools
Length of output: 1466
Add a nil safety check for Fetches in fetchGroups method.
If Fetches is not initialized (e.g., when disableExtractFetches is true), the iteration at line 53 would panic on a nil pointer dereference. Add a guard to handle the case where Fetches is nil:
Suggested fix
func (d *extractDeferFetches) fetchGroups(deferPlan *plan.DeferResponsePlan) (root []*resolve.FetchTreeNode, deffered map[string][]*resolve.FetchTreeNode) {
fetchGroups := make(map[string][]*resolve.FetchTreeNode)
if deferPlan.Response.Response.Fetches == nil {
return nil, fetchGroups
}
for _, fetch := range deferPlan.Response.Response.Fetches.ChildNodes {
deferID := fetch.Item.Fetch.Dependencies().DeferID
if deferID == "" {
root = append(root, fetch)
continue
}
fetchGroups[deferID] = append(fetchGroups[deferID], fetch)
}
return root, fetchGroups
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@v2/pkg/engine/postprocess/extract_defer_fetches.go` around lines 53 - 54, The
fetchGroups method in extractDeferFetches may iterate a nil Fetches and panic;
add a nil-check for deferPlan.Response.Response.Fetches at the start of
extractDeferFetches.fetchGroups (before the for loop over Fetches.ChildNodes)
and return nil for root and an empty map for fetchGroups when Fetches is nil;
keep the rest of the logic unchanged (use the existing symbols
deferPlan.Response.Response.Fetches, Fetches.ChildNodes, and fetchGroups map).
| type FetchDependencies struct { | ||
| FetchID int | ||
| DependsOnFetchIDs []int | ||
| DeferID string |
There was a problem hiding this comment.
Include DeferID in FetchItem.EqualSingleFetch().
DeferID changes when a fetch runs, so two otherwise-identical SingleFetch items are no longer equivalent across defer boundaries. Leaving equality unchanged can merge initial and deferred fetches into the same bucket.
🛠️ Proposed fix
func (f *FetchItem) EqualSingleFetch(other *FetchItem) bool {
+ if f.Fetch.Dependencies().DeferID != other.Fetch.Dependencies().DeferID {
+ return false
+ }
+
if len(f.FetchPath) != len(other.FetchPath) {
return false
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@v2/pkg/engine/resolve/fetch.go` around lines 110 - 113, The equality check in
EqualSingleFetch must include the DeferID from FetchDependencies so fetches
across different defer boundaries aren't treated as equivalent; update the
EqualSingleFetch implementation (the method that compares two
SingleFetch/FetchItem objects) to compare the DeferID field (e.g.,
item.FetchDeps.DeferID vs other.FetchDeps.DeferID), handling nil/empty
dependency structs safely, so that differing DeferID values make the items
unequal and prevent merging initial and deferred fetches.
| type DeferField struct { | ||
| DeferID string | ||
| } |
There was a problem hiding this comment.
Deep-copy DeferField in Field.Copy().
DeferField now carries state. Keeping Defer: f.Defer makes copied response trees share the same defer metadata pointer.
🛠️ Proposed fix
func (f *Field) Copy() *Field {
+ var deferField *DeferField
+ if f.Defer != nil {
+ copied := *f.Defer
+ deferField = &copied
+ }
+
return &Field{
Name: f.Name,
Value: f.Value.Copy(),
Position: f.Position,
- Defer: f.Defer,
+ Defer: deferField,
Stream: f.Stream,
OnTypeNames: f.OnTypeNames,
Info: f.Info,
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@v2/pkg/engine/resolve/node_object.go` around lines 182 - 184, Field.Copy()
currently assigns the DeferField by reference (Defer: f.Defer) causing copied
trees to share the same DeferField pointer; change Field.Copy() to allocate a
new DeferField and copy its contents (e.g., create &DeferField{DeferID:
f.Defer.DeferID} when f.Defer != nil) and assign that to the copied Field's
Defer so each copy gets an independent DeferField instance.
| if r.deferMode && !r.hasErrors() { | ||
| r.printHasNext(true) | ||
| } |
There was a problem hiding this comment.
Don't let the accumulated error buffer terminate incremental delivery.
hasNext is derived from r.hasErrors(), and ResolveDefer reuses the same r.errors buffer across chunks. After the first non-fatal error, later patches can repeat old errors and emit hasNext:false even when more deferred payloads still exist.
Also applies to: 266-306
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@v2/pkg/engine/resolve/resolvable.go` around lines 257 - 259, The deferred
delivery termination is incorrectly driven by r.hasErrors() (which reuses
r.errors across chunks) so repeated old errors can make hasNext false; update
ResolveDefer to stop using r.hasErrors() for hasNext and instead derive the
"more deferred payloads exist" signal from the deferred-work state (e.g., a new
per-chunk flag or a check on the deferred queue/iterator like
r.deferQueue/remainingCount or a helper r.hasMoreDeferred()) and call
r.printHasNext(true) when that derived condition is true; ensure r.errors is not
used to decide continuation across chunks (or clear per-chunk errors) and apply
the same change to the other occurrences in the 266-306 range.
| if !ctx.ExecutionOptions.SkipLoader { | ||
| t.loader.Init(ctx, response.Response.Info, t.resolvable) | ||
|
|
||
| // fetch initial response | ||
| if err := t.loader.ResolveFetchNode(response.Response.Fetches); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| t.resolvable.deferMode = true | ||
| t.resolvable.deferID = "" | ||
|
|
||
| // render initial response | ||
| err = t.resolvable.Resolve(ctx.ctx, response.Response.Data, response.Response.Fetches, writer) |
There was a problem hiding this comment.
Render defer responses even when SkipLoader is enabled.
All output generation currently sits behind if !ctx.ExecutionOptions.SkipLoader, so this method returns without writing even the initial payload in skip-loader mode. That diverges from ResolveGraphQLResponse and the subscription paths, which still render the response shape without fetching.
Suggested fix
- if !ctx.ExecutionOptions.SkipLoader {
- t.loader.Init(ctx, response.Response.Info, t.resolvable)
-
- // fetch initial response
- if err := t.loader.ResolveFetchNode(response.Response.Fetches); err != nil {
- return nil, err
- }
-
- t.resolvable.deferMode = true
- t.resolvable.deferID = ""
-
- // render initial response
- err = t.resolvable.Resolve(ctx.ctx, response.Response.Data, response.Response.Fetches, writer)
+ if !ctx.ExecutionOptions.SkipLoader {
+ t.loader.Init(ctx, response.Response.Info, t.resolvable)
+ if err := t.loader.ResolveFetchNode(response.Response.Fetches); err != nil {
+ return nil, err
+ }
+ }
+
+ t.resolvable.deferMode = true
+ t.resolvable.deferID = ""
+
+ // render initial response
+ err = t.resolvable.Resolve(ctx.ctx, response.Response.Data, response.Response.Fetches, writer)
if err != nil {
return nil, err
}
@@
- for i, deferGroup := range response.Defers {
- if err := t.loader.ResolveFetchNode(deferGroup.Fetches); err != nil {
- return nil, err
- }
+ for i, deferGroup := range response.Defers {
+ if !ctx.ExecutionOptions.SkipLoader {
+ if err := t.loader.ResolveFetchNode(deferGroup.Fetches); err != nil {
+ return nil, err
+ }
+ }
- t.resolvable.deferID = deferGroup.DeferID
+ t.resolvable.deferID = deferGroup.DeferID
@@
- writer.Complete()
- }
+ writer.Complete()Also applies to: 482-491, 508-511
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@v2/pkg/engine/resolve/resolve.go` around lines 456 - 468, Currently all
rendering is gated by ctx.ExecutionOptions.SkipLoader; change control flow so
loader initialization and ResolveFetchNode are only skipped when SkipLoader is
true, but t.resolvable.Resolve is always called to render the initial response
shape. Concretely, keep the t.loader.Init(...) and the ResolveFetchNode(...)
calls inside an if !ctx.ExecutionOptions.SkipLoader branch, but move the lines
that set t.resolvable.deferMode, t.resolvable.deferID and the call to
t.resolvable.Resolve(...) outside that branch so they execute regardless of
SkipLoader; apply the same refactor wherever similar gating appears (the blocks
around t.loader.Init / ResolveFetchNode and t.resolvable.Resolve noted in this
file).
| // when the deffered field is on the entity and the parent field is on the same datasource | ||
| // we won't have hasRequiresKey set. | ||
| // but in case this field has requires directive it will be resolved by entity call, | ||
| // and it will have requires key configuration | ||
| if !hasRequiresKey && fieldRequirementsConfigs != nil { |
There was a problem hiding this comment.
// When the deferred field is on the entity, and the parent field is on the same datasource, hasRequiresKey will be false.
// But if this field has the "requires" directive, it will be resolved by entity call,
// and it will have the "requires" key configuration.| if f.items[i].IsRootNode && hasRequiresKey || hasKeyDependency { | ||
| return | ||
| } | ||
|
|
There was a problem hiding this comment.
double checking, did you mean this?
if (f.items[i].IsRootNode && hasRequiresKey) || hasKeyDependency {| // if parent item is in the same defer - | ||
| // we should not mark it as a defer parent, | ||
| // because defer parents are planned twice - in a deffered planner and regular | ||
| break |
| possibleTypeNames []string | ||
|
|
||
| deferInfo *DeferInfo | ||
| deferParentPath bool |
There was a problem hiding this comment.
does this variable signal this "include me in the deferred planner
as a structural ancestor" ? If yes, then maybe we could rename it?
There was a problem hiding this comment.
is it possible that this deferParentPath var is set to true when len(deferIDs) == 0? if not possible, then it is a redundant variable.
There was a problem hiding this comment.
Ok, I think this var can be derived from len(deferIDs)
|
|
||
| deferInfo *DeferInfo | ||
| deferParentPath bool | ||
| deferIDs []string |
There was a problem hiding this comment.
kind of vague name. I think, this variable means the defer IDs of descendant deferred fields that flow through this node. Maybe descendantDeferIDs or childDeferIDs ?
| treeNodeId uint | ||
| possibleTypeNames []string | ||
|
|
||
| deferInfo *DeferInfo |
There was a problem hiding this comment.
maybe ?
ownDeferInfo *DeferInfo // this node's own defer directive| current = parentIdToUpdate | ||
| } | ||
|
|
||
| for _, parentIdx := range parentIndexesToAddDeferID { |
There was a problem hiding this comment.
why do you collect them first and mark&add later?
| currentFields []objectFields | ||
| currentObjectFields []objectFields | ||
| currentField *resolve.Field | ||
| currentFields []*resolve.Field |
There was a problem hiding this comment.
what about fieldAncestors or fieldStack? the same is true for currentObjectFields that looks like a stack containing objectFields of ancestors.
| *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 |
There was a problem hiding this comment.
this and previous comments are like redundant.
| // 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{ |
There was a problem hiding this comment.
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) | |||
There was a problem hiding this comment.
this field is not used anymore
| return false | ||
| } | ||
|
|
||
| return slices.ContainsFunc(treeNodeChildren(node), func(child int) bool { |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
// 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.| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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 "" | ||
| } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
You could use Document.FieldInternalDeferID from ast instead
| } | ||
|
|
||
| func (v *requiredFieldsVisitor) isRootLevel() bool { | ||
| return len(v.OperationNodes) == 1 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
noticed, that this check can be simplified to
if !bytes.Equal(fieldName, typeNameFieldBytes) || v.config.isTypeNameForEntityInterface {
Defer Support Implementation
This branch adds end-to-end support for the @defer directive in GraphQL execution.
pkg/astnormalization
new rules
updated rules
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
plan.goDeferResponsePlantype andDeferResponsePlanKindconstantplanner_configuration.gopathConfigurationgetsdeferID; path index stores full config instead of empty struct; newDeferID()/PathWithFieldRef()accessorspath_builder_visitor.gocurrentFieldInfostruct instead of passing separate arguments;objectFetchConfigurationnow havedeferIDnode_selection_visitor.godeferInfo/parentFieldDeferID; deduplication key is now per defer scoperequired_fields_visitor.go@__defer_internalannotations to the fields to put them into the correct scope; newresolveDeferredAlias()helper is added to create proper defer aliasdatasource_filter_node_suggestions.goNodeSuggestiongetsDeferInfo; newProcessDefer()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 pointdatasource_filter_collect_nodes_visitor.go@__defer_internalduring tree building to populateDeferInfoon suggestionsabstract_selection_rewriter*.gotypeNameSelection()takes adeferIDto give an injected__typenamea proper scopenode_selection_builder.goProcessDefer()after process if node selection is finished - to propagate defer ids to parentsvisitor.goDeferResponsePlan; misc cleanupanalyze_plan_kind.gopkg/postprocess
postprocess.go
new processor extract_defer_fetches.go
Splits a flat DeferResponsePlan fetch tree into two buckets:
pkg/engine/postprocess
postprocess.go
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
loader.go
resolvable.go
resolve.go
New entry point ResolveGraphQLDeferResponse() is added:
Integration tests
closes ENG-8799
closes ENG-7978
closes ENG-7976
Checklist