Skip to content

Commit 5dd2b5c

Browse files
committed
Fix query planning for @Shareable mutation fields
1 parent 56f75f3 commit 5dd2b5c

File tree

4 files changed

+141
-8
lines changed

4 files changed

+141
-8
lines changed

query-graphs-js/src/__tests__/graphPath.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function createOptions(supergraph: Schema, queryGraph: QueryGraph): Simultaneous
7272
[],
7373
[],
7474
new Map(),
75+
null,
7576
);
7677
}
7778

query-graphs-js/src/graphPath.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,7 +1573,11 @@ function advancePathWithNonCollectingAndTypePreservingTransitions<TTrigger, V ex
15731573
let backToPreviousSubgraph: boolean;
15741574
if (subgraphEnteringEdge.edge.transition.kind === 'SubgraphEnteringTransition') {
15751575
assert(toAdvance.root instanceof RootVertex, () => `${toAdvance} should be a root path if it starts with subgraph entering edge ${subgraphEnteringEdge.edge}`);
1576-
prevSubgraphEnteringVertex = rootVertexForSubgraph(toAdvance.graph, edge.tail.source, toAdvance.root.rootKind);
1576+
// Since mutation options need to originate from the same subgraph, we pretend we cannot find a root vertex
1577+
// in another subgraph (effectively skipping the optimization).
1578+
prevSubgraphEnteringVertex = toAdvance.root.rootKind !== 'mutation'
1579+
? rootVertexForSubgraph(toAdvance.graph, edge.tail.source, toAdvance.root.rootKind)
1580+
: undefined;
15771581
// If the entering edge is the root entering of subgraphs, then the "prev subgraph" is really `edge.tail.source` and
15781582
// so `edge` always get us back to that (but `subgraphEnteringEdge.edge.head.source` would be `FEDERATED_GRAPH_ROOT_SOURCE`,
15791583
// so the test we do in the `else` branch would not work here).
@@ -2383,6 +2387,7 @@ export function createInitialOptions<V extends Vertex>(
23832387
excludedEdges: ExcludedDestinations,
23842388
excludedConditions: ExcludedConditions,
23852389
overrideConditions: Map<string, boolean>,
2390+
initialSubgraphConstraint: string | null,
23862391
): SimultaneousPathsWithLazyIndirectPaths<V>[] {
23872392
const lazyInitialPath = new SimultaneousPathsWithLazyIndirectPaths(
23882393
[initialPath],
@@ -2393,7 +2398,12 @@ export function createInitialOptions<V extends Vertex>(
23932398
overrideConditions,
23942399
);
23952400
if (isFederatedGraphRootType(initialPath.tail.type)) {
2396-
const initialOptions = lazyInitialPath.indirectOptions(initialContext, 0);
2401+
let initialOptions = lazyInitialPath.indirectOptions(initialContext, 0);
2402+
if (initialSubgraphConstraint !== null) {
2403+
initialOptions.paths = initialOptions
2404+
.paths
2405+
.filter((path) => path.tail.source === initialSubgraphConstraint);
2406+
}
23972407
return createLazyOptions(initialOptions.paths.map(p => [p]), lazyInitialPath, initialContext, overrideConditions);
23982408
} else {
23992409
return [lazyInitialPath];

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6127,6 +6127,85 @@ describe('mutations', () => {
61276127
}
61286128
`);
61296129
});
6130+
6131+
it('executes a single mutation operation on a @shareable field', () => {
6132+
const subgraph1 = {
6133+
name: 'Subgraph1',
6134+
typeDefs: gql`
6135+
type Query {
6136+
dummy: Int
6137+
}
6138+
6139+
type Mutation {
6140+
f: F @shareable
6141+
}
6142+
6143+
type F @key(fields: "id") {
6144+
id: ID!
6145+
x: Int
6146+
}
6147+
`,
6148+
};
6149+
6150+
const subgraph2 = {
6151+
name: 'Subgraph2',
6152+
typeDefs: gql`
6153+
type Mutation {
6154+
f: F @shareable
6155+
}
6156+
6157+
type F @key(fields: "id", resolvable: false) {
6158+
id: ID!
6159+
y: Int
6160+
}
6161+
`,
6162+
};
6163+
6164+
const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2);
6165+
const operation = operationFromDocument(
6166+
api,
6167+
gql`
6168+
mutation {
6169+
f {
6170+
x
6171+
y
6172+
}
6173+
}
6174+
`,
6175+
);
6176+
6177+
const plan = queryPlanner.buildQueryPlan(operation);
6178+
expect(plan).toMatchInlineSnapshot(`
6179+
QueryPlan {
6180+
Sequence {
6181+
Fetch(service: "Subgraph2") {
6182+
{
6183+
f {
6184+
__typename
6185+
id
6186+
y
6187+
}
6188+
}
6189+
},
6190+
Flatten(path: "f") {
6191+
Fetch(service: "Subgraph1") {
6192+
{
6193+
... on F {
6194+
__typename
6195+
id
6196+
}
6197+
} =>
6198+
{
6199+
... on F {
6200+
x
6201+
}
6202+
}
6203+
},
6204+
},
6205+
},
6206+
}
6207+
`);
6208+
});
61306209
});
61316210

61326211
describe('interface type-explosion', () => {

query-planner-js/src/buildPlan.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ class QueryPlanningTraversal<RV extends Vertex> {
398398
initialContext: PathContext,
399399
typeConditionedFetching: boolean,
400400
nonLocalSelectionsState: NonLocalSelectionsState | null,
401+
initialSubgraphConstraint: string | null,
401402
excludedDestinations: ExcludedDestinations = [],
402403
excludedConditions: ExcludedConditions = [],
403404
) {
@@ -418,6 +419,7 @@ class QueryPlanningTraversal<RV extends Vertex> {
418419
excludedDestinations,
419420
excludedConditions,
420421
parameters.overrideConditions,
422+
initialSubgraphConstraint,
421423
);
422424
this.stack = mapOptionsToSelections(selectionSet, initialOptions);
423425
if (
@@ -527,8 +529,9 @@ class QueryPlanningTraversal<RV extends Vertex> {
527529
// If we have no options, it means there is no way to build a plan for that branch, and
528530
// that means the whole query planning has no plan.
529531
// This should never happen for a top-level query planning (unless the supergraph has *not* been
530-
// validated), but can happen when computing sub-plans for a key condition.
531-
if (this.isTopLevel) {
532+
// validated), but can happen when computing sub-plans for a key condition and when computing
533+
// a top-level plan for a mutation field on a specific subgraph.
534+
if (this.isTopLevel && this.rootKind !== 'mutation') {
532535
debug.groupEnd(() => `No valid options to advance ${selection} from ${advanceOptionsToString(options)}`);
533536
throw new Error(`Was not able to find any options for ${selection}: This shouldn't have happened.`);
534537
} else {
@@ -794,6 +797,7 @@ class QueryPlanningTraversal<RV extends Vertex> {
794797
context,
795798
this.typeConditionedFetching,
796799
null,
800+
null,
797801
excludedDestinations,
798802
addConditionExclusion(excludedConditions, edge.conditions),
799803
).findBestPlan();
@@ -3593,7 +3597,7 @@ function computePlanInternal({
35933597

35943598
const { operation, processor } = parameters;
35953599
if (operation.rootKind === 'mutation') {
3596-
const dependencyGraphs = computeRootSerialDependencyGraph(
3600+
const dependencyGraphs = computeRootSerialDependencyGraphForMutation(
35973601
parameters,
35983602
hasDefers,
35993603
nonLocalSelectionsState,
@@ -3770,13 +3774,52 @@ function computeRootParallelBestPlan(
37703774
emptyContext,
37713775
parameters.config.typeConditionedFetching,
37723776
nonLocalSelectionsState,
3777+
null,
37733778
);
37743779
const plan = planningTraversal.findBestPlan();
37753780
// Getting no plan means the query is essentially unsatisfiable (it's a valid query, but we can prove it will never return a result),
37763781
// so we just return an empty plan.
37773782
return plan ?? createEmptyPlan(parameters);
37783783
}
37793784

3785+
function computeRootParallelBestPlanForMutation(
3786+
parameters: PlanningParameters<RootVertex>,
3787+
selection: SelectionSet,
3788+
startFetchIdGen: number,
3789+
hasDefers: boolean,
3790+
nonLocalSelectionsState: NonLocalSelectionsState | null,
3791+
): [FetchDependencyGraph, OpPathTree<RootVertex>, number] {
3792+
let bestPlan:
3793+
| [FetchDependencyGraph, OpPathTree<RootVertex>, number]
3794+
| undefined;
3795+
const mutationSubgraphs = parameters.federatedQueryGraph
3796+
.outEdges(parameters.root).map((edge) => edge.tail.source);
3797+
for (const mutationSubgraph of mutationSubgraphs) {
3798+
const planningTraversal = new QueryPlanningTraversal(
3799+
parameters,
3800+
selection,
3801+
startFetchIdGen,
3802+
hasDefers,
3803+
parameters.root.rootKind,
3804+
defaultCostFunction,
3805+
emptyContext,
3806+
parameters.config.typeConditionedFetching,
3807+
nonLocalSelectionsState,
3808+
mutationSubgraph,
3809+
);
3810+
const plan = planningTraversal.findBestPlan();
3811+
if (!bestPlan || (plan && plan[2] < bestPlan[2])) {
3812+
bestPlan = plan;
3813+
}
3814+
}
3815+
if (!bestPlan) {
3816+
throw new Error(
3817+
`Was not able to plan ${parameters.operation.toString(false, false)} starting from a single subgraph: This shouldn't have happened.`,
3818+
);
3819+
}
3820+
return bestPlan;
3821+
}
3822+
37803823
function createEmptyPlan(
37813824
parameters: PlanningParameters<RootVertex>,
37823825
): [FetchDependencyGraph, OpPathTree<RootVertex>, number] {
@@ -3794,7 +3837,7 @@ function onlyRootSubgraph(graph: FetchDependencyGraph): string {
37943837
return subgraphs[0];
37953838
}
37963839

3797-
function computeRootSerialDependencyGraph(
3840+
function computeRootSerialDependencyGraphForMutation(
37983841
parameters: PlanningParameters<RootVertex>,
37993842
hasDefers: boolean,
38003843
nonLocalSelectionsState: NonLocalSelectionsState | null,
@@ -3805,7 +3848,7 @@ function computeRootSerialDependencyGraph(
38053848
const splittedRoots = splitTopLevelFields(operation.selectionSet);
38063849
const graphs: FetchDependencyGraph[] = [];
38073850
let startingFetchId = 0;
3808-
let [prevDepGraph, prevPaths] = computeRootParallelBestPlan(
3851+
let [prevDepGraph, prevPaths] = computeRootParallelBestPlanForMutation(
38093852
parameters,
38103853
splittedRoots[0],
38113854
startingFetchId,
@@ -3814,7 +3857,7 @@ function computeRootSerialDependencyGraph(
38143857
);
38153858
let prevSubgraph = onlyRootSubgraph(prevDepGraph);
38163859
for (let i = 1; i < splittedRoots.length; i++) {
3817-
const [newDepGraph, newPaths] = computeRootParallelBestPlan(
3860+
const [newDepGraph, newPaths] = computeRootParallelBestPlanForMutation(
38183861
parameters,
38193862
splittedRoots[i],
38203863
prevDepGraph.nextFetchId(),

0 commit comments

Comments
 (0)