Skip to main content

apollo_federation/query_graph/
mod.rs

1use std::fmt::Display;
2use std::fmt::Formatter;
3use std::hash::Hash;
4use std::sync::Arc;
5
6use apollo_compiler::Name;
7use apollo_compiler::Node;
8use apollo_compiler::collections::IndexMap;
9use apollo_compiler::collections::IndexSet;
10use apollo_compiler::schema::NamedType;
11use apollo_compiler::schema::Type;
12use petgraph::Direction;
13use petgraph::graph::DiGraph;
14use petgraph::graph::EdgeIndex;
15use petgraph::graph::EdgeReference;
16use petgraph::graph::NodeIndex;
17use petgraph::visit::EdgeRef;
18
19use crate::error::FederationError;
20use crate::error::SingleFederationError;
21use crate::internal_error;
22use crate::operation::Field;
23use crate::operation::InlineFragment;
24use crate::operation::SelectionSet;
25use crate::schema::ValidFederationSchema;
26use crate::schema::field_set::parse_field_set;
27use crate::schema::position::CompositeTypeDefinitionPosition;
28use crate::schema::position::FieldDefinitionPosition;
29use crate::schema::position::InterfaceFieldDefinitionPosition;
30use crate::schema::position::ObjectFieldArgumentDefinitionPosition;
31use crate::schema::position::ObjectTypeDefinitionPosition;
32use crate::schema::position::OutputTypeDefinitionPosition;
33use crate::schema::position::SchemaRootDefinitionKind;
34use crate::utils::FallibleIterator;
35
36pub mod build_query_graph;
37pub(crate) mod condition_resolver;
38pub(crate) mod graph_path;
39pub mod output;
40pub(crate) mod path_tree;
41
42pub use build_query_graph::build_federated_query_graph;
43
44use crate::query_graph::condition_resolver::ConditionResolution;
45use crate::query_graph::condition_resolver::ConditionResolver;
46use crate::query_graph::graph_path::ExcludedConditions;
47use crate::query_graph::graph_path::ExcludedDestinations;
48use crate::query_graph::graph_path::OpGraphPathContext;
49use crate::query_graph::graph_path::OpGraphPathTrigger;
50use crate::query_graph::graph_path::OpPathElement;
51use crate::query_plan::QueryPlanCost;
52use crate::query_plan::query_planner::EnabledOverrideConditions;
53use crate::query_plan::query_planning_traversal::non_local_selections_estimation;
54
55#[derive(Debug, Clone, PartialEq, Eq, Hash)]
56pub(crate) struct QueryGraphNode {
57    /// The GraphQL type this node points to.
58    pub(crate) type_: QueryGraphNodeType,
59    /// An identifier of the underlying schema containing the `type_` this node points to. This is
60    /// mainly used in federated query graphs, where the `source` is a subgraph name.
61    pub(crate) source: Arc<str>,
62    /// True if there is a cross-subgraph edge that is reachable from this node.
63    pub(crate) has_reachable_cross_subgraph_edges: bool,
64    /// @provides works by creating duplicates of the node/type involved in the provides and adding
65    /// the provided edges only to those copies. This means that with @provides, you can have more
66    /// than one node per-type-and-subgraph in a query graph. Which is fine, but this `provide_id`
67    /// allows distinguishing if a node was created as part of this @provides duplication or not.
68    /// The value of this field has no other meaning than to be unique per-@provide, and so all the
69    /// nodes copied for a given @provides application will have the same `provide_id`. Overall,
70    /// this mostly exists for debugging visualization.
71    pub(crate) provide_id: Option<u32>,
72    // If present, this node represents a root node of the corresponding kind.
73    pub(crate) root_kind: Option<SchemaRootDefinitionKind>,
74}
75
76impl QueryGraphNode {
77    pub(crate) fn is_root_node(&self) -> bool {
78        matches!(self.type_, QueryGraphNodeType::FederatedRootType(_))
79    }
80}
81
82impl Display for QueryGraphNode {
83    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}({})", self.type_, self.source)?;
85        if let Some(provide_id) = self.provide_id {
86            write!(f, "-{}", provide_id)?;
87        }
88        if self.is_root_node() {
89            write!(f, "*")?;
90        }
91        Ok(())
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From, derive_more::IsVariant)]
96pub(crate) enum QueryGraphNodeType {
97    SchemaType(OutputTypeDefinitionPosition),
98    FederatedRootType(SchemaRootDefinitionKind),
99}
100
101impl Display for QueryGraphNodeType {
102    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
103        match self {
104            QueryGraphNodeType::SchemaType(pos) => pos.fmt(f),
105            QueryGraphNodeType::FederatedRootType(root_kind) => {
106                write!(f, "[{root_kind}]")
107            }
108        }
109    }
110}
111
112impl TryFrom<QueryGraphNodeType> for CompositeTypeDefinitionPosition {
113    type Error = FederationError;
114
115    fn try_from(value: QueryGraphNodeType) -> Result<Self, Self::Error> {
116        match value {
117            QueryGraphNodeType::SchemaType(ty) => Ok(ty.try_into()?),
118            QueryGraphNodeType::FederatedRootType(_) => Err(FederationError::internal(format!(
119                r#"Type "{value}" was unexpectedly not a composite type"#
120            ))),
121        }
122    }
123}
124
125impl TryFrom<QueryGraphNodeType> for ObjectTypeDefinitionPosition {
126    type Error = FederationError;
127
128    fn try_from(value: QueryGraphNodeType) -> Result<Self, Self::Error> {
129        match value {
130            QueryGraphNodeType::SchemaType(ty) => Ok(ty.try_into()?),
131            QueryGraphNodeType::FederatedRootType(_) => Err(FederationError::internal(format!(
132                r#"Type "{value}" was unexpectedly not an object type"#
133            ))),
134        }
135    }
136}
137
138/// Contains all of the data necessary to connect the object field argument (`argument_coordinate`)
139/// with the `@fromContext` to its (grand)parent types contain a matching selection.
140#[derive(Debug, PartialEq, Clone)]
141pub struct ContextCondition {
142    context: String,
143    subgraph_name: Arc<str>,
144    // This is purposely left unparsed in query graphs, due to @fromContext selection sets being
145    // duck-typed.
146    selection: String,
147    types_with_context_set: IndexSet<CompositeTypeDefinitionPosition>,
148    // PORT_NOTE: This field was renamed because the JS name (`namedParameter`) left confusion to
149    // how it was different from the argument name.
150    argument_name: Name,
151    // PORT_NOTE: This field was renamed because the JS name (`coordinate`) was too vague.
152    argument_coordinate: ObjectFieldArgumentDefinitionPosition,
153    // PORT_NOTE: This field was renamed from the JS name (`argType`) for consistency with the rest
154    // of the naming in this struct.
155    argument_type: Node<Type>,
156}
157
158#[derive(Debug, PartialEq, Clone)]
159pub(crate) struct QueryGraphEdge {
160    /// Indicates what kind of edge this is and what the edge does/represents. For instance, if the
161    /// edge represents a field, the `transition` will be a `FieldCollection` transition and will
162    /// link to the definition of the field it represents.
163    pub(crate) transition: QueryGraphEdgeTransition,
164    /// Optional conditions on an edge.
165    ///
166    /// Conditions are a select of selections (in the GraphQL sense) that the traversal of a query
167    /// graph needs to "collect" (traverse edges with transitions corresponding to those selections)
168    /// in order to be able to collect that edge.
169    ///
170    /// Conditions are primarily used for edges corresponding to @key, in which case they correspond
171    /// to the fields composing the @key. In other words, for an @key edge, conditions basically
172    /// represent the fact that you need the key to be able to use an @key edge.
173    ///
174    /// Outside of keys, @requires edges also rely on conditions.
175    pub(crate) conditions: Option<Arc<SelectionSet>>,
176    /// Edges can require that an override condition (provided during query
177    /// planning) be met in order to be taken. This is used for progressive
178    /// @override, where (at least) 2 subgraphs can resolve the same field, but
179    /// one of them has an @override with a label. If the override condition
180    /// matches the query plan parameters, this edge can be taken.
181    pub(crate) override_condition: Option<OverrideCondition>,
182    /// All arguments with `@fromContext` that need to be matched to an upstream graph path field
183    /// whose parent type has the corresponding `@context`.
184    pub(crate) required_contexts: Vec<ContextCondition>,
185}
186
187impl QueryGraphEdge {
188    pub(crate) fn new(
189        transition: QueryGraphEdgeTransition,
190        conditions: Option<Arc<SelectionSet>>,
191    ) -> Self {
192        Self {
193            transition,
194            conditions,
195            override_condition: None,
196            required_contexts: Vec::new(),
197        }
198    }
199
200    fn satisfies_override_conditions(
201        &self,
202        conditions_to_check: &EnabledOverrideConditions,
203    ) -> bool {
204        if let Some(override_condition) = &self.override_condition {
205            override_condition.check(conditions_to_check)
206        } else {
207            true
208        }
209    }
210}
211
212impl Display for QueryGraphEdge {
213    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
214        if matches!(
215            self.transition,
216            QueryGraphEdgeTransition::SubgraphEnteringTransition
217        ) && self.conditions.is_none()
218        {
219            return Ok(());
220        }
221
222        match (&self.override_condition, &self.conditions) {
223            (Some(override_condition), Some(conditions)) => write!(
224                f,
225                "{}, {} ⊢ {}",
226                conditions, override_condition, self.transition
227            ),
228            (Some(override_condition), None) => {
229                write!(f, "{} ⊢ {}", override_condition, self.transition)
230            }
231            (None, Some(conditions)) => write!(f, "{} ⊢ {}", conditions, self.transition),
232            _ => self.transition.fmt(f),
233        }
234    }
235}
236#[derive(Debug, Clone, PartialEq, Eq, Hash)]
237pub(crate) struct OverrideCondition {
238    pub(crate) label: String,
239    pub(crate) condition: bool,
240}
241
242impl OverrideCondition {
243    pub(crate) fn check(&self, enabled_conditions: &EnabledOverrideConditions) -> bool {
244        self.condition == enabled_conditions.contains(&self.label)
245    }
246}
247
248impl Display for OverrideCondition {
249    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
250        write!(f, "{} = {}", self.label, self.condition)
251    }
252}
253
254/// The type of query graph edge "transition".
255///
256/// An edge transition encodes what the edge corresponds to, in the underlying GraphQL schema.
257#[derive(Debug, Clone, PartialEq, Eq, Hash)]
258pub(crate) enum QueryGraphEdgeTransition {
259    /// A field edge, going from (a node for) the field parent type to the field's (base) type.
260    FieldCollection {
261        /// The name of the schema containing the field.
262        source: Arc<str>,
263        /// The object/interface field being collected.
264        field_definition_position: FieldDefinitionPosition,
265        /// Whether this field is part of an @provides.
266        is_part_of_provides: bool,
267    },
268    /// A downcast edge, going from a composite type (object, interface, or union) to another
269    /// composite type that intersects that type (i.e. has at least one possible runtime object type
270    /// in common with it).
271    Downcast {
272        /// The name of the schema containing the from/to types.
273        source: Arc<str>,
274        /// The parent type of the type condition, i.e. the type of the selection set containing
275        /// the type condition.
276        from_type_position: CompositeTypeDefinitionPosition,
277        /// The type of the type condition, i.e. the type coming after "... on".
278        to_type_position: CompositeTypeDefinitionPosition,
279    },
280    /// A key edge (only found in federated query graphs) going from an entity type in a particular
281    /// subgraph to the same entity type but in another subgraph. Key transition edges _must_ have
282    /// `conditions` corresponding to the key fields.
283    KeyResolution,
284    /// A root type edge (only found in federated query graphs) going from a root type (query,
285    /// mutation or subscription) of a subgraph to the (same) root type of another subgraph. It
286    /// encodes the fact that if a subgraph field returns a root type, any subgraph can be queried
287    /// from there.
288    RootTypeResolution {
289        /// The kind of schema root resolved.
290        root_kind: SchemaRootDefinitionKind,
291    },
292    /// A subgraph-entering edge, which is a special case only used for edges coming out of the root
293    /// nodes of "federated" query graphs. It does not correspond to any physical GraphQL elements
294    /// but can be understood as the fact that the router is always free to start querying any of
295    /// the subgraph services as needed.
296    SubgraphEnteringTransition,
297    /// A "fake" downcast edge (only found in federated query graphs) going from an @interfaceObject
298    /// type to an implementation. This encodes the fact that an @interfaceObject type "stands-in"
299    /// for any possible implementations (in the supergraph) of the corresponding interface. It is
300    /// "fake" because the corresponding edge stays on the @interfaceObject type (this is also why
301    /// the "to type" is only a name: that to/casted type does not actually exist in the subgraph
302    /// in which the corresponding edge will be found).
303    InterfaceObjectFakeDownCast {
304        /// The name of the schema containing the from type.
305        source: Arc<str>,
306        /// The parent type of the type condition, i.e. the type of the selection set containing
307        /// the type condition.
308        from_type_position: CompositeTypeDefinitionPosition,
309        /// The type of the type condition, i.e. the type coming after "... on".
310        to_type_name: Name,
311    },
312}
313
314impl QueryGraphEdgeTransition {
315    pub(crate) fn collect_operation_elements(&self) -> bool {
316        match self {
317            QueryGraphEdgeTransition::FieldCollection { .. } => true,
318            QueryGraphEdgeTransition::Downcast { .. } => true,
319            QueryGraphEdgeTransition::KeyResolution => false,
320            QueryGraphEdgeTransition::RootTypeResolution { .. } => false,
321            QueryGraphEdgeTransition::SubgraphEnteringTransition => false,
322            QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { .. } => true,
323        }
324    }
325}
326
327impl Display for QueryGraphEdgeTransition {
328    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
329        match self {
330            QueryGraphEdgeTransition::FieldCollection {
331                field_definition_position,
332                ..
333            } => {
334                write!(f, "{}", field_definition_position.field_name())
335            }
336            QueryGraphEdgeTransition::Downcast {
337                to_type_position, ..
338            } => {
339                write!(f, "... on {}", to_type_position.type_name())
340            }
341            QueryGraphEdgeTransition::KeyResolution => {
342                write!(f, "key()")
343            }
344            QueryGraphEdgeTransition::RootTypeResolution { root_kind } => {
345                write!(f, "{}()", root_kind)
346            }
347            QueryGraphEdgeTransition::SubgraphEnteringTransition => {
348                write!(f, "∅")
349            }
350            QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { to_type_name, .. } => {
351                write!(f, "... on {}", to_type_name)
352            }
353        }
354    }
355}
356
357#[derive(Debug)]
358pub struct QueryGraph {
359    /// The "current" source of the query graph. For query graphs representing a single source
360    /// graph, this will only ever be one value, but it will change for "federated" query graphs
361    /// while they're being built (and after construction, will become FEDERATED_GRAPH_ROOT_SOURCE,
362    /// which is a reserved placeholder value).
363    current_source: Arc<str>,
364    /// The nodes/edges of the query graph. Note that nodes/edges should never be removed, so
365    /// indexes are immutable when a node/edge is created.
366    graph: DiGraph<QueryGraphNode, QueryGraphEdge>,
367    /// The sources on which the query graph was built, which is a set (potentially of size 1) of
368    /// GraphQL schema keyed by the name identifying them. Note that the `source` strings in the
369    /// nodes/edges of a query graph are guaranteed to be valid key in this map.
370    sources: IndexMap<Arc<str>, ValidFederationSchema>,
371    /// For federated query graphs, this is a map from subgraph names to their schemas. This is the
372    /// same as `sources`, but is missing the dummy source FEDERATED_GRAPH_ROOT_SOURCE which isn't
373    /// really a subgraph.
374    subgraphs_by_name: IndexMap<Arc<str>, ValidFederationSchema>,
375    /// For federated query graphs, this is the supergraph schema; otherwise, this is `None`.
376    supergraph_schema: Option<ValidFederationSchema>,
377    /// A map (keyed by source) that associates type names of the underlying schema on which this
378    /// query graph was built to each of the nodes that points to a type of that name. Note that for
379    /// a "federated" query graph source, each type name will only map to a single node.
380    types_to_nodes_by_source: IndexMap<Arc<str>, IndexMap<NamedType, IndexSet<NodeIndex>>>,
381    /// A map (keyed by source) that associates schema root kinds to root nodes.
382    root_kinds_to_nodes_by_source:
383        IndexMap<Arc<str>, IndexMap<SchemaRootDefinitionKind, NodeIndex>>,
384    /// Maps an edge to the possible edges that can follow it "productively", that is without
385    /// creating a trivially inefficient path.
386    ///
387    /// More precisely, this map is equivalent to looking at the out edges of a given edge's tail
388    /// node and filtering those edges that "never make sense" after the given edge, which mainly
389    /// amounts to avoiding chaining @key edges when we know there is guaranteed to be a better
390    /// option. As an example, suppose we have 3 subgraphs A, B and C which all defined an
391    /// `@key(fields: "id")` on some entity type `T`. Then it is never interesting to take that @key
392    /// edge from B -> C after A -> B because if we're in A and want to get to C, we can always do
393    /// A -> C (of course, this is only true because it's the "same" key).
394    ///
395    /// See `precompute_non_trivial_followup_edges` for more details on which exact edges are
396    /// filtered.
397    ///
398    /// Lastly, note that the main reason for having this field is that its result is pre-computed.
399    /// Which in turn is done for performance reasons: having the same key defined in multiple
400    /// subgraphs is _the_ most common pattern, and while our later algorithms (composition
401    /// validation and query planning) would know to not select those trivially inefficient
402    /// "detours", they might have to redo those checks many times and pre-computing once it is
403    /// significantly faster (and pretty easy). FWIW, when originally introduced, this optimization
404    /// lowered composition validation on a big composition (100+ subgraphs) from ~4 minutes to
405    /// ~10 seconds.
406    non_trivial_followup_edges: IndexMap<EdgeIndex, Vec<EdgeIndex>>,
407    // PORT_NOTE: This field was renamed from the JS name (`subgraphToArgIndices`) to better
408    // align with downstream code.
409    /// Maps subgraph names to another map, for any subgraph with usages of `@fromContext`. This
410    /// other map then maps subgraph argument positions/coordinates (for `@fromContext` arguments)
411    /// to a unique identifier string (specifically, unique across pairs of subgraph names and
412    /// argument coordinates). This identifier is called the "context ID".
413    arguments_to_context_ids_by_source:
414        IndexMap<Arc<str>, IndexMap<ObjectFieldArgumentDefinitionPosition, Name>>,
415    /// To speed up the estimation of counting non-local selections, we precompute specific metadata
416    /// about the query graph and store that here.
417    non_local_selection_metadata: non_local_selections_estimation::QueryGraphMetadata,
418}
419
420impl QueryGraph {
421    pub(crate) fn name(&self) -> &Arc<str> {
422        &self.current_source
423    }
424
425    pub(crate) fn graph(&self) -> &DiGraph<QueryGraphNode, QueryGraphEdge> {
426        &self.graph
427    }
428
429    pub(crate) fn supergraph_schema(&self) -> Result<ValidFederationSchema, FederationError> {
430        self.supergraph_schema
431            .clone()
432            .ok_or_else(|| internal_error!("Supergraph schema unexpectedly missing"))
433    }
434
435    pub(crate) fn node_weight(&self, node: NodeIndex) -> Result<&QueryGraphNode, FederationError> {
436        self.graph
437            .node_weight(node)
438            .ok_or_else(|| internal_error!("Node unexpectedly missing"))
439    }
440
441    fn node_weight_mut(&mut self, node: NodeIndex) -> Result<&mut QueryGraphNode, FederationError> {
442        self.graph
443            .node_weight_mut(node)
444            .ok_or_else(|| internal_error!("Node unexpectedly missing"))
445    }
446
447    pub(crate) fn edge_weight(&self, edge: EdgeIndex) -> Result<&QueryGraphEdge, FederationError> {
448        self.graph
449            .edge_weight(edge)
450            .ok_or_else(|| internal_error!("Edge unexpectedly missing"))
451    }
452
453    fn edge_weight_mut(&mut self, edge: EdgeIndex) -> Result<&mut QueryGraphEdge, FederationError> {
454        self.graph
455            .edge_weight_mut(edge)
456            .ok_or_else(|| internal_error!("Edge unexpectedly missing"))
457    }
458
459    pub(crate) fn edge_head_weight(
460        &self,
461        edge: EdgeIndex,
462    ) -> Result<&QueryGraphNode, FederationError> {
463        let (head_id, _) = self.edge_endpoints(edge)?;
464        self.node_weight(head_id)
465    }
466
467    pub(crate) fn edge_endpoints(
468        &self,
469        edge: EdgeIndex,
470    ) -> Result<(NodeIndex, NodeIndex), FederationError> {
471        self.graph
472            .edge_endpoints(edge)
473            .ok_or_else(|| internal_error!("Edge unexpectedly missing"))
474    }
475
476    fn schema(&self) -> Result<&ValidFederationSchema, FederationError> {
477        self.schema_by_source(&self.current_source)
478    }
479
480    pub(crate) fn schema_by_source(
481        &self,
482        source: &str,
483    ) -> Result<&ValidFederationSchema, FederationError> {
484        self.sources
485            .get(source)
486            .ok_or_else(|| internal_error!(r#"Schema for "{source}" unexpectedly missing"#))
487    }
488
489    pub(crate) fn subgraph_schemas(&self) -> &IndexMap<Arc<str>, ValidFederationSchema> {
490        &self.subgraphs_by_name
491    }
492
493    pub(crate) fn subgraphs(&self) -> impl Iterator<Item = (&Arc<str>, &ValidFederationSchema)> {
494        self.subgraphs_by_name.iter()
495    }
496
497    /// Returns the node indices whose name matches the given type name.
498    pub(crate) fn nodes_for_type(
499        &self,
500        name: &Name,
501    ) -> Result<&IndexSet<NodeIndex>, FederationError> {
502        self.types_to_nodes()?
503            .get(name)
504            .ok_or_else(|| internal_error!("No nodes unexpectedly found for type"))
505    }
506
507    pub(crate) fn types_to_nodes(
508        &self,
509    ) -> Result<&IndexMap<NamedType, IndexSet<NodeIndex>>, FederationError> {
510        self.types_to_nodes_by_source(&self.current_source)
511    }
512
513    pub(super) fn types_to_nodes_by_source(
514        &self,
515        source: &str,
516    ) -> Result<&IndexMap<NamedType, IndexSet<NodeIndex>>, FederationError> {
517        self.types_to_nodes_by_source.get(source).ok_or_else(|| {
518            SingleFederationError::Internal {
519                message: "Types-to-nodes map unexpectedly missing".to_owned(),
520            }
521            .into()
522        })
523    }
524
525    fn types_to_nodes_mut(
526        &mut self,
527    ) -> Result<&mut IndexMap<NamedType, IndexSet<NodeIndex>>, FederationError> {
528        self.types_to_nodes_by_source
529            .get_mut(&self.current_source)
530            .ok_or_else(|| {
531                SingleFederationError::Internal {
532                    message: "Types-to-nodes map unexpectedly missing".to_owned(),
533                }
534                .into()
535            })
536    }
537
538    pub(crate) fn root_kinds_to_nodes(
539        &self,
540    ) -> Result<&IndexMap<SchemaRootDefinitionKind, NodeIndex>, FederationError> {
541        self.root_kinds_to_nodes_by_source
542            .get(&self.current_source)
543            .ok_or_else(|| {
544                SingleFederationError::Internal {
545                    message: "Root-kinds-to-nodes map unexpectedly missing".to_owned(),
546                }
547                .into()
548            })
549    }
550
551    pub(crate) fn root_kinds_to_nodes_by_source(
552        &self,
553        source: &str,
554    ) -> Result<&IndexMap<SchemaRootDefinitionKind, NodeIndex>, FederationError> {
555        self.root_kinds_to_nodes_by_source
556            .get(source)
557            .ok_or_else(|| {
558                SingleFederationError::Internal {
559                    message: "Root-kinds-to-nodes map unexpectedly missing".to_owned(),
560                }
561                .into()
562            })
563    }
564
565    fn root_kinds_to_nodes_mut(
566        &mut self,
567    ) -> Result<&mut IndexMap<SchemaRootDefinitionKind, NodeIndex>, FederationError> {
568        self.root_kinds_to_nodes_by_source
569            .get_mut(&self.current_source)
570            .ok_or_else(|| {
571                SingleFederationError::Internal {
572                    message: "Root-kinds-to-nodes map unexpectedly missing".to_owned(),
573                }
574                .into()
575            })
576    }
577
578    pub(crate) fn context_id_by_source_and_argument(
579        &self,
580        source: &str,
581        argument: &ObjectFieldArgumentDefinitionPosition,
582    ) -> Result<&Name, FederationError> {
583        self.arguments_to_context_ids_by_source
584            .get(source)
585            .and_then(|r| r.get(argument))
586            .ok_or_else(|| {
587                internal_error!("context ID unexpectedly missing for @fromContext argument")
588            })
589    }
590
591    pub(crate) fn is_context_used(&self) -> bool {
592        !self.arguments_to_context_ids_by_source.is_empty()
593    }
594
595    pub(crate) fn non_local_selection_metadata(
596        &self,
597    ) -> &non_local_selections_estimation::QueryGraphMetadata {
598        &self.non_local_selection_metadata
599    }
600
601    /// All outward edges from the given node (including self-key and self-root-type-resolution
602    /// edges). Primarily used by `@defer`, when needing to re-enter a subgraph for a deferred
603    /// section.
604    pub(crate) fn out_edges_with_federation_self_edges(
605        &self,
606        node: NodeIndex,
607    ) -> Vec<EdgeReference<QueryGraphEdge>> {
608        Self::sorted_edges(self.graph.edges_directed(node, Direction::Outgoing))
609    }
610
611    /// The outward edges from the given node, minus self-key and self-root-type-resolution edges,
612    /// as they're rarely useful (currently only used by `@defer`).
613    pub(crate) fn out_edges(&self, node: NodeIndex) -> Vec<EdgeReference<QueryGraphEdge>> {
614        Self::sorted_edges(self.graph.edges_directed(node, Direction::Outgoing).filter(
615            |edge_ref| {
616                !(edge_ref.source() == edge_ref.target()
617                    && matches!(
618                        edge_ref.weight().transition,
619                        QueryGraphEdgeTransition::KeyResolution
620                            | QueryGraphEdgeTransition::RootTypeResolution { .. }
621                    ))
622            },
623        ))
624    }
625
626    /// Edge iteration order is unspecified in petgraph, but appears to be
627    /// *reverse* insertion order in practice.
628    /// This can affect generated query plans, such as when two options have the same cost.
629    /// To match the JS code base, we want to iterate in insertion order.
630    ///
631    /// Sorting by edge indices relies on documented behavior:
632    /// <https://docs.rs/petgraph/latest/petgraph/graph/struct.Graph.html#graph-indices>
633    ///
634    /// As of this writing, edges of the query graph are removed
635    /// in `FederatedQueryGraphBuilder::update_edge_tail` which specifically preserves indices
636    /// by pairing with an insertion.
637    fn sorted_edges<'graph>(
638        edges: impl Iterator<Item = EdgeReference<'graph, QueryGraphEdge>>,
639    ) -> Vec<EdgeReference<'graph, QueryGraphEdge>> {
640        let mut edges: Vec<_> = edges.collect();
641        edges.sort_by_key(|e| -> EdgeIndex { e.id() });
642        edges
643    }
644
645    pub(crate) fn is_self_key_or_root_edge(
646        &self,
647        edge: EdgeIndex,
648    ) -> Result<bool, FederationError> {
649        let edge_weight = self.edge_weight(edge)?;
650        let (head, tail) = self.edge_endpoints(edge)?;
651        let head_weight = self.node_weight(head)?;
652        let tail_weight = self.node_weight(tail)?;
653        Ok(head_weight.source == tail_weight.source
654            && matches!(
655                edge_weight.transition,
656                QueryGraphEdgeTransition::KeyResolution
657                    | QueryGraphEdgeTransition::RootTypeResolution { .. }
658            ))
659    }
660
661    // PORT_NOTE: In the JS codebase, this was named `hasValidDirectKeyEdge`.
662    pub(crate) fn has_satisfiable_direct_key_edge(
663        &self,
664        from_node: NodeIndex,
665        to_subgraph: &str,
666        condition_resolver: &mut impl ConditionResolver,
667        max_cost: QueryPlanCost,
668    ) -> Result<bool, FederationError> {
669        for edge_ref in self.out_edges(from_node) {
670            let edge_weight = edge_ref.weight();
671            if !matches!(
672                edge_weight.transition,
673                QueryGraphEdgeTransition::KeyResolution
674            ) {
675                continue;
676            }
677
678            let tail = edge_ref.target();
679            let tail_weight = self.node_weight(tail)?;
680            if tail_weight.source.as_ref() != to_subgraph {
681                continue;
682            }
683
684            let condition_resolution = condition_resolver.resolve(
685                edge_ref.id(),
686                &OpGraphPathContext::default(),
687                &ExcludedDestinations::default(),
688                &ExcludedConditions::default(),
689                None,
690            )?;
691            let ConditionResolution::Satisfied { cost, .. } = condition_resolution else {
692                continue;
693            };
694
695            // During composition validation, we consider all conditions to have cost 1.
696            if cost <= max_cost {
697                return Ok(true);
698            }
699        }
700        Ok(false)
701    }
702
703    pub(crate) fn locally_satisfiable_key(
704        &self,
705        edge_index: EdgeIndex,
706    ) -> Result<Option<SelectionSet>, FederationError> {
707        let edge_head = self.edge_head_weight(edge_index)?;
708        let QueryGraphNodeType::SchemaType(type_position) = &edge_head.type_ else {
709            return Err(FederationError::internal(
710                "Unable to compute locally_satisfiable_key. Edge head was unexpectedly pointing to a federated root type",
711            ));
712        };
713        let Some(subgraph_schema) = self.sources.get(&edge_head.source) else {
714            return Err(FederationError::internal(format!(
715                "Could not find subgraph source {}",
716                edge_head.source
717            )));
718        };
719        let Some(metadata) = subgraph_schema.subgraph_metadata() else {
720            return Err(FederationError::internal(format!(
721                "Could not find federation metadata for source {}",
722                edge_head.source
723            )));
724        };
725        let key_directive_definition = metadata
726            .federation_spec_definition()
727            .key_directive_definition(subgraph_schema)?;
728        let external_metadata = metadata.external_metadata();
729        let composite_type_position: CompositeTypeDefinitionPosition =
730            type_position.clone().try_into()?;
731        let type_ = composite_type_position.get(subgraph_schema.schema())?;
732        type_
733            .directives()
734            .get_all(&key_directive_definition.name)
735            .map(|key| {
736                metadata
737                    .federation_spec_definition()
738                    .key_directive_arguments(key)
739            })
740            .and_then(|key_value| {
741                parse_field_set(
742                    subgraph_schema,
743                    composite_type_position.type_name().clone(),
744                    key_value.fields,
745                )
746            })
747            .find_ok(|selection| !external_metadata.selects_any_external_field(selection))
748    }
749
750    pub(crate) fn edge_for_field(
751        &self,
752        node: NodeIndex,
753        field: &Field,
754        override_conditions: &EnabledOverrideConditions,
755    ) -> Option<EdgeIndex> {
756        let mut candidates = self.out_edges(node).into_iter().filter_map(|edge_ref| {
757            let edge_weight = edge_ref.weight();
758            let QueryGraphEdgeTransition::FieldCollection {
759                field_definition_position,
760                ..
761            } = &edge_weight.transition
762            else {
763                return None;
764            };
765
766            if !edge_weight.satisfies_override_conditions(override_conditions) {
767                return None;
768            }
769
770            // We explicitly avoid comparing parent type's here, to allow interface object
771            // fields to match operation fields with the same name but differing types.
772            if field.field_position.field_name() == field_definition_position.field_name() {
773                Some(edge_ref.id())
774            } else {
775                None
776            }
777        });
778        if let Some(candidate) = candidates.next() {
779            // PORT_NOTE: The JS codebase used an assertion rather than a debug assertion here. We
780            // consider it unlikely for there to be more than one candidate given all the code paths
781            // that create edges, so we've downgraded this to a debug assertion.
782            debug_assert!(
783                candidates.next().is_none(),
784                "Unexpectedly found multiple candidates",
785            );
786            Some(candidate)
787        } else {
788            None
789        }
790    }
791
792    pub(crate) fn edge_for_inline_fragment(
793        &self,
794        node: NodeIndex,
795        inline_fragment: &InlineFragment,
796    ) -> Option<EdgeIndex> {
797        let Some(type_condition_pos) = &inline_fragment.type_condition_position else {
798            // No type condition means the type hasn't changed, meaning there is no edge to take.
799            return None;
800        };
801        let mut candidates = self.out_edges(node).into_iter().filter_map(|edge_ref| {
802            let edge_weight = edge_ref.weight();
803            let QueryGraphEdgeTransition::Downcast {
804                to_type_position, ..
805            } = &edge_weight.transition
806            else {
807                return None;
808            };
809            // We explicitly avoid comparing type kinds, to allow interface object types to
810            // match operation inline fragments (where the supergraph type kind is interface,
811            // but the subgraph type kind is object).
812            if type_condition_pos.type_name() == to_type_position.type_name() {
813                Some(edge_ref.id())
814            } else {
815                None
816            }
817        });
818        if let Some(candidate) = candidates.next() {
819            // PORT_NOTE: The JS codebase used an assertion rather than a debug assertion here. We
820            // consider it unlikely for there to be more than one candidate given all the code paths
821            // that create edges, so we've downgraded this to a debug assertion.
822            debug_assert!(
823                candidates.next().is_none(),
824                "Unexpectedly found multiple candidates",
825            );
826            Some(candidate)
827        } else {
828            None
829        }
830    }
831
832    pub(crate) fn edge_for_op_graph_path_trigger(
833        &self,
834        node: NodeIndex,
835        op_graph_path_trigger: &OpGraphPathTrigger,
836        override_conditions: &EnabledOverrideConditions,
837    ) -> Option<Option<EdgeIndex>> {
838        let OpGraphPathTrigger::OpPathElement(op_path_element) = op_graph_path_trigger else {
839            return None;
840        };
841        match op_path_element {
842            OpPathElement::Field(field) => self
843                .edge_for_field(node, field, override_conditions)
844                .map(Some),
845            OpPathElement::InlineFragment(inline_fragment) => {
846                if inline_fragment.type_condition_position.is_some() {
847                    self.edge_for_inline_fragment(node, inline_fragment)
848                        .map(Some)
849                } else {
850                    Some(None)
851                }
852            }
853        }
854    }
855
856    /// Given the possible runtime types at the head of the given edge, returns the possible runtime
857    /// types after traversing the edge.
858    // PORT_NOTE: Named `updateRuntimeTypes` in the JS codebase.
859    pub(crate) fn advance_possible_runtime_types(
860        &self,
861        possible_runtime_types: &IndexSet<ObjectTypeDefinitionPosition>,
862        edge: Option<EdgeIndex>,
863    ) -> Result<IndexSet<ObjectTypeDefinitionPosition>, FederationError> {
864        let Some(edge) = edge else {
865            return Ok(possible_runtime_types.clone());
866        };
867
868        let edge_weight = self.edge_weight(edge)?;
869        let (_, tail) = self.edge_endpoints(edge)?;
870        let tail_weight = self.node_weight(tail)?;
871        let QueryGraphNodeType::SchemaType(tail_type_pos) = &tail_weight.type_ else {
872            return Err(FederationError::internal(
873                "Unexpectedly encountered federation root node as tail node.",
874            ));
875        };
876        match &edge_weight.transition {
877            QueryGraphEdgeTransition::FieldCollection {
878                source,
879                field_definition_position,
880                ..
881            } => {
882                let Ok(_): Result<CompositeTypeDefinitionPosition, _> =
883                    tail_type_pos.clone().try_into()
884                else {
885                    return Ok(IndexSet::default());
886                };
887                let schema = self.schema_by_source(source)?;
888                let mut new_possible_runtime_types = IndexSet::default();
889                for possible_runtime_type in possible_runtime_types {
890                    let field_pos =
891                        possible_runtime_type.field(field_definition_position.field_name().clone());
892                    let Some(field) = field_pos.try_get(schema.schema()) else {
893                        continue;
894                    };
895                    let field_type_pos: CompositeTypeDefinitionPosition = schema
896                        .get_type(field.ty.inner_named_type().clone())?
897                        .try_into()?;
898                    new_possible_runtime_types
899                        .extend(schema.possible_runtime_types(field_type_pos)?);
900                }
901                Ok(new_possible_runtime_types)
902            }
903            QueryGraphEdgeTransition::Downcast {
904                source,
905                to_type_position,
906                ..
907            } => Ok(self
908                .schema_by_source(source)?
909                .possible_runtime_types(to_type_position.clone())?
910                .intersection(possible_runtime_types)
911                .cloned()
912                .collect()),
913            QueryGraphEdgeTransition::KeyResolution => {
914                let tail_type_pos: CompositeTypeDefinitionPosition =
915                    tail_type_pos.clone().try_into()?;
916                Ok(self
917                    .schema_by_source(&tail_weight.source)?
918                    .possible_runtime_types(tail_type_pos)?)
919            }
920            QueryGraphEdgeTransition::RootTypeResolution { .. } => {
921                let OutputTypeDefinitionPosition::Object(tail_type_pos) = tail_type_pos.clone()
922                else {
923                    return Err(FederationError::internal(
924                        "Unexpectedly encountered non-object root operation type.",
925                    ));
926                };
927                Ok(IndexSet::from_iter([tail_type_pos]))
928            }
929            QueryGraphEdgeTransition::SubgraphEnteringTransition => {
930                let OutputTypeDefinitionPosition::Object(tail_type_pos) = tail_type_pos.clone()
931                else {
932                    return Err(FederationError::internal(
933                        "Unexpectedly encountered non-object root operation type.",
934                    ));
935                };
936                Ok(IndexSet::from_iter([tail_type_pos]))
937            }
938            QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { .. } => {
939                Ok(possible_runtime_types.clone())
940            }
941        }
942    }
943
944    /// Returns a selection set that can be used as a key for the given type, and that can be
945    /// entirely resolved in the same subgraph. Returns None if such a key does not exist for the
946    /// given type.
947    pub(crate) fn get_locally_satisfiable_key(
948        &self,
949        node_index: NodeIndex,
950    ) -> Result<Option<SelectionSet>, FederationError> {
951        let node = self.node_weight(node_index)?;
952        let type_name = match &node.type_ {
953            QueryGraphNodeType::SchemaType(ty) => {
954                CompositeTypeDefinitionPosition::try_from(ty.clone())?
955            }
956            QueryGraphNodeType::FederatedRootType(_) => {
957                return Err(FederationError::internal(format!(
958                    "get_locally_satisfiable_key must be called on a composite type, got {}",
959                    node.type_
960                )));
961            }
962        };
963        let schema = self.schema_by_source(&node.source)?;
964        let Some(metadata) = schema.subgraph_metadata() else {
965            return Err(FederationError::internal(format!(
966                "Could not find subgraph metadata for source {}",
967                node.source
968            )));
969        };
970        let key_directive_definition = metadata
971            .federation_spec_definition()
972            .key_directive_definition(schema)?;
973
974        let ty = type_name.get(schema.schema())?;
975
976        ty.directives()
977            .get_all(&key_directive_definition.name)
978            .filter_map(|key| {
979                key.specified_argument_by_name("fields")
980                    .and_then(|arg| arg.as_str())
981            })
982            .map(|value| parse_field_set(schema, ty.name().clone(), value))
983            .find_ok(|selection| {
984                !metadata
985                    .external_metadata()
986                    .selects_any_external_field(selection)
987            })
988    }
989
990    pub(crate) fn is_cross_subgraph_edge(&self, edge: EdgeIndex) -> Result<bool, FederationError> {
991        let (head, tail) = self.edge_endpoints(edge)?;
992        let head_weight = self.node_weight(head)?;
993        let tail_weight = self.node_weight(tail)?;
994        Ok(head_weight.source != tail_weight.source)
995    }
996
997    pub(crate) fn is_provides_edge(&self, edge: EdgeIndex) -> Result<bool, FederationError> {
998        let edge_weight = self.edge_weight(edge)?;
999        let QueryGraphEdgeTransition::FieldCollection {
1000            is_part_of_provides,
1001            ..
1002        } = &edge_weight.transition
1003        else {
1004            return Ok(false);
1005        };
1006        Ok(*is_part_of_provides)
1007    }
1008
1009    pub(crate) fn has_an_implementation_with_provides(
1010        &self,
1011        source: &Arc<str>,
1012        interface_field_definition_position: InterfaceFieldDefinitionPosition,
1013    ) -> Result<bool, FederationError> {
1014        let schema = self.schema_by_source(source)?;
1015        let Some(metadata) = schema.subgraph_metadata() else {
1016            return Err(FederationError::internal(format!(
1017                "Interface should have come from a federation subgraph {}",
1018                source
1019            )));
1020        };
1021
1022        let provides_directive_definition = metadata
1023            .federation_spec_definition()
1024            .provides_directive_definition(schema)?;
1025
1026        Ok(schema
1027            .possible_runtime_types(interface_field_definition_position.parent().into())?
1028            .into_iter()
1029            .map(|object_type_definition_position| {
1030                let field_pos = object_type_definition_position
1031                    .field(interface_field_definition_position.field_name.clone());
1032                field_pos.get(schema.schema())
1033            })
1034            .ok_and_any(|field| field.directives.has(&provides_directive_definition.name))?)
1035    }
1036}