Skip to content

@requires after an entity hop is resolved with an extra _entities fetch to the owning subgraph instead of being inlined #1539

Description

@TheDadi

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions