use std::sync::Arc;
use apollo_compiler::Name;
use apollo_compiler::Node;
use apollo_compiler::ast::Type;
use apollo_compiler::collections::IndexMap;
use petgraph::graph::EdgeIndex;
use crate::error::FederationError;
use crate::operation::SelectionSet;
use crate::query_graph::QueryGraph;
use crate::query_graph::graph_path::ExcludedConditions;
use crate::query_graph::graph_path::ExcludedDestinations;
use crate::query_graph::graph_path::operation::OpGraphPathContext;
use crate::query_graph::path_tree::OpPathTree;
use crate::query_plan::QueryPlanCost;
#[derive(Debug, Clone)]
pub(crate) struct ContextMapEntry {
pub(crate) levels_in_data_path: usize,
pub(crate) levels_in_query_path: usize,
pub(crate) path_tree: Option<Arc<OpPathTree>>,
pub(crate) selection_set: SelectionSet,
pub(crate) argument_name: Name,
pub(crate) argument_type: Node<Type>,
pub(crate) context_id: Name,
}
pub(crate) trait ConditionResolver {
fn resolve(
&mut self,
edge: EdgeIndex,
context: &OpGraphPathContext,
excluded_destinations: &ExcludedDestinations,
excluded_conditions: &ExcludedConditions,
extra_conditions: Option<&SelectionSet>,
) -> Result<ConditionResolution, FederationError>;
}
#[derive(Debug, Clone)]
pub(crate) enum ConditionResolution {
Satisfied {
cost: QueryPlanCost,
path_tree: Option<Arc<OpPathTree>>,
context_map: Option<IndexMap<Name, ContextMapEntry>>,
},
Unsatisfied {
reason: Option<UnsatisfiedConditionReason>,
},
}
impl std::fmt::Display for ConditionResolution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConditionResolution::Satisfied {
cost,
path_tree,
context_map,
} => {
writeln!(f, "Satisfied: cost={cost}")?;
if let Some(path_tree) = path_tree {
writeln!(f, "path_tree:\n{path_tree}")?;
}
if let Some(context_map) = context_map {
writeln!(f, ", context_map:\n{context_map:?}")?;
}
Ok(())
}
ConditionResolution::Unsatisfied { reason } => {
writeln!(f, "Unsatisfied: reason={reason:?}")
}
}
}
}
#[derive(Debug, Clone)]
pub(crate) enum UnsatisfiedConditionReason {
NoPostRequireKey,
NoSetContext,
}
impl ConditionResolution {
pub(crate) fn no_conditions() -> Self {
Self::Satisfied {
cost: 0.0,
path_tree: None,
context_map: None,
}
}
pub(crate) fn unsatisfied_conditions() -> Self {
Self::Unsatisfied { reason: None }
}
}
#[derive(Debug, derive_more::IsVariant)]
pub(crate) enum ConditionResolutionCacheResult {
Hit(ConditionResolution),
Miss,
NotApplicable,
}
pub(crate) struct ConditionResolverCache {
edge_states: IndexMap<EdgeIndex, (ConditionResolution, ExcludedDestinations)>,
}
impl ConditionResolverCache {
pub(crate) fn new() -> Self {
Self {
edge_states: Default::default(),
}
}
pub(crate) fn contains(
&mut self,
edge: EdgeIndex,
context: &OpGraphPathContext,
excluded_destinations: &ExcludedDestinations,
excluded_conditions: &ExcludedConditions,
extra_conditions: Option<&SelectionSet>,
) -> ConditionResolutionCacheResult {
if extra_conditions.is_some() {
return ConditionResolutionCacheResult::NotApplicable;
}
if !context.is_empty() || !excluded_conditions.is_empty() {
return ConditionResolutionCacheResult::NotApplicable;
}
if let Some((cached_resolution, cached_excluded_destinations)) = self.edge_states.get(&edge)
{
if cached_excluded_destinations == excluded_destinations {
return ConditionResolutionCacheResult::Hit(cached_resolution.clone());
}
ConditionResolutionCacheResult::NotApplicable
} else {
ConditionResolutionCacheResult::Miss
}
}
pub(crate) fn insert(
&mut self,
edge: EdgeIndex,
resolution: ConditionResolution,
excluded_destinations: ExcludedDestinations,
) {
self.edge_states
.insert(edge, (resolution, excluded_destinations));
}
}
pub(crate) trait CachingConditionResolver {
fn query_graph(&self) -> &QueryGraph;
fn resolve_without_cache(
&self,
edge: EdgeIndex,
context: &OpGraphPathContext,
excluded_destinations: &ExcludedDestinations,
excluded_conditions: &ExcludedConditions,
extra_conditions: Option<&SelectionSet>,
) -> Result<ConditionResolution, FederationError>;
fn resolver_cache(&mut self) -> &mut ConditionResolverCache;
fn resolve_with_cache(
&mut self,
edge: EdgeIndex,
context: &OpGraphPathContext,
excluded_destinations: &ExcludedDestinations,
excluded_conditions: &ExcludedConditions,
extra_conditions: Option<&SelectionSet>,
) -> Result<ConditionResolution, FederationError> {
let cache_result = self.resolver_cache().contains(
edge,
context,
excluded_destinations,
excluded_conditions,
extra_conditions,
);
if let ConditionResolutionCacheResult::Hit(cached_resolution) = cache_result {
return Ok(cached_resolution);
}
let resolution = self.resolve_without_cache(
edge,
context,
excluded_destinations,
excluded_conditions,
extra_conditions,
)?;
if cache_result.is_miss() {
self.resolver_cache()
.insert(edge, resolution.clone(), excluded_destinations.clone());
}
Ok(resolution)
}
}
impl<T: CachingConditionResolver> ConditionResolver for T {
fn resolve(
&mut self,
edge: EdgeIndex,
context: &OpGraphPathContext,
excluded_destinations: &ExcludedDestinations,
excluded_conditions: &ExcludedConditions,
extra_conditions: Option<&SelectionSet>,
) -> Result<ConditionResolution, FederationError> {
let graph = &self.query_graph();
let edge_data = graph.edge_weight(edge)?;
assert!(
edge_data.conditions.is_some() || extra_conditions.is_some(),
"Should not have been called for edge without conditions"
);
self.resolve_with_cache(
edge,
context,
excluded_destinations,
excluded_conditions,
extra_conditions,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query_graph::graph_path::operation::OpGraphPathContext;
#[test]
fn test_condition_resolver_cache() {
let mut cache = ConditionResolverCache::new();
let edge1 = EdgeIndex::new(1);
let empty_context = OpGraphPathContext::default();
let empty_destinations = ExcludedDestinations::default();
let empty_conditions = ExcludedConditions::default();
assert!(
cache
.contains(
edge1,
&empty_context,
&empty_destinations,
&empty_conditions,
None
)
.is_miss()
);
cache.insert(
edge1,
ConditionResolution::unsatisfied_conditions(),
empty_destinations.clone(),
);
assert!(
cache
.contains(
edge1,
&empty_context,
&empty_destinations,
&empty_conditions,
None
)
.is_hit(),
);
let edge2 = EdgeIndex::new(2);
assert!(
cache
.contains(
edge2,
&empty_context,
&empty_destinations,
&empty_conditions,
None
)
.is_miss()
);
}
}