Skip to content

Commit 308f954

Browse files
committed
Fix query planning for @Shareable mutation fields
1 parent 0243a7c commit 308f954

File tree

6 files changed

+225
-12
lines changed

6 files changed

+225
-12
lines changed

apollo-federation/src/query_graph/graph_path.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1801,10 +1801,17 @@ where
18011801
"Encountered non-root path with a subgraph-entering transition",
18021802
));
18031803
};
1804-
self.graph
1805-
.root_kinds_to_nodes_by_source(&edge_tail_weight.source)?
1806-
.get(root_kind)
1807-
.copied()
1804+
// Since mutation options need to originate from the same subgraph, we
1805+
// pretend we cannot find a root node in another subgraph (effectively
1806+
// skipping the optimization).
1807+
if *root_kind == SchemaRootDefinitionKind::Mutation {
1808+
None
1809+
} else {
1810+
self.graph
1811+
.root_kinds_to_nodes_by_source(&edge_tail_weight.source)?
1812+
.get(root_kind)
1813+
.copied()
1814+
}
18081815
} else {
18091816
Some(last_subgraph_entering_edge_head)
18101817
};

apollo-federation/src/query_graph/graph_path/operation.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2289,6 +2289,7 @@ pub(crate) fn create_initial_options(
22892289
excluded_edges: ExcludedDestinations,
22902290
excluded_conditions: ExcludedConditions,
22912291
override_conditions: &OverrideConditions,
2292+
initial_subgraph_constraint: Option<Arc<str>>,
22922293
disabled_subgraphs: &IndexSet<Arc<str>>,
22932294
) -> Result<Vec<SimultaneousPathsWithLazyIndirectPaths>, FederationError> {
22942295
let initial_paths = SimultaneousPaths::from(initial_path);
@@ -2310,8 +2311,18 @@ pub(crate) fn create_initial_options(
23102311
.paths
23112312
.iter()
23122313
.cloned()
2313-
.map(SimultaneousPaths::from)
2314-
.collect();
2314+
.map(|path| {
2315+
let Some(initial_subgraph_constraint) = &initial_subgraph_constraint else {
2316+
return Ok::<_, FederationError>(Some(path));
2317+
};
2318+
let tail_weight = path.graph.node_weight(path.tail)?;
2319+
Ok(if &tail_weight.source == initial_subgraph_constraint {
2320+
Some(path)
2321+
} else {
2322+
None
2323+
})
2324+
})
2325+
.process_results(|iter| iter.flatten().map(SimultaneousPaths::from).collect())?;
23152326
Ok(lazy_initial_path.create_lazy_options(options, initial_context))
23162327
} else {
23172328
Ok(vec![lazy_initial_path])

apollo-federation/src/query_plan/query_planner.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use apollo_compiler::collections::IndexMap;
99
use apollo_compiler::collections::IndexSet;
1010
use apollo_compiler::validation::Valid;
1111
use itertools::Itertools;
12+
use petgraph::visit::EdgeRef;
1213
use serde::Deserialize;
1314
use serde::Serialize;
1415
use tracing::trace;
@@ -576,7 +577,7 @@ impl QueryPlanner {
576577
}
577578
}
578579

579-
fn compute_root_serial_dependency_graph(
580+
fn compute_root_serial_dependency_graph_for_mutation(
580581
parameters: &QueryPlanningParameters,
581582
has_defers: bool,
582583
non_local_selection_state: &mut Option<non_local_selections_estimation::State>,
@@ -606,7 +607,7 @@ fn compute_root_serial_dependency_graph(
606607
mut fetch_dependency_graph,
607608
path_tree: mut prev_path,
608609
..
609-
} = compute_root_parallel_best_plan(
610+
} = compute_root_parallel_best_plan_for_mutation(
610611
parameters,
611612
selection_set,
612613
has_defers,
@@ -618,7 +619,7 @@ fn compute_root_serial_dependency_graph(
618619
fetch_dependency_graph: new_dep_graph,
619620
path_tree: new_path,
620621
..
621-
} = compute_root_parallel_best_plan(
622+
} = compute_root_parallel_best_plan_for_mutation(
622623
parameters,
623624
selection_set,
624625
has_defers,
@@ -777,6 +778,7 @@ fn compute_root_parallel_best_plan(
777778
parameters.operation.root_kind,
778779
FetchDependencyGraphToCostProcessor,
779780
non_local_selection_state.as_mut(),
781+
None,
780782
)?;
781783

782784
// Getting no plan means the query is essentially unsatisfiable (it's a valid query, but we can prove it will never return a result),
@@ -786,6 +788,43 @@ fn compute_root_parallel_best_plan(
786788
.unwrap_or_else(|| BestQueryPlanInfo::empty(parameters)))
787789
}
788790

791+
fn compute_root_parallel_best_plan_for_mutation(
792+
parameters: &QueryPlanningParameters,
793+
selection: SelectionSet,
794+
has_defers: bool,
795+
non_local_selection_state: &mut Option<non_local_selections_estimation::State>,
796+
) -> Result<BestQueryPlanInfo, FederationError> {
797+
parameters.federated_query_graph.out_edges(parameters.head).into_iter().map(|edge_ref| {
798+
let mutation_subgraph = parameters.federated_query_graph.node_weight(edge_ref.target())?.source.clone();
799+
let planning_traversal = QueryPlanningTraversal::new(
800+
parameters,
801+
selection.clone(),
802+
has_defers,
803+
parameters.operation.root_kind,
804+
FetchDependencyGraphToCostProcessor,
805+
non_local_selection_state.as_mut(),
806+
Some(mutation_subgraph),
807+
)?;
808+
planning_traversal.find_best_plan()
809+
}).process_results(|iter| iter
810+
.flatten()
811+
.min_by(|a, b| a.cost.total_cmp(&b.cost))
812+
.map(Ok)
813+
.unwrap_or_else(|| {
814+
if parameters.disabled_subgraphs.is_empty() {
815+
Err(FederationError::internal(format!(
816+
"Was not able to plan {} starting from a single subgraph: This shouldn't have happened.",
817+
parameters.operation,
818+
)))
819+
} else {
820+
// If subgraphs were disabled, this could be expected, and we indicate this in
821+
// the error accordingly.
822+
Err(SingleFederationError::NoPlanFoundWithDisabledSubgraphs.into())
823+
}
824+
})
825+
)?
826+
}
827+
789828
fn compute_plan_internal(
790829
parameters: &mut QueryPlanningParameters,
791830
processor: &mut FetchDependencyGraphToQueryPlanProcessor,
@@ -797,7 +836,7 @@ fn compute_plan_internal(
797836
let (main, deferred, primary_selection, cost) = if root_kind
798837
== SchemaRootDefinitionKind::Mutation
799838
{
800-
let dependency_graphs = compute_root_serial_dependency_graph(
839+
let dependency_graphs = compute_root_serial_dependency_graph_for_mutation(
801840
parameters,
802841
has_defers,
803842
non_local_selection_state,

apollo-federation/src/query_plan/query_planning_traversal.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
222222
root_kind: SchemaRootDefinitionKind,
223223
cost_processor: FetchDependencyGraphToCostProcessor,
224224
non_local_selection_state: Option<&mut non_local_selections_estimation::State>,
225+
initial_subgraph_constraint: Option<Arc<str>>,
225226
) -> Result<Self, FederationError> {
226227
Self::new_inner(
227228
parameters,
@@ -231,6 +232,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
231232
root_kind,
232233
cost_processor,
233234
non_local_selection_state,
235+
initial_subgraph_constraint,
234236
Default::default(),
235237
Default::default(),
236238
Default::default(),
@@ -251,6 +253,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
251253
root_kind: SchemaRootDefinitionKind,
252254
cost_processor: FetchDependencyGraphToCostProcessor,
253255
non_local_selection_state: Option<&mut non_local_selections_estimation::State>,
256+
initial_subgraph_constraint: Option<Arc<str>>,
254257
initial_context: OpGraphPathContext,
255258
excluded_destinations: ExcludedDestinations,
256259
excluded_conditions: ExcludedConditions,
@@ -304,6 +307,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
304307
excluded_destinations,
305308
excluded_conditions,
306309
&parameters.override_conditions,
310+
initial_subgraph_constraint,
307311
&parameters.disabled_subgraphs,
308312
)?;
309313

@@ -519,8 +523,9 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
519523
// If we have no options, it means there is no way to build a plan for that branch, and
520524
// that means the whole query planning process will generate no plan. This should never
521525
// happen for a top-level query planning (unless the supergraph has *not* been
522-
// validated), but can happen when computing sub-plans for a key condition.
523-
return if self.is_top_level {
526+
// validated), but can happen when computing sub-plans for a key condition and when
527+
// computing a top-level plan for a mutation field on a specific subgraph.
528+
return if self.is_top_level && self.root_kind != SchemaRootDefinitionKind::Mutation {
524529
if self.parameters.disabled_subgraphs.is_empty() {
525530
Err(FederationError::internal(format!(
526531
"Was not able to find any options for {selection}: This shouldn't have happened.",
@@ -1161,6 +1166,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> {
11611166
self.root_kind,
11621167
self.cost_processor,
11631168
None,
1169+
None,
11641170
context.clone(),
11651171
excluded_destinations.clone(),
11661172
excluded_conditions.add_item(edge_conditions),

apollo-federation/tests/query_plan/build_query_plan_tests.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,77 @@ fn it_executes_mutation_operations_in_sequence() {
478478
);
479479
}
480480

481+
#[test]
482+
fn it_executes_a_single_mutation_operation_on_a_shareable_field() {
483+
let planner = planner!(
484+
Subgraph1: r#"
485+
type Query {
486+
dummy: Int
487+
}
488+
489+
type Mutation {
490+
f: F @shareable
491+
}
492+
493+
type F @key(fields: "id") {
494+
id: ID!
495+
x: Int
496+
}
497+
"#,
498+
Subgraph2: r#"
499+
type Mutation {
500+
f: F @shareable
501+
}
502+
503+
type F @key(fields: "id", resolvable: false) {
504+
id: ID!
505+
y: Int
506+
}
507+
"#,
508+
);
509+
assert_plan!(
510+
&planner,
511+
r#"
512+
mutation {
513+
f {
514+
x
515+
y
516+
}
517+
}
518+
"#,
519+
@r###"
520+
QueryPlan {
521+
Sequence {
522+
Fetch(service: "Subgraph2") {
523+
{
524+
f {
525+
__typename
526+
id
527+
y
528+
}
529+
}
530+
},
531+
Flatten(path: "f") {
532+
Fetch(service: "Subgraph1") {
533+
{
534+
... on F {
535+
__typename
536+
id
537+
}
538+
} =>
539+
{
540+
... on F {
541+
x
542+
}
543+
}
544+
},
545+
},
546+
},
547+
}
548+
"###
549+
);
550+
}
551+
481552
/// @requires references external field indirectly
482553
#[test]
483554
fn key_where_at_external_is_not_at_top_level_of_selection_of_requires() {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Composed from subgraphs with hash: 7206fa0505b6ab6548c5d6460861c3fcdc746b91
2+
schema
3+
@link(url: "https://specs.apollo.dev/link/v1.0")
4+
@link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION)
5+
{
6+
query: Query
7+
mutation: Mutation
8+
}
9+
10+
directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
11+
12+
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
13+
14+
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
15+
16+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
17+
18+
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
19+
20+
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
21+
22+
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
23+
24+
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
25+
26+
type F
27+
@join__type(graph: SUBGRAPH1, key: "id")
28+
@join__type(graph: SUBGRAPH2, key: "id", resolvable: false)
29+
{
30+
id: ID!
31+
x: Int @join__field(graph: SUBGRAPH1)
32+
y: Int @join__field(graph: SUBGRAPH2)
33+
}
34+
35+
input join__ContextArgument {
36+
name: String!
37+
type: String!
38+
context: String!
39+
selection: join__FieldValue!
40+
}
41+
42+
scalar join__DirectiveArguments
43+
44+
scalar join__FieldSet
45+
46+
scalar join__FieldValue
47+
48+
enum join__Graph {
49+
SUBGRAPH1 @join__graph(name: "Subgraph1", url: "none")
50+
SUBGRAPH2 @join__graph(name: "Subgraph2", url: "none")
51+
}
52+
53+
scalar link__Import
54+
55+
enum link__Purpose {
56+
"""
57+
`SECURITY` features provide metadata necessary to securely resolve fields.
58+
"""
59+
SECURITY
60+
61+
"""
62+
`EXECUTION` features provide metadata necessary for operation execution.
63+
"""
64+
EXECUTION
65+
}
66+
67+
type Mutation
68+
@join__type(graph: SUBGRAPH1)
69+
@join__type(graph: SUBGRAPH2)
70+
{
71+
f: F
72+
}
73+
74+
type Query
75+
@join__type(graph: SUBGRAPH1)
76+
@join__type(graph: SUBGRAPH2)
77+
{
78+
dummy: Int @join__field(graph: SUBGRAPH1)
79+
}

0 commit comments

Comments
 (0)