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

fix handling of __typename optimizations #3156

Merged
merged 3 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/shaggy-phones-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@apollo/query-planner": patch
"@apollo/federation-internals": patch
---

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.
55 changes: 36 additions & 19 deletions internals-js/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
}

abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> extends DirectiveTargetElement<T> {
private attachements?: Map<string, string>;
private attachments?: Map<string, string>;

constructor(
schema: Schema,
Expand Down Expand Up @@ -97,21 +97,21 @@

protected abstract collectVariablesInElement(collector: VariableCollector): void;

addAttachement(key: string, value: string) {
if (!this.attachements) {
this.attachements = new Map();
addAttachment(key: string, value: string) {

Check warning on line 100 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L100

Added line #L100 was not covered by tests
if (!this.attachments) {
this.attachments = new Map();

Check warning on line 102 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L102

Added line #L102 was not covered by tests
}
this.attachements.set(key, value);
this.attachments.set(key, value);

Check warning on line 104 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L104

Added line #L104 was not covered by tests
}

getAttachement(key: string): string | undefined {
return this.attachements?.get(key);
getAttachment(key: string): string | undefined {

Check warning on line 107 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L107

Added line #L107 was not covered by tests
return this.attachments?.get(key);
}

protected copyAttachementsTo(elt: AbstractOperationElement<any>) {
if (this.attachements) {
for (const [k, v] of this.attachements.entries()) {
elt.addAttachement(k, v);
protected copyAttachmentsTo(elt: AbstractOperationElement<any>) {
if (this.attachments) {
for (const [k, v] of this.attachments.entries()) {
elt.addAttachment(k, v);

Check warning on line 114 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L113-L114

Added lines #L113 - L114 were not covered by tests
}
}
}
Expand Down Expand Up @@ -170,6 +170,17 @@
baseType(): NamedType {
return baseType(this.definition.type!);
}

copy(): Field<TArgs> {
const newField = new Field<TArgs>(

Check warning on line 175 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L174-L175

Added lines #L174 - L175 were not covered by tests
this.definition,
this.args,
this.appliedDirectives,
this.alias,
);
this.copyAttachmentsTo(newField);
return newField;

Check warning on line 182 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L181-L182

Added lines #L181 - L182 were not covered by tests
}

withUpdatedArguments(newArgs: TArgs): Field<TArgs> {
const newField = new Field<TArgs>(
Expand All @@ -178,7 +189,7 @@
this.appliedDirectives,
this.alias,
);
this.copyAttachementsTo(newField);
this.copyAttachmentsTo(newField);

Check warning on line 192 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L192

Added line #L192 was not covered by tests
return newField;
}

Expand All @@ -189,7 +200,7 @@
this.appliedDirectives,
this.alias,
);
this.copyAttachementsTo(newField);
this.copyAttachmentsTo(newField);
return newField;
}

Expand All @@ -200,7 +211,7 @@
this.appliedDirectives,
newAlias,
);
this.copyAttachementsTo(newField);
this.copyAttachmentsTo(newField);

Check warning on line 214 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L214

Added line #L214 was not covered by tests
return newField;
}

Expand All @@ -211,7 +222,7 @@
newDirectives,
this.alias,
);
this.copyAttachementsTo(newField);
this.copyAttachmentsTo(newField);

Check warning on line 225 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L225

Added line #L225 was not covered by tests
return newField;
}

Expand Down Expand Up @@ -505,13 +516,13 @@
// schema (typically, the supergraph) than `this.sourceType` (typically, a subgraph), then the new condition uses the
// definition of the proper schema (the supergraph in such cases, instead of the subgraph).
const newFragment = new FragmentElement(newSourceType, newCondition?.name, this.appliedDirectives);
this.copyAttachementsTo(newFragment);
this.copyAttachmentsTo(newFragment);
return newFragment;
}

withUpdatedDirectives(newDirectives: Directive<OperationElement>[]): FragmentElement {
const newFragment = new FragmentElement(this.sourceType, this.typeCondition, newDirectives);
this.copyAttachementsTo(newFragment);
this.copyAttachmentsTo(newFragment);

Check warning on line 525 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L525

Added line #L525 was not covered by tests
return newFragment;
}

Expand Down Expand Up @@ -590,7 +601,7 @@
}

const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
this.copyAttachementsTo(updated);
this.copyAttachmentsTo(updated);

Check warning on line 604 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L604

Added line #L604 was not covered by tests
return updated;
}

Expand Down Expand Up @@ -655,7 +666,7 @@
.concat(new Directive<FragmentElement>(deferDirective.name, newDeferArgs));

const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
this.copyAttachementsTo(updated);
this.copyAttachmentsTo(updated);

Check warning on line 669 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L669

Added line #L669 was not covered by tests
return updated;
}

Expand Down Expand Up @@ -3042,6 +3053,12 @@
return this.element.definition.name === typenameFieldName;
}

withAttachment(key: string, value: string): FieldSelection {
const updatedField = this.element.copy();
updatedField.addAttachment(key, value);
return this.withUpdatedElement(updatedField);

Check warning on line 3059 in internals-js/src/operations.ts

View check run for this annotation

Codecov / codecov/patch

internals-js/src/operations.ts#L3056-L3059

Added lines #L3056 - L3059 were not covered by tests
}

withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
if (this.element === field && this.selectionSet === selectionSet) {
return this;
Expand Down
19 changes: 12 additions & 7 deletions query-planner-js/src/buildPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
import { generateAllPlansAndFindBest } from "./generateAllPlans";
import { QueryPlan, ResponsePath, SequenceNode, PlanNode, ParallelNode, FetchNode, SubscriptionNode, trimSelectionNodes } from "./QueryPlan";


const debug = newDebugLogger('plan');

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

Check warning on line 3451 in query-planner-js/src/buildPlan.ts

View check run for this annotation

Codecov / codecov/patch

query-planner-js/src/buildPlan.ts#L3451

Added line #L3451 was not covered by tests
: i;
}
}

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

// We have a operation element, field or inline fragment. We first check if it's been "tagged" to remember that __typename
// must be queried. See the comment on the `optimizeSiblingTypenames()` method to see why this exists.
const typenameAttachment = operation.getAttachement(SIBLING_TYPENAME_KEY);
const typenameAttachment = operation.getAttachment(SIBLING_TYPENAME_KEY);
if (typenameAttachment !== undefined) {
// We need to add the query __typename for the current type in the current group.
// Note that the value of the "attachement" is the alias or '' if there is no alias
// Note that the value of the "attachment" is the alias or '' if there is no alias
const alias = typenameAttachment === '' ? undefined : typenameAttachment;
const typenameField = new Field(operation.parentType.typenameField()!, undefined, undefined, alias);
group.addAtPath(path.inGroup().concat(typenameField));
Expand Down Expand Up @@ -4631,12 +4636,12 @@
function addBackTypenameInAttachments(selectionSet: SelectionSet): SelectionSet {
return selectionSet.lazyMap((s) => {
const updated = s.mapToSelectionSet((ss) => addBackTypenameInAttachments(ss));
const typenameAttachment = s.element.getAttachement(SIBLING_TYPENAME_KEY);
const typenameAttachment = s.element.getAttachment(SIBLING_TYPENAME_KEY);
if (typenameAttachment === undefined) {
return updated;
} else {
// We need to add the query __typename for the current type in the current group.
// Note that the value of the "attachement" is the alias or '' if there is no alias
// Note that the value of the "attachment" is the alias or '' if there is no alias
const alias = typenameAttachment === '' ? undefined : typenameAttachment;
const typenameField = new Field(s.element.parentType.typenameField()!, undefined, undefined, alias);
return [
Expand Down