[Sketch] Hoisted Query Selections#5295
Draft
captbaritone wants to merge 18 commits into
Draft
Conversation
Add support for `__query { ... }` in any selection set, which allows
selecting fields from the query root type. The compiler hoists these
selections to the query root at compile time.
- Register synthetic `__query` field in schema (InMemory + FlatBuffer)
- Intercept `__query` in build phase, validate against query root type
- Hoisting transform with O(N) performance and cycle-safe caching
- Integrated into both normalization and operation_text pipelines
- Condition preservation: @include/@Skip wrappers maintained
- Error on __query in mutation/subscription operations
Verifies the full compiler → server → runtime pipeline: the compiler hoists __query selections to the query root, the server receives valid GraphQL, and non-__query fields render correctly.
Add reader pipeline support so fragments can read __query data from
the root record in the store:
Compiler:
- Reader transform converts __query LinkedField → InlineFragment with
@__queryRootSelection directive (runs after flatten)
- Codegen recognizes the directive and emits QueryRootSelection node
Runtime:
- New QueryRootSelection case in RelayReader reads selections from
ROOT_ID instead of the current record
- Results merge into the current fragment's data object
E2E test verifies the full pipeline: a fragment with __query { greeting }
reads greeting from the query root while name comes from the User record.
- Remove __query interception from graphql-ir/build.rs (generic crate should not have Relay-specific features) - Register __query in schema named_field() instead — the IR builder finds it via normal field lookup and builds selections against its return type (Query) automatically - Gate all transforms behind enable_query_root_selection feature flag (disabled by default) - Update tests and e2e fixture to enable the flag
- @Skip on __query directly - Nested conditions: @include → @Skip → @include(on __query) — all three conditions accumulate on the hoisted selection - Conditions inside __query (regular field-level, not hoisted) - Cross-fragment: condition on parent field + condition on __query in the fragment (documents current behavior: fragment-level conditions propagate, but caller-level conditions only propagate in normalization pipeline where fragments are inlined)
Previously, the operation_text pipeline only propagated conditions found within the same fragment as __query. Conditions from ancestor fields/fragments in the caller were lost because fragments are processed independently (not inlined). Now each fragment spread carries the full condition stack from its call site. When resolving transitive hoisted selections, the conditions from each fragment boundary are accumulated, so deeply nested @skip/@include across arbitrary fragment spreads all wrap the final hoisted selections.
- Unify wrap_in_conditions/wrap_in_owned_conditions into one function using ConditionInfo type with a wrap() method - Consolidate 4 fragment maps into 1 (FragmentExtraction struct) - Use push/pop on mutable Vec for condition_stack instead of slice-and-collect (avoids O(depth) allocation per Condition node) - Use .remove() instead of .get().cloned() in resolve_transitive_hoisted - Remove empty-branch comment (inverted condition)
When a field (e.g. viewer) already exists at the query root and is also selected via __query in a fragment, the flatten transform merges their selections. Integration test confirms the final operation text has a single viewer with combined selections (id + name).
When viewer exists at query root with @Skip and is also hoisted from __query without conditions, flatten correctly keeps them separate (different condition = different selection). Both appear in the operation text — the server evaluates each independently.
QueryRootSelection reader now writes results into a __query sub-object on the fragment data rather than merging flat. This avoids field name collisions between __query selections and fields on the parent type. Access pattern: data.__query.viewer.canDeleteUsers (not data.viewer)
When a user hovers over __query in their editor, the LSP shows:
"Relay extension: select fields from the query root within any
fragment. Selections inside __query { ... } are hoisted to the query
root at compile time, letting a fragment declare its dependency on
root-level data without threading it through every parent component.
Access the data via data.__query."
- Run rustfmt on all changed Rust files - Add QueryRootSelection case to createUpdatableProxy.js exhaustive switch (Flow requires all ReaderSelection union members handled) - Cast data.__query to SelectorData in RelayReader (Flow can't infer the type of dynamic property access on SelectorData)
- Regenerate compile_relay_artifacts_test.rs and hoist_query_selections_test.rs via fixture_tests_bin (CI checks that these match the auto-generated output) - Fix Flow: use fresh SelectorData object for __query instead of casting from unknown (Flow disallows any casts and colon-cast syntax)
- Add query_selection_field() to InterfaceOnlySchema test impl - Update compact serializer to strip 6 synthetic fields (was 5) - Update schema test snapshots to include __query field
…t files - Fix compact serializer to strip 6 synthetic fields (was 5) - Add query_selection_field to InterfaceOnlySchema test impl - Update relay-compiler-config-schema.json for new feature flag - Regenerate all fixture test files via fixture_tests_bin
3f3ccb2 to
18dbe3d
Compare
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.
As discussed with folks (@mjmahone, @PascalSenn and @michaelstaib I think?) at GraphQL Conf 2026
This sketch explores a feature which allows access to the Query root from within any selection set.
Motivation
A common pain point in developing applications while using fragment co-location is that you need access to some global value in a deeply nested component. For example, you may want to check whether the current viewer has some specific permission or query for the set of options that are valid for a specific drop-down menu.
Today, this requires threading a separate fragment on Query all the way through to this potentially deeply nested component in parallel with the component's existing fragment, which is likely related to the data that component is actively rendering. Today, when this is impractical, we recommend updating the schema and re-exposing the root field(s) on whatever arbitrary type you already have a fragment on.
Hoisted Query Selections allows you to define the root data you need as a
__queryselection on any type and the Relay compiler/ can "hoist" the selections up to the root of all queries which include the fragment with the__query. Relay Runtime can then ensure the data is materialized out of the normalized store.Example
Let's look at an example of how this works. Note that the real benefit here is not felt locally in the component that uses the data as much as its felt in the potentially long chain (potentially multiple long chains) of components between the query roots and the component that needs the data.
Before
After
Tradeoffs
Pros
@skip/@includealong the path from the query root to the fragment where__queryis selected, Relay can include those conditions in the root selections.Cons
__querybe ignored? Should it error? Ignoring it could be confusing, and making it an error could significantly reduce the feature's utility since spreading fragments into mutations is a common pattern.