1use std::cell::Cell;
2use std::num::NonZeroU32;
3use std::ops::Deref;
4use std::sync::Arc;
5
6use apollo_compiler::ExecutableDocument;
7use apollo_compiler::Name;
8use apollo_compiler::collections::IndexMap;
9use apollo_compiler::collections::IndexSet;
10use apollo_compiler::validation::Valid;
11use itertools::Itertools;
12use serde::Serialize;
13use tracing::trace;
14
15use super::ConditionNode;
16use super::fetch_dependency_graph::FetchIdGenerator;
17use crate::ApiSchemaOptions;
18use crate::Supergraph;
19use crate::bail;
20use crate::error::FederationError;
21use crate::error::SingleFederationError;
22use crate::operation::NamedFragments;
23use crate::operation::NormalizedDefer;
24use crate::operation::Operation;
25use crate::operation::SelectionSet;
26use crate::operation::normalize_operation;
27use crate::query_graph::QueryGraph;
28use crate::query_graph::QueryGraphNodeType;
29use crate::query_graph::build_federated_query_graph;
30use crate::query_graph::path_tree::OpPathTree;
31use crate::query_plan::PlanNode;
32use crate::query_plan::QueryPlan;
33use crate::query_plan::SequenceNode;
34use crate::query_plan::TopLevelPlanNode;
35use crate::query_plan::fetch_dependency_graph::FetchDependencyGraph;
36use crate::query_plan::fetch_dependency_graph::FetchDependencyGraphNodePath;
37use crate::query_plan::fetch_dependency_graph::compute_nodes_for_tree;
38use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphProcessor;
39use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToCostProcessor;
40use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToQueryPlanProcessor;
41use crate::query_plan::query_planning_traversal::BestQueryPlanInfo;
42use crate::query_plan::query_planning_traversal::QueryPlanningParameters;
43use crate::query_plan::query_planning_traversal::QueryPlanningTraversal;
44use crate::query_plan::query_planning_traversal::convert_type_from_subgraph;
45use crate::query_plan::query_planning_traversal::non_local_selections_estimation;
46use crate::schema::ValidFederationSchema;
47use crate::schema::position::AbstractTypeDefinitionPosition;
48use crate::schema::position::CompositeTypeDefinitionPosition;
49use crate::schema::position::InterfaceTypeDefinitionPosition;
50use crate::schema::position::ObjectTypeDefinitionPosition;
51use crate::schema::position::OutputTypeDefinitionPosition;
52use crate::schema::position::SchemaRootDefinitionKind;
53use crate::schema::position::TypeDefinitionPosition;
54use crate::utils::logging::snapshot;
55
56#[derive(Debug, Clone, Hash, Serialize)]
57pub struct QueryPlannerConfig {
58 pub generate_query_fragments: bool,
63
64 pub subgraph_graphql_validation: bool,
72
73 pub incremental_delivery: QueryPlanIncrementalDeliveryConfig,
79
80 pub debug: QueryPlannerDebugConfig,
84
85 pub type_conditioned_fetching: bool,
92}
93
94#[allow(clippy::derivable_impls)] impl Default for QueryPlannerConfig {
96 fn default() -> Self {
97 Self {
98 generate_query_fragments: false,
99 subgraph_graphql_validation: false,
100 incremental_delivery: Default::default(),
101 debug: Default::default(),
102 type_conditioned_fetching: false,
103 }
104 }
105}
106
107#[derive(Debug, Clone, Default, Hash, Serialize)]
108pub struct QueryPlanIncrementalDeliveryConfig {
109 #[serde(default)]
119 pub enable_defer: bool,
120}
121
122#[derive(Debug, Clone, Hash, Serialize)]
123pub struct QueryPlannerDebugConfig {
124 pub max_evaluated_plans: NonZeroU32,
142
143 pub paths_limit: Option<u32>,
156}
157
158impl Default for QueryPlannerDebugConfig {
159 fn default() -> Self {
160 Self {
161 max_evaluated_plans: NonZeroU32::new(10_000).unwrap(),
162 paths_limit: None,
163 }
164 }
165}
166
167#[derive(Debug, PartialEq, Default, Serialize)]
169pub struct QueryPlanningStatistics {
170 pub evaluated_plan_count: Cell<usize>,
171 pub evaluated_plan_paths: Cell<usize>,
172}
173
174#[derive(Debug, Clone)]
175pub struct QueryPlanOptions {
176 pub override_conditions: Vec<String>,
183 pub non_local_selections_limit_enabled: bool,
186}
187
188impl Default for QueryPlanOptions {
189 fn default() -> Self {
190 Self {
191 override_conditions: Vec::new(),
192 non_local_selections_limit_enabled: true,
193 }
194 }
195}
196
197#[derive(Debug, Default, Clone)]
198pub(crate) struct EnabledOverrideConditions(IndexSet<String>);
199
200impl Deref for EnabledOverrideConditions {
201 type Target = IndexSet<String>;
202
203 fn deref(&self) -> &Self::Target {
204 &self.0
205 }
206}
207
208pub struct QueryPlanner {
209 config: QueryPlannerConfig,
210 federated_query_graph: Arc<QueryGraph>,
211 supergraph_schema: ValidFederationSchema,
212 api_schema: ValidFederationSchema,
213 interface_types_with_interface_objects: IndexSet<InterfaceTypeDefinitionPosition>,
216 abstract_types_with_inconsistent_runtime_types: IndexSet<Name>,
221}
222
223impl QueryPlanner {
224 #[cfg_attr(
225 feature = "snapshot_tracing",
226 tracing::instrument(level = "trace", skip_all, name = "QueryPlanner::new")
227 )]
228 pub fn new(
229 supergraph: &Supergraph,
230 config: QueryPlannerConfig,
231 ) -> Result<Self, FederationError> {
232 let supergraph_schema = supergraph.schema.clone();
233 let api_schema = supergraph.to_api_schema(ApiSchemaOptions {
234 include_defer: config.incremental_delivery.enable_defer,
235 ..Default::default()
236 })?;
237 let query_graph = build_federated_query_graph(
238 supergraph_schema.clone(),
239 api_schema.clone(),
240 Some(true),
241 Some(true),
242 )?;
243
244 let interface_types_with_interface_objects = supergraph
245 .schema
246 .get_types()
247 .filter_map(|position| match position {
248 TypeDefinitionPosition::Interface(interface_position) => Some(interface_position),
249 _ => None,
250 })
251 .map(|position| {
252 let is_interface_object = query_graph
253 .subgraphs()
254 .map(|(_name, schema)| {
255 let Some(position) = schema.try_get_type(position.type_name.clone()) else {
256 return Ok(false);
257 };
258 schema.is_interface_object_type(position)
259 })
260 .process_results(|mut iter| iter.any(|b| b))?;
261 Ok::<_, FederationError>((position, is_interface_object))
262 })
263 .process_results(|iter| {
264 iter.flat_map(|(position, is_interface_object)| {
265 if is_interface_object {
266 Some(position)
267 } else {
268 None
269 }
270 })
271 .collect::<IndexSet<_>>()
272 })?;
273
274 let is_inconsistent = |position: AbstractTypeDefinitionPosition| {
275 let mut sources = query_graph.subgraphs().filter_map(|(_name, subgraph)| {
276 match subgraph.try_get_type(position.type_name().clone())? {
277 TypeDefinitionPosition::Object(_) => None,
282 TypeDefinitionPosition::Interface(interface) => Some(
283 subgraph
284 .referencers()
285 .get_interface_type(&interface.type_name)
286 .ok()?
287 .object_types
288 .clone(),
289 ),
290 TypeDefinitionPosition::Union(union_) => Some(
291 union_
292 .try_get(subgraph.schema())?
293 .members
294 .iter()
295 .map(|member| ObjectTypeDefinitionPosition::new(member.name.clone()))
296 .collect(),
297 ),
298 _ => None,
299 }
300 });
301
302 let Some(expected_runtimes) = sources.next() else {
303 return false;
304 };
305 !sources.all(|runtimes| runtimes == expected_runtimes)
306 };
307
308 let abstract_types_with_inconsistent_runtime_types = supergraph
309 .schema
310 .get_types()
311 .filter_map(|position| AbstractTypeDefinitionPosition::try_from(position).ok())
312 .filter(|position| is_inconsistent(position.clone()))
313 .map(|position| position.type_name().clone())
314 .collect::<IndexSet<_>>();
315
316 Ok(Self {
317 config,
318 federated_query_graph: Arc::new(query_graph),
319 supergraph_schema,
320 api_schema,
321 interface_types_with_interface_objects,
322 abstract_types_with_inconsistent_runtime_types,
323 })
324 }
325
326 pub fn subgraph_schemas(&self) -> &IndexMap<Arc<str>, ValidFederationSchema> {
327 self.federated_query_graph.subgraph_schemas()
328 }
329
330 #[cfg_attr(
332 feature = "snapshot_tracing",
333 tracing::instrument(level = "trace", skip_all, name = "QueryPlanner::build_query_plan")
334 )]
335 pub fn build_query_plan(
336 &self,
337 document: &Valid<ExecutableDocument>,
338 operation_name: Option<Name>,
339 options: QueryPlanOptions,
340 ) -> Result<QueryPlan, FederationError> {
341 let operation = document
342 .operations
343 .get(operation_name.as_ref().map(|name| name.as_str()))
344 .map_err(|_| {
345 if operation_name.is_some() {
346 SingleFederationError::UnknownOperation
347 } else {
348 SingleFederationError::OperationNameNotProvided
349 }
350 })?;
351 if operation.selection_set.is_empty() {
352 crate::bail!("Invalid operation: empty selection set")
354 }
355
356 let is_subscription = operation.is_subscription();
357
358 let statistics = QueryPlanningStatistics::default();
359
360 let normalized_operation = normalize_operation(
361 operation,
362 NamedFragments::new(&document.fragments, &self.api_schema),
363 &self.api_schema,
364 &self.interface_types_with_interface_objects,
365 )?;
366
367 let NormalizedDefer {
368 operation: normalized_operation,
369 assigned_defer_labels,
370 defer_conditions,
371 has_defers,
372 } = normalized_operation.with_normalized_defer()?;
373 if has_defers && is_subscription {
374 return Err(SingleFederationError::DeferredSubscriptionUnsupported.into());
375 }
376
377 if normalized_operation.selection_set.is_empty() {
378 return Ok(QueryPlan::default());
379 }
380
381 snapshot!(
382 "NormalizedOperation",
383 serde_json_bytes::json!({
384 "original": &operation.serialize().to_string(),
385 "normalized": &normalized_operation.to_string()
386 })
387 .to_string(),
388 "normalized operation"
389 );
390
391 let Some(root) = self
392 .federated_query_graph
393 .root_kinds_to_nodes()?
394 .get(&normalized_operation.root_kind)
395 else {
396 bail!(
397 "Shouldn't have a {0} operation if the subgraphs don't have a {0} root",
398 normalized_operation.root_kind
399 )
400 };
401
402 let operation_compression = if self.config.generate_query_fragments {
403 SubgraphOperationCompression::GenerateFragments
404 } else {
405 SubgraphOperationCompression::Disabled
406 };
407 let mut processor = FetchDependencyGraphToQueryPlanProcessor::new(
408 normalized_operation.variables.clone(),
409 normalized_operation.directives.clone(),
410 operation_compression,
411 operation.name.clone(),
412 assigned_defer_labels,
413 );
414 let mut parameters = QueryPlanningParameters {
415 supergraph_schema: self.supergraph_schema.clone(),
416 federated_query_graph: self.federated_query_graph.clone(),
417 operation: Arc::new(normalized_operation),
418 head: *root,
419 head_must_be_root: true,
422 statistics: &statistics,
423 abstract_types_with_inconsistent_runtime_types: self
424 .abstract_types_with_inconsistent_runtime_types
425 .clone()
426 .into(),
427 config: self.config.clone(),
428 override_conditions: EnabledOverrideConditions(IndexSet::from_iter(
429 options.override_conditions,
430 )),
431 fetch_id_generator: Arc::new(FetchIdGenerator::new()),
432 };
433
434 let mut non_local_selection_state = options
435 .non_local_selections_limit_enabled
436 .then(non_local_selections_estimation::State::default);
437 let root_node = if !defer_conditions.is_empty() {
438 compute_plan_for_defer_conditionals(
439 &mut parameters,
440 &mut processor,
441 defer_conditions,
442 &mut non_local_selection_state,
443 )
444 } else {
445 compute_plan_internal(
446 &mut parameters,
447 &mut processor,
448 has_defers,
449 &mut non_local_selection_state,
450 )
451 }?;
452
453 let root_node = match root_node {
454 Some(PlanNode::Fetch(root_node)) if is_subscription => Some(
458 TopLevelPlanNode::Subscription(crate::query_plan::SubscriptionNode {
459 primary: root_node,
460 rest: None,
461 }),
462 ),
463 Some(PlanNode::Sequence(root_node)) if is_subscription => {
464 let Some((primary, rest)) = root_node.nodes.split_first() else {
465 bail!("Invalid query plan: Sequence must have at least one node");
467 };
468 let PlanNode::Fetch(primary) = primary.clone() else {
469 bail!("Invalid query plan: Primary node of a subscription is not a Fetch");
470 };
471 let rest = PlanNode::Sequence(SequenceNode {
472 nodes: rest.to_vec(),
473 });
474 Some(TopLevelPlanNode::Subscription(
475 crate::query_plan::SubscriptionNode {
476 primary,
477 rest: Some(Box::new(rest)),
478 },
479 ))
480 }
481 Some(node) if is_subscription => {
482 bail!(
483 "Invalid query plan for subscription: unexpected {} at root",
484 node.node_kind()
485 );
486 }
487 Some(PlanNode::Fetch(inner)) => Some(TopLevelPlanNode::Fetch(inner)),
488 Some(PlanNode::Sequence(inner)) => Some(TopLevelPlanNode::Sequence(inner)),
489 Some(PlanNode::Parallel(inner)) => Some(TopLevelPlanNode::Parallel(inner)),
490 Some(PlanNode::Flatten(inner)) => Some(TopLevelPlanNode::Flatten(inner)),
491 Some(PlanNode::Defer(inner)) => Some(TopLevelPlanNode::Defer(inner)),
492 Some(PlanNode::Condition(inner)) => Some(TopLevelPlanNode::Condition(inner)),
493 None => None,
494 };
495
496 let plan = QueryPlan {
497 node: root_node,
498 statistics,
499 };
500
501 snapshot!(
502 "QueryPlan",
503 plan.to_string(),
504 "QueryPlan from build_query_plan"
505 );
506 snapshot!(
507 plan.statistics,
508 "QueryPlanningStatistics from build_query_plan"
509 );
510
511 Ok(plan)
512 }
513
514 pub fn api_schema(&self) -> &ValidFederationSchema {
516 &self.api_schema
517 }
518
519 pub fn supergraph_schema(&self) -> &ValidFederationSchema {
520 &self.supergraph_schema
521 }
522}
523
524fn compute_root_serial_dependency_graph(
525 parameters: &QueryPlanningParameters,
526 has_defers: bool,
527 non_local_selection_state: &mut Option<non_local_selections_estimation::State>,
528) -> Result<Vec<FetchDependencyGraph>, FederationError> {
529 let QueryPlanningParameters {
530 supergraph_schema,
531 federated_query_graph,
532 operation,
533 ..
534 } = parameters;
535 let root_type: Option<CompositeTypeDefinitionPosition> = if has_defers {
536 supergraph_schema
537 .schema()
538 .root_operation(operation.root_kind.into())
539 .and_then(|name| supergraph_schema.get_type(name.clone()).ok())
540 .and_then(|ty| ty.try_into().ok())
541 } else {
542 None
543 };
544 let mut split_roots = operation.selection_set.clone().split_top_level_fields();
546 let mut digest = Vec::new();
547 let selection_set = split_roots
548 .next()
549 .ok_or_else(|| FederationError::internal("Empty top level fields"))?;
550 let BestQueryPlanInfo {
551 mut fetch_dependency_graph,
552 path_tree: mut prev_path,
553 ..
554 } = compute_root_parallel_best_plan(
555 parameters,
556 selection_set,
557 has_defers,
558 non_local_selection_state,
559 )?;
560 let mut prev_subgraph = only_root_subgraph(&fetch_dependency_graph)?;
561 for selection_set in split_roots {
562 let BestQueryPlanInfo {
563 fetch_dependency_graph: new_dep_graph,
564 path_tree: new_path,
565 ..
566 } = compute_root_parallel_best_plan(
567 parameters,
568 selection_set,
569 has_defers,
570 non_local_selection_state,
571 )?;
572 let new_subgraph = only_root_subgraph(&new_dep_graph)?;
573 if new_subgraph == prev_subgraph {
574 Arc::make_mut(&mut prev_path).extend(&new_path);
584 fetch_dependency_graph = FetchDependencyGraph::new(
585 supergraph_schema.clone(),
586 federated_query_graph.clone(),
587 root_type.clone(),
588 fetch_dependency_graph.fetch_id_generation.clone(),
589 );
590 compute_root_fetch_groups(
591 operation.root_kind,
592 federated_query_graph,
593 &mut fetch_dependency_graph,
594 &prev_path,
595 parameters.config.type_conditioned_fetching,
596 )?;
597 } else {
598 digest.push(std::mem::replace(
603 &mut fetch_dependency_graph,
604 new_dep_graph,
605 ));
606 prev_path = new_path;
607 prev_subgraph = new_subgraph;
608 }
609 }
610 digest.push(fetch_dependency_graph);
611 Ok(digest)
612}
613
614fn only_root_subgraph(graph: &FetchDependencyGraph) -> Result<Arc<str>, FederationError> {
615 let mut iter = graph.root_node_by_subgraph_iter();
616 let (Some((name, _)), None) = (iter.next(), iter.next()) else {
617 return Err(FederationError::internal(format!(
618 "{graph} should have only one root."
619 )));
620 };
621 Ok(name.clone())
622}
623
624#[cfg_attr(
625 feature = "snapshot_tracing",
626 tracing::instrument(level = "trace", skip_all, name = "compute_root_fetch_groups")
627)]
628pub(crate) fn compute_root_fetch_groups(
629 root_kind: SchemaRootDefinitionKind,
630 federated_query_graph: &QueryGraph,
631 dependency_graph: &mut FetchDependencyGraph,
632 path: &OpPathTree,
633 type_conditioned_fetching_enabled: bool,
634) -> Result<(), FederationError> {
635 for child in &path.childs {
642 let edge = child.edge.expect("The root edge should not be None");
643 let (_source_node, target_node) = path.graph.edge_endpoints(edge)?;
644 let target_node = path.graph.node_weight(target_node)?;
645 let subgraph_name = &target_node.source;
646 let root_type: CompositeTypeDefinitionPosition = match &target_node.type_ {
647 QueryGraphNodeType::SchemaType(OutputTypeDefinitionPosition::Object(object)) => {
648 object.clone().into()
649 }
650 ty => {
651 return Err(FederationError::internal(format!(
652 "expected an object type for the root of a subgraph, found {ty}"
653 )));
654 }
655 };
656 let fetch_dependency_node = dependency_graph.get_or_create_root_node(
657 subgraph_name,
658 root_kind,
659 root_type.clone(),
660 )?;
661 snapshot!(
662 "FetchDependencyGraph",
663 dependency_graph.to_dot(),
664 "tree_with_root_node"
665 );
666 let subgraph_schema = federated_query_graph.schema_by_source(subgraph_name)?;
667 let supergraph_root_type = convert_type_from_subgraph(
668 root_type,
669 subgraph_schema,
670 &dependency_graph.supergraph_schema,
671 )?;
672 compute_nodes_for_tree(
673 dependency_graph,
674 &child.tree,
675 fetch_dependency_node,
676 FetchDependencyGraphNodePath::new(
677 dependency_graph.supergraph_schema.clone(),
678 type_conditioned_fetching_enabled,
679 supergraph_root_type,
680 )?,
681 Default::default(),
682 &Default::default(),
683 )?;
684 }
685 Ok(())
686}
687
688fn compute_root_parallel_dependency_graph(
689 parameters: &QueryPlanningParameters,
690 has_defers: bool,
691 non_local_selection_state: &mut Option<non_local_selections_estimation::State>,
692) -> Result<FetchDependencyGraph, FederationError> {
693 trace!("Starting process to construct a parallel fetch dependency graph");
694 let selection_set = parameters.operation.selection_set.clone();
695 let best_plan = compute_root_parallel_best_plan(
696 parameters,
697 selection_set,
698 has_defers,
699 non_local_selection_state,
700 )?;
701 snapshot!(
702 "FetchDependencyGraph",
703 best_plan.fetch_dependency_graph.to_dot(),
704 "Fetch dependency graph returned from compute_root_parallel_best_plan"
705 );
706 Ok(best_plan.fetch_dependency_graph)
707}
708
709fn compute_root_parallel_best_plan(
710 parameters: &QueryPlanningParameters,
711 selection: SelectionSet,
712 has_defers: bool,
713 non_local_selection_state: &mut Option<non_local_selections_estimation::State>,
714) -> Result<BestQueryPlanInfo, FederationError> {
715 let planning_traversal = QueryPlanningTraversal::new(
716 parameters,
717 selection,
718 has_defers,
719 parameters.operation.root_kind,
720 FetchDependencyGraphToCostProcessor,
721 non_local_selection_state.as_mut(),
722 )?;
723
724 Ok(planning_traversal
727 .find_best_plan()?
728 .unwrap_or_else(|| BestQueryPlanInfo::empty(parameters)))
729}
730
731fn compute_plan_internal(
732 parameters: &mut QueryPlanningParameters,
733 processor: &mut FetchDependencyGraphToQueryPlanProcessor,
734 has_defers: bool,
735 non_local_selection_state: &mut Option<non_local_selections_estimation::State>,
736) -> Result<Option<PlanNode>, FederationError> {
737 let root_kind = parameters.operation.root_kind;
738
739 let (main, deferred, primary_selection) = if root_kind == SchemaRootDefinitionKind::Mutation {
740 let dependency_graphs = compute_root_serial_dependency_graph(
741 parameters,
742 has_defers,
743 non_local_selection_state,
744 )?;
745 let mut main = None;
746 let mut deferred = vec![];
747 let mut primary_selection = None::<SelectionSet>;
748 for mut dependency_graph in dependency_graphs {
749 let (local_main, local_deferred) =
750 dependency_graph.process(&mut *processor, root_kind)?;
751 main = match main {
752 Some(unlocal_main) => processor.reduce_sequence([Some(unlocal_main), local_main]),
753 None => local_main,
754 };
755 deferred.extend(local_deferred);
756 let new_selection = dependency_graph.defer_tracking.primary_selection;
757 match primary_selection.as_mut() {
758 Some(selection) => {
759 if let Some(new_selection) = new_selection {
760 selection.add_local_selection_set(&new_selection)?
761 }
762 }
763 None => primary_selection = new_selection,
764 }
765 }
766 (main, deferred, primary_selection)
767 } else {
768 let mut dependency_graph = compute_root_parallel_dependency_graph(
769 parameters,
770 has_defers,
771 non_local_selection_state,
772 )?;
773
774 let (main, deferred) = dependency_graph.process(&mut *processor, root_kind)?;
775 snapshot!(
776 "FetchDependencyGraph",
777 dependency_graph.to_dot(),
778 "Plan after calling FetchDependencyGraph::process"
779 );
780 let primary_selection = dependency_graph.defer_tracking.primary_selection;
782
783 (main, deferred, primary_selection)
784 };
785
786 if deferred.is_empty() {
787 Ok(main)
788 } else {
789 let Some(primary_selection) = primary_selection else {
790 unreachable!("Should have had a primary selection created");
791 };
792 processor.reduce_defer(main, &primary_selection, deferred)
793 }
794}
795
796fn compute_plan_for_defer_conditionals(
797 parameters: &mut QueryPlanningParameters,
798 processor: &mut FetchDependencyGraphToQueryPlanProcessor,
799 defer_conditions: IndexMap<Name, IndexSet<String>>,
800 non_local_selection_state: &mut Option<non_local_selections_estimation::State>,
801) -> Result<Option<PlanNode>, FederationError> {
802 generate_condition_nodes(
803 parameters.operation.clone(),
804 defer_conditions.iter(),
805 &mut |op| {
806 parameters.operation = op;
807 compute_plan_internal(parameters, processor, true, non_local_selection_state)
808 },
809 )
810}
811
812fn generate_condition_nodes<'a>(
813 op: Arc<Operation>,
814 mut conditions: impl Clone + Iterator<Item = (&'a Name, &'a IndexSet<String>)>,
815 on_final_operation: &mut impl FnMut(Arc<Operation>) -> Result<Option<PlanNode>, FederationError>,
816) -> Result<Option<PlanNode>, FederationError> {
817 match conditions.next() {
818 None => on_final_operation(op),
819 Some((cond, labels)) => {
820 let else_op = Arc::unwrap_or_clone(op.clone()).reduce_defer(labels)?;
821 let if_op = op;
822 let node = ConditionNode {
823 condition_variable: cond.clone(),
824 if_clause: generate_condition_nodes(if_op, conditions.clone(), on_final_operation)?
825 .map(Box::new),
826 else_clause: generate_condition_nodes(
827 Arc::new(else_op),
828 conditions.clone(),
829 on_final_operation,
830 )?
831 .map(Box::new),
832 };
833 Ok(Some(PlanNode::Condition(Box::new(node))))
834 }
835 }
836}
837
838pub(crate) enum SubgraphOperationCompression {
839 GenerateFragments,
840 Disabled,
841}
842
843impl SubgraphOperationCompression {
844 pub(crate) fn compress(&mut self, operation: Operation) -> Result<Operation, FederationError> {
846 match self {
847 Self::GenerateFragments => {
848 let mut operation = operation;
849 operation.generate_fragments()?;
850 Ok(operation)
851 }
852 Self::Disabled => Ok(operation),
853 }
854 }
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use crate::subgraph::Subgraph;
861
862 const TEST_SUPERGRAPH: &str = r#"
863schema
864 @link(url: "https://specs.apollo.dev/link/v1.0")
865 @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION)
866{
867 query: Query
868}
869
870directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
871
872directive @join__graph(name: String!, url: String!) on ENUM_VALUE
873
874directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
875
876directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
877
878directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
879
880type Book implements Product
881 @join__implements(graph: PRODUCTS, interface: "Product")
882 @join__implements(graph: REVIEWS, interface: "Product")
883 @join__type(graph: PRODUCTS, key: "id")
884 @join__type(graph: REVIEWS, key: "id")
885{
886 id: ID!
887 price: Price @join__field(graph: PRODUCTS)
888 title: String @join__field(graph: PRODUCTS)
889 vendor: User @join__field(graph: PRODUCTS)
890 pages: Int @join__field(graph: PRODUCTS)
891 avg_rating: Int @join__field(graph: PRODUCTS, requires: "reviews { rating }")
892 reviews: [Review] @join__field(graph: PRODUCTS, external: true) @join__field(graph: REVIEWS)
893}
894
895enum Currency
896 @join__type(graph: PRODUCTS)
897{
898 USD
899 EUR
900}
901
902scalar join__FieldSet
903
904enum join__Graph {
905 ACCOUNTS @join__graph(name: "accounts", url: "")
906 PRODUCTS @join__graph(name: "products", url: "")
907 REVIEWS @join__graph(name: "reviews", url: "")
908}
909
910scalar link__Import
911
912enum link__Purpose {
913 """
914 `SECURITY` features provide metadata necessary to securely resolve fields.
915 """
916 SECURITY
917
918 """
919 `EXECUTION` features provide metadata necessary for operation execution.
920 """
921 EXECUTION
922}
923
924type Movie implements Product
925 @join__implements(graph: PRODUCTS, interface: "Product")
926 @join__implements(graph: REVIEWS, interface: "Product")
927 @join__type(graph: PRODUCTS, key: "id")
928 @join__type(graph: REVIEWS, key: "id")
929{
930 id: ID!
931 price: Price @join__field(graph: PRODUCTS)
932 title: String @join__field(graph: PRODUCTS)
933 vendor: User @join__field(graph: PRODUCTS)
934 length_minutes: Int @join__field(graph: PRODUCTS)
935 avg_rating: Int @join__field(graph: PRODUCTS, requires: "reviews { rating }")
936 reviews: [Review] @join__field(graph: PRODUCTS, external: true) @join__field(graph: REVIEWS)
937}
938
939type Price
940 @join__type(graph: PRODUCTS)
941{
942 value: Int
943 currency: Currency
944}
945
946interface Product
947 @join__type(graph: PRODUCTS)
948 @join__type(graph: REVIEWS)
949{
950 id: ID!
951 price: Price @join__field(graph: PRODUCTS)
952 vendor: User @join__field(graph: PRODUCTS)
953 avg_rating: Int @join__field(graph: PRODUCTS)
954 reviews: [Review] @join__field(graph: REVIEWS)
955}
956
957type Query
958 @join__type(graph: ACCOUNTS)
959 @join__type(graph: PRODUCTS)
960 @join__type(graph: REVIEWS)
961{
962 userById(id: ID!): User @join__field(graph: ACCOUNTS)
963 me: User! @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS)
964 productById(id: ID!): Product @join__field(graph: PRODUCTS)
965 search(filter: SearchFilter): [Product] @join__field(graph: PRODUCTS)
966 bestRatedProducts(limit: Int): [Product] @join__field(graph: REVIEWS)
967}
968
969type Review
970 @join__type(graph: PRODUCTS)
971 @join__type(graph: REVIEWS)
972{
973 rating: Int @join__field(graph: PRODUCTS, external: true) @join__field(graph: REVIEWS)
974 product: Product @join__field(graph: REVIEWS)
975 author: User @join__field(graph: REVIEWS)
976 text: String @join__field(graph: REVIEWS)
977}
978
979input SearchFilter
980 @join__type(graph: PRODUCTS)
981{
982 pattern: String!
983 vendorName: String
984}
985
986type User
987 @join__type(graph: ACCOUNTS, key: "id")
988 @join__type(graph: PRODUCTS, key: "id", resolvable: false)
989 @join__type(graph: REVIEWS, key: "id")
990{
991 id: ID!
992 name: String @join__field(graph: ACCOUNTS)
993 email: String @join__field(graph: ACCOUNTS)
994 password: String @join__field(graph: ACCOUNTS)
995 nickname: String @join__field(graph: ACCOUNTS, override: "reviews")
996 reviews: [Review] @join__field(graph: REVIEWS)
997}
998 "#;
999
1000 #[test]
1001 fn plan_simple_query_for_single_subgraph() {
1002 let supergraph = Supergraph::new(TEST_SUPERGRAPH).unwrap();
1003 let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap();
1004
1005 let document = ExecutableDocument::parse_and_validate(
1006 planner.api_schema().schema(),
1007 r#"
1008 {
1009 userById(id: 1) {
1010 name
1011 email
1012 }
1013 }
1014 "#,
1015 "operation.graphql",
1016 )
1017 .unwrap();
1018 let plan = planner
1019 .build_query_plan(&document, None, Default::default())
1020 .unwrap();
1021 insta::assert_snapshot!(plan, @r###"
1022 QueryPlan {
1023 Fetch(service: "accounts") {
1024 {
1025 userById(id: 1) {
1026 name
1027 email
1028 }
1029 }
1030 },
1031 }
1032 "###);
1033 }
1034
1035 #[test]
1036 fn plan_simple_query_for_multiple_subgraphs() {
1037 let supergraph = Supergraph::new(TEST_SUPERGRAPH).unwrap();
1038 let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap();
1039
1040 let document = ExecutableDocument::parse_and_validate(
1041 planner.api_schema().schema(),
1042 r#"
1043 {
1044 bestRatedProducts {
1045 vendor { name }
1046 }
1047 }
1048 "#,
1049 "operation.graphql",
1050 )
1051 .unwrap();
1052 let plan = planner
1053 .build_query_plan(&document, None, Default::default())
1054 .unwrap();
1055 insta::assert_snapshot!(plan, @r###"
1056 QueryPlan {
1057 Sequence {
1058 Fetch(service: "reviews") {
1059 {
1060 bestRatedProducts {
1061 __typename
1062 ... on Book {
1063 __typename
1064 id
1065 }
1066 ... on Movie {
1067 __typename
1068 id
1069 }
1070 }
1071 }
1072 },
1073 Flatten(path: "bestRatedProducts.@") {
1074 Fetch(service: "products") {
1075 {
1076 ... on Book {
1077 __typename
1078 id
1079 }
1080 ... on Movie {
1081 __typename
1082 id
1083 }
1084 } =>
1085 {
1086 ... on Book {
1087 vendor {
1088 __typename
1089 id
1090 }
1091 }
1092 ... on Movie {
1093 vendor {
1094 __typename
1095 id
1096 }
1097 }
1098 }
1099 },
1100 },
1101 Flatten(path: "bestRatedProducts.@.vendor") {
1102 Fetch(service: "accounts") {
1103 {
1104 ... on User {
1105 __typename
1106 id
1107 }
1108 } =>
1109 {
1110 ... on User {
1111 name
1112 }
1113 }
1114 },
1115 },
1116 },
1117 }
1118 "###);
1119 }
1120
1121 #[test]
1122 fn plan_simple_root_field_query_for_multiple_subgraphs() {
1123 let supergraph = Supergraph::new(TEST_SUPERGRAPH).unwrap();
1124 let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap();
1125
1126 let document = ExecutableDocument::parse_and_validate(
1127 planner.api_schema().schema(),
1128 r#"
1129 {
1130 userById(id: 1) {
1131 name
1132 email
1133 }
1134 bestRatedProducts {
1135 id
1136 avg_rating
1137 }
1138 }
1139 "#,
1140 "operation.graphql",
1141 )
1142 .unwrap();
1143 let plan = planner
1144 .build_query_plan(&document, None, Default::default())
1145 .unwrap();
1146 insta::assert_snapshot!(plan, @r###"
1147 QueryPlan {
1148 Parallel {
1149 Fetch(service: "accounts") {
1150 {
1151 userById(id: 1) {
1152 name
1153 email
1154 }
1155 }
1156 },
1157 Sequence {
1158 Fetch(service: "reviews") {
1159 {
1160 bestRatedProducts {
1161 __typename
1162 id
1163 ... on Book {
1164 __typename
1165 id
1166 reviews {
1167 rating
1168 }
1169 }
1170 ... on Movie {
1171 __typename
1172 id
1173 reviews {
1174 rating
1175 }
1176 }
1177 }
1178 }
1179 },
1180 Flatten(path: "bestRatedProducts.@") {
1181 Fetch(service: "products") {
1182 {
1183 ... on Book {
1184 __typename
1185 id
1186 reviews {
1187 rating
1188 }
1189 }
1190 ... on Movie {
1191 __typename
1192 id
1193 reviews {
1194 rating
1195 }
1196 }
1197 } =>
1198 {
1199 ... on Book {
1200 avg_rating
1201 }
1202 ... on Movie {
1203 avg_rating
1204 }
1205 }
1206 },
1207 },
1208 },
1209 },
1210 }
1211 "###);
1212 }
1213
1214 #[test]
1215 fn test_optimize_no_fragments_generated() {
1216 let supergraph = Supergraph::new(TEST_SUPERGRAPH).unwrap();
1217 let api_schema = supergraph.to_api_schema(Default::default()).unwrap();
1218 let document = ExecutableDocument::parse_and_validate(
1219 api_schema.schema(),
1220 r#"
1221 {
1222 userById(id: 1) {
1223 id
1224 ...userFields
1225 },
1226 another_user: userById(id: 2) {
1227 name
1228 email
1229 }
1230 }
1231 fragment userFields on User {
1232 name
1233 email
1234 }
1235 "#,
1236 "operation.graphql",
1237 )
1238 .unwrap();
1239
1240 let config = QueryPlannerConfig {
1241 generate_query_fragments: true,
1242 ..Default::default()
1243 };
1244 let planner = QueryPlanner::new(&supergraph, config).unwrap();
1245 let plan = planner
1246 .build_query_plan(&document, None, Default::default())
1247 .unwrap();
1248 insta::assert_snapshot!(plan, @r###"
1249 QueryPlan {
1250 Fetch(service: "accounts") {
1251 {
1252 userById(id: 1) {
1253 id
1254 name
1255 email
1256 }
1257 another_user: userById(id: 2) {
1258 name
1259 email
1260 }
1261 }
1262 },
1263 }
1264 "###);
1265 }
1266
1267 #[test]
1268 fn drop_operation_root_level_typename() {
1269 let subgraph1 = Subgraph::parse_and_expand(
1270 "Subgraph1",
1271 "https://Subgraph1",
1272 r#"
1273 type Query {
1274 t: T
1275 }
1276
1277 type T @key(fields: "id") {
1278 id: ID!
1279 x: Int
1280 }
1281 "#,
1282 )
1283 .unwrap();
1284 let subgraphs = vec![&subgraph1];
1285 let supergraph = Supergraph::compose(subgraphs).unwrap();
1286 let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap();
1287 let document = ExecutableDocument::parse_and_validate(
1288 planner.api_schema().schema(),
1289 r#"
1290 query {
1291 __typename
1292 t {
1293 x
1294 }
1295 }
1296 "#,
1297 "operation.graphql",
1298 )
1299 .unwrap();
1300 let plan = planner
1301 .build_query_plan(&document, None, Default::default())
1302 .unwrap();
1303 insta::assert_snapshot!(plan, @r###"
1304 QueryPlan {
1305 Fetch(service: "Subgraph1") {
1306 {
1307 t {
1308 x
1309 }
1310 }
1311 },
1312 }
1313 "###);
1314 }
1315}