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    pub(crate) fn matches_supergraph_transition(
363        &self,
364        other: &Self,
365    ) -> Result<bool, FederationError> {
366        ensure!(
367            other.collect_operation_elements(),
368            "Supergraphs shouldn't have a transition that doesn't collect elements; got {}",
369            other,
370        );
371        Ok(match self {
372            QueryGraphEdgeTransition::FieldCollection {
373                field_definition_position,
374                ..
375            } => {
376                let QueryGraphEdgeTransition::FieldCollection {
377                    field_definition_position: other_field_definition_position,
378                    ..
379                } = other
380                else {
381                    return Ok(false);
382                };
383                field_definition_position.field_name()
384                    == other_field_definition_position.field_name()
385            }
386            QueryGraphEdgeTransition::Downcast {
387                to_type_position, ..
388            } => {
389                let QueryGraphEdgeTransition::Downcast {
390                    to_type_position: other_to_type_position,
391                    ..
392                } = other
393                else {
394                    return Ok(false);
395                };
396                to_type_position.type_name() == other_to_type_position.type_name()
397            }
398            QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { to_type_name, .. } => {
399                let QueryGraphEdgeTransition::InterfaceObjectFakeDownCast {
400                    to_type_name: other_to_type_name,
401                    ..
402                } = other
403                else {
404                    return Ok(false);
405                };
406                to_type_name == other_to_type_name
407            }
408            _ => false,
409        })
410    }
411}
412
413impl Display for QueryGraphEdgeTransition {
414    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
415        match self {
416            QueryGraphEdgeTransition::FieldCollection {
417                field_definition_position,
418                ..
419            } => {
420                write!(f, "{}", field_definition_position.field_name())
421            }
422            QueryGraphEdgeTransition::Downcast {
423                to_type_position, ..
424            } => {
425                write!(f, "... on {}", to_type_position.type_name())
426            }
427            QueryGraphEdgeTransition::KeyResolution => {
428                write!(f, "key()")
429            }
430            QueryGraphEdgeTransition::RootTypeResolution { root_kind } => {
431                write!(f, "{root_kind}()")
432            }
433            QueryGraphEdgeTransition::SubgraphEnteringTransition => {
434                write!(f, "∅")
435            }
436            QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { to_type_name, .. } => {
437                write!(f, "... on {to_type_name}")
438            }
439        }
440    }
441}
442
443#[derive(Debug)]
444pub struct QueryGraph {
445    /// The "current" source of the query graph. For query graphs representing a single source
446    /// graph, this will only ever be one value, but it will change for "federated" query graphs
447    /// while they're being built (and after construction, will become FEDERATED_GRAPH_ROOT_SOURCE,
448    /// which is a reserved placeholder value).
449    current_source: Arc<str>,
450    /// The nodes/edges of the query graph. Note that nodes/edges should never be removed, so
451    /// indexes are immutable when a node/edge is created.
452    graph: DiGraph<QueryGraphNode, QueryGraphEdge>,
453    /// The sources on which the query graph was built, which is a set (potentially of size 1) of
454    /// GraphQL schema keyed by the name identifying them. Note that the `source` strings in the
455    /// nodes/edges of a query graph are guaranteed to be valid key in this map.
456    sources: IndexMap<Arc<str>, ValidFederationSchema>,
457    /// For federated query graphs, this is a map from subgraph names to their schemas. This is the
458    /// same as `sources`, but is missing the dummy source FEDERATED_GRAPH_ROOT_SOURCE which isn't
459    /// really a subgraph.
460    subgraphs_by_name: IndexMap<Arc<str>, ValidFederationSchema>,
461    /// For federated query graphs, this is the supergraph schema; otherwise, this is `None`.
462    supergraph_schema: Option<ValidFederationSchema>,
463    /// A map (keyed by source) that associates type names of the underlying schema on which this
464    /// query graph was built to each of the nodes that points to a type of that name. Note that for
465    /// a "federated" query graph source, each type name will only map to a single node.
466    types_to_nodes_by_source: IndexMap<Arc<str>, IndexMap<NamedType, IndexSet<NodeIndex>>>,
467    /// A map (keyed by source) that associates schema root kinds to root nodes.
468    root_kinds_to_nodes_by_source:
469        IndexMap<Arc<str>, IndexMap<SchemaRootDefinitionKind, NodeIndex>>,
470    /// Maps an edge to the possible edges that can follow it "productively", that is without
471    /// creating a trivially inefficient path.
472    ///
473    /// More precisely, this map is equivalent to looking at the out edges of a given edge's tail
474    /// node and filtering those edges that "never make sense" after the given edge, which mainly
475    /// amounts to avoiding chaining @key edges when we know there is guaranteed to be a better
476    /// option. As an example, suppose we have 3 subgraphs A, B and C which all defined an
477    /// `@key(fields: "id")` on some entity type `T`. Then it is never interesting to take that @key
478    /// edge from B -> C after A -> B because if we're in A and want to get to C, we can always do
479    /// A -> C (of course, this is only true because it's the "same" key).
480    ///
481    /// See `precompute_non_trivial_followup_edges` for more details on which exact edges are
482    /// filtered.
483    ///
484    /// Lastly, note that the main reason for having this field is that its result is pre-computed.
485    /// Which in turn is done for performance reasons: having the same key defined in multiple
486    /// subgraphs is _the_ most common pattern, and while our later algorithms (composition
487    /// validation and query planning) would know to not select those trivially inefficient
488    /// "detours", they might have to redo those checks many times and pre-computing once it is
489    /// significantly faster (and pretty easy). FWIW, when originally introduced, this optimization
490    /// lowered composition validation on a big composition (100+ subgraphs) from ~4 minutes to
491    /// ~10 seconds.
492    non_trivial_followup_edges: IndexMap<EdgeIndex, Vec<EdgeIndex>>,
493    // PORT_NOTE: This field was renamed from the JS name (`subgraphToArgIndices`) to better
494    // align with downstream code.
495    /// Maps subgraph names to another map, for any subgraph with usages of `@fromContext`. This
496    /// other map then maps subgraph argument positions/coordinates (for `@fromContext` arguments)
497    /// to a unique identifier string (specifically, unique across pairs of subgraph names and
498    /// argument coordinates). This identifier is called the "context ID".
499    arguments_to_context_ids_by_source:
500        IndexMap<Arc<str>, IndexMap<ObjectFieldArgumentDefinitionPosition, Name>>,
501    override_condition_labels: IndexSet<Arc<str>>,
502    /// To speed up the estimation of counting non-local selections, we precompute specific metadata
503    /// about the query graph and store that here.
504    non_local_selection_metadata: non_local_selections_estimation::QueryGraphMetadata,
505}
506
507impl QueryGraph {
508    pub(crate) fn name(&self) -> &Arc<str> {
509        &self.current_source
510    }
511
512    pub(crate) fn graph(&self) -> &DiGraph<QueryGraphNode, QueryGraphEdge> {
513        &self.graph
514    }
515
516    pub(crate) fn override_condition_labels(&self) -> &IndexSet<Arc<str>> {
517        &self.override_condition_labels
518    }
519
520    pub(crate) fn supergraph_schema(&self) -> Result<ValidFederationSchema, FederationError> {
521        self.supergraph_schema
522            .clone()
523            .ok_or_else(|| internal_error!("Supergraph schema unexpectedly missing"))
524    }
525
526    pub(crate) fn node_weight(&self, node: NodeIndex) -> Result<&QueryGraphNode, FederationError> {
527        self.graph
528            .node_weight(node)
529            .ok_or_else(|| internal_error!("Node unexpectedly missing"))
530    }
531
532    fn node_weight_mut(&mut self, node: NodeIndex) -> Result<&mut QueryGraphNode, FederationError> {
533        self.graph
534            .node_weight_mut(node)
535            .ok_or_else(|| internal_error!("Node unexpectedly missing"))
536    }
537
538    pub(crate) fn edge_weight(&self, edge: EdgeIndex) -> Result<&QueryGraphEdge, FederationError> {
539        self.graph
540            .edge_weight(edge)
541            .ok_or_else(|| internal_error!("Edge unexpectedly missing"))
542    }
543
544    fn edge_weight_mut(&mut self, edge: EdgeIndex) -> Result<&mut QueryGraphEdge, FederationError> {
545        self.graph
546            .edge_weight_mut(edge)
547            .ok_or_else(|| internal_error!("Edge unexpectedly missing"))
548    }
549
550    pub(crate) fn edge_head_weight(
551        &self,
552        edge: EdgeIndex,
553    ) -> Result<&QueryGraphNode, FederationError> {
554        let (head_id, _) = self.edge_endpoints(edge)?;
555        self.node_weight(head_id)
556    }
557
558    pub(crate) fn edge_endpoints(
559        &self,
560        edge: EdgeIndex,
561    ) -> Result<(NodeIndex, NodeIndex), FederationError> {
562        self.graph
563            .edge_endpoints(edge)
564            .ok_or_else(|| internal_error!("Edge unexpectedly missing"))
565    }
566
567    pub(crate) fn schema(&self) -> Result<&ValidFederationSchema, FederationError> {
568        self.schema_by_source(&self.current_source)
569    }
570
571    pub(crate) fn schema_by_source(
572        &self,
573        source: &str,
574    ) -> Result<&ValidFederationSchema, FederationError> {
575        self.sources
576            .get(source)
577            .ok_or_else(|| internal_error!(r#"Schema for "{source}" unexpectedly missing"#))
578    }
579
580    pub(crate) fn subgraph_schemas(&self) -> &IndexMap<Arc<str>, ValidFederationSchema> {
581        &self.subgraphs_by_name
582    }
583
584    pub(crate) fn subgraphs(&self) -> impl Iterator<Item = (&Arc<str>, &ValidFederationSchema)> {
585        self.subgraphs_by_name.iter()
586    }
587
588    /// Returns the node indices whose name matches the given type name.
589    pub(crate) fn nodes_for_type(
590        &self,
591        name: &Name,
592    ) -> Result<&IndexSet<NodeIndex>, FederationError> {
593        self.types_to_nodes()?
594            .get(name)
595            .ok_or_else(|| internal_error!("No nodes unexpectedly found for type"))
596    }
597
598    pub(crate) fn types_to_nodes(
599        &self,
600    ) -> Result<&IndexMap<NamedType, IndexSet<NodeIndex>>, FederationError> {
601        self.types_to_nodes_by_source(&self.current_source)
602    }
603
604    pub(super) fn types_to_nodes_by_source(
605        &self,
606        source: &str,
607    ) -> Result<&IndexMap<NamedType, IndexSet<NodeIndex>>, FederationError> {
608        self.types_to_nodes_by_source.get(source).ok_or_else(|| {
609            SingleFederationError::Internal {
610                message: "Types-to-nodes map unexpectedly missing".to_owned(),
611            }
612            .into()
613        })
614    }
615
616    fn types_to_nodes_mut(
617        &mut self,
618    ) -> Result<&mut IndexMap<NamedType, IndexSet<NodeIndex>>, FederationError> {
619        self.types_to_nodes_by_source
620            .get_mut(&self.current_source)
621            .ok_or_else(|| {
622                SingleFederationError::Internal {
623                    message: "Types-to-nodes map unexpectedly missing".to_owned(),
624                }
625                .into()
626            })
627    }
628
629    pub(crate) fn root_kinds_to_nodes(
630        &self,
631    ) -> Result<&IndexMap<SchemaRootDefinitionKind, NodeIndex>, FederationError> {
632        self.root_kinds_to_nodes_by_source
633            .get(&self.current_source)
634            .ok_or_else(|| {
635                SingleFederationError::Internal {
636                    message: "Root-kinds-to-nodes map unexpectedly missing".to_owned(),
637                }
638                .into()
639            })
640    }
641
642    pub(crate) fn root_kinds_to_nodes_by_source(
643        &self,
644        source: &str,
645    ) -> Result<&IndexMap<SchemaRootDefinitionKind, NodeIndex>, FederationError> {
646        self.root_kinds_to_nodes_by_source
647            .get(source)
648            .ok_or_else(|| {
649                SingleFederationError::Internal {
650                    message: "Root-kinds-to-nodes map unexpectedly missing".to_owned(),
651                }
652                .into()
653            })
654    }
655
656    fn root_kinds_to_nodes_mut(
657        &mut self,
658    ) -> Result<&mut IndexMap<SchemaRootDefinitionKind, NodeIndex>, FederationError> {
659        self.root_kinds_to_nodes_by_source
660            .get_mut(&self.current_source)
661            .ok_or_else(|| {
662                SingleFederationError::Internal {
663                    message: "Root-kinds-to-nodes map unexpectedly missing".to_owned(),
664                }
665                .into()
666            })
667    }
668
669    pub(crate) fn context_id_by_source_and_argument(
670        &self,
671        source: &str,
672        argument: &ObjectFieldArgumentDefinitionPosition,
673    ) -> Result<&Name, FederationError> {
674        self.arguments_to_context_ids_by_source
675            .get(source)
676            .and_then(|r| r.get(argument))
677            .ok_or_else(|| {
678                internal_error!("context ID unexpectedly missing for @fromContext argument")
679            })
680    }
681
682    pub(crate) fn is_context_used(&self) -> bool {
683        !self.arguments_to_context_ids_by_source.is_empty()
684    }
685
686    pub(crate) fn non_local_selection_metadata(
687        &self,
688    ) -> &non_local_selections_estimation::QueryGraphMetadata {
689        &self.non_local_selection_metadata
690    }
691
692    /// All outward edges from the given node (including self-key and self-root-type-resolution
693    /// edges). Primarily used by `@defer`, when needing to re-enter a subgraph for a deferred
694    /// section.
695    pub(crate) fn out_edges_with_federation_self_edges(
696        &self,
697        node: NodeIndex,
698    ) -> Vec<EdgeReference<'_, QueryGraphEdge>> {
699        Self::sorted_edges(self.graph.edges_directed(node, Direction::Outgoing))
700    }
701
702    /// The outward edges from the given node, minus self-key and self-root-type-resolution edges,
703    /// as they're rarely useful (currently only used by `@defer`).
704    pub(crate) fn out_edges(&self, node: NodeIndex) -> Vec<EdgeReference<'_, QueryGraphEdge>> {
705        Self::sorted_edges(self.graph.edges_directed(node, Direction::Outgoing).filter(
706            |edge_ref| {
707                !(edge_ref.source() == edge_ref.target()
708                    && matches!(
709                        edge_ref.weight().transition,
710                        QueryGraphEdgeTransition::KeyResolution
711                            | QueryGraphEdgeTransition::RootTypeResolution { .. }
712                    ))
713            },
714        ))
715    }
716
717    /// Edge iteration order is unspecified in petgraph, but appears to be
718    /// *reverse* insertion order in practice.
719    /// This can affect generated query plans, such as when two options have the same cost.
720    /// To match the JS code base, we want to iterate in insertion order.
721    ///
722    /// Sorting by edge indices relies on documented behavior:
723    /// <https://docs.rs/petgraph/latest/petgraph/graph/struct.Graph.html#graph-indices>
724    ///
725    /// As of this writing, edges of the query graph are removed
726    /// in `FederatedQueryGraphBuilder::update_edge_tail` which specifically preserves indices
727    /// by pairing with an insertion.
728    fn sorted_edges<'graph>(
729        edges: impl Iterator<Item = EdgeReference<'graph, QueryGraphEdge>>,
730    ) -> Vec<EdgeReference<'graph, QueryGraphEdge>> {
731        let mut edges: Vec<_> = edges.collect();
732        edges.sort_by_key(|e| -> EdgeIndex { e.id() });
733        edges
734    }
735
736    pub(crate) fn is_terminal(&self, node: NodeIndex) -> bool {
737        self.graph.edges_directed(node, Direction::Outgoing).count() == 0
738    }
739
740    pub(crate) fn is_self_key_or_root_edge(
741        &self,
742        edge: EdgeIndex,
743    ) -> Result<bool, FederationError> {
744        let edge_weight = self.edge_weight(edge)?;
745        let (head, tail) = self.edge_endpoints(edge)?;
746        let head_weight = self.node_weight(head)?;
747        let tail_weight = self.node_weight(tail)?;
748        Ok(head_weight.source == tail_weight.source
749            && matches!(
750                edge_weight.transition,
751                QueryGraphEdgeTransition::KeyResolution
752                    | QueryGraphEdgeTransition::RootTypeResolution { .. }
753            ))
754    }
755
756    // PORT_NOTE: In the JS codebase, this was named `hasValidDirectKeyEdge`.
757    pub(crate) fn has_satisfiable_direct_key_edge(
758        &self,
759        from_node: NodeIndex,
760        to_subgraph: &str,
761        condition_resolver: &mut impl ConditionResolver,
762        max_cost: QueryPlanCost,
763    ) -> Result<bool, FederationError> {
764        for edge_ref in self.out_edges(from_node) {
765            let edge_weight = edge_ref.weight();
766            if !matches!(
767                edge_weight.transition,
768                QueryGraphEdgeTransition::KeyResolution
769            ) {
770                continue;
771            }
772
773            let tail = edge_ref.target();
774            let tail_weight = self.node_weight(tail)?;
775            if tail_weight.source.as_ref() != to_subgraph {
776                continue;
777            }
778
779            let condition_resolution = condition_resolver.resolve(
780                edge_ref.id(),
781                &OpGraphPathContext::default(),
782                &ExcludedDestinations::default(),
783                &ExcludedConditions::default(),
784                None,
785            )?;
786            let ConditionResolution::Satisfied { cost, .. } = condition_resolution else {
787                continue;
788            };
789
790            // During composition validation, we consider all conditions to have cost 1.
791            if cost <= max_cost {
792                return Ok(true);
793            }
794        }
795        Ok(false)
796    }
797
798    pub(crate) fn locally_satisfiable_key(
799        &self,
800        edge_index: EdgeIndex,
801    ) -> Result<Option<SelectionSet>, FederationError> {
802        let edge_head = self.edge_head_weight(edge_index)?;
803        let QueryGraphNodeType::SchemaType(type_position) = &edge_head.type_ else {
804            return Err(FederationError::internal(
805                "Unable to compute locally_satisfiable_key. Edge head was unexpectedly pointing to a federated root type",
806            ));
807        };
808        let Some(subgraph_schema) = self.sources.get(&edge_head.source) else {
809            return Err(FederationError::internal(format!(
810                "Could not find subgraph source {}",
811                edge_head.source
812            )));
813        };
814        let Some(metadata) = subgraph_schema.subgraph_metadata() else {
815            return Err(FederationError::internal(format!(
816                "Could not find federation metadata for source {}",
817                edge_head.source
818            )));
819        };
820        let key_directive_definition = metadata
821            .federation_spec_definition()
822            .key_directive_definition(subgraph_schema)?;
823        let external_metadata = metadata.external_metadata();
824        let composite_type_position: CompositeTypeDefinitionPosition =
825            type_position.clone().try_into()?;
826        let type_ = composite_type_position.get(subgraph_schema.schema())?;
827        type_
828            .directives()
829            .get_all(&key_directive_definition.name)
830            .map(|key| {
831                metadata
832                    .federation_spec_definition()
833                    .key_directive_arguments(key)
834            })
835            .and_then(|key_value| {
836                parse_field_set(
837                    subgraph_schema,
838                    composite_type_position.type_name().clone(),
839                    key_value.fields,
840                    true,
841                )
842            })
843            .find_ok(|selection| !external_metadata.selects_any_external_field(selection))
844    }
845
846    pub(crate) fn edge_for_field(
847        &self,
848        node: NodeIndex,
849        field: &Field,
850        override_conditions: &OverrideConditions,
851    ) -> Option<EdgeIndex> {
852        let mut candidates = self.out_edges(node).into_iter().filter_map(|edge_ref| {
853            let edge_weight = edge_ref.weight();
854            let QueryGraphEdgeTransition::FieldCollection {
855                field_definition_position,
856                ..
857            } = &edge_weight.transition
858            else {
859                return None;
860            };
861
862            if !edge_weight.satisfies_override_conditions(override_conditions) {
863                return None;
864            }
865
866            // We explicitly avoid comparing parent type's here, to allow interface object
867            // fields to match operation fields with the same name but differing types.
868            if field.field_position.field_name() == field_definition_position.field_name() {
869                Some(edge_ref.id())
870            } else {
871                None
872            }
873        });
874        if let Some(candidate) = candidates.next() {
875            // PORT_NOTE: The JS codebase used an assertion rather than a debug assertion here. We
876            // consider it unlikely for there to be more than one candidate given all the code paths
877            // that create edges, so we've downgraded this to a debug assertion.
878            debug_assert!(
879                candidates.next().is_none(),
880                "Unexpectedly found multiple candidates",
881            );
882            Some(candidate)
883        } else {
884            None
885        }
886    }
887
888    pub(crate) fn edge_for_inline_fragment(
889        &self,
890        node: NodeIndex,
891        inline_fragment: &InlineFragment,
892    ) -> Option<EdgeIndex> {
893        let Some(type_condition_pos) = &inline_fragment.type_condition_position else {
894            // No type condition means the type hasn't changed, meaning there is no edge to take.
895            return None;
896        };
897        let mut candidates = self.out_edges(node).into_iter().filter_map(|edge_ref| {
898            let edge_weight = edge_ref.weight();
899            let QueryGraphEdgeTransition::Downcast {
900                to_type_position, ..
901            } = &edge_weight.transition
902            else {
903                return None;
904            };
905            // We explicitly avoid comparing type kinds, to allow interface object types to
906            // match operation inline fragments (where the supergraph type kind is interface,
907            // but the subgraph type kind is object).
908            if type_condition_pos.type_name() == to_type_position.type_name() {
909                Some(edge_ref.id())
910            } else {
911                None
912            }
913        });
914        if let Some(candidate) = candidates.next() {
915            // PORT_NOTE: The JS codebase used an assertion rather than a debug assertion here. We
916            // consider it unlikely for there to be more than one candidate given all the code paths
917            // that create edges, so we've downgraded this to a debug assertion.
918            debug_assert!(
919                candidates.next().is_none(),
920                "Unexpectedly found multiple candidates",
921            );
922            Some(candidate)
923        } else {
924            None
925        }
926    }
927
928    pub(crate) fn edge_for_op_graph_path_trigger(
929        &self,
930        node: NodeIndex,
931        op_graph_path_trigger: &OpGraphPathTrigger,
932        override_conditions: &OverrideConditions,
933    ) -> Option<Option<EdgeIndex>> {
934        let OpGraphPathTrigger::OpPathElement(op_path_element) = op_graph_path_trigger else {
935            return None;
936        };
937        match op_path_element {
938            OpPathElement::Field(field) => self
939                .edge_for_field(node, field, override_conditions)
940                .map(Some),
941            OpPathElement::InlineFragment(inline_fragment) => {
942                if inline_fragment.type_condition_position.is_some() {
943                    self.edge_for_inline_fragment(node, inline_fragment)
944                        .map(Some)
945                } else {
946                    Some(None)
947                }
948            }
949        }
950    }
951
952    pub(crate) fn edge_for_transition_graph_path_trigger(
953        &self,
954        node: NodeIndex,
955        transition_graph_path_trigger: &QueryGraphEdgeTransition,
956        override_conditions: &OverrideConditions,
957    ) -> Result<Option<EdgeIndex>, FederationError> {
958        for edge_ref in self.out_edges(node) {
959            let edge_weight = edge_ref.weight();
960            if edge_weight
961                .transition
962                .matches_supergraph_transition(transition_graph_path_trigger)?
963                && edge_weight.satisfies_override_conditions(override_conditions)
964            {
965                return Ok(Some(edge_ref.id()));
966            }
967        }
968        Ok(None)
969    }
970
971    /// Given the possible runtime types at the head of the given edge, returns the possible runtime
972    /// types after traversing the edge.
973    // PORT_NOTE: Named `updateRuntimeTypes` in the JS codebase.
974    pub(crate) fn advance_possible_runtime_types(
975        &self,
976        possible_runtime_types: &IndexSet<ObjectTypeDefinitionPosition>,
977        edge: Option<EdgeIndex>,
978    ) -> Result<IndexSet<ObjectTypeDefinitionPosition>, FederationError> {
979        let Some(edge) = edge else {
980            return Ok(possible_runtime_types.clone());
981        };
982
983        let edge_weight = self.edge_weight(edge)?;
984        let (_, tail) = self.edge_endpoints(edge)?;
985        let tail_weight = self.node_weight(tail)?;
986        let QueryGraphNodeType::SchemaType(tail_type_pos) = &tail_weight.type_ else {
987            return Err(FederationError::internal(
988                "Unexpectedly encountered federation root node as tail node.",
989            ));
990        };
991        match &edge_weight.transition {
992            QueryGraphEdgeTransition::FieldCollection {
993                source,
994                field_definition_position,
995                ..
996            } => {
997                if CompositeTypeDefinitionPosition::try_from(tail_type_pos.clone()).is_err() {
998                    return Ok(IndexSet::default());
999                }
1000                let schema = self.schema_by_source(source)?;
1001                let mut new_possible_runtime_types = IndexSet::default();
1002                for possible_runtime_type in possible_runtime_types {
1003                    let field_pos =
1004                        possible_runtime_type.field(field_definition_position.field_name().clone());
1005                    let Some(field) = field_pos.try_get(schema.schema()) else {
1006                        continue;
1007                    };
1008                    let field_type_pos: CompositeTypeDefinitionPosition = schema
1009                        .get_type(field.ty.inner_named_type().clone())?
1010                        .try_into()?;
1011                    new_possible_runtime_types
1012                        .extend(schema.possible_runtime_types(field_type_pos)?);
1013                }
1014                Ok(new_possible_runtime_types)
1015            }
1016            QueryGraphEdgeTransition::Downcast {
1017                source,
1018                to_type_position,
1019                ..
1020            } => Ok(self
1021                .schema_by_source(source)?
1022                .possible_runtime_types(to_type_position.clone())?
1023                .intersection(possible_runtime_types)
1024                .cloned()
1025                .collect()),
1026            QueryGraphEdgeTransition::KeyResolution => {
1027                let tail_type_pos: CompositeTypeDefinitionPosition =
1028                    tail_type_pos.clone().try_into()?;
1029                Ok(self
1030                    .schema_by_source(&tail_weight.source)?
1031                    .possible_runtime_types(tail_type_pos)?)
1032            }
1033            QueryGraphEdgeTransition::RootTypeResolution { .. } => {
1034                let OutputTypeDefinitionPosition::Object(tail_type_pos) = tail_type_pos.clone()
1035                else {
1036                    return Err(FederationError::internal(
1037                        "Unexpectedly encountered non-object root operation type.",
1038                    ));
1039                };
1040                Ok(IndexSet::from_iter([tail_type_pos]))
1041            }
1042            QueryGraphEdgeTransition::SubgraphEnteringTransition => {
1043                let OutputTypeDefinitionPosition::Object(tail_type_pos) = tail_type_pos.clone()
1044                else {
1045                    return Err(FederationError::internal(
1046                        "Unexpectedly encountered non-object root operation type.",
1047                    ));
1048                };
1049                Ok(IndexSet::from_iter([tail_type_pos]))
1050            }
1051            QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { .. } => {
1052                Ok(possible_runtime_types.clone())
1053            }
1054        }
1055    }
1056
1057    /// Returns a selection set that can be used as a key for the given type, and that can be
1058    /// entirely resolved in the same subgraph. Returns None if such a key does not exist for the
1059    /// given type.
1060    pub(crate) fn get_locally_satisfiable_key(
1061        &self,
1062        node_index: NodeIndex,
1063    ) -> Result<Option<SelectionSet>, FederationError> {
1064        let node = self.node_weight(node_index)?;
1065        let type_name = match &node.type_ {
1066            QueryGraphNodeType::SchemaType(ty) => {
1067                CompositeTypeDefinitionPosition::try_from(ty.clone())?
1068            }
1069            QueryGraphNodeType::FederatedRootType(_) => {
1070                return Err(FederationError::internal(format!(
1071                    "get_locally_satisfiable_key must be called on a composite type, got {}",
1072                    node.type_
1073                )));
1074            }
1075        };
1076        let schema = self.schema_by_source(&node.source)?;
1077        let Some(metadata) = schema.subgraph_metadata() else {
1078            return Err(FederationError::internal(format!(
1079                "Could not find subgraph metadata for source {}",
1080                node.source
1081            )));
1082        };
1083        let key_directive_definition = metadata
1084            .federation_spec_definition()
1085            .key_directive_definition(schema)?;
1086
1087        let ty = type_name.get(schema.schema())?;
1088
1089        ty.directives()
1090            .get_all(&key_directive_definition.name)
1091            .filter_map(|key| {
1092                key.specified_argument_by_name("fields")
1093                    .and_then(|arg| arg.as_str())
1094            })
1095            .map(|value| parse_field_set(schema, ty.name().clone(), value, true))
1096            .find_ok(|selection| {
1097                !metadata
1098                    .external_metadata()
1099                    .selects_any_external_field(selection)
1100            })
1101    }
1102
1103    pub(crate) fn is_cross_subgraph_edge(&self, edge: EdgeIndex) -> Result<bool, FederationError> {
1104        let (head, tail) = self.edge_endpoints(edge)?;
1105        let head_weight = self.node_weight(head)?;
1106        let tail_weight = self.node_weight(tail)?;
1107        Ok(head_weight.source != tail_weight.source)
1108    }
1109
1110    pub(crate) fn is_provides_edge(&self, edge: EdgeIndex) -> Result<bool, FederationError> {
1111        let edge_weight = self.edge_weight(edge)?;
1112        let QueryGraphEdgeTransition::FieldCollection {
1113            is_part_of_provides,
1114            ..
1115        } = &edge_weight.transition
1116        else {
1117            return Ok(false);
1118        };
1119        Ok(*is_part_of_provides)
1120    }
1121
1122    pub(crate) fn has_an_implementation_with_provides(
1123        &self,
1124        source: &Arc<str>,
1125        interface_field_definition_position: InterfaceFieldDefinitionPosition,
1126    ) -> Result<bool, FederationError> {
1127        let schema = self.schema_by_source(source)?;
1128        let Some(metadata) = schema.subgraph_metadata() else {
1129            return Err(FederationError::internal(format!(
1130                "Interface should have come from a federation subgraph {source}"
1131            )));
1132        };
1133
1134        let provides_directive_definition = metadata
1135            .federation_spec_definition()
1136            .provides_directive_definition(schema)?;
1137
1138        Ok(schema
1139            .possible_runtime_types(interface_field_definition_position.parent().into())?
1140            .into_iter()
1141            .map(|object_type_definition_position| {
1142                let field_pos = object_type_definition_position
1143                    .field(interface_field_definition_position.field_name.clone());
1144                field_pos.get(schema.schema())
1145            })
1146            .ok_and_any(|field| field.directives.has(&provides_directive_definition.name))?)
1147    }
1148}