Skip to content

Conversation

ysmolski
Copy link
Contributor

@ysmolski ysmolski commented Aug 21, 2025

The PropagateFetchReasons option of Resolver enables router to send to subgraphs an extension that includes reasons why fields where included in the request.

This data is sent only for the fields marked with @openfed__requireFetchReason in the schema.
Directive is implemented in the router repo.

Router part: wundergraph/cosmo#2160

Summary by CodeRabbit

  • New Features

    • Opt-in per-fetch "fetch reasons" metadata (which fields were requested, why, and by which subgraphs) with public accessors and a toggle to propagate that metadata to upstream subgraphs.
  • Improvements

    • More deterministic planning via stabilized ordering and inverse-dependency tracking.
    • Indexing now recognizes fields carrying fetch-reason semantics.
    • Loader can inject fetch-reasons into upstream requests when enabled.
  • Tests

    • New and updated tests validating collection and propagation of fetch-reason metadata.
  • Documentation

    • Clarified planner configuration comments.
  • Chores

    • Minor benchmark/formatting tweaks.

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.

This option of Resolver enables router to send to subgraphs
an extension that includes reasons why fields where included
in the request.

The name of fields in the extension are open for discussion.
Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

Propagates per-field "fetch reasons" through planner and resolve layers, indexes fetch-reason fields in data-source metadata, computes inverse field-ref dependants, surfaces per-fetch reasons on concrete fetch types and public APIs, and optionally injects extensions.fetch_reasons into upstream subgraph requests; tests updated.

Changes

Cohort / File(s) Summary
Execution engine tests & federation wiring
`execution/engine/execution_engine_test.go`
Adds test flag/helpers to exercise propagation of per-field fetch reasons to subgraphs; asserts outgoing requests include extensions.fetch_reasons when enabled and that FetchReasonFields are set on relevant TypeFields.
Resolve public API, fetch types & loader
`v2/pkg/engine/resolve/fetch.go`, `v2/pkg/engine/resolve/loader.go`, `v2/pkg/engine/resolve/resolve.go`
Adds public FetchReason type and FetchReasons() accessors; adds FieldFetchReasons []FetchReason and CoordinateDependencies []FetchDependency to concrete fetch structs; adds PropagateFetchReasons option and loader flag to inject body.extensions.fetch_reasons into non-introspection subgraph requests.
Planner: inverse mapping and visitor changes
`v2/pkg/engine/plan/planner.go`, `v2/pkg/engine/plan/visitor.go`
Computes inverse field-ref dependants (inverseMapfieldRefDependants), moves coordinate dependency wiring to top-level SingleFetch, adds per-fetch FieldFetchReasons, and implements Visitor.buildFetchReasons / skipField with deduplication and deterministic sorting.
Postprocess: concrete fetch types
`v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go`
Propagates SingleFetch.FieldFetchReasons into concrete fetch structs (BatchEntityFetch, EntityFetch).
Plan data-source metadata & TypeField changes
`v2/pkg/engine/plan/datasource_configuration.go`, `v2/pkg/engine/plan/type_field.go`
Adds FetchReasonFields []string to TypeField; indexes fetchReasonFields for RootNodes/ChildNodes; exposes DataSourceMetadata.RequiresFetchReason(typeName, fieldName) bool and updates NodesInfo; removes some legacy TypeFields helper methods.
Federation datasource tests
`v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go`
Updates fixtures and assertions to include per-fetch FieldFetchReasons and FetchReasonFields, asserting subgraph lists and flags (IsRequires, IsKey, ByUser) across fetch sequences.
Planner docs & deterministic ordering
`v2/pkg/engine/plan/configuration.go`, `v2/pkg/engine/plan/planner.go`
Adds documentation comments to configuration fields and introduces sorting/normalization for inverse dependency maps to ensure deterministic ordering.
Federation metadata receiver change
`v2/pkg/engine/plan/federation_metadata.go`
Changes FederationFieldConfiguration.String() to a pointer receiver.
Misc (bench/test formatting)
`.../execution/...`
Minor non-functional benchmark/formatting adjustments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v2/pkg/engine/plan/visitor.go (1)

1398-1461: Bug: user-requested fields can be dropped when requestedBy exists but resolves to no subgraphs

If fieldRefRequestedBy[fieldRef] exists but yields no BySubgraphs (e.g., nil ObjectFetchConfiguration or unmapped planners), the function currently doesn’t append the record—even when ByUser is true. That loses attribution for genuinely user-requested fields.

Fix: append when requested.ByUser OR there are any BySubgraphs.

Also, avoid shadowing the fetchID parameter inside the inner loop and defensively handle missing TypeName.

 for _, fieldRef := range fields {
   fieldName := v.Operation.FieldNameString(fieldRef)
   if fieldName == "__typename" {
     continue
   }
   requested := resolve.RequestedField{
-    TypeName:  v.fieldEnclosingTypeNames[fieldRef],
+    TypeName:  v.fieldEnclosingTypeNames[fieldRef],
     FieldName: fieldName,
   }
   requested.ByUser = !v.skipField(fieldRef)

   requestedByRefs, ok := v.fieldRefRequestedBy[fieldRef]
   if !ok {
     if requested.ByUser {
       result = append(result, requested)
     }
     continue
   }

   requested.BySubgraphs = make([]string, 0, len(requestedByRefs))
   for _, reqByRef := range requestedByRefs {
-    depFieldPlanners, ok := v.fieldPlanners[reqByRef]
+    depFieldPlanners, ok := v.fieldPlanners[reqByRef]
     if !ok {
       continue
     }

     // find the subgraph's name by iterating over planners that are responsible for reqByRef
-    for _, fetchID := range depFieldPlanners {
-      ofc := v.planners[fetchID].ObjectFetchConfiguration()
+    for _, plannerID := range depFieldPlanners {
+      ofc := v.planners[plannerID].ObjectFetchConfiguration()
       if ofc == nil {
         continue
       }
       requested.BySubgraphs = append(requested.BySubgraphs, ofc.sourceName)

-      depKind, ok := v.fieldDependencyKind[fieldDependencyKey{field: reqByRef, dependsOn: fieldRef}]
+      depKind, ok := v.fieldDependencyKind[fieldDependencyKey{field: reqByRef, dependsOn: fieldRef}]
       if !ok {
         continue
       }
       switch depKind {
       case fieldDependencyKindKey:
         requested.ReasonIsKey = true
       case fieldDependencyKindRequires:
         requested.ReasonIsRequires = true
       }
     }
   }
-  if len(requested.BySubgraphs) > 0 {
+  if requested.ByUser || len(requested.BySubgraphs) > 0 {
     slices.Sort(requested.BySubgraphs)
     requested.BySubgraphs = slices.Compact(requested.BySubgraphs)
     result = append(result, requested)
   }
 }

Optional: if TypeName might be missing (shouldn’t, but defensive), consider skipping such entries or defaulting with a clear sentinel; otherwise downstream JSON may contain empty typeName.

🧹 Nitpick comments (15)
execution/engine/config_factory_federation.go (1)

309-314: Stale commented code references undefined symbol; clarify intent or remove.

The commented-out FetchURLRules line references routerEngineConfig.Headers which isn't in scope here (likely meant routerConfig.EngineConfig.Headers). If you plan to re-enable this soon, fix the symbol and keep a precise TODO; otherwise, delete the block to avoid confusion.

Apply this minimal cleanup to keep the comment accurate until the code is enabled:

-	// dataSourceRules := FetchURLRules(&routerEngineConfig.Headers, routerConfig.BySubgraphs, subscriptionUrl)
+	// TODO(eng-7769): enable once header rules are wired. Likely:
+	// dataSourceRules := FetchURLRules(&routerConfig.EngineConfig.Headers, routerConfig.BySubgraphs, subscriptionUrl)
v2/pkg/engine/plan/configuration.go (1)

30-35: Go doc comments should start with the field name.

To keep golint/godoc happy and improve clarity, start doc comments with the exported field names and tighten phrasing.

Apply:

-	// Tests can suppress generating resolve.FieldInfo in the planner.
-	DisableIncludeInfo bool
+	// DisableIncludeInfo controls whether the planner generates resolve.FieldInfo (useful in tests).
+	DisableIncludeInfo bool

-	// Test can suppress generating field dependencies structures in the planner.
-	DisableIncludeFieldDependencies bool
+	// DisableIncludeFieldDependencies controls whether the planner generates field dependency structures (useful in tests).
+	DisableIncludeFieldDependencies bool
v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go (1)

77-112: Propagating FieldsRequestedBy to BatchEntityFetch looks correct; confirm immutability.

You pass through fetch.FieldsRequestedBy as-is. If the source slice is treated as immutable after planning (expected), this is fine. If anything mutates it later, consider cloning to avoid aliasing.

Would you like a follow-up patch that defensively clones this slice during post-processing?

v2/pkg/engine/resolve/loader.go (2)

1565-1581: Skip propagation for introspection and keep extensions collision-safe.

Two small improvements:

  • Don’t attach fieldsRequestedBy to introspection requests; it’s noise and could confuse tooling.
  • You already assume body.extensions is an object; jsonparser.Set will error if it’s incompatible. Current error handling is fine, but tightening the condition keeps input shape predictable.

Here’s a focused tweak to guard introspection without changing behavior elsewhere.

Apply:

-	if l.propagateFieldsRequestedBy {
+	if l.propagateFieldsRequestedBy && !IsIntrospectionDataSource(res.ds.ID) {
 		fieldsRequestedBy := fetchItem.Fetch.RequestedFields()
 		if fieldsRequestedBy != nil {
 			// We expect that body.extensions is an object
 			var encoded []byte
 			encoded, res.err = json.Marshal(fieldsRequestedBy)
 			if res.err != nil {
 				res.err = errors.WithStack(res.err)
 				return
 			}
 			input, res.err = jsonparser.Set(input, encoded, "body", "extensions", "fieldsRequestedBy")
 			if res.err != nil {
 				res.err = errors.WithStack(res.err)
 				return
 			}
 		}
 	}

1565-1581: Consider namespacing the extension key to prevent collisions.

If subgraphs or clients use a similarly named field, collisions are possible. Consider a vendor-prefixed key like wgFieldsRequestedBy or a namespaced object extensions.wg.fieldsRequestedBy.

v2/pkg/engine/plan/planner.go (1)

231-239: Make inverseMap deterministic to avoid flaky tests.

Map iteration order is random; sorting the resulting slices gives stable outputs for debugging/tests.

Apply:

 func inverseMap(m map[int][]int) map[int][]int {
 	inverse := make(map[int][]int)
 	for k, v := range m {
 		for _, v2 := range v {
 			inverse[v2] = append(inverse[v2], k)
 		}
 	}
-	return inverse
+	// Normalize ordering for deterministic plans/tests
+	for key := range inverse {
+		sort.Ints(inverse[key])
+	}
+	return inverse
 }

And add the import:

// at top of file
import (
	"fmt"
	"strings"
	"sort"
	...
)
execution/engine/execution_engine_test.go (4)

218-219: Add a helper option for symmetry and reuse (nit).

You added propagateFieldsRequestedBy to the internal test options. To keep the test harness consistent with withValueCompletion() and reduce inline closures, add a small helper:

func withPropagateFieldsRequestedBy() executionTestOptions {
	return func(options *_executionTestOptions) {
		options.propagateFieldsRequestedBy = true
	}
}

827-876: Stability of JSON body assertion for fieldsRequestedBy.

The test asserts a byte-for-byte body string including the extensions.fieldsRequestedBy array. If the upstream computation ever changes the order in that slice, this becomes flaky. Consider making the round-tripper support JSON assertions (e.g., decode and compare specific fields ignoring array element order) or build the expected body via json.Marshal on a struct to guarantee stable key ordering.

Apply this diff to reuse a helper option (from my other comment) and avoid the inline closure:

-        func(eto *_executionTestOptions) {
-            eto.propagateFieldsRequestedBy = true
-        },
+        withPropagateFieldsRequestedBy(),

4347-4470: Reduce duplication in fieldsRequestedBy and centralize expected body construction.

Two separate entries for the same coordinate (User.id) carry different reasons (requestedBySubgraphs+reasonIsKey vs requestedByUser). Consider deduplicating per (typeName, fieldName) and merging reasons into a single object before emitting. This will shrink payload size and simplify consumption by subgraphs.

Additionally, the long raw JSON strings are brittle. A small local builder that marshals:

  • query string
  • optional variables
  • extensions.fieldsRequestedBy (slice of structs)

would improve maintainability and cut quoting mistakes. Because tests elsewhere already assert exact JSON bodies, using structs (not maps) will keep key order deterministic.

Example local type to drive json.Marshal deterministically:

type expectedBody struct {
	Query      string                 `json:"query"`
	Variables  any                    `json:"variables,omitempty"`
	Extensions struct {
		FieldsRequestedBy []struct {
			TypeName              string   `json:"typeName"`
			FieldName             string   `json:"fieldName"`
			RequestedByUser       bool     `json:"requestedByUser,omitempty"`
			RequestedBySubgraphs  []string `json:"requestedBySubgraphs,omitempty"`
			ReasonIsKey           bool     `json:"reasonIsKey,omitempty"`
		} `json:"fieldsRequestedBy,omitempty"`
	} `json:"extensions,omitempty"`
}

4537-4539: Use the helper option instead of an inline closure (nit).

Same suggestion as above for consistency.

-            func(eto *_executionTestOptions) {
-                eto.propagateFieldsRequestedBy = true
-            },
+            withPropagateFieldsRequestedBy(),
v2/pkg/engine/plan/visitor.go (2)

49-49: New inverse map is fine, but document/guarantee producer semantics

Adding fieldRefRequestedBy (inverse of fieldRefDependsOnFieldRefs) is appropriate. Please ensure its population (likely in planner.go) is always done before configureFetch runs, and consider adding a brief doc on whether refs included here are only direct dependents or also transitive. This affects expectations for buildFieldsRequestedBy.


636-646: Helper extraction improves clarity; consider naming and fast-path

skipField helper is clear and correctly preserves previous semantics. Minor: if skipFieldsRefs can grow, consider a small set/map for O(1) checks, or keep as-is if the list is tiny.

- for _, skipRef := range v.skipFieldsRefs {
-   if skipRef == ref { return true }
- }
+ // If this grows, switch to map[int]struct{} for O(1).
+ for _, skipRef := range v.skipFieldsRefs {
+   if skipRef == ref { return true }
+ }
v2/pkg/engine/resolve/fetch.go (2)

416-427: Tighten docstring grammar and naming consistency

Minor grammar nits and align wording with the JSON tags (“requestedBySubgraphs”, “requestedByUser”).

-// RequestedField contains an answer about who requested a specific TypeName, FieldName combination.
-// It can be requested by set Subgraphs and by a User.
+// RequestedField explains who requested a specific (typeName, fieldName) combination.
+// A field can be requested by the user and/or by one or more subgraphs, with optional reasons.

350-354: Surface looks good; be explicit about equality semantics

FetchConfiguration now carries FieldsRequestedBy. Note that Equals() does not compare this field (nor CoordinateDependencies). If tests or tooling need to assert exact propagation of requested fields, rely on explicit checks rather than Equals(). Consider adding a comment near Equals() to make this intentional.

 func (fc *FetchConfiguration) Equals(other *FetchConfiguration) bool {
+  // Note: FieldsRequestedBy and CoordinateDependencies are intentionally excluded from equality.
+  // They carry diagnostic/metadata that doesn't affect fetch identity.
v2/pkg/engine/resolve/resolve.go (1)

168-171: Option name and docs LGTM; clarify exposure scope

PropagateFieldsRequestedBy is clear. Consider clarifying in the comment that this only propagates to upstream subgraphs (extensions) and is not exposed to clients unless separately surfaced, to avoid confusion about information leakage.

-// PropagateFieldsRequestedBy enables sending to upstream subgraphs the "fieldsRequestedBy"
-// extension that contains information why each field was requested.
+// PropagateFieldsRequestedBy enables sending to upstream subgraphs (only) the "fieldsRequestedBy"
+// extension that explains why each field was requested. This flag does not expose the data to clients.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c7ddbbe and 57e87ab.

📒 Files selected for processing (10)
  • execution/engine/config_factory_federation.go (1 hunks)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (4 hunks)
  • v2/pkg/engine/plan/configuration.go (1 hunks)
  • v2/pkg/engine/plan/planner.go (2 hunks)
  • v2/pkg/engine/plan/visitor.go (5 hunks)
  • v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go (2 hunks)
  • v2/pkg/engine/resolve/fetch.go (11 hunks)
  • v2/pkg/engine/resolve/loader.go (2 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
v2/pkg/engine/resolve/loader.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • Fetch (20-26)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • RequestedField (419-426)
v2/pkg/engine/plan/visitor.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • RequestedField (419-426)
v2/pkg/engine/resolve/resolve.go (1)
v2/pkg/engine/resolve/resolvable.go (1)
  • ResolvableOptions (62-67)
execution/engine/execution_engine_test.go (4)
v2/pkg/starwars/starwars.go (2)
  • FileSimpleHeroQuery (13-13)
  • Schema (55-59)
v2/pkg/engine/plan/datasource_configuration.go (1)
  • DataSourceMetadata (34-56)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-354)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
🔇 Additional comments (17)
v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go (1)

124-147: Same propagation for EntityFetch; mirror the immutability consideration.

Same note as above; the propagation is correct. Just ensure upstream treats FieldsRequestedBy as read-only post plan.

v2/pkg/engine/resolve/loader.go (1)

152-155: New Loader flag is well-scoped.

Adding propagateFieldsRequestedBy behind a boolean is the right safety valve. Ensure it’s plumbed from ResolverOptions.PropagateFieldsRequestedBy in newTools so tests can toggle it.

v2/pkg/engine/plan/planner.go (1)

156-157: Wiring inverse dependencies is a good call; watch for nondeterministic order.

Deriving fieldRefRequestedBy from fieldRefDependsOn is correct. Note that map iteration in Go is randomized; if ordering matters downstream (tests, deterministic plans), ensure consumers sort.

execution/engine/execution_engine_test.go (2)

4505-4541: Federation scenario with fieldsRequestedBy propagation — good coverage.

This mirrors the non-extension path, verifies both subgraphs see the extension, and exercises key-based entity fetch reasoning. Nicely scoped and readable.


266-267: All good: PropagateFieldsRequestedBy gating confirmed

A ripgrep search shows that:

  • PropagateFieldsRequestedBy is only added to ResolverOptions in resolve.go and correctly passed through to the engine.
  • In loader.go, the "fieldsRequestedBy" extension is set on the request only within the if l.propagateFieldsRequestedBy { … } block, with no occurrences elsewhere.

No further action needed.

v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (4)

3183-3201: LGTM: requires for country/city are accurately attributed.

Marking Address.country and Address.city as requires from address.service, while preserving Account.address as user-requested, matches the dependency chain. No issues spotted.


3295-3313: LGTM: downstream requires on line3/zip are precise.

Both fields are correctly attributed to account.service via requires, and the enclosing Account.address remains user-requested. This aligns with the later fullAddress resolution.


3420-3431: LGTM: user-requested fullAddress attribution is clear.

Capturing Address.fullAddress and Account.address as ByUser in the final fetch is appropriate and keeps the provenance readable.


3097-3135: No helper needed: BySubgraphs is already deterministically sorted

The test’s expected slice literal ([]string{"account.service", "address-enricher.service", "address.service"}) already matches the alphabetical order enforced in the production code, so there’s no risk of flakiness here.

• In v2/pkg/engine/plan/visitor.go (lines 1453–1454), the code calls

slices.Sort(requested.BySubgraphs)
requested.BySubgraphs = slices.Compact(requested.BySubgraphs)

which guarantees that BySubgraphs is sorted and deduplicated before being returned.

You can safely leave the test as-is.

v2/pkg/engine/plan/visitor.go (2)

58-66: Good: tracking maps for “who planned what” and enclosing types

plannerFields, fieldPlanners, and fieldEnclosingTypeNames are the right primitives for later attribution. Nice that they’re initialized in EnterDocument.


1335-1338: Plumbing LGTM; relies on promoted fields via embedding

Assigning CoordinateDependencies and FieldsRequestedBy on singleFetch works via field promotion (FetchConfiguration is embedded). Gated by DisableIncludeFieldDependencies, which is consistent with where the underlying maps are populated.

v2/pkg/engine/resolve/fetch.go (5)

25-26: Interface extension is coherent

Adding RequestedFields() to Fetch cleanly exposes the new metadata without leaking struct details.


108-111: SingleFetch delegation is correct

Returning FetchConfiguration.FieldsRequestedBy via promotion is appropriate and keeps SingleFetch lean.


180-181: BatchEntityFetch wiring is consistent

Storing FieldsRequestedBy directly on BatchEntityFetch and exposing via RequestedFields() matches the post-processing pattern used elsewhere.

Please confirm create_concrete_single_fetch_types.go copies FetchConfiguration.FieldsRequestedBy from SingleFetch to BatchEntityFetch during post-processing.

Also applies to: 191-194


232-234: EntityFetch wiring is consistent

CoordinateDependencies and FieldsRequestedBy are exposed consistently.

Double-check post-processing populates EntityFetch.FieldsRequestedBy and preserves CoordinateDependencies.

Also applies to: 244-247


283-286: ParallelListItemFetch delegation is correct

Delegating to the underlying SingleFetch configuration makes sense here.

v2/pkg/engine/resolve/resolve.go (1)

253-254: Good: loader gets the new flag

Passing propagateFieldsRequestedBy into Loader completes the plumbing.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
v2/pkg/engine/plan/visitor.go (2)

1399-1461: Tighten semantics: exclude “self” and stabilize output order for FieldsRequestedBy

Two suggestions:

  • Exclude the current fetch’s planner/subgraph from BySubgraphs to avoid self-referencing noise.
  • Sort the resulting slice deterministically (by TypeName, FieldName) for stable tests/traces.

Apply this diff:

 func (v *Visitor) buildFieldsRequestedBy(fetchID int) []resolve.RequestedField {
   fields, ok := v.plannerFields[fetchID]
   if !ok {
     return nil
   }
   result := make([]resolve.RequestedField, 0, len(fields))

+  // Identify current subgraph for this fetch (to omit self)
+  currentSubgraphName := ""
+  if ofc := v.planners[fetchID].ObjectFetchConfiguration(); ofc != nil {
+    currentSubgraphName = ofc.sourceName
+  }
+
   for _, fieldRef := range fields {
     fieldName := v.Operation.FieldNameString(fieldRef)
     if fieldName == "__typename" {
       continue
     }
     requested := resolve.RequestedField{
       TypeName:  v.fieldEnclosingTypeNames[fieldRef],
       FieldName: fieldName,
     }
     requested.ByUser = !v.skipField(fieldRef)

     requestedByRefs, ok := v.fieldRefAllowsFieldRefs[fieldRef]
     if !ok {
       if requested.ByUser {
         result = append(result, requested)
       }
       continue
     }

     requested.BySubgraphs = make([]string, 0, len(requestedByRefs))
     for _, reqByRef := range requestedByRefs {
       depFieldPlanners, ok := v.fieldPlanners[reqByRef]
       if !ok {
         continue
       }

       // find the subgraph's name by iterating over planners that are responsible for reqByRef
       for _, plannerID := range depFieldPlanners {
         ofc := v.planners[plannerID].ObjectFetchConfiguration()
-        if ofc == nil {
+        if ofc == nil || ofc.sourceName == currentSubgraphName || plannerID == fetchID {
           continue
         }
         requested.BySubgraphs = append(requested.BySubgraphs, ofc.sourceName)

         depKind, ok := v.fieldDependencyKind[fieldDependencyKey{field: reqByRef, dependsOn: fieldRef}]
         if !ok {
           continue
         }
         switch depKind {
         case fieldDependencyKindKey:
           requested.ReasonIsKey = true
         case fieldDependencyKindRequires:
           requested.ReasonIsRequires = true
         }
       }
     }
     if requested.ByUser || len(requested.BySubgraphs) > 0 {
       slices.Sort(requested.BySubgraphs)
       requested.BySubgraphs = slices.Compact(requested.BySubgraphs)
       result = append(result, requested)
     }
   }

+  // Stable ordering across runs
+  slices.SortFunc(result, func(a, b resolve.RequestedField) int {
+    if c := strings.Compare(a.TypeName, b.TypeName); c != 0 {
+      return c
+    }
+    return strings.Compare(a.FieldName, b.FieldName)
+  })
   return result
 }

636-646: skipField is fine; optimize only if needed

Looping a small slice is OK. If skipFieldsRefs grows, switch to a map[int]struct{} as your TODO suggests.

v2/pkg/engine/resolve/fetch.go (2)

416-425: RequestedField payload shape is sensible; minor naming thought

Schema looks good and compact. If you ever need per-subgraph reasons, you’ll outgrow booleans; for now, this is pragmatic.

Optional future-proofing: consider nesting reasons to avoid widening the struct later:

  • reasons: { key: bool, requires: bool }

No change required for this PR.


350-354: Document metadata exclusions in FetchConfiguration.Equals

The current implementation of FetchConfiguration.Equals (lines 356–364) deliberately omits both FieldsRequestedBy and OperationName. These two fields carry metadata (the set of user/subgraph–requested fields and an optional operation name for tracing) and do not influence the structural equivalence of fetch plans.

Please add an explicit note above the Equals method to clarify this behavior:

 func (fc *FetchConfiguration) Equals(other *FetchConfiguration) bool {
+   // Intentionally ignore metadata fields:
+   // • FieldsRequestedBy: for tracking which fields were requested
+   // • OperationName: for propagating the operation name upstream
+   // These do not affect structural equality of the fetch plan.
    if fc.Input != other.Input {
        return false
    }
    // …
    if fc.SetTemplateOutputToNullOnVariableNull != other.SetTemplateOutputToNullOnVariableNull {
        return false
    }
    return true
 }

This documentation will help future maintainers understand why these fields are excluded, and ensure any caching or testing logic based on Equals remains accurate.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 57e87ab and f04bc09.

📒 Files selected for processing (6)
  • v2/pkg/engine/plan/configuration.go (1 hunks)
  • v2/pkg/engine/plan/planner.go (3 hunks)
  • v2/pkg/engine/plan/visitor.go (7 hunks)
  • v2/pkg/engine/resolve/fetch.go (11 hunks)
  • v2/pkg/engine/resolve/loader.go (2 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • v2/pkg/engine/plan/configuration.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • v2/pkg/engine/resolve/loader.go
  • v2/pkg/engine/plan/planner.go
  • v2/pkg/engine/resolve/resolve.go
🧰 Additional context used
🧬 Code graph analysis (1)
v2/pkg/engine/plan/visitor.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • RequestedField (418-425)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
🔇 Additional comments (8)
v2/pkg/engine/plan/visitor.go (2)

58-66: Clear inline documentation for planner field maps

Good clarifications for plannerFields/fieldPlanners/fieldEnclosingTypeNames; improves maintainability.


49-49: No action required: inverse mapping is always initialized unconditionally in Plan()

I’ve verified that in planner.go the Plan method begins by assigning:

p.planningVisitor.fieldRefDependsOnFieldRefs = selectionsConfig.fieldRefDependsOn
p.planningVisitor.fieldDependencyKind    = selectionsConfig.fieldDependencyKind
p.planningVisitor.fieldRefAllowsFieldRefs = inverseMap(selectionsConfig.fieldRefDependsOn)

This assignment at lines 155–157 is unguarded—there are no early return statements preceding it—so on every call to Plan, fieldRefAllowsFieldRefs is populated.

v2/pkg/engine/resolve/fetch.go (6)

25-26: Good: Fetch API gains RequestedFields() for a uniform surface

This will let loader/transport code query per-fetch field provenance without type switches.


171-181: BatchEntityFetch: per-fetch storage for requested fields

Looks consistent with EntityFetch and ParallelListItemFetch. No issues.


191-194: BatchEntityFetch.RequestedFields implementation is straightforward

LGTM.


224-234: EntityFetch adds FieldsRequestedBy and exposes it

Alignment with BatchEntityFetch is good.


283-286: ParallelListItemFetch delegates RequestedFields to underlying SingleFetch

Makes sense for non-batching paths.


108-111: SingleFetch.RequestedFields is correctly delegated and populated

The RequestedFields method on SingleFetch simply returns FetchConfiguration.FieldsRequestedBy. In the planner (v2/pkg/engine/plan/visitor.go:1338), you can see:

  • singleFetch.FieldsRequestedBy = v.buildFieldsRequestedBy(internal.fetchID)

This confirms that the planner populates FetchConfiguration.FieldsRequestedBy before execution. No further changes are needed here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
v2/pkg/engine/plan/visitor.go (1)

1336-1339: Confirm field ownership on SingleFetch vs embedded FetchConfiguration

You now assign SingleFetch.CoordinateDependencies and SingleFetch.WhoRequestedFields directly. Earlier versions placed these on the embedded FetchConfiguration; there was a compile error noted in a prior review when assigning on the wrong struct.

Please confirm SingleFetch defines these fields. If not, switch to:

- singleFetch.CoordinateDependencies = v.resolveFetchDependencies(internal.fetchID)
- singleFetch.WhoRequestedFields = v.buildRequestedFields(internal.fetchID)
+ singleFetch.FetchConfiguration.CoordinateDependencies = v.resolveFetchDependencies(internal.fetchID)
+ singleFetch.FetchConfiguration.WhoRequestedFields = v.buildRequestedFields(internal.fetchID)
🧹 Nitpick comments (9)
execution/engine/execution_engine_test.go (2)

827-876: Potential GET-vs-body ambiguity: prefer POST when sending extensions

This test configures the subgraph fetch with Method: "GET" but asserts a non-empty JSON body including extensions.whoRequestedFields. Typical GraphQL over HTTP conventions avoid bodies on GET; some clients drop them. Unless the datasource automatically switches to POST when extensions are present, this may be brittle across transports.

  • If the datasource auto-upgrades GET→POST when a body is needed, keep as-is but confirm in loader/datasource tests.
  • Otherwise, set Method: "POST" for clarity and portability.

Suggested change:

-                        Fetch: &graphql_datasource.FetchConfiguration{
-                            URL:    "https://example.com/",
-                            Method: "GET",
-                        },
+                        Fetch: &graphql_datasource.FetchConfiguration{
+                            URL:    "https://example.com/",
+                            Method: "POST",
+                        },

4347-4471: Stability of whoRequestedFields ordering across runs

expectedBody1/2 compare raw JSON strings. buildRequestedFields iterates v.plannerFields[fetchID] insertion order; while likely stable, it’s not explicitly ordered. You already sort and compact requested.BySubgraphs; consider also sorting the top-level whoRequestedFields by (typeName, fieldName) for deterministic test behavior and future-proofing.

I’ve proposed a concrete diff on the builder in v2/pkg/engine/plan/visitor.go to sort the result slice.

v2/pkg/engine/plan/visitor.go (2)

636-647: skipField is O(n); acceptable, but simple to harden if the list grows

Given this runs once per visited field, the linear scan over skipFieldsRefs is likely fine. If skip lists can grow large in big plans, switch to a map[int]struct{} for O(1) lookups.

-  // If this grows, switch to map[int]struct{} for O(1).
-  for _, skipRef := range v.skipFieldsRefs {
-      if skipRef == ref {
-          return true
-      }
-  }
+  // If this grows, consider using a map[int]struct{} for O(1) checks.
+  for _, skipRef := range v.skipFieldsRefs {
+      if skipRef == ref {
+          return true
+      }
+  }

1399-1461: Deterministic whoRequestedFields ordering for test stability

The construction is correct: you exclude __typename, mark requestedByUser via skipField, derive requestedBySubgraphs and reason flags from planners/dependency kinds, then sort/compact subgraphs. To avoid any flakiness in tests that compare raw request bodies, also sort the result slice.

Apply this diff to sort the top-level list by (TypeName, FieldName):

 func (v *Visitor) buildRequestedFields(fetchID int) []resolve.RequestedField {
@@
-    for _, fieldRef := range fields {
+    for _, fieldRef := range fields {
         fieldName := v.Operation.FieldNameString(fieldRef)
         if fieldName == "__typename" {
             continue
         }
@@
-        if requested.ByUser || len(requested.BySubgraphs) > 0 {
+        if requested.ByUser || len(requested.BySubgraphs) > 0 {
             slices.Sort(requested.BySubgraphs)
             requested.BySubgraphs = slices.Compact(requested.BySubgraphs)
             result = append(result, requested)
         }
     }
-
-    return result
+    // Ensure deterministic order of whoRequestedFields
+    slices.SortFunc(result, func(a, b resolve.RequestedField) int {
+        if a.TypeName == b.TypeName {
+            if a.FieldName == b.FieldName {
+                return 0
+            }
+            if a.FieldName < b.FieldName {
+                return -1
+            }
+            return 1
+        }
+        if a.TypeName < b.TypeName {
+            return -1
+        }
+        return 1
+    })
+    return result
 }
v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go (1)

124-129: Same propagation for EntityFetch

Mirrors the batch path; looks good. If other concrete fetch types can include the extension in the future (e.g., ParallelListItemFetch envelopes), consider adding propagation there too for completeness, though not required for entity paths today.

v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (4)

3097-3135: Good addition: captures why parent fields and keys are pulled into the first fetch; ensure order is deterministic

The entries correctly mark Address.line1/line2 as requires from account.service, aggregate Address.id across all dependent subgraphs with ReasonIsKey, and flag the user-driven path (Query.user → User.account → Account.address). One nit: if BySubgraphs is built from a set/map in the planner, its order could be non-deterministic and make this assertion flaky. Consider either sorting BySubgraphs when producing WhoRequestedFields or sorting it in the test before assertion to avoid order-related flakes.


3183-3201: Should representation keys appear in WhoRequestedFields for this fetch?

This fetch relies on Address.id in $representations, but the WhoRequestedFields here only lists Address.country and Address.city (ReasonIsRequires). If WhoRequestedFields is meant to describe all fields “included in the request” (query + variables) for a given fetch, consider adding Address.id with ReasonIsKey as well; if it’s meant to capture only the newly selected fields (not representation keys), a brief comment in the code/tests clarifying the intended scope would help prevent confusion.


3295-3313: Consistent requires attribution; clarify key usage if intended

Similar to the previous block: Address.line3 and Address.zip are correctly attributed to account.service (ReasonIsRequires) for computing Address.fullAddress. If the intent is to also account for keys used in $representations per fetch, consider including Address.id with ReasonIsKey here, or confirm that keys are only tracked on the fetch where they’re actually selected (as in the first fetch).


3420-3431: User-driven final field looks right; optional enhancement

Address.fullAddress and the pathway Account.address are accurately marked as user requested. Optional: to aid future debugging, you could also assert the absence of requires/key reasons here (i.e., that only ByUser is set), to make the contract explicit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f04bc09 and 93b813a.

📒 Files selected for processing (7)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (4 hunks)
  • v2/pkg/engine/plan/visitor.go (7 hunks)
  • v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go (2 hunks)
  • v2/pkg/engine/resolve/fetch.go (11 hunks)
  • v2/pkg/engine/resolve/loader.go (2 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • v2/pkg/engine/resolve/loader.go
  • v2/pkg/engine/resolve/resolve.go
  • v2/pkg/engine/resolve/fetch.go
🧰 Additional context used
🧬 Code graph analysis (3)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • RequestedField (418-425)
execution/engine/execution_engine_test.go (4)
v2/pkg/starwars/starwars.go (2)
  • FileSimpleHeroQuery (13-13)
  • Schema (55-59)
v2/pkg/engine/plan/datasource_configuration.go (1)
  • DataSourceMetadata (34-56)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-354)
v2/pkg/engine/plan/visitor.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • RequestedField (418-425)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
🔇 Additional comments (6)
execution/engine/execution_engine_test.go (3)

218-219: Good addition: toggle for who-requested-fields propagation

Introducing the propagateWhoRequestedFields test flag on _executionTestOptions keeps the test harness clean and avoids leaking resolver concerns into every test case. No issues.


266-267: ResolverOptions wiring looks correct

PropagateWhoRequestedFields is plumbed through ResolverOptions alongside existing flags. This ensures tests can flip the behavior per run without global impact.


4501-4502: Great coverage: with/without propagation path on federated plan

Running the same scenario with and without the extension, and validating per-subgraph payloads (including reasonIsKey and requestedBySubgraphs) exercises the end-to-end plumbing well.

Also applies to: 4505-4540

v2/pkg/engine/plan/visitor.go (2)

1357-1397: Minor: consistent coordinate assembly

Good call building coordinates with FieldName from the operation and TypeName from fieldEnclosingTypeNames, mirroring the same approach for dependency origins. This keeps coordinates stable across aliasing.


49-66: Verify selection dependency maps are populated correctly

I see that fieldRefAllowsFieldRefs is indeed initialized in v2/pkg/engine/plan/planner.go:157 via

p.planningVisitor.fieldRefAllowsFieldRefs = inverseMap(selectionsConfig.fieldRefDependsOn)

and subsequently used in v2/pkg/engine/plan/visitor.go:1417 when building requested fields.

However, please confirm that:

  • selectionsConfig.fieldRefDependsOn is populated with all fieldRef→[]dependentFieldRefs entries before the inverseMap call.
  • The inverse mapping occurs prior to any invocations of buildRequestedFields, so that BySubgraphs and reason flags will be set correctly.

No code change needed if the above is already true; otherwise, ensure dependency analysis runs early enough.

v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go (1)

77-82: PropagateWhoRequestedFields for BatchEntityFetch Verified

  • BatchEntityFetch (v2/pkg/engine/resolve/fetch.go) defines a FieldsRequestedBy slice and its RequestedFields() method returns this slice.
  • Loader’s executeSourceLoad (v2/pkg/engine/resolve/loader.go) checks propagateWhoRequestedFields and calls fetchItem.Fetch.RequestedFields(), encoding and injecting the “whoRequestedFields” extension for both EntityFetch and BatchEntityFetch.

No further changes are needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
execution/engine/execution_engine_test.go (5)

214-219: Add a short doc on the new test knob for clarity

propagateWhoRequestedFields is self-explanatory, but a one-line comment on its intent (propagates field provenance into subgraph request extensions) would help future readers scanning options.

 type _executionTestOptions struct {
 	resolvableOptions resolve.ResolvableOptions
 
 	apolloRouterCompatibilitySubrequestHTTPError bool
-	propagateWhoRequestedFields                  bool
+	// when true, expected test round-trips must include extensions.whoRequestedFields in upstream requests
+	propagateWhoRequestedFields bool
 }

827-876: Good coverage for basic propagation; guard against brittle string comparisons

Nice, this validates that enabling propagation produces the expected extensions payload and correctly switches to POST. However, matching the request body as a raw JSON string is fragile (key order and array ordering can change with innocuous refactors). Consider comparing bodies semantically in the test RoundTripper (parse JSON and deep-equal) or generate expected bodies via json.Marshal of typed structs to keep ordering deterministic.

If you want, I can provide a small change to createTestRoundTripper to accept an expected JSON value and compare canonically. As a local improvement in this file, you can also marshal a struct for expectedBody to avoid hand-crafted JSON strings.


4347-4471: Helper is useful; build expected JSON programmatically to reduce brittleness

The makeDataSource(t, expectRequestedBy) abstraction is solid and keeps the two federation subgraphs readable. The only weak spot is the large hand-written expectedBody JSON strings. Building them via structs and json.Marshal will:

  • eliminate quoting/escaping errors,
  • lock key ordering to struct field order,
  • make diffs smaller if the payload shape evolves.

You can keep the test harness unchanged since the output will still be a compact JSON string.

Apply this focused refactor to use a builder for the two expected bodies:

-			var expectedBody1 string
-			var expectedBody2 string
-			if !expectRequestedBy {
-				expectedBody1 = `{"query":"{accounts {__typename ... on User {some {__typename id}} ... on Admin {some {__typename id}}}}"}`
-				expectedBody2 = `{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on User {__typename title}}}","variables":{"representations":[{"__typename":"User","id":"1"},{"__typename":"User","id":"3"}]}}`
-			} else {
-				expectedBody1 = `{"query":"{accounts {__typename ... on User {some {__typename id}} ... on Admin {some {__typename id}}}}","extensions":{"whoRequestedFields":[{"typeName":"User","fieldName":"id","requestedBySubgraphs":["id-2"],"reasonIsKey":true},{"typeName":"User","fieldName":"some","requestedByUser":true},{"typeName":"User","fieldName":"id","requestedByUser":true},{"typeName":"Admin","fieldName":"some","requestedByUser":true},{"typeName":"Query","fieldName":"accounts","requestedByUser":true}]}}`
-				expectedBody2 = `{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on User {__typename title}}}","variables":{"representations":[{"__typename":"User","id":"1"},{"__typename":"User","id":"3"}]},"extensions":{"whoRequestedFields":[{"typeName":"User","fieldName":"title","requestedByUser":true},{"typeName":"User","fieldName":"some","requestedByUser":true}]}}`
-			}
+			expectedBody1, expectedBody2 := buildFederationExpectedBodies(expectRequestedBy)

Add this helper somewhere in this test file (outside the function):

// support code for the diff above
type gqlReq struct {
	Query      string                 `json:"query"`
	Variables  map[string]any         `json:"variables,omitempty"`
	Extensions map[string]any         `json:"extensions,omitempty"`
}

func mustMarshal(v any) string {
	b, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}
	return string(b)
}

func buildFederationExpectedBodies(withReasons bool) (string, string) {
	// first subgraph body
	body1 := gqlReq{
		Query: `{accounts {__typename ... on User {some {__typename id}} ... on Admin {some {__typename id}}}}`,
	}
	if withReasons {
		body1.Extensions = map[string]any{
			"whoRequestedFields": []any{
				map[string]any{"typeName": "User", "fieldName": "id", "requestedBySubgraphs": []string{"id-2"}, "reasonIsKey": true},
				map[string]any{"typeName": "User", "fieldName": "some", "requestedByUser": true},
				map[string]any{"typeName": "User", "fieldName": "id", "requestedByUser": true},
				map[string]any{"typeName": "Admin", "fieldName": "some", "requestedByUser": true},
				map[string]any{"typeName": "Query", "fieldName": "accounts", "requestedByUser": true},
			},
		}
	}

	// second subgraph body
	body2 := gqlReq{
		Query: `query($representations: [_Any!]!){_entities(representations: $representations){... on User {__typename title}}}`,
		Variables: map[string]any{
			"representations": []any{
				map[string]any{"__typename": "User", "id": "1"},
				map[string]any{"__typename": "User", "id": "3"},
			},
		},
	}
	if withReasons {
		body2.Extensions = map[string]any{
			"whoRequestedFields": []any{
				map[string]any{"typeName": "User", "fieldName": "title", "requestedByUser": true},
				map[string]any{"typeName": "User", "fieldName": "some", "requestedByUser": true},
			},
		}
	}

	return mustMarshal(body1), mustMarshal(body2)
}

4505-4540: End-to-end federation path with propagation enabled

This complements the non-propagating run and validates the extension plumbing across both subgraphs. Consider one more variant where Method remains POST but propagation is disabled (to assert that no extensions key leaks when the flag is false).

I can add a follow-up test variant if you’d like.


827-876: Reduce risk from ordering of whoRequestedFields items

Depending on how the planner assembles WhoRequestedFields, the slice ordering could change with future refactors. If the order is not guaranteed, tests may become flaky. Either:

  • sort the whoRequestedFields slice before marshalling upstream bodies, or
  • adapt the RoundTripper to compare the whoRequestedFields array as a set (order-insensitive).

If ordering is guaranteed by construction, please add a brief comment stating that invariant so future changes don’t unintentionally break it.

Would you like me to add a deterministic sort (e.g., by typeName, then fieldName, then flags) at the injection point?

Also applies to: 4347-4471

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 93b813a and 72b8aff.

📒 Files selected for processing (1)
  • execution/engine/execution_engine_test.go (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
execution/engine/execution_engine_test.go (6)
v2/pkg/starwars/starwars.go (2)
  • FileSimpleHeroQuery (13-13)
  • Schema (55-59)
v2/pkg/engine/plan/datasource_configuration.go (1)
  • DataSourceMetadata (34-56)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-354)
v2/pkg/engine/plan/federation_metadata.go (2)
  • FederationMetaData (10-16)
  • FederationFieldConfigurations (113-113)
v2/pkg/engine/resolve/context.go (1)
  • Request (148-151)
🔇 Additional comments (4)
execution/engine/execution_engine_test.go (4)

262-267: Plumbing the option through ResolverOptions looks correct

Passing PropagateWhoRequestedFields from test options into resolve.ResolverOptions is consistent with the new API surface. No issues spotted.


858-861: POST is required to carry extensions

Good call to switch the fetch method to POST for this case; a GET would drop the body and the extension. This keeps behavior explicit in the test.


4411-4418: Federation config is set on SchemaConfiguration

Good to see federation explicitly enabled with the corresponding ServiceSDL; matches the entity fetches used in this test.


262-267: All good: PropagateWhoRequestedFields is wired and the extension key is consistent

  • The ResolverOptions struct in v2/pkg/engine/resolve/resolve.go includes PropagateWhoRequestedFields and it’s passed through to the loader via
    propagateWhoRequestedFields: options.PropagateWhoRequestedFields.
  • In v2/pkg/engine/resolve/loader.go the JSON extension "whoRequestedFields" is injected (at line 1575) using that flag.
  • Tests in execution/engine/execution_engine_test.go and execution_engine_helpers_test.go uniformly use "whoRequestedFields" in their expected bodies.
  • The createTestRoundTripper implementations in both pkg/graphql and execution/engine compare the raw request body string, so formatting changes would break existing tests.

No further action required here.

@ysmolski ysmolski changed the title feat: add option to send why fields where included in the request feat: send to subgraphs why fields where included in the request Aug 22, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v2/pkg/engine/resolve/fetch.go (1)

20-26: Critical: Implement RequestedFields() on all built-in Fetch types and verify downstream consumers

I ran a quick grep for types that implement FetchKind()—the heuristic for Fetch implementers—and confirmed that none currently define RequestedFields(), so the interface change breaks internal code immediately:

  • SingleFetch (v2/pkg/engine/resolve/fetch.go:163)
  • BatchEntityFetch (v2/pkg/engine/resolve/fetch.go:217)
  • EntityFetch (v2/pkg/engine/resolve/fetch.go:262)
  • ParallelListItemFetch (v2/pkg/engine/resolve/fetch.go:287)

Please add a RequestedFields() []RequestedField method to each of these types (for example, returning nil or the appropriate field list) so they satisfy the updated interface. For example:

func (f *SingleFetch) RequestedFields() []RequestedField {
    return nil
}

Once internal types compile, also audit any out-of-tree implementations of Fetch in downstream repos. If you find external clients relying on the old interface, consider:

  • Providing a default/no-op adapter that implements RequestedFields()
  • Introducing a wrapper interface to preserve backward compatibility
♻️ Duplicate comments (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (1)

3296-3313: Same consideration as the previous fetch about representation keys and determinism.

The comments on Lines 3183–3201 apply here as well (potential inclusion of Address.id as ReasonIsKey and sorting of BySubgraphs for stability).

v2/pkg/engine/plan/visitor.go (1)

1337-1339: Fix assignments to embedded FetchConfiguration to avoid compile error.

SingleFetch does not (currently) declare CoordinateDependencies or FieldsRequestedBy at the top level; they live on the embedded FetchConfiguration. Assigning to the wrong receiver won’t compile.

Apply this diff:

-        singleFetch.CoordinateDependencies = v.resolveFetchDependencies(internal.fetchID)
-        singleFetch.FieldsRequestedBy = v.buildRequestedFields(internal.fetchID)
+        singleFetch.FetchConfiguration.CoordinateDependencies = v.resolveFetchDependencies(internal.fetchID)
+        singleFetch.FetchConfiguration.FieldsRequestedBy = v.buildRequestedFields(internal.fetchID)

To double-check the struct shape in your current branch, run:

#!/bin/bash
# Inspect where FieldsRequestedBy is declared
rg -nP 'type\s+SingleFetch\b[\s\S]*?\{' -n v2/pkg/engine/resolve/fetch.go -A30
rg -nP 'type\s+FetchConfiguration\b[\s\S]*?\{' -n v2/pkg/engine/resolve/fetch.go -A60 | sed -n '1,120p'
🧹 Nitpick comments (11)
v2/pkg/engine/resolve/fetch.go (4)

171-181: Inconsistent storage location for FieldsRequestedBy across fetch types

BatchEntityFetch stores FieldsRequestedBy on the fetch itself, while SingleFetch keeps it in FetchConfiguration. Consider standardizing to a single location (preferably on FetchConfiguration for consistency) or add a short comment explaining the rationale to avoid confusion for contributors.

Apply a clarifying comment locally if you keep the current layout:

 type BatchEntityFetch struct {
   FetchDependencies
-
+  // FieldsRequestedBy contains the per-fetch field request provenance.
+  // Note: Unlike SingleFetch, this is stored on the fetch itself rather than FetchConfiguration.
   Input                  BatchInput
   DataSource             DataSource
   PostProcessing         PostProcessingConfiguration
   DataSourceIdentifier   []byte
   Trace                  *DataSourceLoadTrace
   Info                   *FetchInfo
   CoordinateDependencies []FetchDependency
   FieldsRequestedBy      []RequestedField
 }

226-234: Mirror comment from BatchEntityFetch: storage consistency

EntityFetch mirrors BatchEntityFetch by storing FieldsRequestedBy on the struct itself and not in a FetchConfiguration. If you keep this asymmetry with SingleFetch/ParallelListItemFetch, please document it to prevent accidental future regressions.


350-354: Equality semantics: FieldsRequestedBy and OperationName are not compared

FetchConfiguration.Equals does not compare FieldsRequestedBy or OperationName. If plan equality is used for deduplication or caching, differing values here won’t affect equality. That could be fine if these are diagnostics-only, but please confirm.

Option A (document intent): add an explicit comment in Equals:

 func (fc *FetchConfiguration) Equals(other *FetchConfiguration) bool {
@@
-    // Note: we do not compare CoordinateDependencies, as they contain more detailed
+    // Note: we do not compare CoordinateDependencies, as they contain more detailed
     // dependencies information that is already present in the FetchDependencies on the fetch itself
+    // Note: we intentionally do not compare FieldsRequestedBy and OperationName,
+    // as they are used for diagnostics/propagation only and should not affect plan equality.

Option B (enforce equality): include these fields in the comparison if they must influence plan identity.


416-425: Shape and naming of RequestedField; consider extensibility and consistent casing

  • The by/reason information is encoded as booleans on the whole coordinate. If a coordinate is requested by multiple subgraphs for different reasons, we lose per-subgraph reason fidelity. Consider a structure like per-subgraph entries or a reasons []string enum (["key","requires"]) for future-proofing.
  • JSON key casing: struct fields use lowerCamelCase (requestedBySubgraphs/requestedByUser), while the request extension key added in loader.go is "FieldsRequestedBy" (PascalCase). Recommend aligning on lowerCamelCase for consistency with other extension fields (e.g., statusCode).

If you decide to compact the reasons:

 type RequestedField struct {
   TypeName         string   `json:"typeName"`
   FieldName        string   `json:"fieldName"`
-  BySubgraphs      []string `json:"requestedBySubgraphs,omitempty"`
-  ByUser           bool     `json:"requestedByUser,omitempty"`
-  ReasonIsKey      bool     `json:"reasonIsKey,omitempty"`
-  ReasonIsRequires bool     `json:"reasonIsRequires,omitempty"`
+  BySubgraphs      []string `json:"requestedBySubgraphs,omitempty"`
+  ByUser           bool     `json:"requestedByUser,omitempty"`
+  Reasons          []string `json:"reasons,omitempty"` // e.g. ["key","requires"]
 }

At minimum, ensure BySubgraphs is de-duplicated and stable-sorted before marshalling to keep payloads deterministic.

v2/pkg/engine/resolve/loader.go (2)

1565-1581: Only inject when non-empty and align extension key casing; keep extensions object robust

  • Skip injection when the slice is empty to reduce payload churn.
  • Prefer "fieldsRequestedBy" (lowerCamelCase) to be consistent with other extension keys (e.g., statusCode).
  • The jsonparser.Set call is correct and will create missing objects, but will error if extensions is not an object. Your error handling is good.

Apply this diff:

-    fieldsRequestedBy := fetchItem.Fetch.RequestedFields()
-    if fieldsRequestedBy != nil {
+    fieldsRequestedBy := fetchItem.Fetch.RequestedFields()
+    if len(fieldsRequestedBy) > 0 {
         var encoded []byte
         encoded, res.err = json.Marshal(fieldsRequestedBy)
         if res.err != nil {
             res.err = errors.WithStack(res.err)
             return
         }
-        // We expect that body.extensions is an object
-        input, res.err = jsonparser.Set(input, encoded, "body", "extensions", "FieldsRequestedBy")
+        // Ensure body.extensions exists (jsonparser will create intermediate objects) and set the field.
+        input, res.err = jsonparser.Set(input, encoded, "body", "extensions", "fieldsRequestedBy")
         if res.err != nil {
             res.err = errors.WithStack(res.err)
             return
         }
     }

If you anticipate frequent reuse of the same slice per fetch, consider precomputing and caching the marshalled bytes on the fetch to avoid per-request marshalling.


1565-1581: Add coverage: verify propagation behavior across fetch kinds and introspection exclusion

Please add tests that assert:

  • Nothing is injected for introspection data sources.
  • fieldsRequestedBy is present only when the option is enabled.
  • Injection works for SingleFetch, EntityFetch, BatchEntityFetch, and ParallelListItemFetch.
  • Empty slices are not injected once the guard is added.

I can draft table-driven tests around executeSourceLoad/loadSingleFetch with fake DataSource stubs to validate the input body payloads. Let me know if you want me to push a test scaffolding.

v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (1)

3183-3201: Should representation keys also appear in FieldsRequestedBy, or is this selection-set only?

This fetch sends Address.id in variables.representations, but id isn’t included in FieldsRequestedBy. If the extension is meant to explain “why fields were included in the request” to the receiving subgraph, keys passed via variables may also be relevant (e.g., mark id with ReasonIsKey). If the intent is “selection set only,” consider documenting that and keeping keys out for noise reduction.

Would you like me to add a test/helper asserting the chosen semantics (selection-set-only vs selection+variables) so future changes don’t regress this?

execution/engine/execution_engine_test.go (2)

827-876: Solid happy-path coverage for per-fetch extensions; one naming consideration.

The round-tripper assertion verifies the extension payload and HTTP method switch to POST when extensions are present. Minor: consider aligning the extension key casing with existing keys like "valueCompletion" (camelCase) for consistency, e.g., "fieldsRequestedBy". If you decide to change it, we can update the tests and loader in one pass.

If you opt in, I can provide a scoped diff for the tests once the extension key name is finalized.


4505-4541: Great federated coverage; suggest adding one more case for '@requires'.

This test exercises keys and cross-subgraph dependency reporting. Consider adding a similar case with @requires to prove ReasonIsRequires toggles correctly and that requestedBySubgraphs attribution matches the providing subgraph.

Happy to draft a minimal schema+plan snippet for @requires if helpful.

v2/pkg/engine/plan/visitor.go (1)

1399-1461: Requested-field computation is coherent and stable; minor nit on allocations.

Logic cleanly:

  • filters out __typename,
  • flags user vs dependency,
  • collects subgraph names from planners,
  • marks key/requires,
  • sorts and compacts.

Nit: FieldNameString and repeated slice appends can allocate in tight loops on large selections. Not urgent, but consider pre-sizing BySubgraphs using the max of fieldPlanners[reqByRef] lengths, or caching FieldNameString results if this shows up in profiles.

v2/pkg/engine/resolve/resolve.go (1)

168-171: Clarify transport requirement in the option doc.

Since extensions are carried in the GraphQL request body, this effectively requires HTTP POST to subgraphs. Add a note to avoid surprises when a datasource is configured for GET.

Apply this doc tweak:

-    // PropagateFieldsRequestedBy enables sending to upstream subgraphs (only) the "FieldsRequestedBy"
-    // extension that explains why each field was requested. This flag does not expose the data to clients.
+    // PropagateFieldsRequestedBy enables sending to upstream subgraphs (only) the "FieldsRequestedBy"
+    // extension that explains why each field was requested. This flag does not expose the data to clients.
+    // Note: This requires GraphQL over HTTP POST (JSON body). GET datasources will not include this extension.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 72b8aff and d96c569.

📒 Files selected for processing (7)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (4 hunks)
  • v2/pkg/engine/plan/visitor.go (7 hunks)
  • v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go (2 hunks)
  • v2/pkg/engine/resolve/fetch.go (11 hunks)
  • v2/pkg/engine/resolve/loader.go (2 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go
🧰 Additional context used
🧬 Code graph analysis (6)
v2/pkg/engine/resolve/loader.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • Fetch (20-26)
execution/engine/execution_engine_test.go (4)
v2/pkg/starwars/starwars.go (2)
  • FileSimpleHeroQuery (13-13)
  • Schema (55-59)
v2/pkg/engine/plan/datasource_configuration.go (1)
  • DataSourceMetadata (34-56)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-354)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • RequestedField (418-425)
v2/pkg/engine/resolve/resolve.go (1)
v2/pkg/engine/resolve/resolvable.go (1)
  • ResolvableOptions (62-67)
v2/pkg/engine/resolve/fetch.go (2)
v2/pkg/engine/plan/datasource_configuration.go (1)
  • DataSource (256-269)
v2/pkg/engine/resolve/datasource.go (1)
  • DataSource (12-15)
v2/pkg/engine/plan/visitor.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • RequestedField (418-425)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
🔇 Additional comments (11)
v2/pkg/engine/resolve/fetch.go (5)

108-110: LGTM: SingleFetch.RequestedFields exposes per-fetch metadata

Simple passthrough to FetchConfiguration.FieldsRequestedBy. Looks good.


126-142: Docs-only tweaks in PostProcessingConfiguration

Non-functional comment changes; nothing to flag.


191-194: LGTM: BatchEntityFetch.RequestedFields passthrough

Straightforward accessor.


244-247: LGTM: EntityFetch.RequestedFields passthrough

Accessor reads from the struct field as expected.


283-286: LGTM: ParallelListItemFetch.RequestedFields delegates to underlying SingleFetch

Correctly exposes the same metadata per list item fetch.

v2/pkg/engine/resolve/loader.go (1)

154-155: ✅ propagateFieldsRequestedBy correctly wired (default remains false)

All looks wired up as intended:

  • In v2/pkg/engine/resolve/resolve.go (line 253), options.PropagateFieldsRequestedBy is passed through into the loader’s propagateFieldsRequestedBy field.
  • The Loader struct in v2/pkg/engine/resolve/loader.go defines propagateFieldsRequestedBy bool, so the flag has a proper home.
  • Because Go’s zero value for bool is false and there’s no override, existing users won’t see any payload changes by default.

No changes needed here.

v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (2)

3420-3431: LGTM: user-requested fields are captured cleanly at the final fetch.

Capturing Address.fullAddress and Account.address as ByUser=true aligns with the intent of the extension. No issues spotted.


3097-3135: BySubgraphs ordering is already deterministic in buildRequestedFields

I checked v2/pkg/engine/plan/visitor.go around lines 1453–1455, and you’ll see we already call slices.Sort(requested.BySubgraphs) followed by slices.Compact(requested.BySubgraphs) before returning the slice. That guarantees the same lexicographical order of BySubgraphs every run, so this test won’t flake. No changes needed here.

execution/engine/execution_engine_test.go (2)

218-219: Plumbs the new test knob cleanly.

Adding propagateFieldsRequestedBy to the local options struct is straightforward and keeps the feature flag scoped to tests. LGTM.


266-267: ResolverOptions wiring looks correct.

PropagateFieldsRequestedBy is passed through to the engine; no other behavior is altered. LGTM.

v2/pkg/engine/resolve/resolve.go (1)

237-256: Loader wiring LGTM; validate GET-to-POST behavior when needed.

Flag propagation to Loader is correct. One operational consideration: if a datasource is configured with Method: GET and the flag is enabled, do we silently omit the extension or upgrade to POST? Either behavior is fine, but it should be explicit.

If the current behavior is “omit”, consider logging once per fetch when the flag is set but the transport can’t carry extensions. If you prefer “upgrade”, we can add a guarded switch in the GraphQL loader path.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v2/pkg/engine/plan/visitor.go (1)

49-66: The grep output shows no assignments or initializations for plannerFields, fieldPlanners, or fieldEnclosingTypeNames anywhere in v2/pkg/engine/plan. Only fieldRefAllowsFieldRefs is populated via inverseMap in the planner. The other new maps remain uninitialized and will be nil at runtime.

Please add initialization and population logic for these maps in your planner setup—similar to how you handle fieldRefAllowsFieldRefs—to ensure they’re safe to read:

• In planner.go, after constructing planningVisitor, initialize:

  • plannerFields = make(map[int][]int)
  • fieldPlanners = make(map[int][]int)
  • fieldEnclosingTypeNames = make(map[int]string)

• Populate each map from selectionsConfig (or other source) before invoking planningWalker.ResetVisitors().

Once these are wired up, your visitor’s reads for these maps will never encounter nil/map lookups.

♻️ Duplicate comments (1)
execution/engine/execution_engine_test.go (1)

4336-4473: Nested federation test: expectations and metadata align with planner semantics.

  • expectedBody1’s FieldsRequestedBy covers: User.id (byUser and bySubgraphs with reasonIsKey) and Admin.some (byUser). Matches ProtectedFieldNames and dependency chain.
  • expectedBody2 intentionally has no extensions (no protected fields on subgraph 2). This fixes the earlier “some” leak.

LGTM.

🧹 Nitpick comments (10)
v2/pkg/engine/plan/type_field.go (3)

4-7: Document field semantics and intent (esp. “Protected”).

Add brief GoDoc-style comments to avoid ambiguity and help downstream users.

 type TypeField struct {
-	TypeName            string
-	FieldNames          []string
-	ExternalFieldNames  []string
-	ProtectedFieldNames []string
+	TypeName            string   // GraphQL type name
+	FieldNames          []string // Fields owned/resolved by this subgraph
+	ExternalFieldNames  []string // Fields that are externally resolved (federated)
+	ProtectedFieldNames []string // Fields included due to auth/policy requirements
 }

If you also want a type-level comment (outside the changed lines):

// TypeField describes fields associated with a GraphQL type, including
// externally resolved and authorization-protected fields.

4-7: Enforce uniqueness and deterministic ordering of name slices.

Downstream set-membership and diff-based tests benefit from canonicalized data. Consider normalizing when constructing instances.

// Helper: returns sorted unique copy.
func uniqueSorted(in []string) []string {
	if len(in) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(in))
	out := make([]string, 0, len(in))
	for _, s := range in {
		if _, ok := seen[s]; ok {
			continue
		}
		seen[s] = struct{}{}
		out = append(out, s)
	}
	slices.Sort(out) // Go 1.21+
	return out
}

func NewTypeField(typeName string, fields, external, protected []string) TypeField {
	return TypeField{
		TypeName:            typeName,
		FieldNames:          uniqueSorted(fields),
		ExternalFieldNames:  uniqueSorted(external),
		ProtectedFieldNames: uniqueSorted(protected),
	}
}

4-7: If serialized, prefer non-nil slices for stable JSON shape.

If TypeField is marshaled into subgraph request extensions, decide between null vs [] and keep it consistent (initialize to empty slices if needed).

v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (3)

1086-1089: ProtectedFieldNames on Address: confirm intent and keep consistent across subgraphs

You protect id, line1, line2 here. Later for Address in address.service you only protect line3, zip. If asymmetry is intentional, add a short comment; otherwise, align both (e.g., protect id in both or neither).


1268-1271: ProtectedFieldNames asymmetry with users subgraph

Here id is not protected, unlike users.service. Consider adding id for consistency, or document why it's different.

Apply if you want to align:

-                        ProtectedFieldNames: []string{"line3", "zip"},
+                        ProtectedFieldNames: []string{"id", "line3", "zip"},

3263-3276: Requires reasons captured well; consider expanding coverage

Zip and line3 are correctly marked ReasonIsRequires by account.service. Optionally add a case where ByUser=true is exercised, and/or a fetch showing requires from a sibling (e.g., address-enricher) to validate multi-hop attribution.

execution/engine/execution_engine_test.go (1)

827-879: Good positive test for extension payload on simple hero.

  • Body asserts the extensions.FieldsRequestedBy structure and key naming.
  • ProtectedFieldNames on Query.hero and Character.name align with filter logic.

Consider also asserting absence when flag is off for this same scenario (mirrors nested test). Optional.

v2/pkg/engine/plan/datasource_configuration.go (1)

99-146: Micro-optimization: pre-size child maps.

You pre-sized root maps using slice lengths; do the same for child to reduce rehashing.

-        if _, ok := d.childNodesIndex[typeName]; !ok {
-            d.childNodesIndex[typeName] = fieldsIndex{
-                fields:          make(map[string]struct{}),
-                externalFields:  make(map[string]struct{}),
-                protectedFields: make(map[string]struct{}),
-            }
-        }
+        if _, ok := d.childNodesIndex[typeName]; !ok {
+            d.childNodesIndex[typeName] = fieldsIndex{
+                fields:          make(map[string]struct{}, len(d.ChildNodes[i].FieldNames)),
+                externalFields:  make(map[string]struct{}, len(d.ChildNodes[i].ExternalFieldNames)),
+                protectedFields: make(map[string]struct{}, len(d.ChildNodes[i].ProtectedFieldNames)),
+            }
+        }
v2/pkg/engine/plan/visitor.go (2)

636-646: Potential hot path: convert to set if it grows.

You already noted switching to map if skipFieldsRefs expands. Keep an eye on size in large ops.


1399-1466: Requested-fields builder works; consider deduping by coordinate.

Today, identical coordinates can appear multiple times (e.g., byUser and bySubgraphs for User.id), inflating payloads. Optional: merge per (typeName, fieldName) to a single entry with combined flags and sources for simpler consumption and smaller bodies.

Example minimal refactor inside this block:

- result := make([]resolve.RequestedField, 0, len(fields))
+ merged := make(map[string]*resolve.RequestedField, len(fields))
  ...
- requested := resolve.RequestedField{ TypeName: typeName, FieldName: fieldName }
+ key := typeName + "\x00" + fieldName
+ rf, ok := merged[key]
+ if !ok {
+   rf = &resolve.RequestedField{ TypeName: typeName, FieldName: fieldName }
+   merged[key] = rf
+ }
+ requested := *rf
  ...
- if requested.ByUser || len(requested.BySubgraphs) > 0 {
-   slices.Sort(requested.BySubgraphs)
-   requested.BySubgraphs = slices.Compact(requested.BySubgraphs)
-   result = append(result, requested)
- }
+ if requested.ByUser || len(requested.BySubgraphs) > 0 {
+   slices.Sort(requested.BySubgraphs)
+   requested.BySubgraphs = slices.Compact(requested.BySubgraphs)
+   *rf = requested
+ }
  ...
- return result
+ // flatten in stable order
+ out := make([]resolve.RequestedField, 0, len(merged))
+ for _, v := range merged {
+   out = append(out, *v)
+ }
+ slices.SortFunc(out, func(a, b resolve.RequestedField) int {
+   if a.TypeName == b.TypeName {
+     if a.FieldName == b.FieldName { return 0 }
+     if a.FieldName < b.FieldName { return -1 }
+     return 1
+   }
+   if a.TypeName < b.TypeName { return -1 }
+   return 1
+ })
+ return out

Note: would require test updates if order changes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d96c569 and 17136a9.

📒 Files selected for processing (8)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (4 hunks)
  • v2/pkg/engine/plan/datasource_configuration.go (5 hunks)
  • v2/pkg/engine/plan/federation_metadata.go (1 hunks)
  • v2/pkg/engine/plan/type_field.go (1 hunks)
  • v2/pkg/engine/plan/visitor.go (7 hunks)
  • v2/pkg/engine/resolve/fetch.go (11 hunks)
  • v2/pkg/engine/resolve/loader.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • v2/pkg/engine/resolve/loader.go
  • v2/pkg/engine/resolve/fetch.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-09T09:34:51.269Z
Learnt from: SkArchon
PR: wundergraph/graphql-go-tools#1218
File: v2/pkg/engine/resolve/loader.go:58-60
Timestamp: 2025-07-09T09:34:51.269Z
Learning: In Go's bytes package, the String() method on *bytes.Buffer handles nil receivers gracefully by returning an empty string rather than panicking, making additional nil checks unnecessary when calling String() on potentially nil *bytes.Buffer instances.

Applied to files:

  • v2/pkg/engine/plan/federation_metadata.go
🧬 Code graph analysis (3)
v2/pkg/engine/plan/visitor.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • RequestedField (418-425)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • RequestedField (418-425)
execution/engine/execution_engine_test.go (4)
v2/pkg/engine/plan/datasource_configuration.go (2)
  • DataSource (293-306)
  • DataSourceMetadata (34-56)
v2/pkg/engine/plan/type_field.go (1)
  • TypeField (3-8)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-354)
🔇 Additional comments (13)
v2/pkg/engine/plan/federation_metadata.go (1)

108-111: Confirm value-based fmt.Stringer usage for FederationFieldConfiguration

I ran targeted searches but found no direct uses of fmt.Print* on value instances or explicit calls to String()/Format() on FederationFieldConfiguration. However, absence of matches isn’t proof of safety. Please manually verify that no code paths pass a value (non-pointer) to fmt verbs expecting fmt.Stringer, as the pointer-receiver change alters:

  • Value-based fmt.Sprintf("%s", fValue) will no longer call String(), producing Go’s default struct formatting instead of JSON.
  • A nil-pointer f now marshals to "null" rather than {}; confirm this semantic shift is acceptable.

If you need backward-compatible behavior, consider:

  1. Adding an explicit nil guard in String():
    func (f *FederationFieldConfiguration) String() string {
    +   if f == nil {
    +     return "null"
    +   }
        b, _ := json.Marshal(f)
        return string(b)
    }
  2. Implementing a Format on the value receiver to preserve %s/%v behavior:
    func (f FederationFieldConfiguration) Format(s fmt.State, verb rune) {
        _, _ = fmt.Fprint(s, (&f).String())
    }

Please review all formatting and logging sites to ensure no regressions occur.

v2/pkg/engine/plan/type_field.go (1)

4-7: No lingering usages of removed TypeFields helpers

I’ve verified that there are no remaining calls to HasNode, HasExternalNode, or HasNodeWithTypename across the codebase. The only matches for TypeFields.*. are in type signatures and function definitions (not method invocations on the struct), so all helper call sites have been successfully removed.

v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (1)

3099-3122: No action needed—BySubgraphs is already sorted and deduplicated
The implementation of buildRequestedFields in v2/pkg/engine/plan/visitor.go calls slices.Sort(requested.BySubgraphs) followed by slices.Compact(requested.BySubgraphs) before returning its results (lines 1459–1461). This ensures a deterministic, lexicographically ordered, and deduplicated slice of subgraph names, matching the test’s expectations. You can ignore the suggestion to add sorting logic here.

Likely an incorrect or invalid review comment.

execution/engine/execution_engine_test.go (3)

218-219: Test option plumbing looks good.

Adding propagateFieldsRequestedBy to _executionTestOptions is clear and default-safe.


262-267: ResolverOptions flag wired correctly.

PropagateFieldsRequestedBy is passed through to the engine; no regressions seen.


4503-4543: Nice A/B coverage.

  • “run” (flag off) vs “run with extension” (flag on) validates toggle behavior for nested fetches.
v2/pkg/engine/plan/datasource_configuration.go (4)

54-56: Indexing by typeName improves lookup performance.

The new root/child indexes with protectedFields support are appropriate for fast membership checks.


67-76: NodesInfo expanded coherently.

HasProtectedRootNode/HasProtectedChildNode API is consistent with existing Has* methods.


178-188: Protected-root accessor is straightforward and safe on nil indexes.

LGTM.


223-233: Protected-child accessor mirrors root accessor correctly.

LGTM.

v2/pkg/engine/plan/visitor.go (3)

342-345: Capturing enclosing type names early is necessary for coordinates.

This enables stable GraphCoordinate and extension entries.


211-223: Recording planner-field associations on LeaveField is correct.

Post-order ensures children precede parents, matching test ordering of FieldsRequestedBy.


1344-1397: Dependency extraction to GraphCoordinates looks consistent.

Uses fieldEnclosingTypeNames and dependencyKind to flag IsKey/IsRequires. LGTM.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
v2/pkg/engine/plan/visitor.go (1)

1336-1339: Assignments to promoted fields are valid; previous compile error is resolved

Using singleFetch.CoordinateDependencies and singleFetch.FieldsRequestedBy works via the embedded FetchConfiguration; no need to qualify further.

🧹 Nitpick comments (5)
v2/pkg/engine/plan/visitor.go (2)

636-646: Consider O(1) membership for skipFieldsRefs

Linear scan is fine for small slices; if this grows, switch to a map[int]struct{} or build a lazy index once per document.

 func (v *Visitor) skipField(ref int) bool {
-	// If this grows, switch to map[int]struct{} for O(1).
-	for _, skipRef := range v.skipFieldsRefs {
-		if skipRef == ref {
-			return true
-		}
-	}
-	return false
+	// If this grows, switch to map[int]struct{} for O(1).
+	for _, skipRef := range v.skipFieldsRefs {
+		if skipRef == ref {
+			return true
+		}
+	}
+	return false
 }

1399-1489: Prevent duplicate BySubgraphs entries when merging duplicates

If the same (type, field) appears multiple times in plannerFields, the merge path appends BySubgraphs without re-deduping, leading to duplicates.

Apply this minimal fix inside the merge branch:

-            if i, ok := index[key]; ok {
+            if i, ok := index[key]; ok {
                 // True should overwrite false.
                 result[i].ByUser = result[i].ByUser || byUser
                 if len(bySubgraphs) > 0 {
-                    result[i].BySubgraphs = append(result[i].BySubgraphs, bySubgraphs...)
+                    result[i].BySubgraphs = append(result[i].BySubgraphs, bySubgraphs...)
+                    slices.Sort(result[i].BySubgraphs)
+                    result[i].BySubgraphs = slices.Compact(result[i].BySubgraphs)
                     result[i].ReasonIsKey = result[i].ReasonIsKey || isKey
                     result[i].ReasonIsRequires = result[i].ReasonIsRequires || isRequires
                 }
                 continue
             }
v2/pkg/engine/resolve/fetch.go (3)

350-351: Document semantics of FieldsRequestedBy on FetchConfiguration

Add a short doc comment: protected fields only; used for upstream extension propagation; no execution impact.

 type FetchConfiguration struct {
@@
-	FieldsRequestedBy []RequestedField
+	// FieldsRequestedBy contains provenance for protected fields (schema @protected).
+	// Used for optional propagation to subgraphs via request extensions; does not affect execution.
+	FieldsRequestedBy []RequestedField

356-386: Clarify equality: explicitly ignore FieldsRequestedBy

Equals already ignores CoordinateDependencies; call out FieldsRequestedBy too to avoid confusion.

 	// Note: we do not compare CoordinateDependencies, as they contain more detailed
 	// dependencies information that is already present in the FetchDependencies on the fetch itself
+	// Note: we also intentionally do not compare FieldsRequestedBy, as it is diagnostic/telemetry-only
+	// and does not affect fetch execution semantics.

416-425: Align JSON field names with existing GraphCoordinate conventions

Use typeName/fieldName for consistency and to avoid conflation with GraphQL’s __typename.

 type RequestedField struct {
-	TypeName         string   `json:"__typename"`
-	FieldName        string   `json:"field"`
+	TypeName         string   `json:"typeName"`
+	FieldName        string   `json:"fieldName"`
 	BySubgraphs      []string `json:"bySubgraphs,omitempty"`
 	ByUser           bool     `json:"byUser,omitempty"`
 	ReasonIsKey      bool     `json:"reasonIsKey,omitempty"`
 	ReasonIsRequires bool     `json:"reasonIsRequires,omitempty"`
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 17136a9 and 0267751.

📒 Files selected for processing (4)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/plan/visitor.go (7 hunks)
  • v2/pkg/engine/resolve/fetch.go (11 hunks)
  • v2/pkg/engine/resolve/loader.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • v2/pkg/engine/resolve/loader.go
  • execution/engine/execution_engine_test.go
🔇 Additional comments (9)
v2/pkg/engine/plan/visitor.go (2)

49-49: Good addition: inverse dependency map captured

Storing the inverse mapping (fieldRefAllowsFieldRefs) on the visitor simplifies reasoning when assembling “requested by” provenance.


58-66: Planner/field tracking maps look correct

The three maps (plannerFields, fieldPlanners, fieldEnclosingTypeNames) are initialized in EnterDocument and populated in AllowVisitor/EnterField — solid groundwork for per-fetch attribution.

v2/pkg/engine/resolve/fetch.go (7)

104-111: OK: methods use promoted fields from embedded FetchConfiguration

Returning s.CoordinateDependencies / s.FieldsRequestedBy via embedding keeps the API simple.


171-181: BatchEntityFetch wiring looks consistent

New storage and accessor for FieldsRequestedBy aligns with SingleFetch semantics.


191-194: OK: BatchEntityFetch.RequestedFields implemented


224-234: EntityFetch carries FieldsRequestedBy and CoordinateDependencies

Matches BatchEntityFetch; consistent across variants.


244-247: OK: EntityFetch.RequestedFields implemented


283-285: OK: ParallelListItemFetch delegates to underlying SingleFetch


25-26: Ensure all Fetch implementations define RequestedFields()

The Fetch interface in v2/pkg/engine/resolve/fetch.go was extended with:

RequestedFields() []RequestedField

Because Go requires that every type implementing an interface must define all its methods, please manually verify that each concrete type providing a FetchKind() method also implements RequestedFields(). A quick way to locate them is:

rg -nP 'func\s*\(\*?[A-Za-z_][A-Za-z0-9_]*\)\s*FetchKind\(' -C1 v2/pkg/engine/resolve

Then, for every listed type T, ensure there is:

func (t *T) RequestedFields() []RequestedField {
    // TODO: return the list of fields this fetch operation needs
}

This check couldn’t be fully automated with the previous script—absence of results isn’t evidence that everything is covered. Please confirm by eyeballing or enhancing the search pattern to match any variations in receiver names or return-type qualifiers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
v2/pkg/engine/resolve/fetch.go (1)

420-429: Unify JSON field names with GraphCoordinate; avoid “__typename”.

To prevent confusion with GraphQL’s introspection field and to keep consistency with GraphCoordinate {typeName, fieldName}, prefer typeName/fieldName over __typename/field. This is an externalized API—best to settle it now.

 type RequestedField struct {
-  TypeName         string   `json:"__typename"`
-  FieldName        string   `json:"field"`
+  TypeName         string   `json:"typeName"`
+  FieldName        string   `json:"fieldName"`
   BySubgraphs      []string `json:"bySubgraphs,omitempty"`
   ByUser           bool     `json:"byUser,omitempty"`
   ReasonIsKey      bool     `json:"reasonIsKey,omitempty"`
   ReasonIsRequires bool     `json:"reasonIsRequires,omitempty"`
 }

Follow-ups:

  • Update loader serialization and tests expecting the old keys.
  • If different reasons apply per subgraph, consider future-proofing with a per-subgraph detail structure; otherwise clarify that reasons are aggregated across the fetch.
🧹 Nitpick comments (5)
v2/pkg/engine/resolve/fetch.go (5)

20-26: Document semantics of the new interface method.

Add a brief contract so callers know it’s per-fetch, protected-only, and read-only.

 type Fetch interface {
   FetchKind() FetchKind
   Dependencies() *FetchDependencies
   DataSourceInfo() DataSourceInfo
   DependenciesCoordinates() []FetchDependency
+  // RequestedFields returns provenance for protected fields used by this fetch.
+  // The returned slice must be treated as read-only and may be nil.
   RequestedFields() []RequestedField
 }

108-110: Return a cloned slice to avoid external mutation.

Expose a defensive copy so callers can’t mutate internal state.

 func (s *SingleFetch) RequestedFields() []RequestedField {
-  return s.FieldsRequestedBy
+  return slices.Clone(s.FieldsRequestedBy)
 }

191-194: Defensive copy for requested fields.

Mirror SingleFetch behavior here.

 func (b *BatchEntityFetch) RequestedFields() []RequestedField {
-  return b.FieldsRequestedBy
+  return slices.Clone(b.FieldsRequestedBy)
 }

244-247: Defensive copy for requested fields.

Keep the slice immutable to callers.

 func (e *EntityFetch) RequestedFields() []RequestedField {
-  return e.FieldsRequestedBy
+  return slices.Clone(e.FieldsRequestedBy)
 }

283-285: Defensive copy for requested fields.

Mirror other getters to prevent accidental mutation.

 func (p *ParallelListItemFetch) RequestedFields() []RequestedField {
-  return p.Fetch.FieldsRequestedBy
+  return slices.Clone(p.Fetch.FieldsRequestedBy)
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0267751 and 23526ad.

📒 Files selected for processing (1)
  • v2/pkg/engine/resolve/fetch.go (12 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
🔇 Additional comments (7)
v2/pkg/engine/resolve/fetch.go (7)

104-106: LGTM: use promoted field for deps.

Switching to s.CoordinateDependencies removes unnecessary coupling to FetchConfiguration.


127-142: Docs improvement LGTM.

The expanded post-processing comments are clearer.


170-181: Per-fetch storage on BatchEntityFetch: ensure stable, deduped content.

Looks good. Upstream builder should dedupe and sort FieldsRequestedBy (by typeName, fieldName) for deterministic payloads and tests.

Would you like me to add a sorting step in the planner where the slice is built?


224-234: EntityFetch wiring LGTM.

Field reordering plus per-fetch provenance storage is consistent with other fetch types.


279-282: LGTM: use promoted deps from inner fetch.

Keeps ParallelListItemFetch thin and consistent.


350-356: Good placement and equality semantics.

Storing FieldsRequestedBy on FetchConfiguration and excluding it from Equals keeps plan equality stable while enabling propagation. Consider explicitly documenting that a nil slice should suppress the extension entirely in loader.


368-373: Equality exclusions LGTM.

Not comparing derived metadata (CoordinateDependencies, FieldsRequestedBy) is correct.

@ysmolski ysmolski changed the title feat: send to subgraphs why fields where included in the request feat: build a structure that contains reasons for fields being requested from a subgraph Aug 28, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
execution/engine/execution_engine_test.go (1)

4357-4358: Should second subgraph also receive fetch_reasons?

expectedBody2 omits extensions. If the intention is to propagate only for types/fields marked via FetchReasonFields, this is fine. Consider adding a variant where id-2 has FetchReasonFields including title and assert the extension is present to cover the multi-subgraph case.

🧹 Nitpick comments (11)
v2/pkg/engine/plan/datasource_configuration.go (4)

79-82: Optional: compress three sets into one bitmask map to reduce memory

If memory becomes a concern on large schemas, replace three maps with one map[string]uint8 and use bit flags (field/external/fetchReason). Keeps lookups O(1) while cutting overhead.


129-134: Nit: preallocate child maps like root maps

Minor perf consistency: pre-size child maps using the known lengths (mirrors root indexing).

Apply:

-            d.childNodesIndex[typeName] = fieldsIndex{
-                fields:            make(map[string]struct{}),
-                externalFields:    make(map[string]struct{}),
-                fetchReasonFields: make(map[string]struct{}),
-            }
+            d.childNodesIndex[typeName] = fieldsIndex{
+                fields:            make(map[string]struct{}, len(d.ChildNodes[i].FieldNames)),
+                externalFields:    make(map[string]struct{}, len(d.ChildNodes[i].ExternalFieldNames)),
+                fetchReasonFields: make(map[string]struct{}, len(d.ChildNodes[i].FetchReasonFields)),
+            }

178-189: DRY the Has methods via a small helper*

Several Has* methods duplicate nil/typeName checks. Consider a private helper to reduce repetition and future drift.

Add outside the changed ranges:

func hasField(idx map[string]fieldsIndex, typeName, fieldName string, sel func(fieldsIndex) map[string]struct{}) bool {
	if idx == nil {
		return false
	}
	fi, ok := idx[typeName]
	if !ok {
		return false
	}
	_, ok = sel(fi)[fieldName]
	return ok
}

// Then:
func (d *DataSourceMetadata) HasFetchReasonRootNode(typeName, fieldName string) bool {
	return hasField(d.rootNodesIndex, typeName, fieldName, func(fi fieldsIndex) map[string]struct{} { return fi.fetchReasonFields })
}

223-234: Apply the same helper for child fetch-reason checks

Reuse the helper for symmetry with root checks.

execution/engine/execution_engine_test.go (3)

218-219: Unify option naming: use “PropagateFetchReasons” consistently.

Tests use propagateFieldsRequestedBy while ResolverOptions uses PropagateFetchReasons. Rename the test toggle for clarity and grep-ability.

-    propagateFieldsRequestedBy                   bool
+    propagateFetchReasons                        bool
@@
-                PropagateFetchReasons:                        opts.propagateFieldsRequestedBy,
+                PropagateFetchReasons:                        opts.propagateFetchReasons,

Also update the call sites:

-            eto.propagateFieldsRequestedBy = true
+            eto.propagateFetchReasons = true

Also applies to: 266-267


827-875: Good coverage of request extension payload for simple fetch.

POST + extensions.fetch_reasons with expected keys and values looks correct. Consider adding a negative test that uses POST with propagation disabled to assert the extension is omitted (guards against regressions).


214-221: Optional: table-driven flag wiring.

executionTestOptions is growing. Consider table-driving flags in tests to reduce boilerplate.

v2/pkg/engine/plan/visitor.go (2)

636-646: skipField is O(n); switch to a set if this grows.

If skipFieldsRefs becomes large, convert to map[int]struct{} for O(1) lookups.

- for _, skipRef := range v.skipFieldsRefs {
-     if skipRef == ref { return true }
- }
+ if v.skipFieldsSet != nil {
+     _, ok := v.skipFieldsSet[ref]
+     return ok
+ }

1399-1490: Deterministic ordering for fetch_reasons to avoid flaky tests.

Order of plannerFields can vary; sort reasons by (TypeName, FieldName) before returning.

 func (v *Visitor) buildFetchReasons(fetchID int) []resolve.FetchReason {
@@
-    return reasons
+    // Ensure stable order for deterministic request payloads
+    slices.SortFunc(reasons, func(a, b resolve.FetchReason) int {
+        if c := strings.Compare(a.TypeName, b.TypeName); c != 0 {
+            return c
+        }
+        return strings.Compare(a.FieldName, b.FieldName)
+    })
+    return reasons
 }
v2/pkg/engine/resolve/fetch.go (2)

421-431: Align JSON field names across payloads.

FetchReason uses typename/field while GraphCoordinate uses typeName/fieldName. Prefer a single convention (camelCase) for client ergonomics.

-type FetchReason struct {
-    TypeName      string   `json:"typename"`
-    FieldName     string   `json:"field"`
-    FromSubgraphs []string `json:"from_subgraphs,omitempty"`
-    FromUser      bool     `json:"from_user,omitempty"`
-    IsKey         bool     `json:"is_key,omitempty"`
-    IsRequires    bool     `json:"is_requires,omitempty"`
-}
+type FetchReason struct {
+    TypeName      string   `json:"typeName"`
+    FieldName     string   `json:"fieldName"`
+    FromSubgraphs []string `json:"fromSubgraphs,omitempty"`
+    FromUser      bool     `json:"fromUser,omitempty"`
+    IsKey         bool     `json:"isKey,omitempty"`
+    IsRequires    bool     `json:"isRequires,omitempty"`
+}

Note: requires updating tests’ expected bodies accordingly.


350-354: Docstring: clarify scope (“protected fields”).

Consider clarifying that FieldFetchReasons are produced only for fields marked in TypeField.FetchReasonFields (proxy for “protected” fields) to align repo docs with behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 23526ad and cd2c802.

📒 Files selected for processing (9)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (4 hunks)
  • v2/pkg/engine/plan/datasource_configuration.go (5 hunks)
  • v2/pkg/engine/plan/type_field.go (1 hunks)
  • v2/pkg/engine/plan/visitor.go (7 hunks)
  • v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go (2 hunks)
  • v2/pkg/engine/resolve/fetch.go (12 hunks)
  • v2/pkg/engine/resolve/loader.go (2 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • v2/pkg/engine/resolve/loader.go
  • v2/pkg/engine/plan/type_field.go
  • v2/pkg/engine/postprocess/create_concrete_single_fetch_types.go
  • v2/pkg/engine/resolve/resolve.go
🧰 Additional context used
🧬 Code graph analysis (3)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • FetchReason (423-430)
execution/engine/execution_engine_test.go (5)
v2/pkg/starwars/starwars.go (2)
  • FileSimpleHeroQuery (13-13)
  • Schema (55-59)
v2/pkg/engine/plan/datasource_configuration.go (2)
  • DataSource (293-306)
  • DataSourceMetadata (34-56)
v2/pkg/engine/plan/type_field.go (1)
  • TypeField (3-8)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-357)
v2/pkg/engine/plan/visitor.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • FetchReason (423-430)
🔇 Additional comments (15)
v2/pkg/engine/plan/datasource_configuration.go (4)

54-56: LGTM: type-scoped indexes for fast lookups

Per-type maps for root/child node indexes are clear and efficient.


108-115: LGTM: root index preallocation

Good use of capacities for fields/external/fetchReason maps to minimize rehashing.


116-124: LGTM: root index population

Straightforward, safe writes; duplicates naturally deduped by map set.


136-144: LGTM: child index population

Consistent with root population; read-only after init so safe for concurrent reads.

v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (2)

1086-1089: LGTM: opt-in list for reason-capturing looks precise.

The FetchReasonFields for Address (id, line1, line2) is a sensible minimal set. Please ensure we have (or add) a negative test asserting that fields not listed here never appear in FieldFetchReasons, to prevent future regressions.


1268-1271: LGTM: targeted reasons on address.service.

Limiting FetchReasonFields to line3 and zip on Address keeps the signal focused. Same request as above: consider a check that other Address fields are not emitted into fetch reasons for this DS.

execution/engine/execution_engine_test.go (3)

839-839: Confirm key casing for extension fields.

You serialize reasons with keys typename and field. Elsewhere (GraphCoordinate) you use typeName and fieldName. Decide on a single convention across all payloads to avoid client confusion.

Would you like to standardize on camelCase (typeName, fieldName) and update tests accordingly?


4507-4542: Nice end-to-end validation of nested propagation.

run with extension toggles propagation and reuses the shared data source builder. Looks good.


4349-4472: ```shell
#!/bin/bash

Search for configureFetch invocations to verify it's called after fieldRefAllowsFieldRefs is set

rg -nC3 'configureFetch' -n v2/pkg/engine


</blockquote></details>
<details>
<summary>v2/pkg/engine/plan/visitor.go (3)</summary><blockquote>

`207-223`: **Planner-to-field bookkeeping looks correct.**

Storing plannerFields and fieldPlanners on LeaveField prevents duplication and keeps fetch-local ordering.

---

`1336-1339`: **Correct placement of dependencies and reasons on SingleFetch.**

Assigning CoordinateDependencies and FieldFetchReasons here is the right layer.

---

`49-66`: **Inverse and direct dependency maps are populated correctly**  
In NewPlanner (`planner.go`), `fieldRefAllowsFieldRefs` is initialized via `inverseMap(selectionsConfig.fieldRefDependsOn)`; in `Visitor.LeaveField` (`visitor.go`), `plannerFields`, `fieldPlanners` and `fieldEnclosingTypeNames` are appended for each field.

</blockquote></details>
<details>
<summary>v2/pkg/engine/resolve/fetch.go (3)</summary><blockquote>

`20-26`: **Interface extension is coherent.**

Adding FetchReasons() to Fetch and implementing on all fetch kinds is consistent with the propagation feature.

---

`100-111`: **Accessors correctly read promoted/own fields.**

Returning CoordinateDependencies and FieldFetchReasons from the right structs looks good.




Also applies to: 187-194, 240-247, 279-285

---

`350-357`: **Treating FieldFetchReasons as derived metadata is correct.**

Excluding FieldFetchReasons (and CoordinateDependencies) from Equals prevents false cache misses.




Also applies to: 369-374

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@ysmolski
Copy link
Contributor Author

@devsergiy, Things were renamed using the agreed terminology. Open for review.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
v2/pkg/engine/resolve/resolve.go (2)

152-154: Fix doc comment mismatch with field name

Comment mentions AllowedCustomSubgraphErrorFields but the field is AllowedSubgraphErrorFields.

-	// AllowedCustomSubgraphErrorFields defines which fields are allowed in the subgraph error when in passthrough mode
+	// AllowedSubgraphErrorFields defines which fields are allowed in the subgraph error when in passthrough mode

164-167: Align fetch-reasons option naming between Resolver and Loader

  • Rename Resolver option PropagateFetchReasons → PropagateFieldFetchReasons to match loader flag propagateFieldFetchReasons.
  • Clarify comment: PropagateFieldFetchReasons enables sending the “fetch_reasons” extension under response.body.extensions to upstream GraphQL subgraphs; never exposed to clients.

Apply:

--- a/v2/pkg/engine/resolve/resolve.go
@@ -164,7 +164,7 @@ type Options struct {
-   // PropagateFetchReasons enables sending to upstream subgraphs (only) the "fetch_reasons"
-   // extension that explains why each field was requested. This flag does not expose the data to clients.
-   PropagateFetchReasons bool
+   // PropagateFieldFetchReasons enables sending the "fetch_reasons" extension
+   // under response.body.extensions to upstream GraphQL subgraphs; never exposed to clients.
+   PropagateFieldFetchReasons bool
--- a/v2/pkg/engine/resolve/resolve.go
@@ -249 +249
-           propagateFieldFetchReasons: options.PropagateFetchReasons,
+           propagateFieldFetchReasons: options.PropagateFieldFetchReasons,

Also update all references (including execution_engine_test.go).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cd2c802 and 15f569f.

📒 Files selected for processing (1)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
v2/pkg/engine/resolve/resolve.go (1)
v2/pkg/engine/resolve/resolvable.go (1)
  • ResolvableOptions (62-67)
🔇 Additional comments (1)
v2/pkg/engine/resolve/resolve.go (1)

169-169: Doc: New() comment tweak looks good

No issues.

@devsergiy devsergiy changed the title feat: build a structure that contains reasons for fields being requested from a subgraph feat: add support of field selection reasons extensions Aug 28, 2025
@@ -46,6 +47,7 @@ type Visitor struct {
skipFieldsRefs []int
fieldRefDependsOnFieldRefs map[int][]int
fieldDependencyKind map[fieldDependencyKey]fieldDependencyKind
fieldRefAllowsFieldRefs map[int][]int // inverse of fieldRefDependsOnFieldRefs
Copy link
Member

Choose a reason for hiding this comment

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

maybe fieldRefDependencyForFieldRefs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah. It is a tricky stuff. I did not want it to be similar to the other field "fieldRefDependsOnFieldRefs"...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What about fieldRefDependants? Dependant is the inverse of "depends on".

@@ -54,13 +56,13 @@ type Visitor struct {
indirectInterfaceFields map[int]indirectInterfaceField
pathCache map[astvisitor.VisitorKind]map[int]string

// plannerFields tells which fields are planned on which planners
// map plannerID -> []fieldRef
// plannerFields maps plannerID to fieldRefs planned on this planner.
Copy link
Member

Choose a reason for hiding this comment

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

I think it is better to keep an explicit explanation of what the key and the value are in a map

Copy link
Member

Choose a reason for hiding this comment

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

Even better will be explanation like this

map[plannerID][]fieldRef

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It (key and value explanation) is included into this phrase "plannerFields maps plannerID to fieldRefs".
In general, "T maps A to B" is equivalent to T map[A]B

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
v2/pkg/engine/plan/visitor.go (1)

1338-1341: Assign via embedded struct for clarity (no-op behavior-wise).

Being explicit avoids confusion if SingleFetch gains same-named fields later.

-		singleFetch.CoordinateDependencies = v.resolveFetchDependencies(internal.fetchID)
-		singleFetch.FieldFetchReasons = v.buildFetchReasons(internal.fetchID)
+		singleFetch.FetchConfiguration.CoordinateDependencies = v.resolveFetchDependencies(internal.fetchID)
+		singleFetch.FetchConfiguration.FieldFetchReasons = v.buildFetchReasons(internal.fetchID)
🧹 Nitpick comments (3)
execution/engine/execution_engine_test.go (2)

218-218: Rename test option to match ResolverOptions and feature naming.

Use “fetchReasons” consistently (not “fieldsRequestedBy”).

-	propagateFieldsRequestedBy                   bool
+	propagateFetchReasons                        bool
-				PropagateFetchReasons:                        opts.propagateFieldsRequestedBy,
+				PropagateFetchReasons:                        opts.propagateFetchReasons,
-			eto.propagateFieldsRequestedBy = true
+			eto.propagateFetchReasons = true
-				eto.propagateFieldsRequestedBy = true
+				eto.propagateFetchReasons = true

Also applies to: 267-267, 876-876, 4540-4540


4349-4472: Add a requires-chain coverage to FetchReasons.

Current tests validate IsKey. Add a case where a field is included due to @requires to assert is_requires=true in the propagated extension.

Also applies to: 4507-4543

v2/pkg/engine/plan/visitor.go (1)

637-640: Tighten the comment for skipField.

Minor doc polish.

-// skipField returns true if the field was added by the query planner as a dependency.
-// For another field and should not be included in the response.
-// If it returns false, the user requests the field.
+// skipField returns true if the field was added by the planner as a dependency
+// for another field and should not be included in the response.
+// If it returns false, the field was requested by the client.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e77ab73 and 3b3c1ab.

📒 Files selected for processing (4)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (4 hunks)
  • v2/pkg/engine/plan/visitor.go (8 hunks)
  • v2/pkg/engine/resolve/fetch.go (12 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
execution/engine/execution_engine_test.go (5)
v2/pkg/starwars/starwars.go (2)
  • FileSimpleHeroQuery (13-13)
  • Schema (55-59)
v2/pkg/engine/plan/datasource_configuration.go (2)
  • DataSource (296-309)
  • DataSourceMetadata (34-56)
v2/pkg/engine/plan/type_field.go (1)
  • TypeField (3-8)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-357)
v2/pkg/engine/plan/visitor.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • FetchReason (423-430)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
🔇 Additional comments (3)
v2/pkg/engine/resolve/fetch.go (3)

104-110: FetchReasons API wiring looks correct.

All fetch kinds expose FetchReasons through concrete implementations. Good separation and promotion usage.

Also applies to: 279-285, 191-197, 244-247


350-354: Exclude FieldFetchReasons from equality — aligns with intent.

Treating reasons as derived metadata (non-semantic) is appropriate for plan caching.

Also applies to: 359-374


421-430: JSON contract for FetchReason is concise and matches tests.

Naming (typename, field, by_subgraphs, etc.) is consistent with expected payloads.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
v2/pkg/engine/plan/planner.go (1)

232-244: Minor: pre-size the map; optionally dedup inputs

Pre-size to reduce rehashing; dedup only if upstream can emit duplicates.

Apply this diff:

-func inverseMap(m map[int][]int) map[int][]int {
-	inverse := make(map[int][]int)
+func inverseMap(m map[int][]int) map[int][]int {
+	inverse := make(map[int][]int, len(m))
 	for k, v := range m {
 		for _, v2 := range v {
 			inverse[v2] = append(inverse[v2], k)
 		}
 	}
v2/pkg/engine/plan/visitor.go (3)

637-648: Fix comment grammar; keep TODO concise

Small readability nit; logic can stay linear for now.

Apply this diff:

-// skipField returns true if the field was added by the query planner as a dependency.
-// For another field and should not be included in the response.
-// If it returns false, the user requests the field.
+// skipField returns true if the field was added by the query planner as a dependency
+// for another field and should not be included in the response.
+// If it returns false, the field was requested by the client.
 func (v *Visitor) skipField(ref int) bool {
-	// TODO: If this grows, switch to map[int]struct{} for O(1).
+	// TODO: If this grows, switch to map[int]struct{} for O(1).
 	for _, skipRef := range v.skipFieldsRefs {
 		if skipRef == ref {
 			return true
 		}
 	}
 	return false
 }

1358-1396: Avoid parameter shadowing for readability

Inner loop variable fetchID shadows function param; use a distinct name.

Apply this diff:

-	for _, fetchID := range depFieldPlanners { // planner index == fetchID
-		ofc := v.planners[fetchID].ObjectFetchConfiguration()
+	for _, depPlannerID := range depFieldPlanners { // planner index == fetchID
+		ofc := v.planners[depPlannerID].ObjectFetchConfiguration()
 		if ofc == nil {
 			continue
 		}
 		origin := resolve.FetchDependencyOrigin{
-			FetchID:  fetchID,
+			FetchID:  depPlannerID,
 			Subgraph: ofc.sourceName,
 			Coordinate: resolve.GraphCoordinate{
 				FieldName: v.Operation.FieldNameString(depFieldRef),
 				TypeName:  v.fieldEnclosingTypeNames[depFieldRef],
 			},
 		}

1485-1489: Optional: move per-reason dedup outside the loop

Sorting/compacting on every merge can be O(k log k) repeatedly. Do a single pass after the loop.

Apply this diff:

-			if reasons[i].BySubgraphs != nil {
-				slices.Sort(reasons[i].BySubgraphs)
-				reasons[i].BySubgraphs = slices.Compact(reasons[i].BySubgraphs)
-			}
+			// Defer sort/compact of BySubgraphs to a final pass.

Then, below the loop and before sorting reasons:

-	slices.SortFunc(reasons, func(a, b resolve.FetchReason) int {
+	for i := range reasons {
+		if reasons[i].BySubgraphs != nil {
+			slices.Sort(reasons[i].BySubgraphs)
+			reasons[i].BySubgraphs = slices.Compact(reasons[i].BySubgraphs)
+		}
+	}
+	slices.SortFunc(reasons, func(a, b resolve.FetchReason) int {
 		return cmp.Or(
 			cmp.Compare(a.TypeName, b.TypeName),
 			cmp.Compare(a.FieldName, b.FieldName),
 		)
 	})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3b3c1ab and 6dfc7ea.

📒 Files selected for processing (3)
  • v2/pkg/engine/plan/planner.go (3 hunks)
  • v2/pkg/engine/plan/visitor.go (8 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-29T09:35:47.950Z
Learnt from: ysmolski
PR: wundergraph/graphql-go-tools#1282
File: v2/pkg/engine/plan/visitor.go:5-5
Timestamp: 2025-08-29T09:35:47.950Z
Learning: The wundergraph/graphql-go-tools project does not support Go versions < 1.23, so compatibility concerns for features available in Go 1.21+ (like cmp.Or) should not be raised.

Applied to files:

  • v2/pkg/engine/plan/visitor.go
🧬 Code graph analysis (2)
v2/pkg/engine/plan/visitor.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • FetchReason (423-430)
v2/pkg/engine/resolve/resolve.go (1)
v2/pkg/engine/resolve/resolvable.go (1)
  • ResolvableOptions (62-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
🔇 Additional comments (10)
v2/pkg/engine/plan/planner.go (2)

5-5: LGTM: deterministic inverse ordering dependency

Importing sort for normalization is appropriate.


157-158: Wire inverse dependencies into planning visitor

Good placement and timing; ensures downstream consumers (e.g., buildFetchReasons) have the inverse relation available before Walk.

v2/pkg/engine/resolve/resolve.go (2)

170-170: Doc tweak LGTM

Constructor comment reads well and is accurate.


250-251: No changes needed
Feature flag plumbing and gating verified: propagateFieldFetchReasons is driven by options.PropagateFetchReasons in ResolverLoader, and fetch reasons are only added when DataSourceMetadata.RequiresFetchReason returns true for @openfed__requireFetchReason fields.

v2/pkg/engine/plan/visitor.go (6)

5-5: LGTM: use cmp for deterministic sort keys

Go 1.23+ is supported per project learnings; cmp.Or usage is fine.


50-50: LGTM: explicit inverse mapping on visitor

Clear naming and purpose; pairs with planner.inverseMap().


59-67: LGTM: tracking planners⇄fields and enclosing type names

The maps enable later dependency/reason computation with minimal coupling.


343-345: LGTM: record enclosing type early (before skip)
Captures type for both user and planner-added fields; required by dependencies/reasons.


1338-1341: SingleFetch CoordinateDependencies and FieldFetchReasons are defined and consumed; no further action needed.


1401-1499: DS.RequiresFetchReason and fetch_reasons extension verified
Implementation covers both root and child nodes (via hasFetchReasonRootNode/ChildNode) and resolve/loader.go injects the fetch_reasons extension. No changes required.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
v2/pkg/engine/resolve/resolve.go (1)

164-165: Resolved: extension key uses “fetch_reasons” (plural). LGTM.

🧹 Nitpick comments (2)
v2/pkg/engine/resolve/resolve.go (2)

152-154: Fix doc/field name mismatch.

Comment says AllowedCustomSubgraphErrorFields but the field is AllowedSubgraphErrorFields. Align to avoid confusion in public API docs.

-	// AllowedCustomSubgraphErrorFields defines which fields are allowed in the subgraph error when in passthrough mode
+	// AllowedSubgraphErrorFields defines which fields are allowed in the subgraph error when in passthrough mode

164-167: Clarify directive gating in the option’s doc.

Explicitly note that only fields annotated with @openfed__requireFetchReason are included.

-	// PropagateFetchReasons enables adding the "fetch_reasons" extension to
-	// upstream subgraph requests. This extension explains why each field was requested.
-	// This flag does not expose the data to clients.
+	// PropagateFetchReasons enables adding the "fetch_reasons" request extension to
+	// upstream subgraph requests. The extension explains why each field was requested.
+	// The router only includes reasons for fields annotated with @openfed__requireFetchReason.
+	// This flag does not expose the data to clients.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6dfc7ea and 4d5fa62.

📒 Files selected for processing (1)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
v2/pkg/engine/resolve/resolve.go (1)
v2/pkg/engine/resolve/resolvable.go (1)
  • ResolvableOptions (62-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
🔇 Additional comments (2)
v2/pkg/engine/resolve/resolve.go (2)

170-170: Doc comment tweak: New() description looks good.


250-251: Verified Loader propagation flag wiring

Loader struct includes the propagateFieldFetchReasons flag, injection into body.extensions.fetch_reasons is correctly gated by that flag (and skips introspection), and no client responses expose fetch_reasons.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
v2/pkg/engine/resolve/loader.go (1)

1611-1611: Re: “will it work if extensions are not present yet?” — yes

jsonparser.Set creates intermediate objects; this path will work when extensions is absent.

execution/engine/execution_engine_test.go (1)

4355-4357: Confirming fix: removed extraneous "some" from second fetch expectations

This resolves the earlier discrepancy where a field not present in the second fetch was listed in the extension.

🧹 Nitpick comments (7)
v2/pkg/engine/resolve/loader.go (3)

178-180: Field name consistency with options

Consider renaming enableRequireFetchReasons to propagateFetchReasons to align with ResolverOptions.EnableRequireFetchReasons and the test nomenclature. This reduces cognitive load.

Apply within this file:

-   enableRequireFetchReasons bool
+   propagateFetchReasons bool

And update references accordingly:

- if l.enableRequireFetchReasons && !IsIntrospectionDataSource(res.ds.ID) {
+ if l.propagateFetchReasons && !IsIntrospectionDataSource(res.ds.ID) {

1601-1617: Gated fetch_reasons injection looks correct; ensure deterministic ordering to avoid test flakes

The guard and path are right, and jsonparser.Set will create body.extensions if missing. One potential source of flakiness: the order of fetchReasons. If generation isn’t already stable, sort by (typename, field) before marshaling or adjust producer-side ordering to guarantee stability.

Would you like me to add a small sort at the producer (where FetchReasons are built) to guarantee order?


1601-1617: Do not silently overwrite an existing extensions.fetch_reasons

If upstream middleware already set extensions.fetch_reasons via l.ctx.Extensions, this will overwrite it. If coexistence is desired, merge/append instead or skip when present.

execution/engine/execution_engine_test.go (4)

218-219: Option naming: align with feature name

propagateFieldsRequestedBy → propagateFetchReasons for consistency with extensions.fetch_reasons and ResolverOptions.EnableRequireFetchReasons.

-    propagateFieldsRequestedBy                   bool
+    propagateFetchReasons                        bool

263-268: Correct wiring of the flag into ResolverOptions

EnableRequireFetchReasons uses the test option as intended. After the suggested rename, adjust the mapping.

-                EnableRequireFetchReasons:                    opts.propagateFieldsRequestedBy,
+                EnableRequireFetchReasons:                    opts.propagateFetchReasons,

827-879: Great positive path coverage for fetch_reasons

Test validates injection and expected shape. If FetchReasons ordering isn’t guaranteed upstream, this exact string assertion could become brittle. Consider normalizing JSON in the round-tripper for body equality or sorting fetchReasons before encode.


4349-4472: makeDataSource: expectations and metadata look coherent; minor robustness nits

  • expectedBody1 matches the annotated FetchReasonFields on the first subgraph. Good.
  • expectedBody2 intentionally omits fetch_reasons; matches metadata (no FetchReasonFields). Good.
  • To reduce brittleness, prefer comparing JSON semantically (object/array equality) in the test transport rather than raw string order.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4d5fa62 and 08ca330.

📒 Files selected for processing (3)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/resolve/loader.go (2 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • v2/pkg/engine/resolve/resolve.go
🧰 Additional context used
🧬 Code graph analysis (2)
v2/pkg/engine/resolve/loader.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • Fetch (20-26)
execution/engine/execution_engine_test.go (5)
v2/pkg/starwars/starwars.go (2)
  • FileSimpleHeroQuery (13-13)
  • Schema (55-59)
v2/pkg/engine/plan/datasource_configuration.go (2)
  • DataSource (296-309)
  • DataSourceMetadata (34-56)
v2/pkg/engine/plan/type_field.go (1)
  • TypeField (3-8)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-357)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
🔇 Additional comments (1)
execution/engine/execution_engine_test.go (1)

4507-4542: Both “with” and “without” extension scenarios covered — nice

Dual runs verify gating behavior via EnableRequireFetchReasons and expected request bodies.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
v2/pkg/engine/resolve/loader.go (1)

1611-1611: Re: “Will it work if extensions are not present yet?”—yes

This path creates/mutates extensions as needed; tests confirm it. Nothing to change.

🧹 Nitpick comments (3)
v2/pkg/engine/resolve/loader.go (2)

33-37: Hoist extension key to a package constant to avoid drift

Use a single constant for the "fetch_reasons" key to keep parity across files/tests and prevent typos.

@@
 const (
   IntrospectionSchemaTypeDataSourceID     = "introspection__schema&__type"
   IntrospectionTypeFieldsDataSourceID     = "introspection__type__fields"
   IntrospectionTypeEnumValuesDataSourceID = "introspection__type__enumValues"
 )
+// request extension keys
+const extensionKeyFetchReasons = "fetch_reasons"
@@
-            input, res.err = jsonparser.Set(input, encoded, "body", "extensions", "fetch_reasons")
+            input, res.err = jsonparser.Set(input, encoded, "body", "extensions", extensionKeyFetchReasons)

Also applies to: 1601-1617


1594-1600: Double-check non-object extensions input

If ctx.Extensions is present but not an object (e.g., "null" or a scalar), jsonparser.Set on body.extensions.fetch_reasons may behave unexpectedly. Consider normalizing extensions to {} when non-object.

Would you like a small helper to coerce body.extensions to an object before adding keys?

Also applies to: 1601-1617

execution/engine/execution_engine_test.go (1)

827-827: Tighten test name

Consider renaming to: “execute simple hero operation with fetch reason propagation”.

- t.Run("execute simple hero operation with propagating to subgraphs reason for fields being requested", runWithoutError(
+ t.Run("execute simple hero operation with fetch reason propagation", runWithoutError(
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 08ca330 and ee74209.

📒 Files selected for processing (3)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/resolve/loader.go (2 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
v2/pkg/engine/resolve/resolve.go (1)
v2/pkg/engine/resolve/resolvable.go (1)
  • ResolvableOptions (62-67)
v2/pkg/engine/resolve/loader.go (1)
v2/pkg/engine/resolve/fetch.go (1)
  • Fetch (20-26)
execution/engine/execution_engine_test.go (5)
v2/pkg/starwars/starwars.go (2)
  • FileSimpleHeroQuery (13-13)
  • Schema (55-59)
v2/pkg/engine/plan/datasource_configuration.go (2)
  • DataSource (296-309)
  • DataSourceMetadata (34-56)
v2/pkg/engine/plan/type_field.go (1)
  • TypeField (3-8)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-357)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
🔇 Additional comments (6)
v2/pkg/engine/resolve/loader.go (1)

1601-1617: fetch_reasons injection logic looks correct and safely gated

  • Gated by option and introspection IDs.
  • Only sets when reasons are non-empty.
  • Proper error propagation via res.err.
v2/pkg/engine/resolve/resolve.go (1)

164-168: Option + wiring LGTM

PropagateFetchReasons is documented clearly and correctly wired through newTools into Loader.

Also applies to: 250-251

execution/engine/execution_engine_test.go (4)

214-219: Test harness flagging/wiring is clean

New propagateFetchReasons option is plumbed into ResolverOptions correctly.

Also applies to: 262-267


827-879: Solid happy-path coverage for fetch_reasons propagation

  • Uses POST to carry extensions.
  • Verifies exact payload including reasons for Query.hero and Character.name.

4355-4357: Nested fetch scenario expectations look consistent

expectedBody1/2 match the planned selections, and the second subgraph body omits fetch_reasons appropriately.

Also applies to: 4426-4431


4349-4472: Deterministic ordering confirmed in buildFetchReasons: the final reasons slice is sorted via slices.SortFunc on (TypeName, FieldName), and each BySubgraphs list is sorted and compacted, guaranteeing stable JSON output.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
execution/engine/execution_engine_test.go (1)

4357-4357: Previous issue about extraneous "some" in expectedBody2 is resolved.

expectedBody2 no longer lists unrelated fields; thanks for fixing.

🧹 Nitpick comments (5)
execution/engine/execution_engine_test.go (5)

214-219: Add a dedicated test option for fetch-reason propagation (LGTM, minor ergonomics).

The new propagateFetchReasons toggle is fine. Consider adding a small helper like withValueCompletion() to keep call sites consistent.

Proposed helper (place near withValueCompletion):

func withPropagateFetchReasons() executionTestOptions {
	return func(o *_executionTestOptions) { o.propagateFetchReasons = true }
}

827-874: Positive test for extension payload is precise and scoped (POST + targeted FetchReasonFields).

  • The expected body matches the POST method and only includes fields whitelisted via FetchReasonFields.
  • Nice: both root (Query.hero) and child (Character.name) reasons are asserted.

Potential flake guard: comparing the outbound body structurally (JSON) rather than byte-for-byte would reduce brittleness if field order changes.


875-878: Prefer a helper to set the option.

Minor consistency win with withValueCompletion().

-		func(eto *_executionTestOptions) {
-			eto.propagateFetchReasons = true
-		},
+		withPropagateFetchReasons(),

Add the helper:

func withPropagateFetchReasons() executionTestOptions {
	return func(o *_executionTestOptions) { o.propagateFetchReasons = true }
}

4349-4472: De-duplicate "id-2" and make expectedBody1 resilient to DS id drift.

Hardcoding id-2 in the embedded JSON is easy to desync from the DS id below. Suggest deriving it once and formatting into the JSON.

-		makeDataSource := func(t *testing.T, expectFetchReasons bool) []plan.DataSource {
+		makeDataSource := func(t *testing.T, expectFetchReasons bool) []plan.DataSource {
+			const secondID = "id-2"
 			var expectedBody1 string
 			var expectedBody2 string
 			if !expectFetchReasons {
 				expectedBody1 = `{"query":"{accounts {__typename ... on User {some {__typename id}} ... on Admin {some {__typename id}}}}"}`
 			} else {
-				expectedBody1 = `{"query":"{accounts {__typename ... on User {some {__typename id}} ... on Admin {some {__typename id}}}}","extensions":{"fetch_reasons":[{"typename":"Admin","field":"some","by_user":true},{"typename":"User","field":"id","by_subgraphs":["id-2"],"by_user":true,"is_key":true}]}}`
+				expectedBody1 = fmt.Sprintf(`{"query":"{accounts {__typename ... on User {some {__typename id}} ... on Admin {some {__typename id}}}}","extensions":{"fetch_reasons":[{"typename":"Admin","field":"some","by_user":true},{"typename":"User","field":"id","by_subgraphs":["%s"],"by_user":true,"is_key":true}]}}`, secondID)
 			}
 			expectedBody2 = `{"query":"query($representations: [_Any!]!){_entities(representations: $representations){... on User {__typename title}}}","variables":{"representations":[{"__typename":"User","id":"1"},{"__typename":"User","id":"3"}]}}`
 
 			return []plan.DataSource{
 				mustGraphqlDataSourceConfiguration(t,
 					"id-1",
@@
-				mustGraphqlDataSourceConfiguration(t,
-					"id-2",
+				mustGraphqlDataSourceConfiguration(t,
+					secondID,

4507-4542: Extension-enabled run is correctly gated by test option and DS expectations.

Nice separation via makeDataSource(t, true) and the option toggle.

Consider switching to withPropagateFetchReasons() for symmetry with other option helpers (see prior suggestion).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ee74209 and f48b53c.

📒 Files selected for processing (1)
  • execution/engine/execution_engine_test.go (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
execution/engine/execution_engine_test.go (6)
v2/pkg/starwars/starwars.go (2)
  • FileSimpleHeroQuery (13-13)
  • Schema (55-59)
v2/pkg/engine/plan/datasource_configuration.go (2)
  • DataSource (296-309)
  • DataSourceMetadata (34-56)
v2/pkg/engine/plan/type_field.go (1)
  • TypeField (3-8)
v2/pkg/engine/datasource/graphql_datasource/configuration.go (4)
  • ConfigurationInput (16-23)
  • FetchConfiguration (124-128)
  • SchemaConfiguration (135-139)
  • FederationConfiguration (130-133)
v2/pkg/engine/resolve/fetch.go (2)
  • Fetch (20-26)
  • FetchConfiguration (314-357)
v2/pkg/engine/plan/configuration.go (1)
  • FieldConfiguration (101-118)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
  • GitHub Check: Build and test (go 1.23 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.23 / windows-latest)
🔇 Additional comments (3)
execution/engine/execution_engine_test.go (3)

262-267: Wiring PropagateFetchReasons into ResolverOptions looks correct.

Good plumb-through; default remains false for all other tests.

Please confirm there are no unintended extensions added in tests that don’t enable the option (a quick grep for "fetch_reasons" in round-tripper expectations should only hit the two new cases).


844-857: FetchReasonFields are minimal and accurate.

Limiting to hero and name avoids leaking reasons on non-annotated fields.


4503-4504: Baseline run without extension uses makeDataSource(..., false) — good control.

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