Skip to content

feat(plan): federation correctness and parity fixes (paired with cosmo) - #1551

Draft
jensneuse wants to merge 10 commits into
masterfrom
planner-improvements
Draft

feat(plan): federation correctness and parity fixes (paired with cosmo)#1551
jensneuse wants to merge 10 commits into
masterfrom
planner-improvements

Conversation

@jensneuse

Copy link
Copy Markdown
Member

Five federation query-planner fixes (paired with composition changes in the cosmo
PR), each strictly gated for zero plan-snapshot regression (0 diffs across 220
real federated graphs / 13,143 operations):

  • honor @provides over union-typed fields — read the provided union-member field
    inline instead of an entity fetch to the owner.
  • resolve abstract field whose union members are split across datasources — was an
    empty selection set → HTTP 500; now resolves.
  • split entity fetch for @requires with conflicting field arguments — same field
    required with different args collapsed to one value (silently wrong data); now
    spec-correct (matches an alternative federation router).
  • honor @provides over interface-typed fields — match interface-keyed provides
    against the rewritten concrete fragment paths.
  • synthesize multi-hop compound @key — gather a missing compound-key member from a
    third subgraph (two-stage entity fetch), gated as a strict fallback (the un-gated
    synthesis drifts ~114 ops; gated → 0).

Each fix is validated against the federation-gateway-audit and has a
full-plan unit test. Based on v2.5.0.

See the cosmo PR for the decision records (adr/), the reviewer guide
(docs/planner-improvements.md), and the router e2e tests.

🤖 Generated with Claude Code

jensneuse and others added 5 commits June 26, 2026 21:17
The planner extracted the @provides suggestion for a union-rooted selection
(`... on Member { field }`) but did not honor it during datasource suggestion
collection: the provided field was looked up by its fragment-qualified path
while the provides entry is keyed by the fragment-stripped path, so the union
member field was never recognized as provided and the planner fell back to a
@key entity fetch to the owning subgraph (correct data, but the @provides
optimization lost). Match the fragment-stripped path against the provides
entries for fields directly inside a union inline fragment, so the provided
member field is read inline and no owner _entities fetch is issued.

Strictly gated: the fallback triggers only for a field on a union inline
fragment whose path changed by fragment removal and for which a provides entry
exists at the fragment-stripped path. Every plan that does not involve a
union-typed @provides is byte-identical (celestial: 0 diffs across 13,143
operations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s datasources

When a union-typed field's members are split across multiple datasources, the
abstract-selection rewriter could prune a datasource's selection set to empty
(its local union subset covers none of the requested members), which failed plan
validation ("selection set is empty") and made the router return HTTP 500.

Add a closest-datasource consolidation pass for split union fields: keep the
union field on its closest datasource (by key-jump score) restricted to the
members it covers, and recover members only available elsewhere via a strict
one-hop fallback to a sibling datasource; skip the destructive union rewrite only
for those fallback-kept fields.

Strictly gated: the pass acts only on a union-returning field with >=2 selected
datasources (the split case); non-union or single-datasource fields are
unchanged. Every plan that already resolves is byte-identical (celestial: 0 diffs
across 13,143 operations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…guments

When two @requires consumers on the same entity require the same providing-
subgraph field with different arguments (e.g. price(currency:"USD") and
price(currency:"EUR")), representation-variable construction deduped the
representation fields by name only, collapsing the two argumented values into one
slot and silently dropping the second consumer's value (wrong data, HTTP 200).
Detect the conflict via an argument-fingerprint-aware predicate and partition the
conflicting consumers into separate _entities fetches, each carrying its own
correctly-argumented value; import the field arguments into the upstream fetch.

Strictly gated: the split fires only on a genuine same-entity/same-field/
different-argument collision. Every representation without such a collision
(ordinary @key and non-conflicting @requires) is byte-identical (celestial: 0
diffs across 13,143 operations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
For an interface-typed @provides (e.g. media: Media @provides(fields:
"animals { id name }") where animals is interface-typed and the leaves are
@external on the concrete implementations), the planner extracted the provides
suggestion keyed by the interface but, after rewriting the abstract selection
into per-implementation inline fragments, looked it up by the rewritten concrete
path and missed -- so the provided member fields were not read from the providing
subgraph (wrong/missing data), unlike alternative federation implementations.

Match interface-keyed provides entries against the rewritten concrete fragment
paths: collect the provided concrete leaf as key material for the rewritten path,
preserve multiple key entries per datasource/path, assign keys from selected
fragment ancestors, prefer the abstract-root datasource that owns provided
children, and drop the now-empty duplicate abstract path. Generalizes the
fragment-stripped-path mechanism shipped for union @provides (isOnAbstractFragment)
to interface inline fragments.

Strictly gated to provided abstract fragment paths: plans without an
abstract-typed @provides are byte-identical (verified by plan-snapshot diff: 0
diffs across 13,143 operations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…key member

A field reachable only through a multi-hop entity route -- a compound @key whose
missing member must first be gathered from a third subgraph -- was unplannable:
the jump-graph route-finder only allowed a source key whose field set exactly
equals the target key, so it could not gather the missing member first, and the
dependent fields resolved to null. Allow a jump where the source key is a SUBSET
of the target compound @key, building the two-stage entity fetch (a per-member
gather hop to obtain the missing member, folded into the representation for the
compound-key fetch).

Strictly gated as a fallback: the subset->compound jump is reachable ONLY after
normal exact-key datasource selection has failed for the field, and stale
key/field dependencies for de-selected datasources are pruned. Plans that already
succeed are byte-identical (celestial: 0 diffs across 13,143 operations); the
un-gated synthesis drifts ~114 operations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3f2b784c-68f2-424f-9edc-d315ffe465e9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR updates GraphQL federation planning to carry fallback source paths, compare required-field argument signatures, skip union rewrites for selected fields, and select abstract-field routes with additional cleanup. It also adds tests for interface provides, union provides, shareable unions, fallback routes, and multi-hop compound-key planning.

Changes

Federation planner routing

Layer / File(s) Summary
Downstream argument import
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
addFieldArguments now imports downstream arguments when no explicit argument configuration exists upstream.
Fallback source connections
v2/pkg/engine/plan/source_connection_graph.go, v2/pkg/engine/plan/source_connection_graph_test.go
KeyJump and path lookup distinguish fallback routes, and tests cover subset-key fallback lookup and exact-route precedence.
Required-argument conflicts
v2/pkg/engine/plan/federation_metadata.go, v2/pkg/engine/plan/path_builder_visitor.go, v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_requires_arguments_test.go
Required-field argument signatures are compared for conflicts, planner reuse skips mismatched requires configs, and the new test covers different currency arguments on the same field.
Abstract-field selection
v2/pkg/engine/plan/abstract_selection_rewriter.go, v2/pkg/engine/plan/datasource_filter_*.go, v2/pkg/engine/plan/path_builder.go
Abstract-field rewrite skipping, provided-field collection, datasource selection, node suggestion lookup, and duplicate-leaf pruning now account for fragment context, union/interface membership, and fallback-key-aware routing.
Abstract-field federation tests
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_interface_provides_test.go, v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_provides_test.go, v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go
New federation tests cover interface-provided fields, union @provides, and shareable unions split across subgraphs.
Fallback-key node selection
v2/pkg/engine/plan/node_selection_*.go, v2/pkg/engine/plan/multihop_compound_key_test.go, v2/pkg/engine/datasource/graphql_datasource/multihop_compound_key_test.go
Node selection now refilters with fallback key jumps, prunes stale requirements, and records jump source paths; the new multi-hop tests assert the resulting planner route.

Review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: federation correctness and parity fixes in the plan layer.
Description check ✅ Passed The description clearly matches the changeset, listing the five federation planner fixes and their gated validation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch planner-improvements

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

jensneuse added a commit to wundergraph/cosmo that referenced this pull request Jun 26, 2026
…ter-tests)

Pin github.com/wundergraph/graphql-go-tools/v2 in both the router and router-tests
modules to the planner-improvements commit carrying the five federation
query-planner fixes (wundergraph/graphql-go-tools#1551), and tidy go.sum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jensneuse

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

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

199-235: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Passing FederationFieldConfiguration by value defeats the parseSelectionSet cache.

parseSelectionSet has a pointer receiver and memoizes the parsed document into f.parsedSelectionSet. Since both requiredFieldArgumentConflict and requiredFieldArgumentsByPath take the config by value, the cache is written to a local copy and discarded, so the selection set is re-parsed on every invocation. HasArgumentConflictWith calls requiredFieldArgumentConflict in an i*j loop, so each requires selection set is parsed repeatedly on the planning path.

Passing pointers (the loop elements (*f)[i] and configs[j] are addressable) lets the memoization take effect.

♻️ Proposed change
-func (f *FederationFieldConfigurations) HasArgumentConflictWith(configs []FederationFieldConfiguration) bool {
-	for i := range *f {
-		for j := range configs {
-			if requiredFieldArgumentConflict((*f)[i], configs[j]) {
+func (f *FederationFieldConfigurations) HasArgumentConflictWith(configs []FederationFieldConfiguration) bool {
+	for i := range *f {
+		for j := range configs {
+			if requiredFieldArgumentConflict(&(*f)[i], &configs[j]) {
 				return true
 			}
 		}
 	}

 	return false
 }

-func requiredFieldArgumentConflict(left, right FederationFieldConfiguration) bool {
+func requiredFieldArgumentConflict(left, right *FederationFieldConfiguration) bool {
 	if left.TypeName != right.TypeName {
 		return false
 	}
@@
-func requiredFieldArgumentsByPath(config FederationFieldConfiguration) (map[string]string, bool) {
+func requiredFieldArgumentsByPath(config *FederationFieldConfiguration) (map[string]string, bool) {
 	if err := config.parseSelectionSet(); err != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v2/pkg/engine/plan/federation_metadata.go` around lines 199 - 235, The
parsing cache is being lost because requiredFieldArgumentConflict and
requiredFieldArgumentsByPath take FederationFieldConfiguration by value, so
parseSelectionSet() memoizes only on a copy. Update these helpers to work with
pointers and call them from HasArgumentConflictWith using the addressable loop
elements (for example, the configs accessed in the i/j loop) so
parseSelectionSet() can reuse the cached parsedSelectionSet instead of reparsing
on every conflict check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go`:
- Around line 747-755: The raw argument-copy branch in the GraphQL datasource
path is adding arguments via ImportArguments and AddArgumentToField without
bringing over matching variable definitions, which can leave dangling $variable
references in the upstream operation. Update this branch in the field-argument
handling logic to also import any referenced variable definitions into the
upstream operation before or while copying the arguments, using the same
variable-import behavior as the configured-argument paths. Also validate the
copied arguments against the upstream field before adding them so only arguments
valid for that field are forwarded.

In `@v2/pkg/engine/plan/datasource_filter_collect_nodes_visitor.go`:
- Around line 575-581: The fragment-provides lookup in
datasource_filter_collect_nodes_visitor.go should handle interface
type-condition fragments before falling back to the object-only check. Update
the logic around the `onInterfaceFragment` handling in the visitor so
`providedFieldKey(...)` is consulted for interface fragments as well, instead of
returning false when `info.enclosingTypeDefinition.Kind` is not an object. Keep
the existing `onUnionFragment` path and reuse the same fragment-stripped lookup
flow for `onInterfaceFragment` to ensure `SomeInterface` fragments resolve
correctly.

In `@v2/pkg/engine/plan/datasource_filter_visitor.go`:
- Around line 223-230: In the abstract-field member merge logic inside the
datasource filter visitor, the cache for item.Path is currently choosing
whichever requestedMembers slice is longer, which can lose union members from
separate selections. Update the merge behavior to union members for the same
path by appending only unique entries from requestedMembers into existingMembers
in the abstractFieldRequestedMembers handling, rather than comparing slice
lengths; this will keep missingMembers computation complete.
- Around line 823-829: The fallback-path selection in the selectedParentHashes
loop can assign a path from hasPathBetweenDs directly to currentNode.requiresKey
without rechecking whether the source can provide the missing fallback key
field. Update the path acceptance logic in datasource_filter_visitor.go so that
when allowFallbackKeyJumps is true, any fallback path is only accepted after
validating sourceConnectionRequiresMissingFallbackKeyField for currentNode and
the chosen path; otherwise keep searching or reject that path before setting
currentNode.requiresKey and currentNode.requiresFallbackKey.

In `@v2/pkg/engine/plan/path_builder.go`:
- Around line 217-223: The PathBuilder.hasProvidedSuggestionForPath check is too
broad because it treats inactive orphaned/unselected suggestions as active
provided routes. Update the matching logic to ignore suggestions that are not
part of the selected plan, so removeDuplicateLeafAbstractFieldPaths only prunes
paths when a provided suggestion is actually active. Use the existing
PathBuilder, hasProvidedSuggestionForPath, and visitor.nodeSuggestions.items
flow to narrow the condition to selected/active suggestions only.

---

Nitpick comments:
In `@v2/pkg/engine/plan/federation_metadata.go`:
- Around line 199-235: The parsing cache is being lost because
requiredFieldArgumentConflict and requiredFieldArgumentsByPath take
FederationFieldConfiguration by value, so parseSelectionSet() memoizes only on a
copy. Update these helpers to work with pointers and call them from
HasArgumentConflictWith using the addressable loop elements (for example, the
configs accessed in the i/j loop) so parseSelectionSet() can reuse the cached
parsedSelectionSet instead of reparsing on every conflict check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ac1f5c79-91f9-4764-bc86-168b49fd79fb

📥 Commits

Reviewing files that changed from the base of the PR and between 478f3c0 and 7f2e9a9.

📒 Files selected for processing (19)
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_interface_provides_test.go
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_provides_test.go
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_requires_arguments_test.go
  • v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.go
  • v2/pkg/engine/datasource/graphql_datasource/multihop_compound_key_test.go
  • v2/pkg/engine/plan/abstract_selection_rewriter.go
  • v2/pkg/engine/plan/datasource_filter_collect_nodes_visitor.go
  • v2/pkg/engine/plan/datasource_filter_node_suggestions.go
  • v2/pkg/engine/plan/datasource_filter_resolvable_visitor.go
  • v2/pkg/engine/plan/datasource_filter_visitor.go
  • v2/pkg/engine/plan/federation_metadata.go
  • v2/pkg/engine/plan/multihop_compound_key_test.go
  • v2/pkg/engine/plan/node_selection_builder.go
  • v2/pkg/engine/plan/node_selection_visitor.go
  • v2/pkg/engine/plan/path_builder.go
  • v2/pkg/engine/plan/path_builder_visitor.go
  • v2/pkg/engine/plan/source_connection_graph.go
  • v2/pkg/engine/plan/source_connection_graph_test.go

Comment thread v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go
Comment thread v2/pkg/engine/plan/datasource_filter_collect_nodes_visitor.go
Comment thread v2/pkg/engine/plan/datasource_filter_visitor.go Outdated
Comment thread v2/pkg/engine/plan/datasource_filter_visitor.go
Comment thread v2/pkg/engine/plan/path_builder.go
jensneuse and others added 5 commits June 26, 2026 23:21
The raw argument-copy path (used when splitting an entity fetch for @requires with
conflicting field arguments) copied argument values verbatim, preserving $variable
references but never importing the matching variable definitions into the upstream
operation -- leaving dangling variable references when a field on that path uses
variable arguments. Recursively import the variable definitions for raw-copied
argument values (idempotent: skips variables already imported by the
configured-argument path). Found in code review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n members

Two planner edge cases found in code review:
- isProvidedField returned false for an interface-typed inline fragment
  (... on SomeInterface { providedField }) before consulting onInterfaceFragment, so
  the fragment-stripped @provides lookup was missed unless the enclosing type was an
  object. Consult onInterfaceFragment before the object-only fallback.
- The requested-union-member cache, keyed by response path, kept the longer member
  slice; repeated selections at the same path contributing disjoint members could drop
  members and compute missingMembers from incomplete coverage. Merge the member sets,
  preserving the subset/superset fast paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tract paths

hasProvidedSuggestionForPath matched provided suggestions regardless of whether they
were orphaned, so removeDuplicateLeafAbstractFieldPaths could remove an active planner
path based on a provided route not part of the selected plan. Skip orphaned
suggestions. Found in code review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…section

B1 (resolve abstract field whose union members are split across datasources)
GATHERED foreign union members across subgraphs. That is wrong for value-type
(non-@key) union members: it served the foreign subgraph's member list and
returned silently-wrong data on the federation-gateway-audit partial-union-complex
suite (1/5). The canonical rule is source-subgraph / intersection resolution: a
union field is resolved from the subgraph(s) that own the current path; value-type
members a sibling cannot produce are absent, not gathered (gather-all is the
unimplemented Apollo feature #2193). Entity-member unions still gather via _entities.

Replace B1 with the intersection pass: for a multi-candidate non-entity union field
it keeps the intersection of candidate members, keeps the resolving subgraph's own
non-shared members as response-only nulls (excluded from the upstream fetch via an
allowField guard), and drops foreign members. Gated to non-entity unions, so
entity-member unions are untouched. Removes B1 and the B1-only union-member-merge
review fix; A1/A2/A3/B2 and review items 1/2/5 are unchanged.

Validated: federation-gateway-audit partial-union-complex 5/5 (was 1/5),
partial-union 2/2, union-intersection 12/12, abstract-types 18/18,
union-interface-distributed 10/10; celestial 0 plan diffs across 220 graphs /
13,143 operations.

Co-Authored-By: Ahmet Soormally <ahmet@mangomm.co.uk>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the federation-gateway-audit child-type-mismatch suite (ENG-6078). When two
inline fragments on concrete object members of an abstract type select the same
response-name field whose subgraph types differ only in nullability, the upstream
operation is invalid. Generate a planner alias (`__sg_merge_<Type>_<field>`) on
the upstream query while preserving the original client-facing response name, and
recover the value via the aliased path.

Separate, tightly-gated concern (own walker phase) that composes with the
partial-union intersection pass. NOT celestial-zero-diff by design: it changes the
upstream wire format for the matching shape, so two pre-existing goldens are
re-baselined. Verified: audit child-type-mismatch 4/4, partial-union-complex stays
5/5; bonus-isolated celestial diff is exactly the intended aliasing (52 __sg_merge_
aliases added across 8 corpus graphs, deterministic, 0 before).

Co-Authored-By: Ahmet Soormally <ahmet@mangomm.co.uk>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant