Skip to content

Commit e0a5075

Browse files
Fragment variable definitions erased in subgraph queries (#3119)
Fixes #3112 Before creating an Operation, we call `collectUsedVariables`, which will only pull values from the selection set and not from fragments. This isn't great, because a variable used in a fragment won't be collected, and it doesn't make sense to collect variables from fragments because it's before they are optimized and many will be unused. The inelegant solution I came up with is to pass in available variables in calls to `optimize` or `generateQueryFragments` for an operation where we can add back in the unused variables. This should be ok, because we are guaranteed that exactly one of them will get called by `toPlanNode`. Pretty sure there won't be too much overhead added because we'll only call this once per subgraph fetch. --------- Co-authored-by: Sachin D. Shinde <sachin@apollographql.com>
1 parent 02c2a34 commit e0a5075

3 files changed

Lines changed: 64 additions & 7 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@apollo/query-planner": patch
3+
"@apollo/federation-internals": patch
4+
---
5+
6+
Fix issue where variable was not passed into subgraph when embedded in a fragment

internals-js/src/operations.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ import { assert, mapKeys, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap
5656
import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
5757
import { v1 as uuidv1 } from 'uuid';
5858

59+
export const DEFAULT_MIN_USAGES_TO_OPTIMIZE = 2;
60+
5961
function validate(condition: any, message: () => string, sourceAST?: ASTNode): asserts condition {
6062
if (!condition) {
6163
throw ERRORS.INVALID_GRAPHQL.err(message(), { nodes: sourceAST });
@@ -934,25 +936,55 @@ export class Operation extends DirectiveTargetElement<Operation> {
934936
this.appliedDirectives,
935937
);
936938
}
939+
940+
private collectUndefinedVariablesFromFragments(fragments: NamedFragments): Variable[] {
941+
const collector = new VariableCollector();
942+
for (const namedFragment of fragments.definitions()) {
943+
namedFragment.selectionSet.usedVariables().forEach(v => {
944+
if (!this.variableDefinitions.definition(v)) {
945+
collector.add(v);
946+
}
947+
});
948+
}
949+
return collector.variables();
950+
}
937951

938952
// Returns a copy of this operation with the provided updated selection set and fragments.
939-
private withUpdatedSelectionSetAndFragments(newSelectionSet: SelectionSet, newFragments: NamedFragments | undefined): Operation {
953+
private withUpdatedSelectionSetAndFragments(
954+
newSelectionSet: SelectionSet,
955+
newFragments: NamedFragments | undefined,
956+
allAvailableVariables?: VariableDefinitions,
957+
): Operation {
940958
if (this.selectionSet === newSelectionSet && newFragments === this.fragments) {
941959
return this;
942960
}
961+
962+
let newVariableDefinitions = this.variableDefinitions;
963+
if (allAvailableVariables && newFragments) {
964+
const undefinedVariables = this.collectUndefinedVariablesFromFragments(newFragments);
965+
if (undefinedVariables.length > 0) {
966+
newVariableDefinitions = new VariableDefinitions();
967+
newVariableDefinitions.addAll(this.variableDefinitions);
968+
newVariableDefinitions.addAll(allAvailableVariables.filter(undefinedVariables));
969+
}
970+
}
943971

944972
return new Operation(
945973
this.schema(),
946974
this.rootKind,
947975
newSelectionSet,
948-
this.variableDefinitions,
976+
newVariableDefinitions,
949977
newFragments,
950978
this.name,
951979
this.appliedDirectives,
952980
);
953981
}
954982

955-
optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation {
983+
optimize(
984+
fragments?: NamedFragments,
985+
minUsagesToOptimize: number = DEFAULT_MIN_USAGES_TO_OPTIMIZE,
986+
allAvailableVariables?: VariableDefinitions,
987+
): Operation {
956988
assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`)
957989
if (!fragments || fragments.isEmpty()) {
958990
return this;
@@ -1001,11 +1033,16 @@ export class Operation extends DirectiveTargetElement<Operation> {
10011033
}
10021034
}
10031035

1004-
return this.withUpdatedSelectionSetAndFragments(optimizedSelection, finalFragments ?? undefined);
1036+
return this.withUpdatedSelectionSetAndFragments(
1037+
optimizedSelection,
1038+
finalFragments ?? undefined,
1039+
allAvailableVariables,
1040+
);
10051041
}
10061042

10071043
generateQueryFragments(): Operation {
10081044
const [minimizedSelectionSet, fragments] = this.selectionSet.minimizeSelectionSet();
1045+
10091046
return new Operation(
10101047
this.schema(),
10111048
this.rootKind,

query-planner-js/src/buildPlan.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
Variable,
2626
VariableDefinition,
2727
VariableDefinitions,
28-
VariableCollector,
2928
newDebugLogger,
3029
selectionOfElement,
3130
selectionSetOfElement,
@@ -64,6 +63,8 @@ import {
6463
isInputType,
6564
possibleRuntimeTypes,
6665
NamedType,
66+
VariableCollector,
67+
DEFAULT_MIN_USAGES_TO_OPTIMIZE,
6768
} from "@apollo/federation-internals";
6869
import {
6970
advanceSimultaneousPathsWithOperation,
@@ -1591,16 +1592,29 @@ class FetchGroup {
15911592
if (this.generateQueryFragments) {
15921593
operation = operation.generateQueryFragments();
15931594
} else {
1594-
operation = operation.optimize(fragments?.forSubgraph(this.subgraphName, subgraphSchema));
1595+
operation = operation.optimize(
1596+
fragments?.forSubgraph(this.subgraphName, subgraphSchema),
1597+
DEFAULT_MIN_USAGES_TO_OPTIMIZE,
1598+
variableDefinitions,
1599+
);
15951600
}
15961601

1602+
// collect all used variables in the selection and in used Fragments
1603+
const usedVariables = new Set(selection.usedVariables().map(v => v.name));
1604+
if (operation.fragments) {
1605+
for (const namedFragment of operation.fragments.definitions()) {
1606+
namedFragment.selectionSet.usedVariables().forEach(v => {
1607+
usedVariables.add(v.name);
1608+
});
1609+
}
1610+
}
15971611
const operationDocument = operationToDocument(operation);
15981612
const fetchNode: FetchNode = {
15991613
kind: 'Fetch',
16001614
id: this.id,
16011615
serviceName: this.subgraphName,
16021616
requires: inputNodes ? trimSelectionNodes(inputNodes.selections) : undefined,
1603-
variableUsages: selection.usedVariables().map(v => v.name),
1617+
variableUsages: Array.from(usedVariables),
16041618
operation: stripIgnoredCharacters(print(operationDocument)),
16051619
operationKind: schemaRootKindToOperationKind(operation.rootKind),
16061620
operationName: operation.name,

0 commit comments

Comments
 (0)