Skip to content

feat: validate presence of optional @requires dependencies#1297

Merged
ysmolski merged 22 commits intomasterfrom
yury/eng-7767-validate-presence-of-optional-requires-dependencies
Sep 24, 2025
Merged

feat: validate presence of optional @requires dependencies#1297
ysmolski merged 22 commits intomasterfrom
yury/eng-7767-validate-presence-of-optional-requires-dependencies

Conversation

@ysmolski
Copy link
Copy Markdown
Contributor

@ysmolski ysmolski commented Sep 17, 2025

In GraphQL Federation, when a field has @requires dependencies on
external nullable field and those dependencies fail to resolve,
the entire entity resolution would lead to malformed data being sent
to later fetches.

For example, 2 subgraphs:

# Subgraph 1
type Entity @key(fields: "id") {
	id: ID
	info: String
}

# Subgraph 2, where fullInfo requires 
# the "info" field to be resolved first.
type Query {
	entity: [Entity]
}
type Entity @key(fields: "id") {
	id: ID
	info: String @external
	fullInfo: String @requires(fields: "info")
}

If the fetch from Subgraph1 returned the null value for
the "info" field along with error pointing to that field, then
this bit of information should not be used in the fetch
from Subgraph 2 that provides info to get the value of
the "fullInfo" field.

This change implements validation of optional @requires dependencies
in GraphQL Federation. The core feature allows the system to
gracefully handle scenarios where entities fail to resolve their
@requires dependencies, particularly for nullable fields.

This change introduces selective handling where only nullable @requires fields
are treated as "optional" - if they fail, the entity is marked as "tainted" but
processing continues for other entities.

Summary by CodeRabbit

  • New Features
    • Optional runtime validation to omit entities when nullable external "@requires" dependencies fail.
    • Taint-tracking to detect and ignore tainted entities from failed dependency fetches.
    • Planner can emit __typename in nested selections to support requires-driven fetches; fetch metadata now records field nullability.
  • Tests
    • Expanded suites for federation scenarios, required-fields logic, tainted-object detection, and conditional response routing for tests.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 17, 2025

Walkthrough

Adds a configuration flag to validate nullable external @requires, propagates it through planner and resolver, records Nullable on fetch reasons, implements taint-tracking in the loader to skip entities whose required fields failed, forces __typename emission in nested selections when enabled, and updates/extends tests and test helpers for these behaviors.

Changes

Cohort / File(s) Summary
Execution test helpers
execution/engine/execution_engine_helpers_test.go
Reworked test round-tripper to support conditional responses keyed by request body, added conditionalTestCase/sendResponse types and createConditionalTestRoundTripper, and adjusted error messaging.
Execution engine tests
execution/engine/execution_engine_test.go
Added test harness option to enable required-external-field validation, threaded it into engine/resolver creation, added federation-like tests covering nullable/optional @requires scenarios and fetch-reason interactions, and added a conditional HTTP client helper.
Federation datasource tests
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go
Updated expected plan data to mark multiple fetch-reason entries as Nullable: true.
Plan configuration
v2/pkg/engine/plan/configuration.go
Added Configuration.ValidateRequiredExternalFields bool with docs to enable validation of nullable @requires (requires BuildFetchReasons).
Node selection plumbing
v2/pkg/engine/plan/node_selection_builder.go
v2/pkg/engine/plan/node_selection_visitor.go
Plumbed new visitor flag addTypenameInNestedSelections from config into node selection visitor and into required-fields configuration.
Required-fields visitor & tests
v2/pkg/engine/plan/required_fields_visitor.go
v2/pkg/engine/plan/required_fields_visitor_test.go
Added enforcement to emit __typename in nested selections when configured; added comprehensive tests for addRequiredFields, fragment generation, typename handling, and many nested scenarios.
Plan visitor / fetch reasons
v2/pkg/engine/plan/visitor.go
Replaced resolveFetchDependencies with buildFetchDependencies, added buildFetchReasons and fieldDefinitionRef, and include Nullable when constructing FetchReasons.
Resolve: FetchReason change
v2/pkg/engine/resolve/fetch.go
Added Nullable bool field to FetchReason (json:"-").
Resolver options
v2/pkg/engine/resolve/resolve.go
Added ResolverOptions.ValidateRequiredExternalFields bool to carry config into the loader.
Loader: taint-tracking & merge changes
v2/pkg/engine/resolve/loader.go
Added validateRequiredExternalFields and taintedObjs state; reworked selection/merge flows to be taint-aware, added taint detection, taint-aware error rendering (renderErrorsFailedDeps), and lifecycle handling for tainted state.
Taint utilities & tests
v2/pkg/engine/resolve/tainted_objects.go
v2/pkg/engine/resolve/tainted_objects_test.go
New taint-tracking helpers (getTaintedIndices, selectObjectAndIndex, taintedObjects) with traversal limits and comprehensive tests covering path traversal and taint detection scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly summarizes the primary change—adding validation/handling for optional @requires dependencies in federation—and matches the PR description and code changes, making it specific and clear for reviewers scanning history.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate Docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@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: 5

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)

1422-1450: Nullable derivation can silently miscompute when the field isn’t found

fieldDefinitionRef may return 0 on miss (depending on upstream API), which passes the ast.InvalidRef check and can read the wrong type, producing incorrect Nullable. Use the boolean “ok” and return InvalidRef when false.

Apply this diff:

-       var nullable bool
-       fieldDefRef := v.fieldDefinitionRef(typeName, fieldName)
-       if fieldDefRef != ast.InvalidRef {
-           typeRef := v.Definition.FieldDefinitionType(fieldDefRef)
-           nullable = !v.Definition.TypeIsNonNull(typeRef)
-       }
+       var nullable bool
+       fieldDefRef := v.fieldDefinitionRef(typeName, fieldName)
+       if fieldDefRef != ast.InvalidRef {
+           typeRef := v.Definition.FieldDefinitionType(fieldDefRef)
+           nullable = !v.Definition.TypeIsNonNull(typeRef)
+       }

(and fix fieldDefinitionRef below).

🧹 Nitpick comments (12)
v2/pkg/engine/plan/node_selection_visitor.go (1)

50-51: Name/intent clarity for new flag.

enforceTypenameForRequired is clear, but it overlaps conceptually with allowTypename used elsewhere. Consider a short comment explaining how it differs from allowTypename to prevent misuse.

-    enforceTypenameForRequired bool
+    // enforceTypenameForRequired forces __typename injection only for @requires-driven additions
+    // (distinct from allowTypename used along key paths).
+    enforceTypenameForRequired bool
v2/pkg/engine/resolve/fetch.go (1)

371-372: Keep Nullable internal-only (do not serialize) or opt-in explicitly

PropagatedFetchReasons are JSON-marshaled into the "fetch_reasons" extension (loader.go) but FetchReason.Nullable has json:"-" so it is not sent to subgraphs — leave as-is if this is a planner-only hint; otherwise change the tag and update tests.

 type FetchReason struct {
   TypeName    string   `json:"typename"`
   FieldName   string   `json:"field"`
   BySubgraphs []string `json:"by_subgraphs,omitempty"`
   ByUser      bool     `json:"by_user,omitempty"`
   IsKey       bool     `json:"is_key,omitempty"`
   IsRequires  bool     `json:"is_requires,omitempty"`
-  Nullable    bool     `json:"-"`
+  // Nullable is derived from the field's AST type. It is intentionally not serialized
+  // into the "fetch_reason" extension and is used internally for planning/analysis.
+  Nullable    bool     `json:"-"`
 }
v2/pkg/engine/plan/required_fields_visitor.go (1)

49-51: Doc tweak: clarify scope beyond “has inline fragments”

The comment says “in the 'requires' key”, but with enforceTypenameForRequired true we also add __typename even when there are no fragments (non-key requires). Consider expanding the comment for accuracy.

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

43-48: Guard rail: validate HandleOptionalRequiresDeps requires BuildFetchReasons

The struct doc states this flag requires BuildFetchReasons=true, but nothing enforces it. Recommend validating during planner/engine construction and either:

  • auto-enable BuildFetchReasons when HandleOptionalRequiresDeps is true, or
  • return a clear error/log a warning.

This prevents silent no-ops or confusing behavior.

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

1453-1454: Fix test message formatting

The assert message has a dangling format specifier with no args. It prints literally “%s”.

-            assert.Equal(t, tt.expectedIndices, indices, "Tainted indices mismatch: %s")
+            assert.Equal(t, tt.expectedIndices, indices, "Tainted indices mismatch")
execution/engine/execution_engine_helpers_test.go (2)

69-93: Assert HTTP method in conditional round-tripper

conditionalTestCase includes expectedMethod but it’s not asserted. Add the check to catch accidental GET/POST regressions.

 		assert.Equal(t, testCase.expectedHost, req.URL.Host)
 		assert.Equal(t, testCase.expectedPath, req.URL.Path)
+		if testCase.expectedMethod != "" {
+			assert.Equal(t, testCase.expectedMethod, req.Method)
+		}

55-67: Optional: reduce brittleness of body-keyed responses

Keying the response map by the raw request body is fragile (whitespace/order). If this becomes flaky, parse/canonicalize the JSON before map lookup.

execution/engine/execution_engine_test.go (3)

279-286: Ensure option coupling in tests

The tests correctly set both BuildFetchReasons and HandleOptionalRequiresDeps. Add a defensive guard to fail fast if someone enables HandleOptionalRequiresDeps without withFetchReasons() in future edits.

-            engineConf.plannerConfig.BuildFetchReasons = opts.propagateFetchReasons
-            engineConf.plannerConfig.HandleOptionalRequiresDeps = opts.handleOptionalRequiresDeps
+            engineConf.plannerConfig.BuildFetchReasons = opts.propagateFetchReasons
+            engineConf.plannerConfig.HandleOptionalRequiresDeps = opts.handleOptionalRequiresDeps
+            if opts.handleOptionalRequiresDeps && !opts.propagateFetchReasons {
+                t.Fatalf("HandleOptionalRequiresDeps requires BuildFetchReasons (use withFetchReasons())")
+            }

4589-4601: Body-equality routing can be brittle

These conditional responses rely on exact body matches. If planner output formatting changes (field order/spacing), tests may fail spuriously. Consider switching the round-tripper to compare canonicalized JSON bodies.


5267-5277: Reuse method assertion for conditional client

Pass expectedMethod in callers (e.g., "POST") and assert it inside the round-tripper (see helpers change). Keeps HTTP semantics explicit in these federation tests.

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

1096-1105: Fix known bug: integer segments in error path rewriting

Use type-aware conversion when rebuilding paths to avoid empty strings for numbers.

Apply this diff:

-                for j := i + 1; j < len(pathItems); j++ {
-                    // BUG: for pathItems containing integers, it will append empty strings
-                    newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
-                }
+                for j := i + 1; j < len(pathItems); j++ {
+                    switch pathItems[j].Type() {
+                    case astjson.TypeString:
+                        newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
+                    case astjson.TypeNumber:
+                        newPath = append(newPath, strconv.Itoa(pathItems[j].GetInt()))
+                    }
+                }

619-644: Optional @requires nulls without errors won't taint — add a feature-gated scan of _entities for nulls.

loader.go only computes taintedIndices when subgraph errors are present (res.postProcessing.SelectResponseErrorsPath → getTaintedIndices), so a subgraph returning null for a requested nullable @requires field without emitting an error will not mark the entity tainted; consider a follow-up that scans responseData._entities[*] for nulls of requested nullable @requires fields (feature-gated).
Location: v2/pkg/engine/resolve/loader.go (error handling / getTaintedIndices call).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8686cb4 and 98d1c56.

📒 Files selected for processing (13)
  • execution/engine/execution_engine_helpers_test.go (1 hunks)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go (5 hunks)
  • v2/pkg/engine/plan/configuration.go (1 hunks)
  • v2/pkg/engine/plan/node_selection_builder.go (1 hunks)
  • v2/pkg/engine/plan/node_selection_visitor.go (2 hunks)
  • v2/pkg/engine/plan/required_fields_visitor.go (2 hunks)
  • v2/pkg/engine/plan/required_fields_visitor_test.go (1 hunks)
  • v2/pkg/engine/plan/visitor.go (7 hunks)
  • v2/pkg/engine/resolve/fetch.go (1 hunks)
  • v2/pkg/engine/resolve/loader.go (16 hunks)
  • v2/pkg/engine/resolve/loader_test.go (2 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
v2/pkg/engine/plan/required_fields_visitor_test.go (1)
v2/pkg/engine/plan/required_fields_visitor.go (2)
  • RequiredFieldsFragment (19-27)
  • QueryPlanRequiredFieldsFragment (29-38)
v2/pkg/engine/plan/visitor.go (2)
v2/pkg/engine/resolve/fetch.go (1)
  • FetchDependency (336-344)
v2/pkg/ast/ast.go (1)
  • InvalidRef (8-8)
v2/pkg/engine/resolve/loader_test.go (2)
v2/pkg/engine/resolve/fetch.go (5)
  • FetchReason (364-372)
  • FetchInfo (376-397)
  • FetchKind (11-11)
  • FetchKindSingle (14-14)
  • FetchDependencies (111-114)
v2/pkg/engine/resolve/loader.go (1)
  • Loader (158-186)
execution/engine/execution_engine_test.go (3)
execution/engine/execution_engine.go (1)
  • NewExecutionEngine (112-148)
v2/pkg/engine/resolve/resolve.go (1)
  • ResolverOptions (92-170)
v2/pkg/engine/datasource/httpclient/nethttpclient.go (1)
  • DefaultNetHttpClient (39-45)
v2/pkg/engine/resolve/loader.go (4)
v2/pkg/engine/resolve/context.go (1)
  • Context (16-35)
v2/pkg/engine/resolve/response.go (1)
  • GraphQLResponse (34-42)
v2/pkg/engine/resolve/resolvable.go (1)
  • Resolvable (26-61)
v2/pkg/engine/resolve/fetch.go (3)
  • Fetch (20-27)
  • FetchInfo (376-397)
  • GraphCoordinate (399-403)
⏰ 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.25 / windows-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
🔇 Additional comments (18)
v2/pkg/engine/resolve/resolve.go (2)

253-254: Propagation to Loader acknowledged — default behavior preserved.

handleOptionalRequiresDeps is only read to gate getTaintedIndices; when false no tainted indices are computed/applied and ignoreTainted is a no-op for an empty map, so error semantics are unchanged.


169-170: Public API addition: check for unkeyed struct literals. Adding HandleOptionalRequiresDeps to ResolverOptions is source-compatible for keyed literals but will break any unkeyed composite (positional) literals. Sweep the repo and examples for unkeyed ResolverOptions usages and convert them to keyed form (FieldName: value). Quick check: rg -n --type=go 'ResolverOptions\s*{' and inspect each match for entries without explicit keys.

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

465-475: Resolved — enforceTypenameForRequired is honored when allowTypename=false

addRequiredFields builds the fragment with config.allowTypename (RequiredFieldsFragment at required_fields_visitor.go:62) but still calls addTypenameSelection when (config.enforceTypenameForRequired && !config.isKey), so __typename is added for non-key requires even if allowTypename is false (see required_fields_visitor.go around lines 156–160).

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

55-57: Confirmed — flag only affects 'requires' additions, not keys or regular selections.

enforceTypenameForRequired is only used by addRequiredFields (v2/pkg/engine/plan/required_fields_visitor.go); the code adds __typename only when keySelectionSetHasFragments OR (enforceTypenameForRequired && !isKey). node_selection_builder.go wires the flag from config.HandleOptionalRequiresDeps.

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

3139-3179: LGTM: FetchReason.Nullable matches SDL nullability for user/account/address

  • Query.user (definition shows user: User), User.account (account: Account), and Account.address (address: Address) are all nullable in the SDL here, so marking these fetch reasons as Nullable: true is correct and prevents false negatives when entities are missing.

3271-3290: LGTM: Account.address reason marked nullable

Address is nullable on Account in the SDL; the added Nullable: true aligns with that.


3426-3446: LGTM: Consistent nullability on address fetch reason

Same rationale as above; good consistency across steps of the requires chain.


3580-3591: LGTM: Final hop keeps Account.address nullable

Keeps behavior consistent across the pipeline; expected.

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

1153-1241: Nice coverage for entity index extraction

Good table tests across mixed paths, missing keys, negatives, and complex cases.

Consider adding one case where the path crosses an object with a non-array numeric key (e.g., {"1": {...}}) to assert it doesn’t get treated as an array index.

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

13-33: Solid table-driven coverage; asserts target the right internal counters

The suite meaningfully exercises keys vs. requires, nested selections, alias remapping, and typename enforcement. Good use of astprinter normalization for structural equality.


168-199: Alias remap expectation looks correct

The remap path assertion for conflicting arguments matches the internal aliasing contract. No issues spotted.


376-429: Typename enforcement cases cover both key and requires

Good distinction: no __typename for keys even with enforcement; added for requires on nested objects. This aligns with the intended wiring.

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

1342-1342: Switched to buildFetchDependencies — OK

The rename and call site update are consistent. No behavior concerns here.


1352-1364: Propagating only required fetch reasons — OK; minor micro-nit

Logic is sound. Minor: skip building the propagated slice when lookup is empty to avoid a loop.
[raise_nitpick_issue]

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

181-187: Good: taint tracking state kept per Loader

Fields and map type are appropriate for pointer identity to entity objects.


195-201: Initialize taint map per execution

Correct lifecycle placement.


590-617: Error handling flow is consistent with Apollo compatibility flags

HasErrors gating and later value-completion suppression are coherent. No change required.


1147-1148: Good: dedicated reason for missing @requires dependencies

Clear message; pairs well with the taint logic.

Comment thread v2/pkg/engine/plan/required_fields_visitor.go
Comment thread v2/pkg/engine/plan/visitor.go
Comment thread v2/pkg/engine/resolve/loader.go
Comment thread v2/pkg/engine/resolve/loader.go Outdated
Comment thread v2/pkg/engine/resolve/loader.go Outdated
Copy link
Copy Markdown
Contributor

@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 (2)
v2/pkg/engine/resolve/loader.go (2)

367-381: Prune tainted items after every traversal step to prevent child leaks (still pending)

Currently pruning only at start (empty path) and once at the end allows children of tainted parents to slip through during intermediate traversals.

Apply this diff:

 func (l *Loader) selectItemsForPath(path []FetchItemPathElement) []*astjson.Value {
-  items := []*astjson.Value{l.resolvable.data}
-  if len(path) == 0 {
-    items = l.ignoreTainted(items)
-    return items
-  }
+  items := []*astjson.Value{l.resolvable.data}
+  if len(path) == 0 {
+    return l.ignoreTainted(items)
+  }
   for i := range path {
     if len(items) == 0 {
       break
     }
     items = l.selectItems(items, path[i])
+    items = l.ignoreTainted(items)
   }
-  items = l.ignoreTainted(items)
   return items
 }

835-867: Array traversal bug in extractEntityIndex (indexes treated as object keys)

Numeric path elements must index arrays, not call Get("idx"). Current code returns nil for arrays and breaks taint detection.

Apply this diff:

-func extractEntityIndex(response *astjson.Value, path []*astjson.Value) (*astjson.Value, int) {
-  index := -1
-  if len(path) == 0 {
-    return nil, index
-  }
-  for _, el := range path {
-    var key string
-    switch el.Type() {
-    case astjson.TypeNumber:
-      parsed := el.GetInt()
-      if parsed < 0 {
-        return nil, index
-      }
-      if index == -1 {
-        // index is assigned only once
-        index = parsed
-      }
-      key = strconv.Itoa(parsed)
-    case astjson.TypeString:
-      key = unsafebytes.BytesToString(el.GetStringBytes())
-    default:
-      return nil, -1
-    }
-    response = response.Get(key)
-    if response == nil {
-      return nil, -1
-    }
-  }
-  return response, index
-}
+func extractEntityIndex(response *astjson.Value, path []*astjson.Value) (*astjson.Value, int) {
+  idx := -1
+  if len(path) == 0 || response == nil {
+    return nil, idx
+  }
+  cur := response
+  for _, el := range path {
+    switch el.Type() {
+    case astjson.TypeNumber:
+      i := el.GetInt()
+      if i < 0 || cur.Type() != astjson.TypeArray {
+        return nil, idx
+      }
+      if idx == -1 {
+        idx = i
+      }
+      arr := cur.GetArray()
+      if i >= len(arr) {
+        return nil, idx
+      }
+      cur = arr[i]
+    case astjson.TypeString:
+      key := unsafebytes.BytesToString(el.GetStringBytes())
+      cur = cur.Get(key)
+      if cur == nil {
+        return nil, idx
+      }
+    default:
+      return nil, idx
+    }
+  }
+  return cur, idx
+}
🧹 Nitpick comments (3)
v2/pkg/engine/resolve/loader.go (3)

655-657: Guard single-item tainting by response shape

Index 0 only makes sense when responseData is an array (e.g., _entities). Add a shape check.

Apply this diff:

-    if slices.Contains(taintedIndices, 0) {
+    if responseData.Type() == astjson.TypeArray && slices.Contains(taintedIndices, 0) {
       l.taintedEntities[items[0]] = struct{}{}
     }

766-833: getTaintedIndices: solid logic; dedupe indices to avoid redundant work

Functionally correct. Consider de-duplicating indices to avoid repeated Contains checks downstream.

Apply this diff:

-func (l *Loader) getTaintedIndices(fetch Fetch, response *astjson.Value, errs *astjson.Value) (indices []int) {
+func (l *Loader) getTaintedIndices(fetch Fetch, response *astjson.Value, errs *astjson.Value) (indices []int) {
+  seen := make(map[int]struct{})
   info := fetch.FetchInfo()
   if info == nil {
     return nil
   }
   // build a map to search with
   requestedForRequires := map[GraphCoordinate]struct{}{}
@@
-        indices = append(indices, index)
+        if _, ok := seen[index]; !ok {
+          indices = append(indices, index)
+          seen[index] = struct{}{}
+        }
         break

1097-1099: Fix BUG in error path rewriting: preserve numeric indices

When rewriting paths, integers are converted to empty strings. Build a []interface{} mixing strings and ints before marshaling.

Apply this diff:

-          // BUG: for pathItems containing integers, it will append empty strings
-          newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
+          var newPathAny []interface{}
+          newPathAny = make([]interface{}, 0, len(pathPrefix)+len(pathItems)-i-1)
+          for _, s := range pathPrefix {
+            newPathAny = append(newPathAny, s)
+          }
+          for j := i + 1; j < len(pathItems); j++ {
+            switch pathItems[j].Type() {
+            case astjson.TypeNumber:
+              newPathAny = append(newPathAny, pathItems[j].GetInt())
+            default:
+              newPathAny = append(newPathAny, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
+            }
+          }
-          newPathJSON, _ := json.Marshal(newPath)
+          newPathJSON, _ := json.Marshal(newPathAny)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98d1c56 and 3ca88bd.

📒 Files selected for processing (4)
  • v2/pkg/engine/plan/required_fields_visitor.go (2 hunks)
  • v2/pkg/engine/plan/visitor.go (7 hunks)
  • v2/pkg/engine/resolve/loader.go (16 hunks)
  • v2/pkg/engine/resolve/loader_test.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • v2/pkg/engine/resolve/loader_test.go
  • v2/pkg/engine/plan/required_fields_visitor.go
  • v2/pkg/engine/plan/visitor.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-08T09:43:07.433Z
Learnt from: Noroth
PR: wundergraph/graphql-go-tools#1262
File: v2/pkg/engine/datasource/grpc_datasource/json_builder.go:0-0
Timestamp: 2025-08-08T09:43:07.433Z
Learning: In v2/pkg/engine/datasource/grpc_datasource/json_builder.go, mergeEntities intentionally uses the loop index when calling indexMap.getResultIndex because the index map is type-aware, making per-type counters unnecessary under the current assumptions. Avoid suggesting per-type ordinal counters for this path in future reviews.

Applied to files:

  • v2/pkg/engine/resolve/loader.go
🧬 Code graph analysis (1)
v2/pkg/engine/resolve/loader.go (3)
v2/pkg/engine/resolve/response.go (1)
  • GraphQLResponse (34-42)
v2/pkg/engine/resolve/resolvable.go (1)
  • Resolvable (26-61)
v2/pkg/engine/resolve/fetch.go (3)
  • Fetch (20-27)
  • FetchInfo (376-397)
  • GraphCoordinate (399-403)
⏰ 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.25 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.25 / windows-latest)
  • GitHub Check: Build and test (go 1.25 / windows-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
🔇 Additional comments (5)
v2/pkg/engine/resolve/loader.go (5)

181-186: Taint tracking lifecycle looks correct

State is initialized per request and cleared in Free(); no leaks.

Also applies to: 192-193, 199-200


383-396: ignoreTainted: OK

Simple and cheap filter; fits the hot path.


680-687: Batch taint marking: LGTM

Index-based taint mapping aligns with batchStats semantics.

Also applies to: 703-705


1148-1149: Clear, user-facing error message

The new constant is concise and accurate.


181-186: Verify HandleOptionalRequiresDeps wiring and test coverage

  • Wiring exists end‑to‑end: plan/configuration.go (HandleOptionalRequiresDeps), plan/node_selection_builder.go (uses config.HandleOptionalRequiresDeps), v2/pkg/engine/resolve/resolve.go (passes options.HandleOptionalRequiresDeps into resolver/loader), v2/pkg/engine/resolve/loader.go (handleOptionalRequiresDeps field + usage when hasErrors && postProcessing.SelectResponseDataPath != nil).
  • Tests: execution/engine/execution_engine_test.go defines withHandleOptionalRequiresDeps() and multiple tests exercise the enabled path (helper at ~line 241; usages around ~lines 279, 4884, 5050, 5251).
  • Action: confirm there are explicit tests asserting the disabled behavior (flag unset); add tests if missing.

Comment thread v2/pkg/engine/resolve/loader.go
Copy link
Copy Markdown
Contributor

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
v2/pkg/engine/resolve/loader.go (2)

684-699: Batch+dedup: defer taint marking until after merges per item

Same pointer-stability concern. Track a per-item taint flag and mark after the last merge for that item.

Apply this diff:

-    for i, stats := range res.batchStats {
-      for _, idx := range stats {
+    for i, stats := range res.batchStats {
+      taintedThis := false
+      for _, idx := range stats {
         if idx == -1 {
           continue
         }
-        if slices.Contains(taintedIndices, idx) {
-          l.taintedEntities[items[i]] = struct{}{}
-        }
         items[i], _, err = astjson.MergeValuesWithPath(items[i], batch[idx], res.postProcessing.MergePath...)
         if err != nil {
           return errors.WithStack(ErrMergeResult{
             Subgraph: res.ds.Name,
             Reason:   err,
             Path:     fetchItem.ResponsePath,
           })
         }
+        if !taintedThis && slices.Contains(taintedIndices, idx) {
+          taintedThis = true
+        }
       }
+      if taintedThis {
+        l.taintedEntities[items[i]] = struct{}{}
+      }
     }

706-718: Non-dedup batch: capture returned value and mark after merge

You ignore the returned pointer and mark before merging. Reassign items[i] and mark after to keep taint filtering effective.

Apply this diff:

-    for i, item := range items {
-      if slices.Contains(taintedIndices, i) {
-        l.taintedEntities[item] = struct{}{}
-      }
-      _, _, err = astjson.MergeValuesWithPath(item, batch[i], res.postProcessing.MergePath...)
+    for i := range items {
+      items[i], _, err = astjson.MergeValuesWithPath(items[i], batch[i], res.postProcessing.MergePath...)
       if err != nil {
         return errors.WithStack(ErrMergeResult{
           Subgraph: res.ds.Name,
           Reason:   err,
           Path:     fetchItem.ResponsePath,
         })
       }
+      if slices.Contains(taintedIndices, i) {
+        l.taintedEntities[items[i]] = struct{}{}
+      }
     }
♻️ Duplicate comments (2)
v2/pkg/engine/resolve/loader.go (2)

367-381: Prune tainted after every traversal step to prevent child leaks

Calling ignoreTainted only at the end lets children of tainted parents slip through on deeper paths. Prune immediately after each selectItems step and early-break when empty.

Apply this diff:

 func (l *Loader) selectItemsForPath(path []FetchItemPathElement) []*astjson.Value {
   items := []*astjson.Value{l.resolvable.data}
   if len(path) == 0 {
-    items = l.ignoreTainted(items)
-    return items
+    return l.ignoreTainted(items)
   }
   for i := range path {
     if len(items) == 0 {
       break
     }
     items = l.selectItems(items, path[i])
+    items = l.ignoreTainted(items)
+    if len(items) == 0 {
+      break
+    }
   }
-  items = l.ignoreTainted(items)
   return items
 }

844-877: Array traversal bug in extractEntityIndex: index arrays, don’t Get("idx")

Numeric path elements must index into arrays; converting to string and calling Get("1") is incorrect/fragile.

Apply this diff:

-func extractEntityIndex(response *astjson.Value, path []*astjson.Value) (*astjson.Value, int) {
-  index := -1
-  if len(path) == 0 {
-    return nil, index
-  }
-  for _, el := range path {
-    var key string
-    switch el.Type() {
-    case astjson.TypeNumber:
-      parsed := el.GetInt()
-      if parsed < 0 {
-        return nil, index
-      }
-      if index == -1 {
-        // index is assigned only once
-        index = parsed
-      }
-      key = strconv.Itoa(parsed)
-    case astjson.TypeString:
-      key = unsafebytes.BytesToString(el.GetStringBytes())
-    default:
-      return nil, -1
-    }
-    response = response.Get(key)
-    if response == nil {
-      return nil, -1
-    }
-  }
-  return response, index
-}
+func extractEntityIndex(response *astjson.Value, path []*astjson.Value) (*astjson.Value, int) {
+  index := -1
+  if len(path) == 0 || response == nil {
+    return nil, index
+  }
+  for _, el := range path {
+    switch el.Type() {
+    case astjson.TypeNumber:
+      i := el.GetInt()
+      if i < 0 || response.Type() != astjson.TypeArray {
+        return nil, index
+      }
+      if index == -1 {
+        index = i
+      }
+      arr := response.GetArray()
+      if i >= len(arr) {
+        return nil, index
+      }
+      response = arr[i]
+    case astjson.TypeString:
+      key := unsafebytes.BytesToString(el.GetStringBytes())
+      response = response.Get(key)
+      if response == nil {
+        return nil, index
+      }
+    default:
+      return nil, index
+    }
+  }
+  return response, index
+}
🧹 Nitpick comments (9)
v2/pkg/engine/resolve/loader_test.go (2)

1153-1249: Safer string quoting for JSON path elements

Building JSON strings via concatenation can break with special characters. Use json.Marshal (or strconv.Quote) to ensure correct escaping.

-				case string:
-					path[i] = astjson.MustParse(`"` + v + `"`)
+				case string:
+					// ensure proper JSON string escaping
+					b, _ := json.Marshal(v)
+					path[i] = astjson.MustParse(string(b))

1250-1463: Harden assertions and align with production behavior

  • getTaintedIndices order isn’t guaranteed; assert order-insensitively to avoid flaky tests.
  • Avoid nil vs empty slice brittleness when expecting “no taints”.
  • Tests exercise optional @requires logic; mirror production by enabling the loader flag for clarity.
-			loader := &Loader{}
+			loader := &Loader{}
+			// mirror production behavior when testing optional @requires handling
+			loader.handleOptionalRequiresDeps = true
@@
-			indices := loader.getTaintedIndicesAndCleanErrors(mockFetch, response, errors)
-
-			assert.Equal(t, tt.expectedIndices, indices, "Tainted indices mismatch: %s")
+			indices := loader.getTaintedIndicesAndCleanErrors(mockFetch, response, errors)
+			// order-insensitive and tolerant to nil vs empty
+			if tt.expectedIndices == nil {
+				assert.Empty(t, indices, "Expected no tainted indices")
+			} else {
+				assert.ElementsMatch(t, tt.expectedIndices, indices, "Tainted indices mismatch")
+			}

Optional: also assert that errors are cleaned as expected in positive-taint cases. I can add that if you want.

execution/engine/execution_engine_test.go (5)

279-286: Ensure fetch reasons are built when optional @requires handling is enabled

Optional @requires handling depends on fetch reasons being available. Consider enabling BuildFetchReasons when either propagateFetchReasons or handleOptionalRequiresDeps is set to avoid silent misconfiguration in future tests.

-			engineConf.plannerConfig.BuildFetchReasons = opts.propagateFetchReasons
+			engineConf.plannerConfig.BuildFetchReasons = opts.propagateFetchReasons || opts.handleOptionalRequiresDeps

4560-4718: Make conditional round-tripper robust to field order in printed queries

The key {"query":"{accounts {id __typename}}"} is brittle; the planner may print __typename before id. Either (a) normalize/parse the request JSON in createConditionalTestRoundTripper, or (b) accept both orders here.

 						responses: map[string]sendResponse{
-							`{"query":"{accounts {id __typename}}"}`: {
+							`{"query":"{accounts {id __typename}}"}`: {
 								statusCode: 200,
 								body:       `{"data":{"accounts":[{"__typename":"User","id":"1"},{"__typename":"User","id":"3"}]}}`,
 							},
+							// tolerate alternative field order
+							`{"query":"{accounts {__typename id}}"}`: {
+								statusCode: 200,
+								body:       `{"data":{"accounts":[{"__typename":"User","id":"1"},{"__typename":"User","id":"3"}]}}`,
+							},

If you prefer, I can update the round-tripper to JSON-parse and compare query ASTs/normalized JSON to avoid string exact matches.


4720-4885: Apply same order-tolerance to this test

Duplicate the “__typename id” variant (or improve the round-tripper to be order-insensitive) for the first accounts fetch.


4887-5051: Apply same order-tolerance to this test

First accounts fetch uses {id __typename}; accept {__typename id} as well.


5053-5252: Apply same order-tolerance to this test

First accounts fetch uses {id __typename}; accept {__typename id} as well.

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

1079-1116: Rewrite error paths: preserve numeric segments

The BUG comment is valid. Build []any and copy strings as strings and numbers as ints.

Apply this diff:

-        // rewrite the path to pathPrefix + pathItems after _entities
-        newPath := make([]string, 0, len(pathPrefix)+len(pathItems)-i)
-        newPath = append(newPath, pathPrefix...)
-        for j := i + 1; j < len(pathItems); j++ {
-          // BUG: for pathItems containing integers, it will append empty strings
-          newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
-        }
-        newPathJSON, _ := json.Marshal(newPath)
+        // rewrite the path to pathPrefix + pathItems after _entities, preserving numbers
+        newPath := make([]any, 0, len(pathPrefix)+len(pathItems)-i)
+        for _, seg := range pathPrefix {
+          newPath = append(newPath, seg)
+        }
+        for j := i + 1; j < len(pathItems); j++ {
+          switch pathItems[j].Type() {
+          case astjson.TypeNumber:
+            newPath = append(newPath, pathItems[j].GetInt())
+          case astjson.TypeString:
+            newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
+          }
+        }
+        newPathJSON, _ := json.Marshal(newPath)

659-663: Optional: speed up membership checks

If taintedIndices can grow, precompute a small bool set/map to avoid repeated slices.Contains calls.

Apply this outside-local diff where convenient:

tainted := make(map[int]struct{}, len(taintedIndices))
for _, i := range taintedIndices { tainted[i] = struct{}{} }
// then: if _, ok := tainted[idx]; ok { ... }

Also applies to: 684-699, 706-713

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ca88bd and d51dc4b.

📒 Files selected for processing (3)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/resolve/loader.go (16 hunks)
  • v2/pkg/engine/resolve/loader_test.go (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-08T09:43:07.433Z
Learnt from: Noroth
PR: wundergraph/graphql-go-tools#1262
File: v2/pkg/engine/datasource/grpc_datasource/json_builder.go:0-0
Timestamp: 2025-08-08T09:43:07.433Z
Learning: In v2/pkg/engine/datasource/grpc_datasource/json_builder.go, mergeEntities intentionally uses the loop index when calling indexMap.getResultIndex because the index map is type-aware, making per-type counters unnecessary under the current assumptions. Avoid suggesting per-type ordinal counters for this path in future reviews.

Applied to files:

  • v2/pkg/engine/resolve/loader.go
🧬 Code graph analysis (3)
v2/pkg/engine/resolve/loader_test.go (2)
v2/pkg/engine/resolve/fetch.go (5)
  • FetchReason (364-372)
  • FetchInfo (376-397)
  • FetchKind (11-11)
  • FetchKindSingle (14-14)
  • FetchDependencies (111-114)
v2/pkg/engine/resolve/loader.go (1)
  • Loader (158-186)
execution/engine/execution_engine_test.go (3)
execution/engine/execution_engine.go (1)
  • NewExecutionEngine (112-148)
v2/pkg/engine/resolve/resolve.go (1)
  • ResolverOptions (92-170)
v2/pkg/engine/datasource/httpclient/nethttpclient.go (1)
  • DefaultNetHttpClient (39-45)
v2/pkg/engine/resolve/loader.go (2)
v2/pkg/engine/resolve/resolvable.go (1)
  • Resolvable (26-61)
v2/pkg/engine/resolve/fetch.go (4)
  • Fetch (20-27)
  • FetchInfo (376-397)
  • GraphCoordinate (399-403)
  • FetchItem (29-34)
⏰ 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). (1)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
🔇 Additional comments (14)
v2/pkg/engine/resolve/loader_test.go (2)

6-6: LGTM: imports

Using fmt and astjson here makes sense for path construction and JSON handling in these tests.

Also applies to: 13-14


1465-1480: LGTM: minimal mock fetch

Simple mock with FetchInfo suffices for exercising getTaintedIndicesAndCleanErrors.

execution/engine/execution_engine_test.go (3)

222-223: LGTM: test option added

handleOptionalRequiresDeps on the test options struct is clear and scoped.


241-246: LGTM: helper for enabling optional @requires

withHandleOptionalRequiresDeps is consistent with the existing options style.


5267-5277: Helper definitions present — no action required

createConditionalTestRoundTripper, conditionalTestCase, and sendResponse are defined in execution/engine/execution_engine_helpers_test.go, so testConditionalNetHttpClient has the required helpers.

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

192-193: Lifecycle reset LGTM

taintedEntities is cleared on Free() and reinitialized per response. Looks good.

Also applies to: 199-200


383-396: ignoreTainted is fine, but depends on stable pointers

Given pointer-based keys, ensure you add the post-merge pointers to taintedEntities (see diffs around merging blocks).

Would you like me to add a small invariant test to assert that tainted items are pruned on nested paths?


584-590: Response data selection LGTM

The SelectResponseDataPath handling reads cleanly and is used consistently below.


593-619: Nice: drop optional-@requires errors in-place and still add a single generic error

This preserves unrelated subgraph errors. Good balance.


878-951: mergeErrors signature simplification LGTM

Consolidating the values parameter cleans up call sites.


1158-1159: Constant message LGTM

missingRequiresDependencies reads clearly in user-facing errors.


536-566: Error-path handling overall

The status-code fallbacks and invalid shape handling are consistent with prior behavior. No issues spotted.

Also applies to: 568-573


770-842: In-place error filtering — astjson.Value.Del supports numeric-string array indices (OK)

Verified: astjson.Value.Del accepts decimal-string indexes and removes the array element (shifting remaining items); the current reverse-order deletion (errors.Del(strconv.Itoa(errorIdx))) is safe — no array-rebuild required. (pkg.go.dev)


367-381: Confirm MergeValuesWithPath semantics (in-place vs returning new pointer)

Some call sites reassign the returned Value while others ignore it — verify astjson.MergeValuesWithPath's contract and make usage consistent (prefer reassigning the returned value unless the function is guaranteed to mutate the first arg in-place).
Locations: v2/pkg/engine/resolve/loader.go:662, 691, 710, 1318; v2/pkg/engine/resolve/resolvable.go:137, 144.

Comment thread v2/pkg/engine/resolve/loader.go
Comment thread v2/pkg/engine/resolve/loader.go Outdated
If entity was fetched and stored deep
in the resolvable.data, we should detect it and not
use it.
Copy link
Copy Markdown
Contributor

@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

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/loader.go (1)

1127-1135: Fix numeric path handling in optionallyRewriteErrorPaths

Current code appends empty strings for integer path items. Preserve numeric indices.

-        newPath := make([]string, 0, len(pathPrefix)+len(pathItems)-i)
-        newPath = append(newPath, pathPrefix...)
+        newPath := make([]any, 0, len(pathPrefix)+len(pathItems)-i)
+        for _, s := range pathPrefix {
+          newPath = append(newPath, s)
+        }
         for j := i + 1; j < len(pathItems); j++ {
-          // BUG: for pathItems containing integers, it will append empty strings
-          newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
+          switch pathItems[j].Type() {
+          case astjson.TypeString:
+            newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
+          case astjson.TypeNumber:
+            newPath = append(newPath, pathItems[j].GetInt())
+          }
         }
         newPathJSON, _ := json.Marshal(newPath)
♻️ Duplicate comments (4)
v2/pkg/engine/resolve/loader.go (4)

367-379: Prune tainted items at each traversal step to prevent child leaks

Filter after every traversal hop, not only at the start/end, so children of tainted parents don’t slip through.

 func (l *Loader) selectItemsForPath(path []FetchItemPathElement) []*astjson.Value {
   items := []*astjson.Value{l.resolvable.data}
   if len(path) == 0 {
-    return l.filterNonTainted(items)
+    return l.filterNonTainted(items)
   }
   for i := range path {
     if len(items) == 0 {
       break
     }
     items = l.selectItems(items, path[i])
+    items = l.filterNonTainted(items)
   }
-  return l.filterNonTainted(items)
+  return items
 }

685-689: Mark taint after MergeValuesWithPath to keep pointer identity correct

Move the taint mark after the merge so the map holds the post-merge pointer.

-    if slices.Contains(taintedIndices, 0) {
-      l.taintedEntities[items[0]] = struct{}{}
-    }
-    items[0], _, err = astjson.MergeValuesWithPath(items[0], responseData, res.postProcessing.MergePath...)
+    items[0], _, err = astjson.MergeValuesWithPath(items[0], responseData, res.postProcessing.MergePath...)
     if err != nil {
       return errors.WithStack(ErrMergeResult{
         Subgraph: res.ds.Name,
         Reason:   err,
         Path:     fetchItem.ResponsePath,
       })
     }
+    if slices.Contains(taintedIndices, 0) {
+      l.taintedEntities[items[0]] = struct{}{}
+    }

870-902: extractEntityIndex: index arrays via GetArray/bounds, not Get("idx")

Array traversal using string keys is incorrect and breaks on astjson arrays. Use type/bounds checks and direct indexing.

 func extractEntityIndex(response *astjson.Value, path []*astjson.Value) (*astjson.Value, int) {
-  index := -1
-  if len(path) == 0 {
-    return nil, index
-  }
-  for _, el := range path {
-    var key string
-    switch el.Type() {
-    case astjson.TypeNumber:
-      parsed := el.GetInt()
-      if parsed < 0 {
-        return nil, index
-      }
-      if index == -1 {
-        // index is assigned only once
-        index = parsed
-      }
-      key = strconv.Itoa(parsed)
-    case astjson.TypeString:
-      key = unsafebytes.BytesToString(el.GetStringBytes())
-    default:
-      return nil, -1
-    }
-    response = response.Get(key)
-    if response == nil {
-      return nil, -1
-    }
-  }
-  return response, index
+  idx := -1
+  if len(path) == 0 || response == nil {
+    return nil, idx
+  }
+  cur := response
+  for _, el := range path {
+    switch el.Type() {
+    case astjson.TypeNumber:
+      i := el.GetInt()
+      if i < 0 || cur.Type() != astjson.TypeArray {
+        return nil, idx
+      }
+      if idx == -1 {
+        idx = i
+      }
+      arr := cur.GetArray()
+      if i >= len(arr) {
+        return nil, idx
+      }
+      cur = arr[i]
+    case astjson.TypeString:
+      key := unsafebytes.BytesToString(el.GetStringBytes())
+      cur = cur.Get(key)
+      if cur == nil {
+        return nil, idx
+      }
+    default:
+      return nil, idx
+    }
+  }
+  return cur, idx
 }

709-717: Batch (deduped) merges: re-mark taint after each merge

Same pointer-identity issue here. Mark taint after assigning the merged pointer.

-        if slices.Contains(taintedIndices, idx) {
-          l.taintedEntities[items[i]] = struct{}{}
-        }
-        items[i], _, err = astjson.MergeValuesWithPath(items[i], batch[idx], res.postProcessing.MergePath...)
+        items[i], _, err = astjson.MergeValuesWithPath(items[i], batch[idx], res.postProcessing.MergePath...)
         if err != nil {
           return errors.WithStack(ErrMergeResult{
             Subgraph: res.ds.Name,
             Reason:   err,
             Path:     fetchItem.ResponsePath,
           })
         }
+        if slices.Contains(taintedIndices, idx) {
+          l.taintedEntities[items[i]] = struct{}{}
+        }
🧹 Nitpick comments (2)
v2/pkg/engine/resolve/loader.go (2)

381-422: isTaintedEntity is O(N) deep-scan; prefer post-merge re-marking to avoid hot-path walks

Once you re-mark post-merge pointers (see other comments), you can drop the recursive scan and only check direct membership, which is much cheaper in select hot paths.


871-873: Typo in comment

“atl east” → “at least”.

-// Path should contain atl east the index as the first element. Other elements would lead
+// Path should contain at least the index as the first element. Other elements lead
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d51dc4b and 867c550.

📒 Files selected for processing (1)
  • v2/pkg/engine/resolve/loader.go (16 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-08T09:43:07.433Z
Learnt from: Noroth
PR: wundergraph/graphql-go-tools#1262
File: v2/pkg/engine/datasource/grpc_datasource/json_builder.go:0-0
Timestamp: 2025-08-08T09:43:07.433Z
Learning: In v2/pkg/engine/datasource/grpc_datasource/json_builder.go, mergeEntities intentionally uses the loop index when calling indexMap.getResultIndex because the index map is type-aware, making per-type counters unnecessary under the current assumptions. Avoid suggesting per-type ordinal counters for this path in future reviews.

Applied to files:

  • v2/pkg/engine/resolve/loader.go
🧬 Code graph analysis (1)
v2/pkg/engine/resolve/loader.go (3)
v2/pkg/engine/resolve/response.go (1)
  • GraphQLResponse (34-42)
v2/pkg/engine/resolve/resolvable.go (1)
  • Resolvable (26-61)
v2/pkg/engine/resolve/fetch.go (4)
  • Fetch (20-27)
  • FetchInfo (376-397)
  • GraphCoordinate (399-403)
  • FetchItem (29-34)
⏰ 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.25 / windows-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.25 / windows-latest)
🔇 Additional comments (1)
v2/pkg/engine/resolve/loader.go (1)

809-868: fastjson.Value.Del supports numeric-string indices — no change required.
Value.Del accepts decimal-string keys to remove array elements (e.g. v.Get("bar").Del("0")), so errors.Del(strconv.Itoa(errorIdx)) combined with reverse iteration correctly removes array entries; do not rebuild the array.

Likely an incorrect or invalid review comment.

Comment thread v2/pkg/engine/resolve/loader.go Outdated
Comment thread v2/pkg/engine/resolve/loader.go
Comment thread v2/pkg/engine/resolve/loader.go Outdated
and rename couple of things
Copy link
Copy Markdown
Contributor

@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 (2)

367-379: Fix tainted children leak: prune after each traversal step

Child items of tainted parents can still pass through because filtering only happens at the end. Prune after each step to prevent downstream fetches from using children of tainted entities.

Apply:

 func (l *Loader) selectItemsForPath(path []FetchItemPathElement) []*astjson.Value {
   items := []*astjson.Value{l.resolvable.data}
   if len(path) == 0 {
     return l.filterNonTainted(items)
   }
   for i := range path {
     if len(items) == 0 {
       break
     }
     items = l.selectItems(items, path[i])
+    // Immediately drop tainted items to avoid leaking children from tainted parents
+    items = l.filterNonTainted(items)
   }
-  return l.filterNonTainted(items)
+  return items
 }

872-904: Array traversal bug in extractEntityIndex (indexes arrays via Get("idx"))

On numeric path elements you convert to string and call Get("idx"), which is incorrect for arrays and will fail to locate entities. Index arrays via GetArray()[i] with bounds/type checks.

Apply:

-func extractEntityIndex(response *astjson.Value, path []*astjson.Value) (*astjson.Value, int) {
-  index := -1
-  if len(path) == 0 {
-    return nil, index
-  }
-  for _, el := range path {
-    var key string
-    switch el.Type() {
-    case astjson.TypeNumber:
-      parsed := el.GetInt()
-      if parsed < 0 {
-        return nil, index
-      }
-      if index == -1 {
-        // index is assigned only once
-        index = parsed
-      }
-      key = strconv.Itoa(parsed)
-    case astjson.TypeString:
-      key = unsafebytes.BytesToString(el.GetStringBytes())
-    default:
-      return nil, -1
-    }
-    response = response.Get(key)
-    if response == nil {
-      return nil, -1
-    }
-  }
-  return response, index
-}
+func extractEntityIndex(response *astjson.Value, path []*astjson.Value) (*astjson.Value, int) {
+  index := -1
+  if len(path) == 0 || response == nil {
+    return nil, index
+  }
+  for _, el := range path {
+    switch el.Type() {
+    case astjson.TypeNumber:
+      i := el.GetInt()
+      if i < 0 || response.Type() != astjson.TypeArray {
+        return nil, index
+      }
+      if index == -1 {
+        index = i
+      }
+      arr := response.GetArray()
+      if i >= len(arr) {
+        return nil, index
+      }
+      response = arr[i]
+    case astjson.TypeString:
+      key := unsafebytes.BytesToString(el.GetStringBytes())
+      response = response.Get(key)
+      if response == nil {
+        return nil, index
+      }
+    default:
+      return nil, index
+    }
+  }
+  return response, index
+}
🧹 Nitpick comments (5)
v2/pkg/engine/plan/configuration.go (1)

43-48: Clarify scope: only nullable @requires are treated as optional

Doc the nullability constraint explicitly to avoid confusion, e.g., “Only nullable @requires fields are treated as optional; non‑nullable remain enforced. This requires BuildFetchReasons=true; otherwise this option is a no‑op.”

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

381-394: Avoid recursive deep scans in isTaintedEntity (hot path)

Once per-step pruning is in place, recursion over arrays/objects becomes unnecessary and expensive. Consider replacing isTaintedEntity with a simple pointer-set lookup and keep filtering only at traversal boundaries.

Also applies to: 396-422


1105-1148: Fix path rewriting for integer components

Rewriting error paths currently drops integer indices (empty strings). Handle numbers explicitly.

Apply:

-        for j := i + 1; j < len(pathItems); j++ {
-          // BUG: for pathItems containing integers, it will append empty strings
-          newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
-        }
+        for j := i + 1; j < len(pathItems); j++ {
+          switch pathItems[j].Type() {
+          case astjson.TypeString:
+            newPath = append(newPath, unsafebytes.BytesToString(pathItems[j].GetStringBytes()))
+          case astjson.TypeNumber:
+            newPath = append(newPath, strconv.Itoa(pathItems[j].GetInt()))
+          default:
+            // skip other types
+          }
+        }
v2/pkg/engine/resolve/resolve.go (1)

169-170: Document new ResolverOptions flag

Add a short comment explaining behavior and the BuildFetchReasons prerequisite, mirroring the planner config field docs.

execution/engine/execution_engine_test.go (1)

4560-5051: Solid coverage for optional @requires (nullable vs non-nullable)

Tests exercise both behaviors and assert error shaping. Consider adding one case with ValidateRequiredExternalFields=true but BuildFetchReasons=false to assert it’s a no-op.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 867c550 and f714c79.

📒 Files selected for processing (5)
  • execution/engine/execution_engine_test.go (5 hunks)
  • v2/pkg/engine/plan/configuration.go (1 hunks)
  • v2/pkg/engine/plan/node_selection_builder.go (1 hunks)
  • v2/pkg/engine/resolve/loader.go (14 hunks)
  • v2/pkg/engine/resolve/resolve.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • v2/pkg/engine/plan/node_selection_builder.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-08T09:43:07.433Z
Learnt from: Noroth
PR: wundergraph/graphql-go-tools#1262
File: v2/pkg/engine/datasource/grpc_datasource/json_builder.go:0-0
Timestamp: 2025-08-08T09:43:07.433Z
Learning: In v2/pkg/engine/datasource/grpc_datasource/json_builder.go, mergeEntities intentionally uses the loop index when calling indexMap.getResultIndex because the index map is type-aware, making per-type counters unnecessary under the current assumptions. Avoid suggesting per-type ordinal counters for this path in future reviews.

Applied to files:

  • v2/pkg/engine/resolve/loader.go
🧬 Code graph analysis (2)
v2/pkg/engine/resolve/loader.go (4)
v2/pkg/engine/resolve/context.go (1)
  • Context (16-35)
v2/pkg/engine/resolve/response.go (1)
  • GraphQLResponse (34-42)
v2/pkg/engine/resolve/resolvable.go (1)
  • Resolvable (26-61)
v2/pkg/engine/resolve/fetch.go (4)
  • Fetch (20-27)
  • FetchInfo (376-397)
  • GraphCoordinate (399-403)
  • FetchItem (29-34)
execution/engine/execution_engine_test.go (3)
execution/engine/execution_engine.go (1)
  • NewExecutionEngine (112-148)
v2/pkg/engine/resolve/resolve.go (1)
  • ResolverOptions (92-170)
v2/pkg/engine/datasource/httpclient/nethttpclient.go (1)
  • DefaultNetHttpClient (39-45)
⏰ 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.25 / windows-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.25 / windows-latest)
🔇 Additional comments (2)
v2/pkg/engine/resolve/resolve.go (1)

253-254: Propagation of option to Loader looks good

Flag is correctly wired from ResolverOptions to Loader.

execution/engine/execution_engine_test.go (1)

5267-5277: Good helper for conditional routing

The conditional NetHTTP client simplifies federation-style test setups.

Comment thread v2/pkg/engine/plan/configuration.go Outdated
Comment thread v2/pkg/engine/plan/configuration.go Outdated
Comment thread v2/pkg/engine/plan/node_selection_visitor.go Outdated
Comment thread v2/pkg/engine/plan/required_fields_visitor.go Outdated
Comment thread v2/pkg/engine/plan/visitor.go Outdated
Comment thread v2/pkg/engine/resolve/loader.go Outdated
Comment thread v2/pkg/engine/resolve/loader.go Outdated
Comment thread v2/pkg/engine/resolve/loader.go Outdated
Comment thread v2/pkg/engine/resolve/loader.go Outdated
Comment thread v2/pkg/engine/resolve/loader.go Outdated
@ysmolski ysmolski requested a review from devsergiy September 22, 2025 09:21
Copy link
Copy Markdown
Contributor

@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 (8)
v2/pkg/engine/resolve/tainted_objects.go (4)

3-8: Fix import ordering (gci).

Please run gci/goimports; CI reports gci formatting failure.


10-22: Doc comment is outdated—errors are not removed anymore.

The implementation doesn’t delete errors; it only returns tainted indices and leaves error handling to the caller. Update the comment to avoid confusion.

-// are required for other fetches. If an error was used to find such an entity, then this error is
-// removed from the errors.
+// are required for other fetches. The function does not mutate the errors array; callers decide how
+// to propagate or filter errors.

49-53: Harden path handling and de‑duplicate indices.

  • Guard type assumptions (require string for "_entities" and for the final field name).
  • Avoid duplicate indices to reduce merge-time O(n*m) checks downstream.
 func getTaintedIndices(fetch Fetch, data *astjson.Value, errors *astjson.Value) (indices []int) {
+    seen := make(map[int]struct{})
@@
-        for i, item := range pathItems {
-            if unsafebytes.BytesToString(item.GetStringBytes()) != "_entities" {
+        for i, item := range pathItems {
+            if item.Type() != astjson.TypeString || unsafebytes.BytesToString(item.GetStringBytes()) != "_entities" {
                 continue
             }
@@
-            fieldName := unsafebytes.BytesToString(pathItems[lastIndex].GetStringBytes())
+            if pathItems[lastIndex].Type() != astjson.TypeString {
+                break
+            }
+            fieldName := unsafebytes.BytesToString(pathItems[lastIndex].GetStringBytes())
@@
-            indices = append(indices, index)
+            if _, ok := seen[index]; !ok {
+                indices = append(indices, index)
+                seen[index] = struct{}{}
+            }
             break
         }
     }
     return
 }

Also applies to: 63-71, 83-86


128-141: Name clarity: prefer “filter/remove” over “clear”.

clearTainted reads like it wipes the set; “removeTainted” or “filterOutTainted” better conveys filtering the slice.

-func (t taintedObjects) clearTainted(items []*astjson.Value) []*astjson.Value {
+func (t taintedObjects) filterOutTainted(items []*astjson.Value) []*astjson.Value {

And update call sites accordingly.

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

3-9: Fix import ordering (gci).

Linters report gci failure; run gci/goimports to sort groups.


108-299: Add edge‑case tests: numeric terminal path and duplicate optional‑requires errors.

To lock in the hardened logic and dedup, add cases where the last path segment is a number (should be ignored) and where multiple errors point to the same entity (should return a single index).

@@ func TestGetTaintedIndices(t *testing.T) {
         {
             name: "invalid error path format",
@@
             expectedIndices: nil,
         },
+        {
+            name: "terminal numeric segment should be ignored",
+            fetchReasons: []FetchReason{
+                {TypeName: "User", FieldName: "email", IsRequires: true, Nullable: true},
+            },
+            responseJSON: `[
+                {"__typename":"User","id":"1","emails":["a@example.com","b@example.com"]}
+            ]`,
+            errorsJSON: `[
+                {"message":"index error","path":["_entities",0,"emails",1]}
+            ]`,
+            expectedIndices: nil,
+        },
+        {
+            name: "duplicate errors for same entity are de-duplicated",
+            fetchReasons: []FetchReason{
+                {TypeName: "Product", FieldName: "reviews", IsRequires: true, Nullable: true},
+            },
+            responseJSON: `[
+                {"__typename":"Product","upc":"1","reviews":null}
+            ]`,
+            errorsJSON: `[
+                {"message":"Cannot resolve field reviews","path":["_entities",0,"reviews"]},
+                {"message":"Cannot resolve field reviews","path":["_entities",0,"reviews"]}
+            ]`,
+            expectedIndices: []int{0},
+        },

Would you like me to push a patch updating the tests once you adopt the function changes?

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

645-647: Avoid O(n*m) membership checks—use a set for tainted indices.

Convert taintedIndices to a set once and use O(1) lookups in all branches.

-    var taintedIndices []int
+    var taintedIndices []int
+    var taintedSet map[int]struct{}
@@
-                if l.validateRequiredExternalFields && res.postProcessing.SelectResponseDataPath != nil {
-                    taintedIndices = getTaintedIndices(fetchItem.Fetch, responseData, responseErrors)
-                }
+                if l.validateRequiredExternalFields && res.postProcessing.SelectResponseDataPath != nil {
+                    taintedIndices = getTaintedIndices(fetchItem.Fetch, responseData, responseErrors)
+                    if len(taintedIndices) > 0 {
+                        taintedSet = make(map[int]struct{}, len(taintedIndices))
+                        for _, ti := range taintedIndices {
+                            taintedSet[ti] = struct{}{}
+                        }
+                    }
+                }
@@
-        if slices.Contains(taintedIndices, 0) {
+        if _, ok := taintedSet[0]; ok {
             l.taintedObjs.add(items[0])
         }
@@
-                if slices.Contains(taintedIndices, idx) {
+                if _, ok := taintedSet[idx]; ok {
                     l.taintedObjs.add(items[i])
                 }
@@
-        if slices.Contains(taintedIndices, i) {
+        if _, ok := taintedSet[i]; ok {
             l.taintedObjs.add(items[i])
         }

Also applies to: 674-676, 695-697


1071-1081: Give the generic error a stable extension code.

To help clients detect this condition, include an extensions.code (e.g., OPTIONAL_REQUIRES_DEPS_FAILED) instead of a plain message.

- msg := fmt.Sprintf(`{"message":"Failed to obtain field dependencies from Subgraph '%s'%s."}`, res.ds.Name, path)
+ msg := fmt.Sprintf(`{"message":"Failed to obtain field dependencies from Subgraph '%s'%s.","extensions":{"code":"OPTIONAL_REQUIRES_DEPS_FAILED"}}`, res.ds.Name, path)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 077dd94 and 20145e2.

📒 Files selected for processing (5)
  • v2/pkg/engine/plan/node_selection_visitor.go (2 hunks)
  • v2/pkg/engine/plan/required_fields_visitor.go (2 hunks)
  • v2/pkg/engine/resolve/loader.go (21 hunks)
  • v2/pkg/engine/resolve/tainted_objects.go (1 hunks)
  • v2/pkg/engine/resolve/tainted_objects_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • v2/pkg/engine/plan/node_selection_visitor.go
  • v2/pkg/engine/plan/required_fields_visitor.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-08T09:43:07.433Z
Learnt from: Noroth
PR: wundergraph/graphql-go-tools#1262
File: v2/pkg/engine/datasource/grpc_datasource/json_builder.go:0-0
Timestamp: 2025-08-08T09:43:07.433Z
Learning: In v2/pkg/engine/datasource/grpc_datasource/json_builder.go, mergeEntities intentionally uses the loop index when calling indexMap.getResultIndex because the index map is type-aware, making per-type counters unnecessary under the current assumptions. Avoid suggesting per-type ordinal counters for this path in future reviews.

Applied to files:

  • v2/pkg/engine/resolve/loader.go
🧬 Code graph analysis (3)
v2/pkg/engine/resolve/tainted_objects.go (1)
v2/pkg/engine/resolve/fetch.go (2)
  • FetchInfo (376-397)
  • GraphCoordinate (399-403)
v2/pkg/engine/resolve/tainted_objects_test.go (1)
v2/pkg/engine/resolve/fetch.go (3)
  • FetchReason (364-372)
  • FetchInfo (376-397)
  • FetchDependencies (111-114)
v2/pkg/engine/resolve/loader.go (4)
v2/pkg/engine/resolve/context.go (1)
  • Context (16-35)
v2/pkg/engine/resolve/response.go (1)
  • GraphQLResponse (34-42)
v2/pkg/engine/resolve/resolvable.go (1)
  • Resolvable (26-61)
v2/pkg/engine/resolve/fetch.go (3)
  • FetchItemPathElement (78-82)
  • Fetch (20-27)
  • FetchItem (29-34)
🪛 GitHub Check: Linters (1.25)
v2/pkg/engine/resolve/tainted_objects.go

[failure] 6-6:
File is not properly formatted (gci)

v2/pkg/engine/resolve/tainted_objects_test.go

[failure] 7-7:
File is not properly formatted (gci)

⏰ 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.25 / windows-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.25 / windows-latest)
🔇 Additional comments (2)
v2/pkg/engine/resolve/loader.go (2)

365-377: Prune tainted items after every traversal step to prevent child leaks.

Filtering only at the end allows selecting into children of tainted parents. Prune on each hop.

 func (l *Loader) selectItemsForPath(path []FetchItemPathElement) []*astjson.Value {
-    items := []*astjson.Value{l.resolvable.data}
-    if len(path) == 0 {
-        return l.taintedObjs.clearTainted(items)
-    }
+    items := []*astjson.Value{l.resolvable.data}
+    if len(path) == 0 {
+        return l.taintedObjs.clearTainted(items)
+    }
     for i := range path {
         if len(items) == 0 {
             break
         }
-        items = selectItems(items, path[i])
+        items = selectItems(items, path[i])
+        items = l.taintedObjs.clearTainted(items)
+        if len(items) == 0 {
+            break
+        }
     }
-    return l.taintedObjs.clearTainted(items)
+    return items
 }

556-571: Confirmed: nil SelectResponseDataPath intentionally means “use full response” (GraphQL sets the data path).

GraphQL datasource provides DefaultPostProcessingConfiguration.SelectResponseDataPath = []string{"data"} (v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go), so GraphQL responses will hit the Get(...) branch; when SelectResponseDataPath == nil the loader intentionally uses the whole parsed response and resolvable.go handles merging based on that. No change required.

Comment thread v2/pkg/engine/resolve/tainted_objects.go
Copy link
Copy Markdown
Contributor

@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 (4)
v2/pkg/engine/resolve/tainted_objects.go (3)

3-9: Remove unused import after fixing array traversal.

strconv becomes unused after the selectObjectAndIndex change. Clean up imports.

-import (
-	"strconv"
-
-	"github.com/wundergraph/astjson"
+import (
+	"github.com/wundergraph/astjson"

49-67: Harden path parsing: ensure last element is a field name.

Guard against non-string terminal path items to avoid panics/false positives.

-			fieldName := unsafebytes.BytesToString(pathItems[lastIndex].GetStringBytes())
+			if pathItems[lastIndex].Type() != astjson.TypeString {
+				break
+			}
+			fieldName := unsafebytes.BytesToString(pathItems[lastIndex].GetStringBytes())

39-86: Optional: de-duplicate tainted indices.

Multiple errors can reference the same entity index. Dedup to avoid redundant work later.

-func getTaintedIndices(fetch Fetch, data *astjson.Value, errors *astjson.Value) (indices []int) {
+func getTaintedIndices(fetch Fetch, data *astjson.Value, errors *astjson.Value) (indices []int) {
+	seen := make(map[int]struct{})
 	...
-			indices = append(indices, index)
+			if _, ok := seen[index]; !ok {
+				indices = append(indices, index)
+				seen[index] = struct{}{}
+			}
v2/pkg/engine/resolve/loader.go (1)

973-986: Avoid injecting empty path segments when rewriting error paths.

Appending "" for unknown path item types produces confusing paths. Skip unknown types instead.

-					default:
-						newPath = append(newPath, "")
+					default:
+						// skip unknown path element types
+						continue
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83d3d73 and c3966d1.

📒 Files selected for processing (3)
  • v2/pkg/engine/resolve/loader.go (21 hunks)
  • v2/pkg/engine/resolve/tainted_objects.go (1 hunks)
  • v2/pkg/engine/resolve/tainted_objects_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • v2/pkg/engine/resolve/tainted_objects_test.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-08T09:43:07.433Z
Learnt from: Noroth
PR: wundergraph/graphql-go-tools#1262
File: v2/pkg/engine/datasource/grpc_datasource/json_builder.go:0-0
Timestamp: 2025-08-08T09:43:07.433Z
Learning: In v2/pkg/engine/datasource/grpc_datasource/json_builder.go, mergeEntities intentionally uses the loop index when calling indexMap.getResultIndex because the index map is type-aware, making per-type counters unnecessary under the current assumptions. Avoid suggesting per-type ordinal counters for this path in future reviews.

Applied to files:

  • v2/pkg/engine/resolve/tainted_objects.go
  • v2/pkg/engine/resolve/loader.go
🧬 Code graph analysis (2)
v2/pkg/engine/resolve/tainted_objects.go (1)
v2/pkg/engine/resolve/fetch.go (2)
  • FetchInfo (376-397)
  • GraphCoordinate (399-403)
v2/pkg/engine/resolve/loader.go (4)
v2/pkg/engine/resolve/context.go (1)
  • Context (16-35)
v2/pkg/engine/resolve/response.go (1)
  • GraphQLResponse (34-42)
v2/pkg/engine/resolve/resolvable.go (1)
  • Resolvable (26-61)
v2/pkg/engine/resolve/fetch.go (3)
  • FetchItemPathElement (78-82)
  • Fetch (20-27)
  • FetchItem (29-34)
⏰ 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.25 / windows-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
  • GitHub Check: Build and test (go 1.25 / ubuntu-latest)
🔇 Additional comments (6)
v2/pkg/engine/resolve/loader.go (5)

574-599: Confirm intended ordering: generic dependency error is added in addition to subgraph errors.

You now add a generic “Failed to obtain field dependencies …” error and still merge all subgraph errors. If that’s the desired UX, LGTM; otherwise, filter optional-@requires errors first before merging.


636-647: Good: mark taint after merge to keep pointer identity stable.

Tainting post-MergeValuesWithPath avoids losing the taint due to pointer replacement.


661-677: Good: batch+dedupe path captures merge result and taints after merge.

Capturing the returned merged value per item and marking taint afterwards is correct.


686-697: Good: non-deduped batch marks taint after merge as well.

Prevents stale-pointer tainting.


365-377: Prune tainted after every traversal step to prevent child leaks.

Filtering only at the end lets children of tainted parents pass through. Prune after each select to maintain taint semantics.

 func (l *Loader) selectItemsForPath(path []FetchItemPathElement) []*astjson.Value {
-	items := []*astjson.Value{l.resolvable.data}
-	if len(path) == 0 {
-		return l.taintedObjs.filterOutTainted(items)
-	}
+	items := []*astjson.Value{l.resolvable.data}
+	if len(path) == 0 {
+		return l.taintedObjs.filterOutTainted(items)
+	}
 	for i := range path {
 		if len(items) == 0 {
 			break
 		}
-		items = selectItems(items, path[i])
+		items = selectItems(items, path[i])
+		items = l.taintedObjs.filterOutTainted(items)
 	}
-	return l.taintedObjs.filterOutTainted(items)
+	return items
 }
v2/pkg/engine/resolve/tainted_objects.go (1)

90-122: Fix array traversal: index arrays directly; don’t use string keys for numeric path elements.

Numeric elements in error paths represent array indices. Using strconv+Get("idx") on arrays is incorrect and will miss/return nil. This also causes wrong taint detection. Handle arrays via GetArray() with bounds/type checks, and guard against nil response.

Apply:

-func selectObjectAndIndex(response *astjson.Value, path []*astjson.Value) (*astjson.Value, int) {
-	index := -1
-	if len(path) == 0 {
-		return nil, index
-	}
-	for _, el := range path {
-		var key string
-		switch el.Type() {
-		case astjson.TypeNumber:
-			parsed := el.GetInt()
-			if parsed < 0 {
-				return nil, index
-			}
-			if index == -1 {
-				// index is assigned only once
-				index = parsed
-			}
-			key = strconv.Itoa(parsed)
-		case astjson.TypeString:
-			key = unsafebytes.BytesToString(el.GetStringBytes())
-		default:
-			return nil, -1
-		}
-		response = response.Get(key)
-		if response == nil {
-			return nil, -1
-		}
-	}
-	return response, index
-}
+func selectObjectAndIndex(response *astjson.Value, path []*astjson.Value) (*astjson.Value, int) {
+	index := -1
+	if response == nil || len(path) == 0 {
+		return nil, index
+	}
+	for _, el := range path {
+		switch el.Type() {
+		case astjson.TypeNumber:
+			i := el.GetInt()
+			if i < 0 || response.Type() != astjson.TypeArray {
+				return nil, index
+			}
+			if index == -1 {
+				index = i
+			}
+			arr := response.GetArray()
+			if i >= len(arr) {
+				return nil, index
+			}
+			response = arr[i]
+		case astjson.TypeString:
+			key := unsafebytes.BytesToString(el.GetStringBytes())
+			response = response.Get(key)
+			if response == nil {
+				return nil, index
+			}
+		default:
+			return nil, index
+		}
+	}
+	return response, index
+}

Note: drop the now-unused strconv import.

Comment thread v2/pkg/engine/resolve/loader.go
Comment thread execution/engine/execution_engine_test.go Outdated
Copy link
Copy Markdown
Contributor

@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 (3)
execution/engine/execution_engine_test.go (3)

241-246: Name this helper consistently with existing pattern

For consistency with withValueCompletion and withFetchReasons, consider renaming to withValidateRequiredExternalFields.

Apply locally in this file:

-func validateRequiredExternalFields() executionTestOptions {
+func withValidateRequiredExternalFields() executionTestOptions {
   return func(options *_executionTestOptions) {
     options.validateRequiredExternalFields = true
   }
 }

Then update the call sites in this file accordingly (e.g., Lines ~4886, ~5053, ~5255).


4722-4722: Clarify test name to reflect intent

Current name can be misread. Consider: “non-nullable @requires dependencies are enforced (not optional)”.

-        t.Run("do not validate non-nullable @requires dependencies", func(t *testing.T) {
+        t.Run("non-nullable @requires dependencies are enforced (not optional)", func(t *testing.T) {

5272-5282: Test helpers exist — make createConditionalTestRoundTripper match JSON structurally

  • execution/engine/execution_engine_helpers_test.go defines conditionalTestCase and createConditionalTestRoundTripper and they are accessible from the tests.
  • createConditionalTestRoundTripper currently matches request bodies by exact string (require.Containsf(..., string(gotBody))), which is brittle. Update it to compare JSON structurally (unmarshal req.Body into interface{} and compare with unmarshalled map keys using reflect.DeepEqual, or compare compact/normalized JSON strings via json.Compact) — or store responses keyed by normalized JSON when the map is built.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aebc7a1 and 2d9a236.

📒 Files selected for processing (2)
  • execution/engine/execution_engine_test.go (5 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 comments (6)
execution/engine/execution_engine_test.go (6)

221-223: Option plumbing added to test harness

Adding validateRequiredExternalFields to _executionTestOptions is fine.


4560-4561: Wrapping the new scenarios under a single t.Run block is good

This addresses the prior review to group these cases.


4562-4720: Happy-path federation requires/external scenario is well specified

Requests include needed __typename and representations; metadata (keys/requires/external) align with the SDL. Expected result matches.


4889-5054: Nullable @requires validation case is accurate

Asserting both dependency-obtainment and fetch errors here makes sense with the flag enabled.


5056-5256: Nested nullable @requires coverage looks solid

Good inclusion of __typename in nested selections and selective representations for the second fetch; expectations match the taint/skip behavior.


279-286: Verify planner/resolver support for ValidateRequiredExternalFields

Wiring looks correct in execution/engine/execution_engine_test.go:279–286. Automated repo search couldn't run in this environment — confirm plannerConfig and ResolverOptions declare ValidateRequiredExternalFields and that planner/engine code reads/enforces it (check call sites/conditionals).

@ysmolski ysmolski merged commit ba75e25 into master Sep 24, 2025
12 checks passed
@ysmolski ysmolski deleted the yury/eng-7767-validate-presence-of-optional-requires-dependencies branch September 24, 2025 14:39
ysmolski added a commit that referenced this pull request Sep 24, 2025
🤖 I have created a release *beep* *boop*
---


##
[2.0.0-rc.228](v2.0.0-rc.227...v2.0.0-rc.228)
(2025-09-24)


### Features

* validate presence of optional "requires" dependencies
([#1297](#1297))
([ba75e25](ba75e25))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Adds validation to ensure optional "requires" dependencies are present
when needed.

* **Documentation**
* Updates changelog for v2.0.0-rc.228 to document the new validation and
confirm no public API changes.

* **Chores**
  * Bumps the v2 release candidate to 2.0.0-rc.228.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Yury Smolski <140245+ysmolski@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
ysmolski pushed a commit that referenced this pull request Oct 21, 2025
🤖 I have created a release *beep* *boop*
---


##
[1.6.0](execution/v1.5.0...execution/v1.6.0)
(2025-10-21)


### Features

* support the oneOf directive
([#1308](#1308))
([251cb02](251cb02))
* validate presence of optional [@requires](https://github.com/requires)
dependencies
([#1297](#1297))
([ba75e25](ba75e25))


### Bug Fixes

* bump engine to v2.0.0-rc.231 for execution
([#1329](#1329))
([ebddb25](ebddb25))
* propagate fetch reasons for interface-related fields
([#1312](#1312))
([5ee3014](5ee3014))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Added oneOf directive support
  * Added validation of optional @requires dependencies

* **Bug Fixes**
  * Fixed fetch reason propagation for interface-related fields

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

3 participants