use std::fmt;
use apollo_compiler::Node;
use apollo_compiler::ast;
use apollo_compiler::executable;
use apollo_compiler::executable::FieldSet;
use apollo_compiler::executable::Name;
use itertools::Itertools;
use super::query_plan_analysis::AnalysisContext;
use super::response_shape::Clause;
use super::response_shape::PossibleDefinitions;
use super::response_shape::ResponseShape;
use super::response_shape::compute_response_shape_for_selection_set;
use super::response_shape_compare::compare_representative_field;
use super::response_shape_compare::compare_response_shapes_with_constraint;
use super::subgraph_constraint::SubgraphConstraint;
use crate::FederationError;
use crate::bail;
use crate::internal_error;
use crate::link::federation_spec_definition::FederationSpecDefinition;
use crate::link::federation_spec_definition::KeyDirectiveArguments;
use crate::link::federation_spec_definition::get_federation_spec_definition_from_subgraph;
use crate::query_plan::requires_selection;
use crate::schema::ValidFederationSchema;
use crate::schema::position::CompositeTypeDefinitionPosition;
use crate::schema::position::INTROSPECTION_TYPENAME_FIELD_NAME;
use crate::schema::position::TypeDefinitionPosition;
use crate::utils::FallibleIterator;
#[derive(Debug, derive_more::From)]
pub(crate) enum AnalysisErrorMessage {
FederationError(FederationError),
QueryPlanError(String),
}
pub(crate) struct AnalysisError {
message: AnalysisErrorMessage,
context: Vec<String>,
}
impl AnalysisError {
pub(crate) fn with_context(mut self, context: impl Into<String>) -> Self {
self.context.push(context.into());
self
}
}
impl fmt::Display for AnalysisErrorMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AnalysisErrorMessage::FederationError(err) => write!(f, "{err}"),
AnalysisErrorMessage::QueryPlanError(err) => write!(f, "{err}"),
}
}
}
impl fmt::Display for AnalysisError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", self.message)?;
let num_contexts = self.context.len();
for (i, ctx) in self.context.iter().enumerate() {
writeln!(f, "[{index}/{num_contexts}] {ctx}", index = i + 1)?;
}
Ok(())
}
}
impl From<FederationError> for AnalysisError {
fn from(err: FederationError) -> Self {
AnalysisError {
message: AnalysisErrorMessage::FederationError(err),
context: vec![],
}
}
}
impl From<String> for AnalysisError {
fn from(err: String) -> Self {
AnalysisError {
message: AnalysisErrorMessage::QueryPlanError(err),
context: vec![],
}
}
}
fn compute_response_shape_for_field_set(
schema: &ValidFederationSchema,
parent_type: Name,
field_set: &str,
) -> Result<ResponseShape, FederationError> {
let field_set =
FieldSet::parse_and_validate(schema.schema(), parent_type, field_set, "field_set.graphql")?;
compute_response_shape_for_selection_set(schema, &field_set.selection_set)
}
fn compute_response_shape_for_field_set_with_typename(
schema: &ValidFederationSchema,
parent_type: Name,
field_set: &str,
) -> Result<ResponseShape, FederationError> {
let field_set =
FieldSet::parse_and_validate(schema.schema(), parent_type, field_set, "field_set.graphql")?;
let mut selection_set = field_set.into_inner().selection_set;
let typename = selection_set
.new_field(schema.schema(), INTROSPECTION_TYPENAME_FIELD_NAME.clone())
.map_err(|_| {
internal_error!(
"Unexpected error: {} not found in schema",
INTROSPECTION_TYPENAME_FIELD_NAME
)
})?;
selection_set.push(typename);
compute_response_shape_for_selection_set(schema, &selection_set)
}
fn compute_response_shape_for_require_selection(
schema: &ValidFederationSchema,
require: &requires_selection::Selection,
) -> Result<ResponseShape, FederationError> {
let requires_selection::Selection::InlineFragment(inline) = require else {
bail!("Expected require selection to be an inline fragment, but got: {require:#?}")
};
let Some(type_condition) = &inline.type_condition else {
bail!("Expected a type condition on require inline fragment")
};
let selections = convert_requires_selections(schema, type_condition, &inline.selections)?;
let mut selection_set = executable::SelectionSet::new(type_condition.clone());
selection_set.extend(selections);
compute_response_shape_for_selection_set(schema, &selection_set)
}
fn convert_requires_selections(
schema: &ValidFederationSchema,
ty: &Name,
selections: &[requires_selection::Selection],
) -> Result<Vec<executable::Selection>, FederationError> {
let mut result = Vec::new();
for selection in selections {
match selection {
requires_selection::Selection::Field(field) => {
let field_def = get_field_definition(schema, ty, &field.name)?;
let converted = executable::Field::new(field.name.clone(), field_def);
let converted = if field.selections.is_empty() {
converted
} else {
let sub_ty = converted.ty().inner_named_type().clone();
let sub_selections =
convert_requires_selections(schema, &sub_ty, &field.selections)?;
converted.with_selections(sub_selections)
};
result.push(converted.into());
}
requires_selection::Selection::InlineFragment(inline) => {
let converted = if let Some(type_condition) = &inline.type_condition {
executable::InlineFragment::with_type_condition(type_condition.clone())
} else {
executable::InlineFragment::without_type_condition(ty.clone())
};
let sub_ty = converted.selection_set.ty.clone();
let sub_selections =
convert_requires_selections(schema, &sub_ty, &inline.selections)?;
result.push(converted.with_selections(sub_selections).into());
}
}
}
Ok(result)
}
fn get_field_definition(
schema: &ValidFederationSchema,
parent_type: &Name,
field_name: &Name,
) -> Result<Node<ast::FieldDefinition>, FederationError> {
let parent_type_pos: CompositeTypeDefinitionPosition =
schema.get_type(parent_type.clone())?.try_into()?;
let field_def_pos = parent_type_pos.field(field_name.clone())?;
field_def_pos
.get(schema.schema())
.map(|component| component.node.clone())
.map_err(|err| err.into())
}
fn collect_require_condition(
supergraph_schema: &ValidFederationSchema,
subgraph_schema: &ValidFederationSchema,
federation_spec_definition: &FederationSpecDefinition,
response_shape: &ResponseShape,
) -> Result<ResponseShape, FederationError> {
let requires_directive_definition =
federation_spec_definition.requires_directive_definition(subgraph_schema)?;
let parent_type = response_shape.default_type_condition();
let mut result = ResponseShape::new(parent_type.clone());
let all_variants = response_shape
.iter()
.flat_map(|(_key, defs)| defs.iter())
.flat_map(|(_, per_type_cond)| per_type_cond.conditional_variants());
for variant in all_variants {
let field_def = &variant.representative_field().definition;
for directive in field_def
.directives
.get_all(&requires_directive_definition.name)
{
let requires_application =
federation_spec_definition.requires_directive_arguments(directive)?;
let rs = compute_response_shape_for_field_set(
supergraph_schema,
parent_type.clone(),
requires_application.fields,
)?;
result.merge_with(&rs.add_boolean_conditions(variant.boolean_clause()))?;
}
}
Ok(result)
}
fn key_directive_matches(
context: &AnalysisContext,
state: &ResponseShape,
boolean_clause: &Clause,
entity_require_shape: &ResponseShape,
require_condition: &ResponseShape,
key_directive_application: &KeyDirectiveArguments<'_>,
) -> Result<(), AnalysisError> {
let key_type_condition = entity_require_shape.default_type_condition();
let key_condition = compute_response_shape_for_field_set_with_typename(
context.supergraph_schema(),
key_type_condition.clone(),
key_directive_application.fields,
)?;
let mut condition = key_condition.clone();
condition.merge_with(require_condition)?;
if !key_only_compare_response_shapes(entity_require_shape, &condition) {
return Err(format!(
"The `requires` item does not match the subgraph schema\n\
* @key field set: {key_condition}\n\
* @requires field set: {require_condition}"
)
.into());
}
let final_require_shape = condition.add_boolean_conditions(boolean_clause);
let path_constraint = SubgraphConstraint::at_root(context.subgraphs_by_name());
let assumption = Clause::default(); compare_response_shapes_with_constraint(
&path_constraint,
&assumption,
&final_require_shape,
state,
)
.map_err(|e| {
format!(
"The state does not satisfy the subgraph schema requirements:\n\
* schema requires: ({condition_type}) {condition}\n\
* Comparison error:\n{e}\n\
* state: ({state_type}) {state}",
condition_type = condition.default_type_condition(),
state_type = state.default_type_condition(),
)
.into()
})
}
fn check_require(
context: &AnalysisContext,
subgraph_schema: &ValidFederationSchema,
state: &ResponseShape,
boolean_clause: &Clause,
entity_response_shape: &ResponseShape,
entity_require: &requires_selection::Selection,
) -> Result<(), AnalysisError> {
let subgraph_entity_type_name = entity_response_shape.default_type_condition();
let subgraph_entity_type_pos = subgraph_schema.get_type(subgraph_entity_type_name.clone())?;
let directives = match &subgraph_entity_type_pos {
TypeDefinitionPosition::Object(type_pos) => {
let type_def = type_pos
.get(subgraph_schema.schema())
.map_err(FederationError::from)?;
&type_def.directives
}
TypeDefinitionPosition::Interface(type_pos) => {
let type_def = type_pos
.get(subgraph_schema.schema())
.map_err(FederationError::from)?;
&type_def.directives
}
_ => bail!("check_require: unexpected kind of entity type: {subgraph_entity_type_name}"),
};
let entity_require_shape =
compute_response_shape_for_require_selection(context.supergraph_schema(), entity_require)
.map_err(|err| {
format!(
"check_require: failed to compute response shape:\n{err}\n\
require selection: {entity_require}"
)
})?;
let federation_spec_definition = get_federation_spec_definition_from_subgraph(subgraph_schema)?;
let require_condition = collect_require_condition(
context.supergraph_schema(),
subgraph_schema,
federation_spec_definition,
entity_response_shape,
)
.map_err(|err| {
format!(
"check_require: failed to collect require conditions from the subgraph schema:\n\
{err}\n\
entity_response_shape: {entity_response_shape}"
)
})?;
let key_directive_definition =
federation_spec_definition.key_directive_definition(subgraph_schema)?;
let mut mismatch_cases = Vec::new();
let mut unresolvable_cases = Vec::new();
let found = directives
.get_all(&key_directive_definition.name)
.map(|directive| federation_spec_definition.key_directive_arguments(directive))
.ok_and_any(|ref key_directive_application| {
match key_directive_matches(
context,
state,
boolean_clause,
&entity_require_shape,
&require_condition,
key_directive_application,
) {
Ok(_) => {
if key_directive_application.resolvable {
true
} else {
unresolvable_cases.push(format!(
"The matched @key directive is not resolvable.\n\
* @key field set: {key_field_set}",
key_field_set = key_directive_application.fields
));
false
}
}
Err(e) => {
mismatch_cases.push(e);
false
}
}
})?;
if found {
Ok(())
} else {
if unresolvable_cases.is_empty() {
if mismatch_cases.len() == 1 {
Err(format!(
"check_require: no matching require condition found (@key didn't match)\n\
* plan requires: {entity_require_shape}\n\
Mismatch description:\n{mismatch_cases}",
mismatch_cases = mismatch_cases.iter().map(|e| e.to_string()).join("\n")
)
.into())
} else {
Err(format!(
"check_require: no matching require condition found (all @key directives failed to match)\n\
* plan requires: {entity_require_shape}\n\
Mismatches:\n{mismatch_cases}",
mismatch_cases = mismatch_cases.iter().enumerate().map(|(i, e)|
format!("[{index}/{bound}] {e}", index=i+1, bound=mismatch_cases.len())
).join("\n")
).into())
}
} else {
Err(format!(
"check_require: @key matched, but none of them are resolvable.\n\
* plan requires: {entity_require_shape}\n\
Unresolvable cases:\n{unresolvable_cases}",
unresolvable_cases = unresolvable_cases
.iter()
.enumerate()
.map(|(i, e)| format!(
"[{index}/{bound}] {e}",
index = i + 1,
bound = unresolvable_cases.len()
))
.join("\n")
)
.into())
}
}
}
pub(crate) fn check_requires(
context: &AnalysisContext,
subgraph_schema: &ValidFederationSchema,
state: &ResponseShape,
boolean_clause: &Clause,
response_shapes: &[ResponseShape],
requires: &[requires_selection::Selection],
) -> Result<(), AnalysisError> {
match requires.len().cmp(&response_shapes.len()) {
std::cmp::Ordering::Less => Err(
"check_requires: Fewer number of requires items than entity fetch cases"
.to_string()
.into(),
),
std::cmp::Ordering::Equal => {
for (rs, require) in response_shapes.iter().zip(requires.iter()) {
check_require(
context,
subgraph_schema,
state,
boolean_clause,
rs,
require,
)
.map_err(|e| {
e.with_context(format!(
"check_requires: Subgraph require check failed for type condition: {type_condition}",
type_condition = rs.default_type_condition()
))
})?;
}
Ok(())
}
std::cmp::Ordering::Greater => {
requires.iter().try_for_each(|require| {
let mut errors = Vec::new();
let has_any = response_shapes.iter().any(|rs| {
match check_require(
context,
subgraph_schema,
state,
boolean_clause,
rs,
require,
) {
Ok(_) => true,
Err(e) => { errors.push(e); false }
}
});
if has_any {
Ok(())
} else {
Err(format!(
"check_requires: Subgraph require check failed to find a matching entity fetch for the requires item: {require}\nErrors:\n{errors}",
errors = errors.iter().enumerate().map(|(i, e)|
format!("[{index}/{bound}] {e}", index=i+1, bound=errors.len())
).join("\n"),
).into())
}
})
}
}
}
mod key_only_response_shape_compare {
use super::super::response_shape::DefinitionVariant;
use super::super::response_shape::PossibleDefinitionsPerTypeCondition;
use super::*;
pub(super) fn key_only_compare_response_shapes(
this: &ResponseShape,
other: &ResponseShape,
) -> bool {
this.len() == other.len()
&& this.iter().all(|(key, this_def)| {
let Some(other_def) = other.get(key) else {
return false;
};
key_only_compare_possible_definitions(this_def, other_def)
})
}
fn key_only_compare_possible_definitions(
this: &PossibleDefinitions,
other: &PossibleDefinitions,
) -> bool {
this.len() == other.len()
&& this.iter().all(|(this_cond, this_def)| {
let Some(other_def) = other.get(this_cond) else {
return false;
};
key_only_compare_possible_definitions_per_type_condition(this_def, other_def)
})
}
fn key_only_compare_possible_definitions_per_type_condition(
this: &PossibleDefinitionsPerTypeCondition,
other: &PossibleDefinitionsPerTypeCondition,
) -> bool {
if this.conditional_variants().len() != 1 {
return false;
}
this.conditional_variants().iter().all(|this_def| {
let Some(merged_def) = merge_variants_ignoring_boolean_conditions(other) else {
return false;
};
key_only_compare_definition_variant(this_def, &merged_def)
})
}
fn merge_variants_ignoring_boolean_conditions(
other: &PossibleDefinitionsPerTypeCondition,
) -> Option<DefinitionVariant> {
let mut iter = other.conditional_variants().iter();
let first = iter.next()?;
let mut result_sub = first.sub_selection_response_shape().cloned();
for variant in iter {
if compare_representative_field(
variant.representative_field(),
first.representative_field(),
)
.is_err()
{
return None;
}
match (&mut result_sub, variant.sub_selection_response_shape()) {
(None, None) => {}
(Some(result_sub), Some(variant_sub)) => {
let result = result_sub.merge_with(variant_sub);
if result.is_err() {
return None;
}
}
_ => {
return None;
}
}
}
Some(first.with_updated_fields(Clause::default(), result_sub))
}
fn key_only_compare_definition_variant(
this: &DefinitionVariant,
other: &DefinitionVariant,
) -> bool {
match (
this.sub_selection_response_shape(),
other.sub_selection_response_shape(),
) {
(None, None) => true,
(Some(this_sub), Some(other_sub)) => {
key_only_compare_response_shapes(this_sub, other_sub)
}
_ => false,
}
}
}
use key_only_response_shape_compare::key_only_compare_response_shapes;