Conversation
The union collapse from nautilus#231 planned each member type's selections against the member type, but then appended the planned fields directly under the union field. A query like foo { ... on Bar { bar } } was sent downstream as `foo { bar }`, which is invalid because a union has no fields of its own. The same applied to the `id` the planner adds for node lookups, so the existing test asserted an invalid query. Wrap each member type's planned selection set back into an inline fragment on that type before adding it to the union field.
The union collapse from nautilus#231 rejected any plain field inside a union selection set with "unsupported selection type inside union: *ast.Field". __typename is the one field the spec allows directly on a union, and clients like Apollo add it to every selection set, so every union-returning field failed to plan. Route __typename to the service that owns the union field. Also resolve fragment spreads by the spread's own name instead of the parent field's name (which dereferenced a nil fragment), falling back to the operation's fragment definitions. Inline fragments and named fragments whose type condition is the union itself (`fragment f on Union { ... }`), or which have no type condition at all, apply to the union rather than to a member type. Flatten their selections into the same per-type groups, so that member fragments nested inside them are still planned against the member type. Otherwise they fall through to the pre-nautilus#231 path, which sends `... on Union` to services that do not know the union and puts the lookup `id` directly on the union field.
cideM
force-pushed
the
fix/union-selection-planning
branch
3 times, most recently
from
September 3, 2026 15:02
619455c to
432058f
Compare
Member
|
Thanks @cideM, will take a look after I return from vacation in ~1.5 weeks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I believe #231 introduced a few regressions in the way union selection sets are planned. We hit the first one in production right after upgrading, and discovered the others along the way.
To be clear: the idea behind #231 is good, and this PR keeps it. So what's the issue then?
Let's take the query below, which is what Apollo Client sends for pretty much any union field, since it adds
__typenameautomatically:1.
__typenameon a union is rejectedSince #231 the planner refuses this query:
{ "errors": [ { "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" }, "message": "unsupported selection type inside union: *ast.Field" } ] }The union branch in
extractSelectiononly accepts inline fragments and fragment spreads. Everything else hits the default case:__typenameis the one field the spec allows directly on a union (3.8 Unions), so any client that adds it breaks on a union field.Fix: accept
__typenameas a field on the union and request it from the service that owns the union field. Covered byTestPlanQuery_unionAllowsTypenameField.2. Member fields are flattened onto the union
Without
__typenamethe query works, but is sent to the wrong service. #231 strips the... on Bar { }wrapper, plans the inner fields againstBar, and then appends the planned fields underfoo(!):That's not valid GraphQL since the union
Foohas no fieldbar.gqlparserreports it asCannot query field "bar" on type "Foo".The
idthe planner adds for dependent node lookups has the same problem, which is why the existing test assertedfoo { id }as the expected query for the union's own service. But the expected query string was itself invalid, so I changed it tofoo { ... on Bar { id } }.Fix: after planning a member type's selections, wrap them back into
... on Member { }before adding them to the union field. Selections that belong to the union itself (__typename) stay unwrapped. Covered byTestPlanQuery_unionSingleLocationKeepsTypeConditions, plus the correctedTestPlanQuery_unionShouldNotBeQueriedOnOtherServices.3. Named fragment spreads panic
The spread case looks up the fragment by the parent (
selection), when it should besubSelection:I also believe that we're missing a lookup on the plan. For example,
groupSelectionSetfirst checks the step, then the plan:Fix: resolve the spread by
subSelection.Name, check the step's definitions first and fall back to the operation's, and return an error instead of dereferencingnilif neither has it. Covered byTestPlanQuery_unionAllowsFragmentSpread.4. Fragments on the union type itself
Fragments don't have to be on a member type. Apollo codebases commonly declare them on the union:
The same goes for an inline fragment without a type condition (
... @include(if: $x) { ... }), which also applies to the union. With the spread lookup fixed, these are grouped under the union type and handed to the pre-#231 code path, so they hit both of the problems above at once: theidis placed directly on the union (foo { __typename id }) and... on Foois sent to a service that doesn't knowFoo.Fix: when a fragment's type condition is the union itself (or absent), flatten its selections into the same per-type groups, recursively. Member fragments nested inside then get planned against the member type like everything else. Covered by
TestPlanQuery_unionAllowsFragmentSpreadOnUnionTypeandTestPlanQuery_unionAllowsInlineFragmentWithoutTypeCondition.Misc
The new tests use an
assertPlannedQuerieshelper that collects the query each step would send, keyed by service URL, and compares that against the expected queries. Comparing the outgoing queries directly is what caught bugs 2 and 4, since the plan structure looked fine while the query strings were invalid. The helper normalizes both sides by sorting every selection set, because the planner groups union selections in a map and the order of the output is not stable otherwise.One thing I have left alone: directives on the fragments inside a union (
... on Bar @include(if: $x) { bar }) are dropped when the selection set is flattened into the map. That was already the case after #231 and seemed like a separate issue.