feat(plan): federation correctness and parity fixes (paired with cosmo) - #1551
feat(plan): federation correctness and parity fixes (paired with cosmo)#1551jensneuse wants to merge 10 commits into
Conversation
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>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe 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. ChangesFederation planner routing
Review effort🎯 5 (Critical) | ⏱️ ~90+ minutes 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
…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>
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
v2/pkg/engine/plan/federation_metadata.go (1)
199-235: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winPassing
FederationFieldConfigurationby value defeats theparseSelectionSetcache.
parseSelectionSethas a pointer receiver and memoizes the parsed document intof.parsedSelectionSet. Since bothrequiredFieldArgumentConflictandrequiredFieldArgumentsByPathtake the config by value, the cache is written to a local copy and discarded, so the selection set is re-parsed on every invocation.HasArgumentConflictWithcallsrequiredFieldArgumentConflictin ani*jloop, so eachrequiresselection set is parsed repeatedly on the planning path.Passing pointers (the loop elements
(*f)[i]andconfigs[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
📒 Files selected for processing (19)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.gov2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_interface_provides_test.gov2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_provides_test.gov2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_requires_arguments_test.gov2/pkg/engine/datasource/graphql_datasource/graphql_datasource_federation_test.gov2/pkg/engine/datasource/graphql_datasource/multihop_compound_key_test.gov2/pkg/engine/plan/abstract_selection_rewriter.gov2/pkg/engine/plan/datasource_filter_collect_nodes_visitor.gov2/pkg/engine/plan/datasource_filter_node_suggestions.gov2/pkg/engine/plan/datasource_filter_resolvable_visitor.gov2/pkg/engine/plan/datasource_filter_visitor.gov2/pkg/engine/plan/federation_metadata.gov2/pkg/engine/plan/multihop_compound_key_test.gov2/pkg/engine/plan/node_selection_builder.gov2/pkg/engine/plan/node_selection_visitor.gov2/pkg/engine/plan/path_builder.gov2/pkg/engine/plan/path_builder_visitor.gov2/pkg/engine/plan/source_connection_graph.gov2/pkg/engine/plan/source_connection_graph_test.go
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>
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):
@providesover union-typed fields — read the provided union-member fieldinline instead of an entity fetch to the owner.
empty selection set → HTTP 500; now resolves.
@requireswith conflicting field arguments — same fieldrequired with different args collapsed to one value (silently wrong data); now
spec-correct (matches an alternative federation router).
@providesover interface-typed fields — match interface-keyed providesagainst the rewritten concrete fragment paths.
@key— gather a missing compound-key member from athird 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