use std::collections::{BTreeSet, HashMap, VecDeque};
use tracing::{instrument, trace};
use crate::query_planner::{
ast::{
merge_path::{MergePath, Segment},
selection_item::SelectionItem,
selection_set::{FieldSelection, SelectionSet},
},
planner::fetch::{selections::FetchStepSelections, state::MultiTypeFetchStep},
state::{
subgraph_state::{SubgraphDefinition, SubgraphState},
supergraph_state::{SubgraphName, SupergraphState, TypeNode},
},
};
#[derive(Debug)]
pub struct SelectionMismatchFinder<'a> {
supergraph_state: &'a SupergraphState,
}
type MismatchesFound = Vec<(String, MergePath)>;
impl<'a> SelectionMismatchFinder<'a> {
pub fn new(supergraph_state: &'a SupergraphState) -> Self {
Self { supergraph_state }
}
#[instrument(level = "trace", skip_all, fields(subgraph_name,))]
pub fn find_mismatches_in_node(
&self,
subgraph_name: &SubgraphName,
selections: &FetchStepSelections<MultiTypeFetchStep>,
) -> MismatchesFound {
let mut mismtaches_found = MismatchesFound::new();
let subgraph_state = self
.supergraph_state
.subgraphs_state
.get(subgraph_name)
.unwrap();
for (definition_name, selection_set) in selections.iter_selections() {
let entrypoint_type = subgraph_state.definitions.get(definition_name).unwrap();
let start_path = MergePath::default();
handle_selection_set(
definition_name,
self.supergraph_state,
subgraph_state,
entrypoint_type,
selection_set,
start_path,
&mut mismtaches_found,
);
trace!("found total of {} mismatches", mismtaches_found.len());
}
mismtaches_found
}
}
#[instrument(level = "trace", skip_all, fields(
parent_def = parent_def.name(),
selection = format!("{}", selection_set)
))]
fn handle_selection_set<'field, 'schema>(
root_def_type_name: &str,
supergraph_state: &'schema SupergraphState,
subgraph_state: &'schema SubgraphState,
parent_def: &'schema SubgraphDefinition,
selection_set: &'field SelectionSet,
parent_path: MergePath,
mismatches_found: &mut MismatchesFound,
) {
let mut encountered_field_to_type: HashMap<&'field str, &'schema TypeNode> = HashMap::new();
let mut traversal_queue =
VecDeque::from([(parent_def, parent_path, selection_set.items.iter())]);
while let Some((type_def, path, selections_group)) = traversal_queue.pop_front() {
for selection_item in selections_group {
match selection_item {
SelectionItem::Field(field) => {
if field.is_introspection_field() {
continue;
}
let next_path = path.push(Segment::Field(
field.into(),
field.arguments_hash(),
field.into(),
));
let next_parent_type_name = handle_field(
root_def_type_name,
supergraph_state,
type_def,
field,
&next_path,
&mut encountered_field_to_type,
mismatches_found,
);
if let Some(next_parent_def) =
next_parent_type_name.and_then(|n| subgraph_state.definitions.get(n))
{
handle_selection_set(
root_def_type_name,
supergraph_state,
subgraph_state,
next_parent_def,
&field.selections,
next_path,
mismatches_found,
);
}
}
SelectionItem::FragmentSpread(_) => {
unreachable!("fragment spread is not expected at this stage")
}
SelectionItem::InlineFragment(fragment) => {
let fragment_type = subgraph_state
.definitions
.get(&fragment.type_condition)
.unwrap();
let fragment_enter_path = path.push(Segment::TypeCondition(
BTreeSet::from([fragment.type_condition.clone()]),
fragment.into(),
));
traversal_queue.push_back((
fragment_type,
fragment_enter_path,
fragment.selections.items.iter(),
));
}
}
}
}
}
fn handle_field<'field, 'schema>(
root_def_type_name: &str,
state: &'schema SupergraphState,
parent_def: &'schema SubgraphDefinition,
field: &'field FieldSelection,
field_path: &MergePath,
encountered_field_to_type: &mut HashMap<&'field str, &'schema TypeNode>,
mismatches_found: &mut MismatchesFound,
) -> Option<&'schema str> {
let parent_def_fields = parent_def.fields().unwrap();
let field_name = field.name.as_str();
let field_identifier = field.selection_identifier();
let field_type = parent_def_fields
.iter()
.find_map(|f| {
if f.name == field_name {
Some(
f.join_field
.as_ref()
.and_then(|jf| jf.type_in_graph.as_ref())
.unwrap_or(&f.field_type),
)
} else {
None
}
})
.unwrap();
if let Some(maybe_conflicting_type) = encountered_field_to_type.get(field_identifier) {
if !maybe_conflicting_type.can_be_merged_with(field_type) {
let left_is_composite = state
.definitions
.get(maybe_conflicting_type.inner_type())
.is_some_and(|v| v.is_composite_type());
let right_is_composite = state
.definitions
.get(field_type.inner_type())
.is_some_and(|v| v.is_composite_type());
if !left_is_composite || !right_is_composite {
trace!(
"found a conflicting type for a selection field '{}', conflict is: '{}' <-> '{}', path: {}",
field_identifier,
maybe_conflicting_type,
field_type,
field_path,
);
mismatches_found.push((root_def_type_name.to_string(), field_path.clone()));
}
}
} else {
encountered_field_to_type.insert(field_identifier, field_type);
}
if field.is_leaf() {
None
} else {
Some(field_type.inner_type())
}
}