Skip to content

Commit af4376f

Browse files
xuorigMarc-Andre Giroux
and
Marc-Andre Giroux
authored
generateQueryFragments generates invalid plans with missing fragment definitions (#2993)
This PR includes a failing test that show that `generateQueryFragments` can generate invalid GraphQL queries. It seems related to the way the hashCode is computed. Any selections of the same length and type are somehow grouped together and an additional fragment definition is not added. Opening this in case somebody gets to the root cause fix while I investigate further. --------- Co-authored-by: Marc-Andre Giroux <[email protected]>
1 parent bbf8394 commit af4376f

File tree

3 files changed

+86
-4
lines changed

3 files changed

+86
-4
lines changed

.changeset/stupid-lemons-rest.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@apollo/query-planner": patch
3+
---
4+
5+
Fix issue with missing fragment definitions due to `generateQueryFragments`.
6+
7+
An incorrect implementation detail in `generateQueryFragments` caused certain queries to be missing fragment definitions. Specifically, subsequent fragment "candidates" with the same type condition and the same length of selections as a previous fragment weren't correctly added to the list of fragments. An example of an affected query is:
8+
9+
```graphql
10+
query {
11+
t {
12+
... on A {
13+
x
14+
y
15+
}
16+
}
17+
t2 {
18+
... on A {
19+
y
20+
z
21+
}
22+
}
23+
}
24+
```
25+
26+
In this case, the second selection set would be converted to an inline fragment spread to subgraph fetches, but the fragment definition would be missing.

internals-js/src/operations.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,7 @@ export class SelectionSet {
15541554
// compute and handle collisions as necessary.
15551555
const mockHashCode = `on${selection.element.typeCondition}` + selection.selectionSet.selections().length;
15561556
const equivalentSelectionSetCandidates = seenSelections.get(mockHashCode);
1557+
15571558
if (equivalentSelectionSetCandidates) {
15581559
// See if any candidates have an equivalent selection set, i.e. {x y} and {y x}.
15591560
const match = equivalentSelectionSetCandidates.find(([candidateSet]) => candidateSet.equals(selection.selectionSet!));
@@ -1572,13 +1573,13 @@ export class SelectionSet {
15721573
`_generated_${mockHashCode}_${equivalentSelectionSetCandidates?.length ?? 0}`,
15731574
selection.element.typeCondition
15741575
).setSelectionSet(minimizedSelectionSet);
1576+
namedFragments.add(fragmentDefinition);
15751577

15761578
// Create a new "hash code" bucket or add to the existing one.
1577-
if (!equivalentSelectionSetCandidates) {
1578-
seenSelections.set(mockHashCode, [[selection.selectionSet, fragmentDefinition]]);
1579-
namedFragments.add(fragmentDefinition);
1580-
} else {
1579+
if (equivalentSelectionSetCandidates) {
15811580
equivalentSelectionSetCandidates.push([selection.selectionSet, fragmentDefinition]);
1581+
} else {
1582+
seenSelections.set(mockHashCode, [[selection.selectionSet, fragmentDefinition]]);
15821583
}
15831584

15841585
return new FragmentSpreadSelection(this.parentType, namedFragments, fragmentDefinition, []);

query-planner-js/src/__tests__/buildPlan.test.ts

+55
Original file line numberDiff line numberDiff line change
@@ -5215,6 +5215,7 @@ describe('Fragment autogeneration', () => {
52155215
type A {
52165216
x: Int
52175217
y: Int
5218+
z: Int
52185219
t: T
52195220
}
52205221
@@ -5425,6 +5426,60 @@ describe('Fragment autogeneration', () => {
54255426
}
54265427
`);
54275428
});
5429+
5430+
it('fragments that share a hash but are not identical generate their own fragment definitions', () => {
5431+
const [api, queryPlanner] = composeAndCreatePlannerWithOptions([subgraph], {
5432+
generateQueryFragments: true,
5433+
});
5434+
const operation = operationFromDocument(
5435+
api,
5436+
gql`
5437+
query {
5438+
t {
5439+
... on A {
5440+
x
5441+
y
5442+
}
5443+
}
5444+
t2 {
5445+
... on A {
5446+
y
5447+
z
5448+
}
5449+
}
5450+
}
5451+
`,
5452+
);
5453+
5454+
const plan = queryPlanner.buildQueryPlan(operation);
5455+
5456+
expect(plan).toMatchInlineSnapshot(`
5457+
QueryPlan {
5458+
Fetch(service: "Subgraph1") {
5459+
{
5460+
t {
5461+
__typename
5462+
..._generated_onA2_0
5463+
}
5464+
t2 {
5465+
__typename
5466+
..._generated_onA2_1
5467+
}
5468+
}
5469+
5470+
fragment _generated_onA2_0 on A {
5471+
x
5472+
y
5473+
}
5474+
5475+
fragment _generated_onA2_1 on A {
5476+
y
5477+
z
5478+
}
5479+
},
5480+
}
5481+
`);
5482+
});
54285483
});
54295484

54305485
test('works with key chains', () => {

0 commit comments

Comments
 (0)