Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query planner generates invalid fetch node selection sets when reuseQueryFragments=true #2952

Closed
tinnou opened this issue Mar 6, 2024 · 2 comments · Fixed by #2963
Closed

Comments

@tinnou
Copy link
Contributor

tinnou commented Mar 6, 2024

Issue Description

Issue Description

When reuseQueryFragments=true (the default config), in certain cases the query planner will generate a fetch that will fail GraphQL validation (Field Selection Merging - overlapping fields can be merged rule) at the subgraph GraphQL server. Specifically, this happens when a fragment from the query is reused and the query planner also queries the same field separately but the two aren't identical. The example below specifically showcases when field arguments mismatch.

Steps to Reproduce

I built a minimum viable example unit test that reproduces building an incorrect plan.

Subgraph discoveryDgs schema:

     type Query {
        pinotPreQuerySearchPage: PinotUIEntity
      }
    
      union PinotUIEntity = PinotBoxShotEntityTreatment | PinotEpisodicBillboardEntityTreatment | PinotBannerWithTrailerEntityTreatment
      
      type PinotBannerWithTrailerEntityTreatment @key(fields: "unifiedEntityId") {
        unifiedEntityId: ID!
        unifiedEntity: UnifiedEntity
      }
      
      type PinotBoxShotEntityTreatment @key(fields: "unifiedEntityId") {
        unifiedEntityId: ID!
        unifiedEntity: UnifiedEntity
      }
      
      type PinotEpisodicBillboardEntityTreatment @key(fields: "unifiedEntityId") {
        unifiedEntityId: ID!
        unifiedEntity: UnifiedEntity
      }
      
      interface UnifiedEntity {
          id: ID!
      }

      type GenericContainer implements UnifiedEntity @key(fields: "id"){
        id: ID!
      }
      
      type Supplemental implements UnifiedEntity @key(fields: "id") {
        id: ID!
      }
      
      type Show implements UnifiedEntity @key(fields: "id") {
        id: ID!
      }

and subgraph gusto schema:

      type Query {
        me: String
      }
      
      interface Video {
        videoId: Int!
        taglineMessage(uiContext: String): String
        bar: String
      }
      
      interface UnifiedEntity {
          id: ID!
      }
      
      type GenericContainer implements UnifiedEntity @key(fields: "id") {
        id: ID!
        taglineMessage(uiContext: String): String
      }
      
      type Supplemental implements UnifiedEntity & Video @key(fields: "id") {
        videoId: Int!
        id: ID!
        taglineMessage(uiContext: String): String
        bar: String
      }
      
      type Show implements UnifiedEntity & Video @key(fields: "id") {
        videoId: Int!
        id: ID!
        taglineMessage(uiContext: String): String
        bar: String
      }

Executing the following query:

    query Search {
          pinotPreQuerySearchPage {
            __typename
            ... on PinotBannerWithTrailerEntityTreatment {
              __typename
              unifiedEntity {
                __typename
                ... on GenericContainer {
                  id
                  taglineMessage(uiContext: "ODP")
                }
              }
            }
            ... on PinotEpisodicBillboardEntityTreatment {
              __typename
              unifiedEntity {
                __typename
                ...BillboardDataVideoSummary
              }
            }
          }
        }
        
        fragment BillboardDataVideoSummary on Video {
          __typename
          taglineMessage(uiContext: "BILLBOARD")
        }

The plan that is currently generated is as follow:

QueryPlan {
  Sequence {
    Fetch(service: "discoveryDgs") {
      {
        pinotPreQuerySearchPage {
          __typename
          ... on PinotBannerWithTrailerEntityTreatment {
            __typename
            unifiedEntity {
              __typename
              ... on GenericContainer {
                __typename
                id
              }
            }
          }
          ... on PinotEpisodicBillboardEntityTreatment {
            __typename
            unifiedEntity {
              __typename
              ... on Show {
                __typename
                id
              }
              ... on Supplemental {
                __typename
                id
              }
            }
          }
        }
      }
    },
    Flatten(path: "pinotPreQuerySearchPage.unifiedEntity") {
      Fetch(service: "gusto") {
        {
          ... on GenericContainer {
            __typename
            id
          }
          ... on Show {
            __typename
            id
          }
          ... on Supplemental {
            __typename
            id
          }
        } =>
        {
          ... on GenericContainer {
            taglineMessage(uiContext: "ODP")
          }
          ... on Show {
            ...BillboardDataVideoSummary
          }
          ... on Supplemental {
            ...BillboardDataVideoSummary
          }
        }
        
        fragment BillboardDataVideoSummary on Video {
          __typename
          taglineMessage(uiContext: "BILLBOARD")
        }
      },
    },
  },
}

The problem here is the selection set of the second fetch:

       {
          ... on GenericContainer {
            taglineMessage(uiContext: "ODP")
          }
          ... on Show {
            ...BillboardDataVideoSummary
          }
          ... on Supplemental {
            ...BillboardDataVideoSummary
          }
        }
        
        fragment BillboardDataVideoSummary on Video {
          __typename
          taglineMessage(uiContext: "BILLBOARD")
        }

It's not obvious, but the taglineMessage is selected across two spreads ... on GenericContainer and fragment BillboardDataVideoSummary on Video and although there is no type on the schema that can be both a GenericContainer and a Video, the GraphQL spec Field Selection Merging validates against fields having differing sets of arguments.

Hence, the selection set produces will error at subgraph GraphQL server validation.
The unit test proves this by calling graphql-js validation on every operation of every fetchNode on the query plan.
The test fails with the following error:

(...)Fields "taglineMessage" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.](...)

Impact

Impacts (some) queries using fragments.

Link to Reproduction

https://github.com/apollographql/federation/compare/main...tinnou:federation:query-planner-fragment-invalid-bfg-822?expand=1

Reproduction Steps

No response

Link to Reproduction

https://github.com/apollographql/federation/compare/main...tinnou:federation:query-planner-fragment-invalid-bfg-822?expand=1

Reproduction Steps

No response

@samuelAndalon
Copy link
Contributor

graphql/graphql-js#4025

a better approach is to programmatically create fragments, instead of attempting to reuse fragments from the original incoming operation. I believe Apollo will copy/paste the fragmentifyDocument function in a release this week.

@duckki
Copy link
Contributor

duckki commented Mar 20, 2024

The query planner took necessary precautions to generate logically sound subgraph queries. But, unfortunately, the GraphQL type system is not strong enough to admit them in some cases. A related GraphQL spec discussion is here (graphql/graphql-spec#1085).

I created PR (#2963) to avoid the issue for now.

duckki added a commit to duckki/federation that referenced this issue Mar 26, 2024
- updated computeExpandedSelectionSetAtType function not to trim fragment's validators if the fragment's type condition is not an object type.
- This change is necessary because `FieldsInSetCanMerge` rule is more restrictive in that case.
- The untrimmed validator should avoid generating invalid queries, but it may be less efficient.

apollographql#2952
duckki added a commit that referenced this issue Apr 3, 2024
… with `reuseQueryFragments` set true (#2963)

Fixed #2952.

### Summary of changes

- updated computeExpandedSelectionSetAtType function not to trim
fragment's validators if the fragment's type condition is not an object
type.
- This change is necessary because `FieldsInSetCanMerge` rule is more
restrictive in that case.
- The untrimmed validator should avoid generating invalid queries, but
it may be less efficient.

### Test plan

- The operations.test.ts confirms the changes of named fragments'
validators.
- Added a new test (based on the github issue's reproducer) confirming
that subgraph queries are no longer invalid.
o0Ignition0o pushed a commit that referenced this issue Apr 16, 2024
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @apollo/[email protected]

### Patch Changes

- Fix a query planning bug where invalid subgraph queries are generated
with `reuseQueryFragments` set true.
([#2952](#2952))
([#2963](#2963))

- Updated dependencies
\[[`ec04c50b4fb832bfd281ecf9c0c2dd7656431b96`](ec04c50),
[`a494631918156f0431ceace74281c076cf1d5d51`](a494631)]:
    -   @apollo/[email protected]
    -   @apollo/[email protected]

## @apollo/[email protected]

### Patch Changes

- Updated dependencies
\[[`ec04c50b4fb832bfd281ecf9c0c2dd7656431b96`](ec04c50),
[`3e2c845c74407a136b9e0066e44c1ad1467d3013`](3e2c845),
[`a494631918156f0431ceace74281c076cf1d5d51`](a494631)]:
    -   @apollo/[email protected]
    -   @apollo/[email protected]
    -   @apollo/[email protected]

## @apollo/[email protected]

### Patch Changes

- Fix a query planning bug where invalid subgraph queries are generated
with `reuseQueryFragments` set true.
([#2952](#2952))
([#2963](#2963))

- Fixed query planner to pass the directives from original query to
subgraph operations (#2961)
([#2967](#2967))

## @apollo/[email protected]

### Patch Changes

- Updated dependencies
\[[`ec04c50b4fb832bfd281ecf9c0c2dd7656431b96`](ec04c50),
[`a494631918156f0431ceace74281c076cf1d5d51`](a494631)]:
    -   @apollo/[email protected]

## @apollo/[email protected]

### Patch Changes

- Fix a query planning bug where invalid subgraph queries are generated
with `reuseQueryFragments` set true.
([#2952](#2952))
([#2963](#2963))

- Type conditioned fetching
([#2949](#2949))

When querying a field that is in a path of 2 or more unions, the query
planner was not able to handle different selections and would
aggressively collapse selections in fetches yielding an incorrect plan.

This change introduces new syntax to express type conditions in (key and
flatten) paths. Type conditioned fetching can be enabled through a flag,
and execution is supported in the router only. (#2938)

- Fixed query planner to pass the directives from original query to
subgraph operations (#2961)
([#2967](#2967))

- Updated dependencies
\[[`ec04c50b4fb832bfd281ecf9c0c2dd7656431b96`](ec04c50),
[`a494631918156f0431ceace74281c076cf1d5d51`](a494631)]:
    -   @apollo/[email protected]
    -   @apollo/[email protected]

## @apollo/[email protected]

### Patch Changes

- Updated dependencies
\[[`ec04c50b4fb832bfd281ecf9c0c2dd7656431b96`](ec04c50),
[`a494631918156f0431ceace74281c076cf1d5d51`](a494631)]:
    -   @apollo/[email protected]

## [email protected]

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants