|
| 1 | +//! Field-level routing as a BULB search space. |
| 2 | +//! |
| 3 | +//! The planner walks the operation selection-by-selection ("pendings"), |
| 4 | +//! consulting the query graph for where each field can be resolved: |
| 5 | +//! - [`state`]: mutable search state (pending stack, checkpoints). |
| 6 | +//! - [`routing`]: enumerating and ranking options for a selection. |
| 7 | +//! - [`commit`]: applying a chosen option to the fetch graph. |
| 8 | +//! - [`conditions`]: condition satisfiability for @requires / @key. |
| 9 | +//! - [`requires`]: hop-edge inputs and condition paths. |
| 10 | +//! |
| 11 | +//! This file holds the search-space type and the |
| 12 | +//! [`BulbSearchSpace`] implementation. |
| 13 | +
|
| 14 | +mod conditions; |
| 15 | +mod requires; |
| 16 | +pub(super) mod state; |
| 17 | + |
| 18 | +use std::sync::Arc; |
| 19 | + |
| 20 | +use apollo_compiler::Name; |
| 21 | +use petgraph::graph::EdgeIndex; |
| 22 | +use petgraph::graph::NodeIndex; |
| 23 | +use petgraph::visit::EdgeRef; |
| 24 | +#[allow(unused_imports)] |
| 25 | +pub(crate) use state::PendingSelection; |
| 26 | +#[allow(unused_imports)] |
| 27 | +pub(crate) use state::PlanState; |
| 28 | + |
| 29 | +use super::shared_path::SharedPath; |
| 30 | +use crate::error::FederationError; |
| 31 | +use crate::operation::Field; |
| 32 | +use crate::operation::InlineFragment; |
| 33 | +use crate::operation::Selection; |
| 34 | +use crate::operation::SelectionId; |
| 35 | +use crate::operation::SelectionSet; |
| 36 | +use crate::query_graph::OverrideConditions; |
| 37 | +use crate::query_graph::QueryGraph; |
| 38 | +use crate::query_graph::graph_path::operation::OpPathElement; |
| 39 | +use crate::schema::ValidFederationSchema; |
| 40 | +use crate::schema::position::CompositeTypeDefinitionPosition; |
| 41 | + |
| 42 | +/// Cache key for routing options. Captures the selection identity at a QG node. |
| 43 | +#[derive(Clone, Debug, PartialEq, Eq, Hash)] |
| 44 | +pub(super) enum RoutingCacheKey { |
| 45 | + Field(Name), |
| 46 | + InlineFragment(Option<Name>), |
| 47 | +} |
| 48 | + |
| 49 | +/// Subgraph, type position, and schema at a query graph node. |
| 50 | +pub(super) struct NodeSource { |
| 51 | + pub(super) subgraph: Arc<str>, |
| 52 | + pub(super) type_pos: CompositeTypeDefinitionPosition, |
| 53 | + pub(super) schema: ValidFederationSchema, |
| 54 | +} |
| 55 | + |
| 56 | +/// Search space presenting field-level routing decisions as a BULB problem. |
| 57 | +pub(crate) struct FieldRoutingSearchSpace { |
| 58 | + pub(crate) query_graph: Arc<QueryGraph>, |
| 59 | + pub(crate) supergraph_schema: ValidFederationSchema, |
| 60 | + pub(crate) override_conditions: OverrideConditions, |
| 61 | + pub(crate) inconsistent_abstract_types: Arc<apollo_compiler::collections::IndexSet<Name>>, |
| 62 | +} |
| 63 | + |
| 64 | +impl FieldRoutingSearchSpace { |
| 65 | + pub(super) fn node_source(&self, node: NodeIndex) -> Result<NodeSource, FederationError> { |
| 66 | + let data = self.query_graph.node_weight(node)?; |
| 67 | + Ok(NodeSource { |
| 68 | + subgraph: data.source.clone(), |
| 69 | + type_pos: data.type_.clone().try_into()?, |
| 70 | + schema: self.query_graph.schema_by_source(&data.source)?.clone(), |
| 71 | + }) |
| 72 | + } |
| 73 | + |
| 74 | + /// Select `__typename` in `fetch_node` at `base_path` so the executor |
| 75 | + /// can identify the concrete type for entity representations. |
| 76 | + pub(super) fn append_typename( |
| 77 | + &self, |
| 78 | + state: &mut PlanState, |
| 79 | + fetch_node: NodeIndex, |
| 80 | + base_path: &SharedPath<Arc<OpPathElement>>, |
| 81 | + source: &NodeSource, |
| 82 | + ) { |
| 83 | + let typename = Arc::new(OpPathElement::Field(Field::new_introspection_typename( |
| 84 | + &source.schema, |
| 85 | + &source.type_pos, |
| 86 | + None, |
| 87 | + ))); |
| 88 | + state |
| 89 | + .graph |
| 90 | + .append_selection(fetch_node, &base_path.pushed(typename), None); |
| 91 | + } |
| 92 | + |
| 93 | + /// Op path at which selections enter an entity fetch group: entity |
| 94 | + /// fetches start from the `_Entity` union, so everything nests under a |
| 95 | + /// `... on <ConcreteType>` condition rebased onto the supergraph schema |
| 96 | + /// (which OpPaths reference). |
| 97 | + pub(super) fn entity_root_path( |
| 98 | + &self, |
| 99 | + type_name: &Name, |
| 100 | + ) -> Result<SharedPath<Arc<OpPathElement>>, FederationError> { |
| 101 | + let rebased: CompositeTypeDefinitionPosition = |
| 102 | + self.supergraph_schema.get_type(type_name)?.try_into()?; |
| 103 | + let condition = InlineFragment { |
| 104 | + schema: self.supergraph_schema.clone(), |
| 105 | + parent_type_position: rebased.clone(), |
| 106 | + type_condition_position: Some(rebased), |
| 107 | + directives: Default::default(), |
| 108 | + selection_id: SelectionId::new(), |
| 109 | + }; |
| 110 | + Ok(SharedPath::new().pushed(Arc::new(OpPathElement::InlineFragment(condition)))) |
| 111 | + } |
| 112 | + |
| 113 | + /// Can condition fields simply be selected in the fetch at `node`? |
| 114 | + /// True when the subgraph resolves every field itself and none carries |
| 115 | + /// @requires (which draws on an entity representation and needs its own |
| 116 | + /// fetch). The graph-based check complements the schema-based one: |
| 117 | + /// @external fields may still resolve at `node` when it is a |
| 118 | + /// provides-copy created by an ancestor's @provides. |
| 119 | + pub(super) fn can_resolve_in_place( |
| 120 | + &self, |
| 121 | + node: NodeIndex, |
| 122 | + conditions: &Arc<SelectionSet>, |
| 123 | + source: &NodeSource, |
| 124 | + ) -> Result<bool, FederationError> { |
| 125 | + let satisfiable = self.can_satisfy( |
| 126 | + conditions, |
| 127 | + &source.type_pos, |
| 128 | + &source.subgraph, |
| 129 | + &source.schema, |
| 130 | + ) || self.conditions_resolvable_at_node(node, conditions)?; |
| 131 | + Ok(satisfiable && !self.conditions_have_requires(node, conditions)?) |
| 132 | + } |
| 133 | + |
| 134 | + /// Outgoing edge indices from a query graph node, sorted and filtered. |
| 135 | + pub(super) fn out_edge_indices(&self, node: NodeIndex) -> Vec<EdgeIndex> { |
| 136 | + self.query_graph |
| 137 | + .out_edges(node) |
| 138 | + .into_iter() |
| 139 | + .map(|edge_ref| edge_ref.id()) |
| 140 | + .collect() |
| 141 | + } |
| 142 | + |
| 143 | + /// Find the outgoing edge for a field at a query graph node. |
| 144 | + pub(super) fn edge_for_field(&self, node: NodeIndex, field: &Field) -> Option<EdgeIndex> { |
| 145 | + self.query_graph |
| 146 | + .edge_for_field(node, field, &self.override_conditions) |
| 147 | + } |
| 148 | + |
| 149 | + /// Find the outgoing downcast edge for an inline fragment at a query |
| 150 | + /// graph node. |
| 151 | + pub(super) fn edge_for_inline_fragment( |
| 152 | + &self, |
| 153 | + node: NodeIndex, |
| 154 | + fragment: &InlineFragment, |
| 155 | + ) -> Option<EdgeIndex> { |
| 156 | + self.query_graph.edge_for_inline_fragment(node, fragment) |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +/// Short human-readable label for a selection, for logging. |
| 161 | +pub(super) fn selection_label(selection: &Selection) -> String { |
| 162 | + match selection { |
| 163 | + Selection::Field(f) => f.field.field_position.to_string(), |
| 164 | + Selection::InlineFragment(f) => { |
| 165 | + format!("... on {:?}", f.inline_fragment.type_condition_position) |
| 166 | + } |
| 167 | + } |
| 168 | +} |
0 commit comments