Skip to main content

apollo_federation/query_plan/
query_planner.rs

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    /// If enabled, the query planner will attempt to extract common subselections into named
59    /// fragments. This can significantly reduce the size of the query sent to subgraphs.
60    ///
61    /// Defaults to false.
62    pub generate_query_fragments: bool,
63
64    /// **TODO:** This option is not implemented, and the behaviour is *always enabled*.
65    /// <https://github.com/apollographql/router/pull/5871>
66    ///
67    /// Whether to run GraphQL validation against the extracted subgraph schemas. Recommended in
68    /// non-production settings or when debugging.
69    ///
70    /// Defaults to false.
71    pub subgraph_graphql_validation: bool,
72
73    // Side-note: implemented as an object instead of single boolean because we expect to add more
74    // to this soon enough. In particular, once defer-passthrough to subgraphs is implemented, the
75    // idea would be to add a new `passthrough_subgraphs` option that is the list of subgraphs to
76    // which we can pass-through some @defer (and it would be empty by default). Similarly, once we
77    // support @stream, grouping the options here will make sense too.
78    pub incremental_delivery: QueryPlanIncrementalDeliveryConfig,
79
80    /// A sub-set of configurations that are meant for debugging or testing. All the configurations
81    /// in this sub-set are provided without guarantees of stability (they may be dangerous) or
82    /// continued support (they may be removed without warning).
83    pub debug: QueryPlannerDebugConfig,
84
85    /// Enables type conditioned fetching.
86    /// This flag is a workaround, which may yield significant
87    /// performance degradation when computing query plans,
88    /// and increase query plan size.
89    ///
90    /// If you aren't aware of this flag, you probably don't need it.
91    pub type_conditioned_fetching: bool,
92}
93
94#[allow(clippy::derivable_impls)] // it's derivable right now, but we might change the defaults
95impl 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    /// Enables `@defer` support in the query planner, breaking up the query plan with [DeferNode]s
110    /// as appropriate.
111    ///
112    /// If false, operations with `@defer` are still accepted, but are planned as if they did not
113    /// contain `@defer` directives.
114    ///
115    /// Defaults to false.
116    ///
117    /// [DeferNode]: crate::query_plan::DeferNode
118    #[serde(default)]
119    pub enable_defer: bool,
120}
121
122#[derive(Debug, Clone, Hash, Serialize)]
123pub struct QueryPlannerDebugConfig {
124    /// Query planning is an exploratory process. Depending on the specificities and feature used by
125    /// subgraphs, there could exist may different theoretical valid (if not always efficient) plans
126    /// for a given query, and at a high level, the query planner generates those possible choices,
127    /// evaluates them, and return the best one. In some complex cases however, the number of
128    /// theoretically possible plans can be very large, and to keep query planning time acceptable,
129    /// the query planner caps the maximum number of plans it evaluates. This config allows to
130    /// configure that cap. Note if planning a query hits that cap, then the planner will still
131    /// always return a "correct" plan, but it may not return _the_ optimal one, so this config can
132    /// be considered a trade-off between the worst-time for query planning computation processing,
133    /// and the risk of having non-optimal query plans (impacting query runtimes).
134    ///
135    /// This value currently defaults to 10000, but this default is considered an implementation
136    /// detail and is subject to change. We do not recommend setting this value unless it is to
137    /// debug a specific issue (with unexpectedly slow query planning for instance). Remember that
138    /// setting this value too low can negatively affect query runtime (due to the use of
139    /// sub-optimal query plans).
140    // TODO: should there additionally be a max_evaluated_cost?
141    pub max_evaluated_plans: NonZeroU32,
142
143    /// Before creating query plans, for each path of fields in the query we compute all the
144    /// possible options to traverse that path via the subgraphs. Multiple options can arise because
145    /// fields in the path can be provided by multiple subgraphs, and abstract types (i.e. unions
146    /// and interfaces) returned by fields sometimes require the query planner to traverse through
147    /// each constituent object type. The number of options generated in this computation can grow
148    /// large if the schema or query are sufficiently complex, and that will increase the time spent
149    /// planning.
150    ///
151    /// This config allows specifying a per-path limit to the number of options considered. If any
152    /// path's options exceeds this limit, query planning will abort and the operation will fail.
153    ///
154    /// The default value is None, which specifies no limit.
155    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// PORT_NOTE: renamed from PlanningStatistics in the JS codebase.
168#[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    /// A set of labels which will be used _during query planning_ to
177    /// enable/disable edges with a matching label in their override condition.
178    /// Edges with override conditions require their label to be present or absent
179    /// from this set in order to be traversable. These labels enable the
180    /// progressive @override feature.
181    // PORT_NOTE: In JS implementation this was a Map
182    pub override_conditions: Vec<String>,
183    /// Impose a limit on the number of non-local selections, which can be a
184    /// performance hazard. On by default.
185    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    /// A set of the names of interface types for which at least one subgraph use an
214    /// @interfaceObject to abstract that interface.
215    interface_types_with_interface_objects: IndexSet<InterfaceTypeDefinitionPosition>,
216    /// A set of the names of interface or union types that have inconsistent "runtime types" across
217    /// subgraphs.
218    // PORT_NOTE: Named `inconsistentAbstractTypesRuntimes` in the JS codebase, which was slightly
219    // confusing.
220    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                    // This is only called for type names that are abstract in the supergraph, so it
278                    // can only be an object in a subgraph if it is an `@interfaceObject`. And as `@interfaceObject`s
279                    // "stand-in" for all possible runtime types, they don't create inconsistencies by themselves
280                    // and we can ignore them.
281                    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    // PORT_NOTE: this receives an `Operation` object in JS which is a concept that doesn't exist in apollo-rs.
331    #[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            // This should never happen because `operation` comes from a known-valid document.
353            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            // PORT_NOTE(@goto-bus-stop): In JS, `root` is a `RootVertex`, which is dynamically
420            // checked at various points in query planning. This is our Rust equivalent of that.
421            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            // If this is a subscription, we want to make sure that we return a SubscriptionNode rather than a PlanNode
455            // We potentially will need to separate "primary" from "rest"
456            // Note that if it is a subscription, we are guaranteed that nothing is deferred.
457            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                    // TODO(@goto-bus-stop): We could probably guarantee this in the type system
466                    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    /// Get Query Planner's API Schema.
515    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    // We have to serially compute a plan for each top-level selection.
545    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            // The new operation (think 'mutation' operation) is on the same subgraph than the previous one, so we can concat them in a single fetch
575            // and rely on the subgraph to enforce seriability. Do note that we need to `concat()` and not `merge()` because if we have
576            // mutation Mut {
577            //    mut1 {...}
578            //    mut2 {...}
579            //    mut1 {...}
580            // }
581            // then we should _not_ merge the 2 `mut1` fields (contrarily to what happens on queried fields).
582
583            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            // PORT_NOTE: It is unclear if they correct thing to do here is get the next ID, use
599            // the current ID that is inside the fetch dep graph's ID generator, or to use the
600            // starting ID. Because this method ensure uniqueness between IDs, this approach was
601            // taken; however, it could be the case that this causes unforseen issues.
602            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    // The root of the pathTree is one of the "fake" root of the subgraphs graph,
636    // which belongs to no subgraph but points to each ones.
637    // So we "unpack" the first level of the tree to find out our top level groups
638    // (and initialize our stack).
639    // Note that we can safely ignore the triggers of that first level
640    // as it will all be free transition, and we know we cannot have conditions.
641    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    // Getting no plan means the query is essentially unsatisfiable (it's a valid query, but we can prove it will never return a result),
725    // so we just return an empty plan.
726    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        // XXX(@goto-bus-stop) Maybe `.defer_tracking` should be on the return value of `process()`..?
781        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    /// Compress a subgraph operation.
845    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}