use std::borrow::Cow;
use std::sync::Arc;
use apollo_compiler::collections::IndexMap;
use apollo_compiler::collections::IndexSet;
use petgraph::graph::EdgeIndex;
use petgraph::graph::NodeIndex;
use tracing::debug;
use tracing::debug_span;
use crate::composition::satisfiability::validation_context::ValidationContext;
use crate::composition::satisfiability::validation_state::SubgraphContextKey;
use crate::composition::satisfiability::validation_state::ValidationState;
use crate::error::CompositionError;
use crate::error::FederationError;
use crate::merger::merge::CompositionOptions;
use crate::operation::SelectionSet;
use crate::query_graph::OverrideConditions;
use crate::query_graph::QueryGraph;
use crate::query_graph::QueryGraphEdgeTransition;
use crate::query_graph::condition_resolver::CachingConditionResolver;
use crate::query_graph::condition_resolver::ConditionResolution;
use crate::query_graph::condition_resolver::ConditionResolverCache;
use crate::query_graph::graph_path::ExcludedConditions;
use crate::query_graph::graph_path::ExcludedDestinations;
use crate::query_graph::graph_path::operation::OpGraphPathContext;
use crate::schema::ValidFederationSchema;
use crate::schema::position::FieldDefinitionPosition;
use crate::supergraph::CompositionHint;
pub(super) struct ValidationTraversal {
top_level_condition_resolver: TopLevelConditionResolver,
stack: Vec<ValidationState>,
previous_visits: IndexMap<NodeIndex, Vec<NodeVisit>>,
validation_errors: Vec<CompositionError>,
validation_hints: Vec<CompositionHint>,
satisfiability_errors_by_mutation_field_and_subgraph:
IndexMap<FieldDefinitionPosition, IndexMap<Arc<str>, Vec<CompositionError>>>,
context: ValidationContext,
total_validation_subgraph_paths: usize,
max_validation_subgraph_paths: usize,
}
struct TopLevelConditionResolver {
query_graph: Arc<QueryGraph>,
condition_resolver_cache: ConditionResolverCache,
}
pub(super) struct NodeVisit {
pub(super) subgraph_context_keys: IndexSet<SubgraphContextKey>,
pub(super) override_conditions: Arc<OverrideConditions>,
}
impl NodeVisit {
pub(super) fn is_superset_or_equal(&self, other: &NodeVisit) -> bool {
self.subgraph_context_keys
.is_superset(&other.subgraph_context_keys)
&& other
.override_conditions
.iter()
.all(|(label, is_enabled)| self.override_conditions.get(label) == Some(is_enabled))
}
}
impl ValidationTraversal {
const DEFAULT_MAX_VALIDATION_SUBGRAPH_PATHS: usize = 1_000_000;
pub(super) fn new(
supergraph_schema: ValidFederationSchema,
api_schema_query_graph: Arc<QueryGraph>,
federated_query_graph: Arc<QueryGraph>,
composition_options: &CompositionOptions,
) -> Result<Self, FederationError> {
let mut validation_traversal = Self {
top_level_condition_resolver: TopLevelConditionResolver {
query_graph: federated_query_graph.clone(),
condition_resolver_cache: ConditionResolverCache::new(),
},
stack: vec![],
previous_visits: Default::default(),
validation_errors: vec![],
validation_hints: vec![],
satisfiability_errors_by_mutation_field_and_subgraph: Default::default(),
context: ValidationContext::new(supergraph_schema)?,
total_validation_subgraph_paths: 0,
max_validation_subgraph_paths: composition_options
.max_validation_subgraph_paths
.unwrap_or(Self::DEFAULT_MAX_VALIDATION_SUBGRAPH_PATHS),
};
for kind in api_schema_query_graph.root_kinds_to_nodes()?.keys() {
validation_traversal.push_stack(ValidationState::new(
api_schema_query_graph.clone(),
federated_query_graph.clone(),
*kind,
)?);
}
Ok(validation_traversal)
}
fn push_stack(&mut self, state: ValidationState) -> Option<CompositionError> {
self.total_validation_subgraph_paths += state.subgraph_path_infos().len();
self.stack.push(state);
if self.total_validation_subgraph_paths > self.max_validation_subgraph_paths {
Some(CompositionError::MaxValidationSubgraphPathsExceeded {
message: format!(
"Maximum number of validation subgraph paths exceeded: {}",
self.total_validation_subgraph_paths
),
})
} else {
None
}
}
fn pop_stack(&mut self) -> Option<ValidationState> {
if let Some(state) = self.stack.pop() {
self.total_validation_subgraph_paths -= state.subgraph_path_infos().len();
Some(state)
} else {
None
}
}
pub(super) fn validate(
&mut self,
errors: &mut Vec<CompositionError>,
hints: &mut Vec<CompositionHint>,
) -> Result<(), FederationError> {
while let Some(state) = self.pop_stack() {
if let Some(error) = self.handle_state(state)? {
errors.push(error);
hints.append(&mut self.validation_hints);
return Ok(());
}
}
for (field_coordinate, errors_by_subgraph) in
&self.satisfiability_errors_by_mutation_field_and_subgraph
{
let some_subgraph_has_no_errors = errors_by_subgraph.values().any(|e| e.is_empty());
if some_subgraph_has_no_errors {
continue;
}
let mut message_parts = vec![format!(
"Supergraph API queries using the mutation field \"{}\" at top-level must be \
satisfiable without needing to call that field from multiple subgraphs, but \
every subgraph with that field encounters satisfiability errors. Please fix \
these satisfiability errors for (at least) one of the following subgraphs with \
the mutation field:",
field_coordinate
)];
for (subgraph, subgraph_errors) in errors_by_subgraph {
message_parts.push(format!(
"- When calling \"{}\" at top-level from subgraph \"{}\":",
field_coordinate, subgraph
));
for error in subgraph_errors {
for line in error.to_string().lines() {
if line.is_empty() {
message_parts.push(String::new());
} else {
message_parts.push(format!(" {line}"));
}
}
}
}
self.validation_errors
.push(CompositionError::SatisfiabilityError {
message: message_parts.join("\n"),
});
}
errors.append(&mut self.validation_errors);
hints.append(&mut self.validation_hints);
Ok(())
}
fn handle_state(
&mut self,
mut state: ValidationState,
) -> Result<Option<CompositionError>, FederationError> {
debug!(
"Validation: {} open states. Validating {}",
self.stack.len() + 1,
state,
);
let span = debug_span!(" |");
let guard = span.enter();
if state.can_skip_visit(&mut self.previous_visits)? {
drop(guard);
debug!("Can skip visit for this state.");
return Ok(None);
}
let edges = state.supergraph_path().next_edges()?.collect::<Vec<_>>();
for edge in edges {
let edge_weight = state.supergraph_path().graph().edge_weight(edge)?;
let mut edge_head_type_name = None;
if let QueryGraphEdgeTransition::FieldCollection {
field_definition_position,
..
} = &edge_weight.transition
{
if field_definition_position.is_introspection_typename_field() {
continue;
} else {
edge_head_type_name = Some(field_definition_position.type_name());
}
}
if let Some(override_condition) = &edge_weight.override_condition
&& state
.selected_override_conditions()
.contains_key(&override_condition.label)
&& !override_condition.check(state.selected_override_conditions())
{
debug!(
"Edge {} doesn't satisfy label condition: {}({}), no need to validate further",
edge_weight,
override_condition.label,
state
.selected_override_conditions()
.get(&override_condition.label)
.map_or("unset".to_owned(), |x| x.to_string()),
);
continue;
}
let matching_contexts = edge_head_type_name
.and_then(|name| self.context.matching_contexts(name))
.map(Cow::Borrowed)
.unwrap_or_else(|| Cow::Owned(IndexSet::default()));
debug!("Validating supergraph edge {}", edge_weight);
let span = debug_span!(" |");
let guard = span.enter();
let num_errors = self.validation_errors.len();
let new_state = state.validate_transition(
&self.context,
edge,
matching_contexts.as_ref(),
&mut self.top_level_condition_resolver,
&mut self.validation_errors,
&mut self.validation_hints,
&mut self.satisfiability_errors_by_mutation_field_and_subgraph,
)?;
if num_errors != self.validation_errors.len() {
drop(guard);
debug!("Validation error!");
continue;
}
if let Some(new_state) = new_state
&& !new_state
.supergraph_path()
.graph()
.is_terminal(new_state.supergraph_path().tail())
{
drop(guard);
debug!("Reached new state {}", new_state);
if let Some(error) = self.push_stack(new_state) {
return Ok(Some(error));
}
continue;
}
drop(guard);
debug!("Reached terminal node/cycle")
}
Ok(None)
}
}
impl CachingConditionResolver for TopLevelConditionResolver {
fn query_graph(&self) -> &QueryGraph {
&self.query_graph
}
fn resolver_cache(&mut self) -> &mut ConditionResolverCache {
&mut self.condition_resolver_cache
}
fn resolve_without_cache(
&self,
edge: EdgeIndex,
context: &OpGraphPathContext,
excluded_destinations: &ExcludedDestinations,
excluded_conditions: &ExcludedConditions,
extra_conditions: Option<&SelectionSet>,
) -> Result<ConditionResolution, FederationError> {
crate::composition::satisfiability::conditions_validation::resolve_condition_plan(
self.query_graph.clone(),
edge,
context,
excluded_destinations,
excluded_conditions,
extra_conditions,
)
}
}