Skip to content

Commit 2d44f34

Browse files
author
Sylvain Lebresne
authored
Faster approach to code for reusing fragments in fetches (#2604)
* Faster approach to code for reusing fragments in fetches This commit somehwat "inverse" the way fragment reuse is tried. Before this commit, fragment reuse was tried depth-first, meaning that we tried reusing fragment on the leaf of the result set and then up on every selection. Unfortunately, doing so creates a number of subtlety that force the checks done on every selection to be somewhat costly, which adds up. This commit change the process a bit to test for fragment reuse "at the top level" first (and so always fully expanded selections), and recurse down for the part that didn't matched anything. In practice, doing so simplify things a bit due to always dealing with expanded selections, and this allow to the checks simpler and more efficient. Overall, this commit usually improves the time spend for trying fragment reuse during query planning (sometimes substantially), hence lowering query planning time. * Review feedback
1 parent 0cbf6a4 commit 2d44f34

8 files changed

Lines changed: 1081 additions & 648 deletions

File tree

.changeset/brown-bats-float.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@apollo/query-planner": patch
3+
"@apollo/composition": patch
4+
"@apollo/federation-internals": patch
5+
"@apollo/gateway": patch
6+
---
7+
8+
Re-work the code use to try to reuse query named fragments to improve performance (thus sometimes improving query
9+
planning performance), to fix a possibly raised assertion error (with a message of form like `Cannot add selection of
10+
field X to selection set of parent type Y`), and to fix a rare issue where an interface or union field was not being
11+
queried for all the types it should be.

composition-js/src/validate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ function buildWitnessOperation(witness: RootPath<Transition>): Operation {
189189
schema,
190190
root.rootKind,
191191
buildWitnessNextStep([...witness].map(e => e[0]), 0)!,
192-
new VariableDefinitions()
192+
new VariableDefinitions(),
193193
);
194194
}
195195

gateway-js/src/executeQueryPlan.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export async function executeQueryPlan(
218218
input: unfilteredData,
219219
introspectionHandling: (f) => executeIntrospection(
220220
operationContext.schema,
221-
f.expandAllFragments().toSelectionNode(),
221+
f.expandFragments().toSelectionNode(),
222222
operationContext.operation.variableDefinitions,
223223
variables,
224224
),

internals-js/src/__tests__/operations.test.ts

Lines changed: 185 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import {
2-
CompositeType,
32
defaultRootName,
43
Schema,
54
SchemaRootKind,
65
} from '../../dist/definitions';
76
import { buildSchema } from '../../dist/buildSchema';
8-
import { MutableSelectionSet, Operation, operationFromDocument, parseOperation, parseSelectionSet } from '../../dist/operations';
7+
import { MutableSelectionSet, Operation, operationFromDocument, parseOperation } from '../../dist/operations';
98
import './matchers';
109
import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, SelectionNode, SelectionSetNode } from 'graphql';
1110

@@ -57,18 +56,19 @@ describe('fragments optimization', () => {
5756
// this are just about testing the reuse of fragments and this make things shorter/easier to write.
5857
// There is tests in `buildPlan.test.ts` that double-check that we don't reuse fragments used only
5958
// once in actual query plans.
60-
const optimized = withoutFragments.optimize(operation.selectionSet.fragments!, 1);
59+
const optimized = withoutFragments.optimize(operation.fragments!, 1);
6160
expect(optimized.toString()).toMatchString(operation.toString());
6261
}
6362

64-
test('handles fragments using other fragments', () => {
63+
test('optimize fragments using other fragments when possible', () => {
6564
const schema = parseSchema(`
6665
type Query {
67-
t: T1
66+
t: I
6867
}
6968
7069
interface I {
7170
b: Int
71+
u: U
7272
}
7373
7474
type T1 implements I {
@@ -154,8 +154,68 @@ describe('fragments optimization', () => {
154154
}
155155
`);
156156

157-
const optimized = withoutFragments.optimize(operation.selectionSet.fragments!);
157+
const optimized = withoutFragments.optimize(operation.fragments!);
158158
expect(optimized.toString()).toMatchString(`
159+
fragment OnU on U {
160+
... on I {
161+
b
162+
}
163+
... on T1 {
164+
a
165+
b
166+
}
167+
... on T2 {
168+
x
169+
y
170+
}
171+
}
172+
173+
{
174+
t {
175+
...OnU
176+
u {
177+
...OnU
178+
}
179+
}
180+
}
181+
`);
182+
});
183+
184+
test('handles fragments using other fragments', () => {
185+
const schema = parseSchema(`
186+
type Query {
187+
t: I
188+
}
189+
190+
interface I {
191+
b: Int
192+
c: Int
193+
u1: U
194+
u2: U
195+
}
196+
197+
type T1 implements I {
198+
a: Int
199+
b: Int
200+
c: Int
201+
me: T1
202+
u1: U
203+
u2: U
204+
}
205+
206+
type T2 implements I {
207+
x: String
208+
y: String
209+
b: Int
210+
c: Int
211+
u1: U
212+
u2: U
213+
}
214+
215+
union U = T1 | T2
216+
`);
217+
218+
const operation = parseOperation(schema, `
159219
fragment OnT1 on T1 {
160220
a
161221
b
@@ -168,17 +228,127 @@ describe('fragments optimization', () => {
168228
169229
fragment OnI on I {
170230
b
231+
c
171232
}
172233
173-
{
234+
fragment OnU on U {
235+
...OnI
236+
...OnT1
237+
...OnT2
238+
}
239+
240+
query {
174241
t {
175-
...OnI
176242
...OnT1
177243
...OnT2
178-
u {
179-
...OnI
244+
u1 {
245+
...OnU
246+
}
247+
u2 {
248+
...OnU
249+
}
250+
... on T1 {
251+
me {
252+
...OnI
253+
}
254+
}
255+
}
256+
}
257+
`);
258+
259+
const withoutFragments = parseOperation(schema, operation.toString(true, true));
260+
expect(withoutFragments.toString()).toMatchString(`
261+
{
262+
t {
263+
... on T1 {
264+
a
265+
b
266+
me {
267+
... on I {
268+
b
269+
c
270+
}
271+
}
272+
}
273+
... on T2 {
274+
x
275+
y
276+
}
277+
u1 {
278+
... on U {
279+
... on I {
280+
b
281+
c
282+
}
283+
... on T1 {
284+
a
285+
b
286+
}
287+
... on T2 {
288+
x
289+
y
290+
}
291+
}
292+
}
293+
u2 {
294+
... on U {
295+
... on I {
296+
b
297+
c
298+
}
299+
... on T1 {
300+
a
301+
b
302+
}
303+
... on T2 {
304+
x
305+
y
306+
}
307+
}
308+
}
309+
}
310+
}
311+
`);
312+
313+
const optimized = withoutFragments.optimize(operation.fragments!);
314+
// We should reuse and keep all fragments, because 1) onU is used twice and 2)
315+
// all the other ones are used once in the query, and once in onU definition.
316+
expect(optimized.toString()).toMatchString(`
317+
fragment OnT1 on T1 {
318+
a
319+
b
320+
}
321+
322+
fragment OnT2 on T2 {
323+
x
324+
y
325+
}
326+
327+
fragment OnI on I {
328+
b
329+
c
330+
}
331+
332+
fragment OnU on U {
333+
...OnI
334+
...OnT1
335+
...OnT2
336+
}
337+
338+
{
339+
t {
340+
... on T1 {
180341
...OnT1
181-
...OnT2
342+
me {
343+
...OnI
344+
}
345+
}
346+
...OnT2
347+
u1 {
348+
...OnU
349+
}
350+
u2 {
351+
...OnU
182352
}
183353
}
184354
}
@@ -509,24 +679,24 @@ describe('fragments optimization', () => {
509679
510680
{
511681
t {
512-
...Frag2
513682
...Frag1
683+
...Frag2
514684
}
515685
}
516686
`,
517687
expanded: `
518688
{
519689
t {
520-
a
521690
b {
522-
__typename
523691
x
692+
__typename
524693
}
694+
c
525695
d {
526696
m
527697
n
528698
}
529-
c
699+
a
530700
}
531701
}
532702
`,
@@ -1265,62 +1435,3 @@ describe('unsatisfiable branches removal', () => {
12651435
expect(withoutUnsatisfiableBranches(input)).toBe(output);
12661436
});
12671437
});
1268-
1269-
test('contains ignores unecessary fragments even when subtyping is involved', () => {
1270-
const schema = parseSchema(`
1271-
type Query {
1272-
a: A!
1273-
}
1274-
1275-
interface IA1 {
1276-
b: IB1!
1277-
}
1278-
1279-
interface IA2 {
1280-
b: IB2!
1281-
}
1282-
1283-
type A implements IA1 & IA2 {
1284-
b: B!
1285-
}
1286-
1287-
interface IB1 {
1288-
v1: Int!
1289-
}
1290-
1291-
interface IB2 {
1292-
v2: Int!
1293-
}
1294-
1295-
type B implements IB1 & IB2 {
1296-
v1: Int!
1297-
v2: Int!
1298-
}
1299-
`);
1300-
1301-
const typeA = schema.type('A') as CompositeType;
1302-
1303-
const s1 = parseSelectionSet({
1304-
parentType: typeA,
1305-
source: '{ b { v1 v2 } }'
1306-
});
1307-
1308-
const s2 = parseSelectionSet({
1309-
parentType: typeA,
1310-
source: '{ ... on IA1 { b { v1 } } ... on IA2 { b { v2 } } }'
1311-
});
1312-
1313-
// Here, A is a concrete type, and IA1 and IA2 are just 2 of its interfaces, so
1314-
// a { ... on IA1 { b { v1 } } ... on IA2 { b { v2 } } }
1315-
// is basically exactly the same as:
1316-
// a { b { v1 } b { v2 } }
1317-
// which is the same as:
1318-
// a { b { v1 v2 } }
1319-
// and that is why we want the `contains` below to work (note that a simple `contains`
1320-
// that doesn't handle this kind of subtlety could have its use too, it would just be
1321-
// a different contract, but we want "our" `contains` to handle this because of named
1322-
// fragments "reconstruction" where that kind of subtlety arises).
1323-
//
1324-
// Here, the added subtlety is that there is interface subtyping involved too.
1325-
expect(s2.contains(s1)).toBeTruthy();
1326-
});

0 commit comments

Comments
 (0)