Skip to main content

apollo_federation/query_graph/
mod.rs

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