Summary
When a field with @requires sits on an entity that was itself obtained inside an _entities fetch, the planner does not add the required field to that fetch. It emits a separate _entities
fetch to the entity's owning subgraph just to read the required field, then a third fetch to the subgraph that owns the @requires field. Apollo's planner inlines the required field into the parent fetch.
For a parent reached from a root field the required field is inlined correctly, so this only shows up after one entity hop. It also happens when the operation already selects the required field explicitly.
Reproduced with @graphql-hive/router-query-planner 0.0.38 and 0.0.44, with supergraphs composed by @theguild/federation-composition 0.26.0 and by @apollo/composition 2.14.4.
Reproduction
Three subgraphs:
# users
extend schema @link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key"])
type Query { user: User }
type User @key(fields: "id") { id: ID! }
# orders
extend schema @link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key"])
type User @key(fields: "id") { id: ID! orders: [Order!]! }
type Order @key(fields: "id") { id: ID! sku: String! }
# catalog
extend schema @link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key", "@external", "@requires"])
type Order @key(fields: "id") { id: ID! sku: String! @external name: String! @requires(fields: "sku") }
Operation:
{ user { orders { name } } }
Plan it with the npm package:
import { readFileSync } from 'node:fs';
import { QueryPlanner } from '@graphql-hive/router-query-planner';
const planner = new QueryPlanner(readFileSync('supergraph.graphql', 'utf8'));
const plan = planner.plan('{ user { orders { name } } }', undefined, new Set(), 0);
console.log(JSON.stringify(plan, null, 2));
Actual plan
Sequence
Fetch(users) {user{__typename id}}
Flatten(user)
Fetch(orders) _entities(...on User{orders{__typename id}})
Flatten(user.orders.@)
Fetch(orders) _entities(...on Order{sku}) <-- extra round trip to the same subgraph
Flatten(user.orders.@)
Fetch(catalog) _entities(...on Order{name})
With { user { orders { sku name } } } the second fetch does select sku, and the third fetch is still emitted.
Expected plan
What @apollo/query-planner 2.14.4 produces for the same supergraph:
Sequence
Fetch(users) {user{__typename id}}
Flatten(user)
Fetch(orders) _entities(...on User{orders{__typename id sku}})
Flatten(user.orders.@)
Fetch(catalog) _entities(...on Order{name})
Impact
- One extra round trip and one extra
_entities resolution per hop. With @apollo/subgraph that means __resolveReference runs on the owning subgraph for every parent, which typically is a
database lookup per item.
- Subgraphs that never implemented
__resolveReference for an entity they own, which is common when the entity is only ever returned from their own fields, answer the extra fetch with the
default reference resolver. That returns only the key, so the required field comes back null. If it is non-null the whole list fails; if it is nullable the @requires field silently resolves
against null input.
Migrating from Apollo Gateway to Hive therefore changes results for such schemas.
Notes
- Root-level parents are handled as expected:
{ someRootOrder { name } } inlines sku into the root fetch.
- The federation version in the
@link does not matter; v2.5 through v2.9 behave the same.
Summary
When a field with
@requiressits on an entity that was itself obtained inside an_entitiesfetch, the planner does not add the required field to that fetch. It emits a separate_entitiesfetch to the entity's owning subgraph just to read the required field, then a third fetch to the subgraph that owns the
@requiresfield. Apollo's planner inlines the required field into the parent fetch.For a parent reached from a root field the required field is inlined correctly, so this only shows up after one entity hop. It also happens when the operation already selects the required field explicitly.
Reproduced with
@graphql-hive/router-query-planner0.0.38 and 0.0.44, with supergraphs composed by@theguild/federation-composition0.26.0 and by@apollo/composition2.14.4.Reproduction
Three subgraphs:
Operation:
{ user { orders { name } } }Plan it with the npm package:
Actual plan
With
{ user { orders { sku name } } }the second fetch does selectsku, and the third fetch is still emitted.Expected plan
What
@apollo/query-planner2.14.4 produces for the same supergraph:Impact
_entitiesresolution per hop. With@apollo/subgraphthat means__resolveReferenceruns on the owning subgraph for every parent, which typically is adatabase lookup per item.
__resolveReferencefor an entity they own, which is common when the entity is only ever returned from their own fields, answer the extra fetch with thedefault reference resolver. That returns only the key, so the required field comes back
null. If it is non-null the whole list fails; if it is nullable the@requiresfield silently resolvesagainst
nullinput.Migrating from Apollo Gateway to Hive therefore changes results for such schemas.
Notes
{ someRootOrder { name } }inlinesskuinto the root fetch.@linkdoes not matter; v2.5 through v2.9 behave the same.