Skip to content

Commit 2192f35

Browse files
fix handling of __typename optimizations (#3156)
Fixes handling of a `__typename` selection during query planning process. When expanding fragments we were keeping references to the same `Field`s regardless where those fragments appeared in our original selection set. This was generally fine as in most cases we would have same inline fragment selection sets across whole operation but was causing problems when we were applying another optimization by collapsing those expanded inline fragments creating a new selection set. As a result, if any single field selection (within that fragment) would perform optimization around the usage of `__typename`, ALL occurrences of that field selection would get that optimization as well. See example below ```graphql foo { f1 { ... on Bar { __typename ...onBar # will be collapsed and sub selections will optimize __typename } } f2 { ...onBar # sub selections will get __typename optimization from above } } fragment onBar on Bar { b c } ``` --------- Co-authored-by: Sachin D. Shinde <sachin@apollographql.com>
1 parent cda078c commit 2192f35

File tree

3 files changed

+56
-26
lines changed

3 files changed

+56
-26
lines changed

.changeset/shaggy-phones-know.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@apollo/query-planner": patch
3+
"@apollo/federation-internals": patch
4+
---
5+
6+
Fixes handling of a `__typename` selection during query planning process.
7+
8+
When expanding fragments we were keeping references to the same `Field`s regardless where those fragments appeared in our original selection set. This was generally fine as in most cases we would have same inline fragment selection sets across whole operation but was causing problems when we were applying another optimization by collapsing those expanded inline fragments creating a new selection set. As a result, if any single field selection (within that fragment) would perform optimization around the usage of `__typename`, ALL occurrences of that field selection would get that optimization as well.

internals-js/src/operations.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function haveSameDirectives<TElement extends OperationElement>(op1: TElement, op
6969
}
7070

7171
abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> extends DirectiveTargetElement<T> {
72-
private attachements?: Map<string, string>;
72+
private attachments?: Map<string, string>;
7373

7474
constructor(
7575
schema: Schema,
@@ -97,21 +97,21 @@ abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> e
9797

9898
protected abstract collectVariablesInElement(collector: VariableCollector): void;
9999

100-
addAttachement(key: string, value: string) {
101-
if (!this.attachements) {
102-
this.attachements = new Map();
100+
addAttachment(key: string, value: string) {
101+
if (!this.attachments) {
102+
this.attachments = new Map();
103103
}
104-
this.attachements.set(key, value);
104+
this.attachments.set(key, value);
105105
}
106106

107-
getAttachement(key: string): string | undefined {
108-
return this.attachements?.get(key);
107+
getAttachment(key: string): string | undefined {
108+
return this.attachments?.get(key);
109109
}
110110

111-
protected copyAttachementsTo(elt: AbstractOperationElement<any>) {
112-
if (this.attachements) {
113-
for (const [k, v] of this.attachements.entries()) {
114-
elt.addAttachement(k, v);
111+
protected copyAttachmentsTo(elt: AbstractOperationElement<any>) {
112+
if (this.attachments) {
113+
for (const [k, v] of this.attachments.entries()) {
114+
elt.addAttachment(k, v);
115115
}
116116
}
117117
}
@@ -170,6 +170,17 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
170170
baseType(): NamedType {
171171
return baseType(this.definition.type!);
172172
}
173+
174+
copy(): Field<TArgs> {
175+
const newField = new Field<TArgs>(
176+
this.definition,
177+
this.args,
178+
this.appliedDirectives,
179+
this.alias,
180+
);
181+
this.copyAttachmentsTo(newField);
182+
return newField;
183+
}
173184

174185
withUpdatedArguments(newArgs: TArgs): Field<TArgs> {
175186
const newField = new Field<TArgs>(
@@ -178,7 +189,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
178189
this.appliedDirectives,
179190
this.alias,
180191
);
181-
this.copyAttachementsTo(newField);
192+
this.copyAttachmentsTo(newField);
182193
return newField;
183194
}
184195

@@ -189,7 +200,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
189200
this.appliedDirectives,
190201
this.alias,
191202
);
192-
this.copyAttachementsTo(newField);
203+
this.copyAttachmentsTo(newField);
193204
return newField;
194205
}
195206

@@ -200,7 +211,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
200211
this.appliedDirectives,
201212
newAlias,
202213
);
203-
this.copyAttachementsTo(newField);
214+
this.copyAttachmentsTo(newField);
204215
return newField;
205216
}
206217

@@ -211,7 +222,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
211222
newDirectives,
212223
this.alias,
213224
);
214-
this.copyAttachementsTo(newField);
225+
this.copyAttachmentsTo(newField);
215226
return newField;
216227
}
217228

@@ -505,13 +516,13 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
505516
// schema (typically, the supergraph) than `this.sourceType` (typically, a subgraph), then the new condition uses the
506517
// definition of the proper schema (the supergraph in such cases, instead of the subgraph).
507518
const newFragment = new FragmentElement(newSourceType, newCondition?.name, this.appliedDirectives);
508-
this.copyAttachementsTo(newFragment);
519+
this.copyAttachmentsTo(newFragment);
509520
return newFragment;
510521
}
511522

512523
withUpdatedDirectives(newDirectives: Directive<OperationElement>[]): FragmentElement {
513524
const newFragment = new FragmentElement(this.sourceType, this.typeCondition, newDirectives);
514-
this.copyAttachementsTo(newFragment);
525+
this.copyAttachmentsTo(newFragment);
515526
return newFragment;
516527
}
517528

@@ -590,7 +601,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
590601
}
591602

592603
const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
593-
this.copyAttachementsTo(updated);
604+
this.copyAttachmentsTo(updated);
594605
return updated;
595606
}
596607

@@ -655,7 +666,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
655666
.concat(new Directive<FragmentElement>(deferDirective.name, newDeferArgs));
656667

657668
const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
658-
this.copyAttachementsTo(updated);
669+
this.copyAttachmentsTo(updated);
659670
return updated;
660671
}
661672

@@ -3042,6 +3053,12 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
30423053
return this.element.definition.name === typenameFieldName;
30433054
}
30443055

3056+
withAttachment(key: string, value: string): FieldSelection {
3057+
const updatedField = this.element.copy();
3058+
updatedField.addAttachment(key, value);
3059+
return this.withUpdatedElement(updatedField);
3060+
}
3061+
30453062
withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
30463063
if (this.element === field && this.selectionSet === selectionSet) {
30473064
return this;

query-planner-js/src/buildPlan.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ import { enforceQueryPlannerConfigDefaults, QueryPlannerConfig, validateQueryPla
106106
import { generateAllPlansAndFindBest } from "./generateAllPlans";
107107
import { QueryPlan, ResponsePath, SequenceNode, PlanNode, ParallelNode, FetchNode, SubscriptionNode, trimSelectionNodes } from "./QueryPlan";
108108

109+
109110
const debug = newDebugLogger('plan');
110111

111112
// Somewhat random string used to optimise handling __typename in some cases. See usage for details. The concrete value
@@ -3391,10 +3392,10 @@ export class QueryPlanner {
33913392
* implements an alternative: to avoid the query planning spending time of exploring options for
33923393
* __typename, we "remove" the __typename selections from the operation. But of course, we still
33933394
* need to ensure that __typename is effectively queried, so as we do that removal, we also "tag"
3394-
* one of the "sibling" selection (using `addAttachement`) to remember that __typename needs to
3395+
* one of the "sibling" selection (using `addAttachment`) to remember that __typename needs to
33953396
* be added back eventually. The core query planning algorithm will ignore that tag, and because
33963397
* __typename has been otherwise removed, we'll save any related work. But as we build the final
3397-
* query plan, we'll check back for those "tags" (see `getAttachement` in `computeGroupsForTree`),
3398+
* query plan, we'll check back for those "tags" (see `getAttachment` in `computeGroupsForTree`),
33983399
* and when we fine one, we'll add back the request to __typename. As this only happen after the
33993400
* query planning algorithm has computed all choices, we achieve our goal of not considering useless
34003401
* choices due to __typename. Do note that if __typename is the "only" selection of some selection
@@ -3413,6 +3414,7 @@ export class QueryPlanner {
34133414
// (for instance, the fragment condition could be "less precise" than the parent type, in which case query planning
34143415
// will ignore it) and tagging those could lose the tagging.
34153416
let firstFieldSelection: FieldSelection | undefined = undefined;
3417+
let firstFieldIndex = -1;
34163418
for (let i = 0; i < selections.length; i++) {
34173419
const selection = selections[i];
34183420
let updated: Selection | undefined;
@@ -3445,6 +3447,9 @@ export class QueryPlanner {
34453447
}
34463448
if (!firstFieldSelection && updated.kind === 'FieldSelection') {
34473449
firstFieldSelection = updated;
3450+
firstFieldIndex = updatedSelections
3451+
? updatedSelections.length
3452+
: i;
34483453
}
34493454
}
34503455

@@ -3473,7 +3478,7 @@ export class QueryPlanner {
34733478
if (typenameSelection) {
34743479
if (firstFieldSelection) {
34753480
// Note that as we tag the element, we also record the alias used if any since that needs to be preserved.
3476-
firstFieldSelection.element.addAttachement(SIBLING_TYPENAME_KEY, typenameSelection.element.alias ? typenameSelection.element.alias : '');
3481+
updatedSelections[firstFieldIndex] = firstFieldSelection.withAttachment(SIBLING_TYPENAME_KEY, typenameSelection.element.alias ? typenameSelection.element.alias : '');
34773482
} else {
34783483
// If we have no other field selection, then we can't optimize __typename and we need to add
34793484
// it back to the updated subselections (we add it first because that's usually where we
@@ -4328,10 +4333,10 @@ function computeGroupsForTree(
43284333

43294334
// We have a operation element, field or inline fragment. We first check if it's been "tagged" to remember that __typename
43304335
// must be queried. See the comment on the `optimizeSiblingTypenames()` method to see why this exists.
4331-
const typenameAttachment = operation.getAttachement(SIBLING_TYPENAME_KEY);
4336+
const typenameAttachment = operation.getAttachment(SIBLING_TYPENAME_KEY);
43324337
if (typenameAttachment !== undefined) {
43334338
// We need to add the query __typename for the current type in the current group.
4334-
// Note that the value of the "attachement" is the alias or '' if there is no alias
4339+
// Note that the value of the "attachment" is the alias or '' if there is no alias
43354340
const alias = typenameAttachment === '' ? undefined : typenameAttachment;
43364341
const typenameField = new Field(operation.parentType.typenameField()!, undefined, undefined, alias);
43374342
group.addAtPath(path.inGroup().concat(typenameField));
@@ -4631,12 +4636,12 @@ function addTypenameFieldForAbstractTypes(selectionSet: SelectionSet, parentType
46314636
function addBackTypenameInAttachments(selectionSet: SelectionSet): SelectionSet {
46324637
return selectionSet.lazyMap((s) => {
46334638
const updated = s.mapToSelectionSet((ss) => addBackTypenameInAttachments(ss));
4634-
const typenameAttachment = s.element.getAttachement(SIBLING_TYPENAME_KEY);
4639+
const typenameAttachment = s.element.getAttachment(SIBLING_TYPENAME_KEY);
46354640
if (typenameAttachment === undefined) {
46364641
return updated;
46374642
} else {
46384643
// We need to add the query __typename for the current type in the current group.
4639-
// Note that the value of the "attachement" is the alias or '' if there is no alias
4644+
// Note that the value of the "attachment" is the alias or '' if there is no alias
46404645
const alias = typenameAttachment === '' ? undefined : typenameAttachment;
46414646
const typenameField = new Field(s.element.parentType.typenameField()!, undefined, undefined, alias);
46424647
return [

0 commit comments

Comments
 (0)