mod best_path;
pub(crate) mod error;
mod excluded;
pub(crate) mod path;
pub(crate) mod pathfinder;
pub(crate) mod utils;
use std::collections::{HashSet, VecDeque};
use crate::query_planner::{
ast::{
merge_path::Condition,
operation::OperationDefinition,
selection_item::SelectionItem,
selection_set::{FieldSelection, InlineFragmentSelection, SelectionSet},
},
graph::{
edge::{Edge, PlannerOverrideContext},
node::Node,
Graph,
},
planner::walker::pathfinder::{find_self_referencing_direct_path, NavigationTarget},
state::supergraph_state::{OperationKind, SupergraphState},
utils::cancellation::CancellationToken,
};
use best_path::{find_best_paths, BestPathTracker};
use error::WalkOperationError;
use excluded::ExcludedFromLookup;
use path::OperationPath;
use pathfinder::{find_direct_paths, find_indirect_paths};
use tracing::{instrument, span, trace, Level};
use utils::get_entrypoints;
pub struct ResolvedOperation<'graph> {
pub operation_kind: OperationKind,
pub root_field_groups: Vec<BestPathsPerLeaf<'graph>>,
}
pub type BestPathsPerLeaf<'graph> = Vec<Vec<OperationPath<'graph>>>;
type WorkItem<'graph, 'op> = (&'op SelectionItem, Vec<OperationPath<'graph>>);
type ResolutionStack<'graph, 'op> = Vec<WorkItem<'graph, 'op>>;
#[instrument(level = "trace", skip_all)]
pub fn walk_operation<'graph, 'op: 'graph>(
graph: &'graph Graph,
supergraph: &'graph SupergraphState,
override_context: &'graph PlannerOverrideContext,
operation: &'op OperationDefinition,
cancellation_token: &'graph CancellationToken,
) -> Result<ResolvedOperation<'graph>, WalkOperationError> {
let operation_kind = operation
.operation_kind
.clone()
.unwrap_or(OperationKind::Query);
let (op_type, selection_set) = operation.parts();
trace!("operation is of type {:?}", op_type);
let root_entrypoints = get_entrypoints(graph, op_type)?;
let initial_paths: Vec<OperationPath<'graph>> = root_entrypoints
.iter()
.map(|edge| OperationPath::new_entrypoint(edge))
.collect();
let mut paths_grouped_by_root_field: Vec<BestPathsPerLeaf> =
Vec::with_capacity(operation.selection_set.items.len());
for selection_item in selection_set.items.iter() {
let mut stack_to_resolve: VecDeque<WorkItem> = VecDeque::new();
stack_to_resolve.push_back((selection_item, initial_paths.to_vec()));
let mut paths_per_leaf: Vec<Vec<OperationPath<'graph>>> = vec![];
while let Some((selection_item, paths)) = stack_to_resolve.pop_front() {
cancellation_token.bail_if_cancelled()?;
let (next_stack_to_resolve, new_paths_per_leaf) = process_selection(
graph,
supergraph,
override_context,
selection_item,
&paths,
&[],
cancellation_token,
)?;
paths_per_leaf.extend(new_paths_per_leaf);
for item in next_stack_to_resolve.into_iter().rev() {
stack_to_resolve.push_front(item);
}
}
paths_grouped_by_root_field.push(paths_per_leaf);
}
Ok(ResolvedOperation {
operation_kind,
root_field_groups: paths_grouped_by_root_field,
})
}
fn process_selection<'graph, 'op: 'graph>(
graph: &'graph Graph,
supergraph: &'graph SupergraphState,
override_context: &'graph PlannerOverrideContext,
selection_item: &'op SelectionItem,
paths: &[OperationPath<'graph>],
fields_to_resolve_locally: &[String],
cancellation_token: &'graph CancellationToken,
) -> Result<
(
ResolutionStack<'graph, 'op>,
Vec<Vec<OperationPath<'graph>>>,
),
WalkOperationError,
> {
let mut stack_to_resolve: ResolutionStack = vec![];
let mut paths_per_leaf: Vec<Vec<OperationPath<'graph>>> = vec![];
match selection_item {
SelectionItem::InlineFragment(fragment) => {
let (next_selection_items, new_paths_per_leaf) = process_inline_fragment(
graph,
supergraph,
override_context,
fragment,
paths,
fields_to_resolve_locally,
cancellation_token,
)?;
paths_per_leaf.extend(new_paths_per_leaf);
stack_to_resolve.extend(next_selection_items);
}
SelectionItem::Field(field) => {
let (next_selection_items, new_paths_per_leaf) = process_field(
graph,
supergraph,
override_context,
field,
paths,
fields_to_resolve_locally,
cancellation_token,
)?;
paths_per_leaf.extend(new_paths_per_leaf);
stack_to_resolve.extend(next_selection_items);
}
SelectionItem::FragmentSpread(_) => {
}
}
Ok((stack_to_resolve, paths_per_leaf))
}
#[instrument(level = "trace", skip_all)]
fn process_selection_set<'graph, 'op: 'graph>(
graph: &'graph Graph,
supergraph: &'graph SupergraphState,
override_context: &'graph PlannerOverrideContext,
selection_set: &'op SelectionSet,
paths: &[OperationPath<'graph>],
fields_to_resolve_locally: &[String],
cancellation_token: &'graph CancellationToken,
) -> Result<
(
ResolutionStack<'graph, 'op>,
Vec<Vec<OperationPath<'graph>>>,
),
WalkOperationError,
> {
let mut stack_to_resolve: ResolutionStack = vec![];
let mut paths_per_leaf: Vec<Vec<OperationPath<'graph>>> = vec![];
for item in selection_set.items.iter() {
let (next_stack_to_resolve, new_paths_per_leaf) = process_selection(
graph,
supergraph,
override_context,
item,
paths,
fields_to_resolve_locally,
cancellation_token,
)?;
paths_per_leaf.extend(new_paths_per_leaf);
stack_to_resolve.extend(next_stack_to_resolve);
}
Ok((stack_to_resolve, paths_per_leaf))
}
#[instrument(level = "trace", skip_all, fields(
type_condition = fragment.type_condition,
))]
fn process_inline_fragment<'graph, 'op: 'graph>(
graph: &'graph Graph,
supergraph: &'graph SupergraphState,
override_context: &'graph PlannerOverrideContext,
fragment: &'op InlineFragmentSelection,
paths: &[OperationPath<'graph>],
fields_to_resolve_locally: &[String],
cancellation_token: &'graph CancellationToken,
) -> Result<
(
ResolutionStack<'graph, 'op>,
Vec<Vec<OperationPath<'graph>>>,
),
WalkOperationError,
> {
trace!(
"Processing inline fragment '{}' on type '{}' (skip: {:?}, include: {:?}) through {} possible paths",
fragment.selections,
fragment.type_condition,
fragment.include_if,
fragment.skip_if,
paths.len()
);
cancellation_token.bail_if_cancelled()?;
let tail_index = graph.get_edge_tail(
&paths
.first()
.unwrap()
.last_segment
.as_ref()
.unwrap()
.edge_index,
)?;
let tail = graph.node(tail_index)?;
let tail_type_name = match tail {
Node::SubgraphType(t) => &t.name,
_ => panic!("Expected a subgraph type when resolving fragments"),
};
if tail_type_name == &fragment.type_condition {
if fragment.include_if.is_none() && fragment.skip_if.is_none() {
return process_selection_set(
graph,
supergraph,
override_context,
&fragment.selections,
paths,
fields_to_resolve_locally,
cancellation_token,
);
}
let condition: Option<Condition> = fragment.into();
let mut next_paths: Vec<OperationPath<'graph>> = Vec::with_capacity(paths.len());
for path in paths {
let path_span = span!(
Level::TRACE,
"explore_path",
path = path.pretty_print(graph)
);
let _enter = path_span.enter();
let direct_path = find_self_referencing_direct_path(
graph,
supergraph,
override_context,
path,
&fragment.type_condition,
condition.as_ref().expect("Condition should be present"),
cancellation_token,
)?;
trace!("advanced: {}", path.pretty_print(graph));
next_paths.push(direct_path);
}
return process_selection_set(
graph,
supergraph,
override_context,
&fragment.selections,
&next_paths,
fields_to_resolve_locally,
cancellation_token,
);
}
trace!(
"Trying to advance to: ... on {}, through {} possible paths",
fragment.type_condition,
paths.len()
);
let mut next_paths: Vec<OperationPath<'graph>> = Vec::with_capacity(paths.len());
for path in paths {
let path_span = span!(
Level::TRACE,
"explore_path",
path = path.pretty_print(graph)
);
let _enter = path_span.enter();
let mut direct_paths = find_direct_paths(
graph,
supergraph,
override_context,
path,
&NavigationTarget::ConcreteType(&fragment.type_condition, fragment.into()),
cancellation_token,
)?;
trace!("Direct paths found: {}", direct_paths.len());
if !direct_paths.is_empty() {
trace!("advanced: {}", path.pretty_print(graph));
next_paths.push(direct_paths.remove(0));
}
if fields_to_resolve_locally.is_empty() {
let mut indirect_paths = find_indirect_paths(
graph,
supergraph,
override_context,
path,
&NavigationTarget::ConcreteType(&fragment.type_condition, fragment.into()),
&ExcludedFromLookup::new(),
cancellation_token,
)?;
if !indirect_paths.is_empty() {
trace!("advanced: {}", path.pretty_print(graph));
next_paths.push(indirect_paths.remove(0));
}
if indirect_paths.is_empty() && direct_paths.is_empty() {
trace!(
"Object type '{}' is not resolvable by '{}', resolve only the __typename",
fragment.type_condition,
tail_type_name
);
}
}
}
if next_paths.is_empty() {
let mut tracker = BestPathTracker::new(graph);
for path in paths {
let path_span = span!(
Level::TRACE,
"explore_path",
path = path.pretty_print(graph)
);
let _enter = path_span.enter();
let direct_paths = find_direct_paths(
graph,
supergraph,
override_context,
path,
&NavigationTarget::Field {
field: &FieldSelection::new_typename(),
target_subgraph_ids: None,
},
cancellation_token,
)?;
trace!("Direct paths found: {}", direct_paths.len());
if !direct_paths.is_empty() {
for p in direct_paths {
tracker.add(&p)?;
}
} else {
return Err(WalkOperationError::NoPathsFound("__typename".to_string()));
}
}
let next_paths = tracker.get_best_paths();
if next_paths.is_empty() {
return Err(WalkOperationError::NoPathsFound("__typename".to_string()));
}
return Ok((vec![], vec![find_best_paths(next_paths)]));
}
process_selection_set(
graph,
supergraph,
override_context,
&fragment.selections,
&next_paths,
fields_to_resolve_locally,
cancellation_token,
)
}
fn narrow_partial_union_paths<'graph>(paths: &mut Vec<OperationPath<'graph>>) {
if paths.len() <= 1 {
return;
}
let Some(union_context) = paths.first().and_then(|path| path.union_context.as_ref()) else {
return;
};
let all_same_field = paths.iter().all(|path| {
path.union_context
.as_ref()
.is_some_and(|ctx| ctx.eq_field(union_context))
});
if !all_same_field {
return;
}
type GraphId<'graph> = &'graph str;
type MembersPerGraph<'graph> = Vec<(GraphId<'graph>, Vec<&'graph str>)>;
let mut members_per_graph: MembersPerGraph<'graph> = Vec::new();
for path in paths.iter() {
let context = path
.union_context
.as_ref()
.expect("union member context should exist at this point");
if !members_per_graph
.iter()
.any(|(graph_id, _)| graph_id == &context.graph_id)
{
members_per_graph.push((context.graph_id, context.possible_members.clone()));
}
}
if members_per_graph.len() <= 1 {
return;
}
let (_, least_members_set) = members_per_graph
.iter()
.min_by_key(|(_, members)| members.len())
.expect("members_per_graph has at least two entries");
let mut shared_members: Vec<&'graph str> = least_members_set.clone();
for (_, members) in &members_per_graph {
shared_members.retain(|member| members.contains(member));
if shared_members.is_empty() {
paths.clear();
return;
}
}
paths.retain_mut(|path| {
let context = path
.union_context
.as_mut()
.expect("union context should exist at this point");
if !shared_members.contains(&context.member_name) {
return false;
}
context.possible_members = shared_members.clone();
true
});
}
#[instrument(level = "trace", skip_all, fields(
field_name = &field.name,
leaf = field.is_leaf()
))]
fn process_field<'graph, 'op: 'graph>(
graph: &'graph Graph,
supergraph: &'graph SupergraphState,
override_context: &'graph PlannerOverrideContext,
field: &'op FieldSelection,
paths: &[OperationPath<'graph>],
fields_to_resolve_locally: &[String],
cancellation_token: &'graph CancellationToken,
) -> Result<
(
ResolutionStack<'graph, 'op>,
Vec<Vec<OperationPath<'graph>>>,
),
WalkOperationError,
> {
let mut next_stack_to_resolve: ResolutionStack = vec![];
let mut paths_per_leaf: Vec<Vec<OperationPath<'graph>>> = vec![];
let mut tracker = BestPathTracker::new(graph);
trace!(
"Trying to advance to: {} through {} possible paths",
field,
paths.len()
);
cancellation_token.bail_if_cancelled()?;
let target_subgraph_ids =
if !paths.is_empty() && !fields_to_resolve_locally.contains(&field.name) {
field_target_subgraph_ids(supergraph, field, paths, graph)?
} else {
None
};
for path in paths {
let path_span = span!(
Level::TRACE,
"explore_path",
path = path.pretty_print(graph)
);
let _enter = path_span.enter();
let mut advanced = false;
let excluded = ExcludedFromLookup::new();
let direct_paths = find_direct_paths(
graph,
supergraph,
override_context,
path,
&NavigationTarget::Field {
field,
target_subgraph_ids: None,
},
cancellation_token,
)?;
trace!("Direct paths found: {}", direct_paths.len());
let found_direct_paths_to_leaf = !direct_paths.is_empty() && field.is_leaf();
if !direct_paths.is_empty() {
advanced = true;
for direct_path in &direct_paths {
tracker.add(direct_path)?;
}
}
if !fields_to_resolve_locally.contains(&field.name) && !found_direct_paths_to_leaf {
let indirect_paths = find_indirect_paths(
graph,
supergraph,
override_context,
path,
&NavigationTarget::Field {
field,
target_subgraph_ids: target_subgraph_ids.as_ref(),
},
&excluded,
cancellation_token,
)?;
trace!("Indirect paths found: {}", indirect_paths.len());
if !indirect_paths.is_empty() {
advanced = true;
for indirect_path in indirect_paths {
tracker.add(&indirect_path)?;
}
}
}
trace!(
"{}: {}",
if advanced {
"advanced"
} else {
"failed to advance"
},
path.pretty_print(graph)
);
}
let mut next_paths = tracker.get_best_paths();
narrow_partial_union_paths(&mut next_paths);
if next_paths.is_empty() {
return Err(WalkOperationError::NoPathsFound(field.name.to_string()));
}
let mut fields_to_resolve_locally: Vec<String> = Vec::new();
if !field.is_leaf() {
let field_move_paths: Vec<_> = next_paths
.iter()
.filter(|path| {
path.last_segment.as_ref().is_some_and(|seg| {
matches!(graph.edge(seg.edge_index).unwrap(), Edge::FieldMove(_))
})
})
.collect();
if !field_move_paths.is_empty() {
let edge_index = field_move_paths[0]
.last_segment
.as_ref()
.unwrap()
.edge_index;
let head_index = graph.get_edge_head(&edge_index)?;
let tail_index = graph.get_edge_tail(&edge_index)?;
let head = graph.node(head_index)?;
let tail = graph.node(tail_index)?;
let parent_type_name = head.name_str();
let parent_def = supergraph
.definitions
.get(parent_type_name)
.ok_or_else(|| WalkOperationError::TypeNotFound(parent_type_name.to_string()))?;
let field_def = parent_def.fields().get(&field.name).ok_or_else(|| {
WalkOperationError::FieldNotFound(
field.name.to_string(),
parent_type_name.to_string(),
)
})?;
let output_type = supergraph
.definitions
.get(tail.name_str())
.ok_or_else(|| WalkOperationError::TypeNotFound(tail.name_str().to_string()))?;
if output_type.is_interface_type()
&& field_def.resolvable_in_graphs(parent_def).len() > 1
&& field
.selections
.items
.iter()
.filter(|item| matches!(item, SelectionItem::InlineFragment(_)))
.count()
> 1
{
fields_to_resolve_locally = output_type
.fields()
.keys()
.map(|name| name.to_string())
.collect();
}
}
}
if !fields_to_resolve_locally.is_empty() {
let path_span = span!(
Level::TRACE,
"Shareable interface detected. Validating that sub-selections can be resolved from a single path."
);
let _enter = path_span.enter();
let mut valid_paths_for_children: Vec<OperationPath<'graph>> =
Vec::with_capacity(next_paths.len());
for candidate_path in &next_paths {
let mut all_children_resolvable = true;
for child_selection in &field.selections.items {
let finding = process_selection(
graph,
supergraph,
override_context,
child_selection,
std::slice::from_ref(candidate_path),
&fields_to_resolve_locally,
cancellation_token,
);
match finding {
Ok((child_stack, child_leaves)) => {
if child_leaves.is_empty() && child_stack.is_empty() {
all_children_resolvable = false;
trace!(
"Path {} failed to resolve child '{}' locally.",
candidate_path.pretty_print(graph),
child_selection
);
break; }
}
Err(_) => {
all_children_resolvable = false;
break; }
}
}
if all_children_resolvable {
trace!(
"Path {} can resolve all children locally and is valid.",
candidate_path.pretty_print(graph)
);
valid_paths_for_children.push(candidate_path.clone());
}
}
next_paths = valid_paths_for_children;
if !field.is_leaf() && next_paths.is_empty() {
return Err(WalkOperationError::NoPathsFound(field.name.to_string()));
}
}
if field.is_leaf() {
paths_per_leaf.push(find_best_paths(next_paths));
} else {
trace!("Found {} paths", next_paths.len());
for next_selection_items in &field.selections.items {
next_stack_to_resolve.push((next_selection_items, next_paths.clone()));
}
}
Ok((next_stack_to_resolve, paths_per_leaf))
}
fn field_target_subgraph_ids<'graph>(
supergraph: &SupergraphState,
field: &FieldSelection,
paths: &[OperationPath<'graph>],
graph: &'graph Graph,
) -> Result<Option<HashSet<String>>, WalkOperationError> {
let mut parent_type_names = HashSet::new();
for path in paths {
let parent_node = graph.node(path.tail())?;
parent_type_names.insert(parent_node.name_str());
}
let mut target_subgraph_ids = HashSet::new();
for parent_type_name in parent_type_names {
let Some(parent_def) = supergraph.definitions.get(parent_type_name) else {
continue;
};
let Some(field_def) = parent_def.fields().get(&field.name) else {
continue;
};
for graph_id in field_def.resolvable_in_graphs(parent_def) {
if let Ok(subgraph_id) = supergraph.resolve_graph_id(&graph_id) {
target_subgraph_ids.insert(subgraph_id.0.to_string());
}
}
}
if target_subgraph_ids.is_empty() {
Ok(None)
} else {
Ok(Some(target_subgraph_ids))
}
}