diff --git a/apollo-federation/cli/src/bench.rs b/apollo-federation/cli/src/bench.rs index c672137982..ed135d8ba4 100644 --- a/apollo-federation/cli/src/bench.rs +++ b/apollo-federation/cli/src/bench.rs @@ -54,7 +54,7 @@ pub(crate) fn run_bench( } }; let now = Instant::now(); - let plan = planner.build_query_plan(&document, None); + let plan = planner.build_query_plan(&document, None, Default::default()); let elapsed = now.elapsed().as_secs_f64() * 1000.0; let mut eval_plans = None; let mut error = None; diff --git a/apollo-federation/cli/src/main.rs b/apollo-federation/cli/src/main.rs index ab42f16151..28bb5f7921 100644 --- a/apollo-federation/cli/src/main.rs +++ b/apollo-federation/cli/src/main.rs @@ -254,7 +254,10 @@ fn cmd_plan( let query_doc = ExecutableDocument::parse_and_validate(planner.api_schema().schema(), query, query_path)?; - print!("{}", planner.build_query_plan(&query_doc, None)?); + print!( + "{}", + planner.build_query_plan(&query_doc, None, Default::default())? + ); Ok(()) } diff --git a/apollo-federation/src/error/mod.rs b/apollo-federation/src/error/mod.rs index 9e4487c30f..d56c9783a6 100644 --- a/apollo-federation/src/error/mod.rs +++ b/apollo-federation/src/error/mod.rs @@ -33,8 +33,6 @@ impl From for String { #[derive(Clone, Debug, strum_macros::Display, PartialEq, Eq)] pub enum UnsupportedFeatureKind { - #[strum(to_string = "progressive overrides")] - ProgressiveOverrides, #[strum(to_string = "defer")] Defer, #[strum(to_string = "context")] diff --git a/apollo-federation/src/link/federation_spec_definition.rs b/apollo-federation/src/link/federation_spec_definition.rs index 67f181ec8b..184e93b690 100644 --- a/apollo-federation/src/link/federation_spec_definition.rs +++ b/apollo-federation/src/link/federation_spec_definition.rs @@ -12,6 +12,7 @@ use lazy_static::lazy_static; use crate::error::FederationError; use crate::error::SingleFederationError; use crate::link::argument::directive_optional_boolean_argument; +use crate::link::argument::directive_optional_string_argument; use crate::link::argument::directive_required_string_argument; use crate::link::cost_spec_definition::CostSpecDefinition; use crate::link::cost_spec_definition::COST_VERSIONS; @@ -51,6 +52,11 @@ pub(crate) struct ProvidesDirectiveArguments<'doc> { pub(crate) fields: &'doc str, } +pub(crate) struct OverrideDirectiveArguments<'doc> { + pub(crate) from: &'doc str, + pub(crate) label: Option<&'doc str>, +} + #[derive(Debug)] pub(crate) struct FederationSpecDefinition { url: Url, @@ -361,6 +367,19 @@ impl FederationSpecDefinition { }) } + pub(crate) fn override_directive_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result<&'schema Node, FederationError> { + self.directive_definition(schema, &FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| { + FederationError::internal(format!( + "Unexpectedly could not find federation spec's \"@{}\" directive definition", + FEDERATION_OVERRIDE_DIRECTIVE_NAME_IN_SPEC + )) + }) + } + pub(crate) fn override_directive( &self, schema: &FederationSchema, @@ -390,6 +409,19 @@ impl FederationSpecDefinition { }) } + pub(crate) fn override_directive_arguments<'doc>( + &self, + application: &'doc Node, + ) -> Result, FederationError> { + Ok(OverrideDirectiveArguments { + from: directive_required_string_argument(application, &FEDERATION_FROM_ARGUMENT_NAME)?, + label: directive_optional_string_argument( + application, + &FEDERATION_OVERRIDE_LABEL_ARGUMENT_NAME, + )?, + }) + } + pub(crate) fn get_cost_spec_definition( &self, schema: &FederationSchema, diff --git a/apollo-federation/src/query_graph/build_query_graph.rs b/apollo-federation/src/query_graph/build_query_graph.rs index 3dd7abbcd6..d4d2acf609 100644 --- a/apollo-federation/src/query_graph/build_query_graph.rs +++ b/apollo-federation/src/query_graph/build_query_graph.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use apollo_compiler::collections::HashMap; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; use apollo_compiler::schema::DirectiveList as ComponentDirectiveList; @@ -21,6 +22,7 @@ use crate::link::federation_spec_definition::KeyDirectiveArguments; use crate::operation::merge_selection_sets; use crate::operation::Selection; use crate::operation::SelectionSet; +use crate::query_graph::OverrideCondition; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdge; use crate::query_graph::QueryGraphEdgeTransition; @@ -140,6 +142,7 @@ impl BaseQueryGraphBuilder { QueryGraphEdge { transition, conditions, + override_condition: None, }, ); let head_weight = self.query_graph.node_weight(head)?; @@ -982,6 +985,7 @@ impl FederatedQueryGraphBuilder { self.add_root_edges()?; self.handle_key()?; self.handle_requires()?; + self.handle_progressive_overrides()?; // Note that @provides must be handled last when building since it requires copying nodes // and their edges, and it's easier to reason about this if we know previous self.handle_provides()?; @@ -1374,6 +1378,102 @@ impl FederatedQueryGraphBuilder { Ok(()) } + /// Handling progressive overrides here. For each progressive @override + /// application (with a label), we want to update the edges to the overridden + /// field within the "to" and "from" subgraphs with their respective override + /// condition (the label and a T/F value). The "from" subgraph will have an + /// override condition of `false`, whereas the "to" subgraph will have an + /// override condition of `true`. + fn handle_progressive_overrides(&mut self) -> Result<(), FederationError> { + let mut edge_to_conditions: HashMap = Default::default(); + + fn collect_edge_condition( + query_graph: &QueryGraph, + target_graph: &str, + target_field: &ObjectFieldDefinitionPosition, + label: &str, + condition: bool, + edge_to_conditions: &mut HashMap, + ) -> Result<(), FederationError> { + let target_field = FieldDefinitionPosition::Object(target_field.clone()); + let subgraph_nodes = query_graph + .types_to_nodes_by_source + .get(target_graph) + .unwrap(); + let parent_node = subgraph_nodes + .get(target_field.type_name()) + .unwrap() + .first() + .unwrap(); + for edge in query_graph.out_edges(*parent_node) { + let edge_weight = query_graph.edge_weight(edge.id())?; + let QueryGraphEdgeTransition::FieldCollection { + field_definition_position, + .. + } = &edge_weight.transition + else { + continue; + }; + + if &target_field == field_definition_position { + edge_to_conditions.insert( + edge.id(), + OverrideCondition { + label: label.to_string(), + condition, + }, + ); + } + } + Ok(()) + } + + for (to_subgraph_name, subgraph) in &self.base.query_graph.subgraphs_by_name { + let subgraph_data = self.subgraphs.get(to_subgraph_name)?; + if let Some(override_referencers) = subgraph + .referencers() + .directives + .get(&subgraph_data.overrides_directive_definition_name) + { + for field_definition_position in &override_referencers.object_fields { + let field = field_definition_position.get(subgraph.schema())?; + for directive in field + .directives + .get_all(&subgraph_data.overrides_directive_definition_name) + { + let application = subgraph_data + .federation_spec_definition + .override_directive_arguments(directive)?; + if let Some(label) = application.label { + collect_edge_condition( + &self.base.query_graph, + to_subgraph_name, + field_definition_position, + label, + true, + &mut edge_to_conditions, + )?; + collect_edge_condition( + &self.base.query_graph, + application.from, + field_definition_position, + label, + false, + &mut edge_to_conditions, + )?; + } + } + } + } + } + + for (edge, condition) in edge_to_conditions { + let mutable_edge = self.base.query_graph.edge_weight_mut(edge)?; + mutable_edge.override_condition = Some(condition); + } + Ok(()) + } + /// Handle @provides by copying the appropriate nodes/edges. fn handle_provides(&mut self) -> Result<(), FederationError> { let mut provide_id = 0; @@ -1987,6 +2087,10 @@ impl FederatedQueryGraphBuilderSubgraphs { ), } })?; + let overrides_directive_definition_name = federation_spec_definition + .override_directive_definition(schema)? + .name + .clone(); subgraphs.map.insert( source.clone(), FederatedQueryGraphBuilderSubgraphData { @@ -1995,6 +2099,7 @@ impl FederatedQueryGraphBuilderSubgraphs { requires_directive_definition_name, provides_directive_definition_name, interface_object_directive_definition_name, + overrides_directive_definition_name, }, ); } @@ -2020,6 +2125,7 @@ struct FederatedQueryGraphBuilderSubgraphData { requires_directive_definition_name: Name, provides_directive_definition_name: Name, interface_object_directive_definition_name: Name, + overrides_directive_definition_name: Name, } #[derive(Debug)] diff --git a/apollo-federation/src/query_graph/graph_path.rs b/apollo-federation/src/query_graph/graph_path.rs index c588f39d7c..88bba9e8a0 100644 --- a/apollo-federation/src/query_graph/graph_path.rs +++ b/apollo-federation/src/query_graph/graph_path.rs @@ -46,6 +46,7 @@ use crate::query_graph::path_tree::OpPathTree; use crate::query_graph::QueryGraph; use crate::query_graph::QueryGraphEdgeTransition; use crate::query_graph::QueryGraphNodeType; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::FetchDataPathElement; use crate::query_plan::QueryPathElement; use crate::query_plan::QueryPlanCost; @@ -1406,17 +1407,24 @@ where feature = "snapshot_tracing", tracing::instrument(skip_all, level = "trace") )] + #[allow(clippy::too_many_arguments)] fn advance_with_non_collecting_and_type_preserving_transitions( self: &Arc, context: &OpGraphPathContext, condition_resolver: &mut impl ConditionResolver, excluded_destinations: &ExcludedDestinations, excluded_conditions: &ExcludedConditions, + override_conditions: &EnabledOverrideConditions, transition_and_context_to_trigger: impl Fn( &QueryGraphEdgeTransition, &OpGraphPathContext, ) -> TTrigger, - node_and_trigger_to_edge: impl Fn(&Arc, NodeIndex, &Arc) -> Option, + node_and_trigger_to_edge: impl Fn( + &Arc, + NodeIndex, + &Arc, + &EnabledOverrideConditions, + ) -> Option, ) -> Result, FederationError> { // If we're asked for indirect paths after an "@interfaceObject fake down cast" but that // down cast comes just after non-collecting edge(s), then we can ignore the ask (skip @@ -1675,6 +1683,7 @@ where direct_path_start_node, edge_tail_type_pos, &node_and_trigger_to_edge, + override_conditions, )? } else { None @@ -1797,12 +1806,20 @@ where start_index: usize, start_node: NodeIndex, end_type_position: &OutputTypeDefinitionPosition, - node_and_trigger_to_edge: impl Fn(&Arc, NodeIndex, &Arc) -> Option, + node_and_trigger_to_edge: impl Fn( + &Arc, + NodeIndex, + &Arc, + &EnabledOverrideConditions, + ) -> Option, + override_conditions: &EnabledOverrideConditions, ) -> Result, FederationError> { let mut current_node = start_node; for index in start_index..self.edges.len() { let trigger = &self.edge_triggers[index]; - let Some(edge) = node_and_trigger_to_edge(&self.graph, current_node, trigger) else { + let Some(edge) = + node_and_trigger_to_edge(&self.graph, current_node, trigger, override_conditions) + else { return Ok(None); }; @@ -1892,8 +1909,13 @@ where } impl OpGraphPath { - fn next_edge_for_field(&self, field: &Field) -> Option { - self.graph.edge_for_field(self.tail, field) + fn next_edge_for_field( + &self, + field: &Field, + override_conditions: &EnabledOverrideConditions, + ) -> Option { + self.graph + .edge_for_field(self.tail, field, override_conditions) } fn next_edge_for_inline_fragment(&self, inline_fragment: &InlineFragment) -> Option { @@ -2027,6 +2049,7 @@ impl OpGraphPath { pub(crate) fn terminate_with_non_requested_typename_field( &self, + override_conditions: &EnabledOverrideConditions, ) -> Result { // If the last step of the path was a fragment/type-condition, we want to remove it before // we get __typename. The reason is that this avoid cases where this method would make us @@ -2066,7 +2089,10 @@ impl OpGraphPath { &tail_type_pos, None, ); - let Some(edge) = self.graph.edge_for_field(path.tail, &typename_field) else { + let Some(edge) = self + .graph + .edge_for_field(path.tail, &typename_field, override_conditions) + else { return Err(FederationError::internal( "Unexpectedly missing edge for __typename field", )); @@ -2401,6 +2427,7 @@ impl OpGraphPath { operation_element: &OpPathElement, context: &OpGraphPathContext, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result<(Option>, Option), FederationError> { let span = debug_span!("Trying to advance {self} directly with {operation_element}"); let _guard = span.enter(); @@ -2417,7 +2444,9 @@ impl OpGraphPath { OutputTypeDefinitionPosition::Object(tail_type_pos) => { // Just take the edge corresponding to the field, if it exists and can be // used. - let Some(edge) = self.next_edge_for_field(operation_field) else { + let Some(edge) = + self.next_edge_for_field(operation_field, override_conditions) + else { debug!( "No edge for field {operation_field} on object type {tail_weight}" ); @@ -2504,7 +2533,7 @@ impl OpGraphPath { let interface_edge = if field_is_of_an_implementation { None } else { - self.next_edge_for_field(operation_field) + self.next_edge_for_field(operation_field, override_conditions) }; let interface_path = if let Some(interface_edge) = &interface_edge { let field_path = self.add_field_edge( @@ -2668,6 +2697,7 @@ impl OpGraphPath { supergraph_schema.clone(), &implementation_inline_fragment.into(), condition_resolver, + override_conditions, )?; // If we find no options for that implementation, we bail (as we need to // simultaneously advance all implementations). @@ -2697,6 +2727,7 @@ impl OpGraphPath { supergraph_schema.clone(), operation_element, condition_resolver, + override_conditions, )?; let Some(field_options_for_implementation) = field_options_for_implementation @@ -2756,7 +2787,9 @@ impl OpGraphPath { } } OutputTypeDefinitionPosition::Union(_) => { - let Some(typename_edge) = self.next_edge_for_field(operation_field) else { + let Some(typename_edge) = + self.next_edge_for_field(operation_field, override_conditions) + else { return Err(FederationError::internal( "Should always have an edge for __typename edge on an union", )); @@ -2871,6 +2904,7 @@ impl OpGraphPath { supergraph_schema.clone(), &implementation_inline_fragment.into(), condition_resolver, + override_conditions, )?; let Some(implementation_options) = implementation_options else { drop(guard); @@ -3277,6 +3311,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { updated_context: &OpGraphPathContext, path_index: usize, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result { // Note that the provided context will usually be one we had during construction (the // `updated_context` will be `self.context` updated by whichever operation we're looking at, @@ -3284,12 +3319,13 @@ impl SimultaneousPathsWithLazyIndirectPaths { // rare), which is why we save recomputation by caching the computed value in that case, but // in case it's different, we compute without caching. if *updated_context != self.context { - self.compute_indirect_paths(path_index, condition_resolver)?; + self.compute_indirect_paths(path_index, condition_resolver, override_conditions)?; } if let Some(indirect_paths) = &self.lazily_computed_indirect_paths[path_index] { Ok(indirect_paths.clone()) } else { - let new_indirect_paths = self.compute_indirect_paths(path_index, condition_resolver)?; + let new_indirect_paths = + self.compute_indirect_paths(path_index, condition_resolver, override_conditions)?; self.lazily_computed_indirect_paths[path_index] = Some(new_indirect_paths.clone()); Ok(new_indirect_paths) } @@ -3299,17 +3335,21 @@ impl SimultaneousPathsWithLazyIndirectPaths { &self, path_index: usize, condition_resolver: &mut impl ConditionResolver, + overridden_conditions: &EnabledOverrideConditions, ) -> Result { self.paths.0[path_index].advance_with_non_collecting_and_type_preserving_transitions( &self.context, condition_resolver, &self.excluded_destinations, &self.excluded_conditions, + overridden_conditions, // The transitions taken by this method are non-collecting transitions, in which case // the trigger is the context (which is really a hack to provide context information for // keys during fetch dependency graph updating). |_, context| OpGraphPathTrigger::Context(context.clone()), - |graph, node, trigger| graph.edge_for_op_graph_path_trigger(node, trigger), + |graph, node, trigger, overridden_conditions| { + graph.edge_for_op_graph_path_trigger(node, trigger, overridden_conditions) + }, ) } @@ -3345,6 +3385,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { supergraph_schema: ValidFederationSchema, operation_element: &OpPathElement, condition_resolver: &mut impl ConditionResolver, + override_conditions: &EnabledOverrideConditions, ) -> Result>, FederationError> { debug!( "Trying to advance paths for operation: path = {}, operation = {operation_element}", @@ -3375,6 +3416,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; debug!("{advance_options:?}"); drop(gaurd); @@ -3422,7 +3464,12 @@ impl SimultaneousPathsWithLazyIndirectPaths { if let OpPathElement::Field(operation_field) = operation_element { // Add whatever options can be obtained by taking some non-collecting edges first. let paths_with_non_collecting_edges = self - .indirect_options(&updated_context, path_index, condition_resolver)? + .indirect_options( + &updated_context, + path_index, + condition_resolver, + override_conditions, + )? .filter_non_collecting_paths_for_field(operation_field)?; if !paths_with_non_collecting_edges.paths.is_empty() { debug!( @@ -3441,6 +3488,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; // If we can't advance the operation element after that path, ignore it, // it's just not an option. @@ -3528,6 +3576,7 @@ impl SimultaneousPathsWithLazyIndirectPaths { operation_element, &updated_context, condition_resolver, + override_conditions, )?; options = advance_options.unwrap_or_else(Vec::new); debug!("{options:?}"); @@ -3552,11 +3601,6 @@ impl SimultaneousPathsWithLazyIndirectPaths { // PORT_NOTE: JS passes a ConditionResolver here, we do not: see port note for // `SimultaneousPathsWithLazyIndirectPaths` -// TODO(@goto-bus-stop): JS passes `override_conditions` here and maintains stores -// references to it in the created paths. AFAICT override conditions -// are shared mutable state among different query graphs, so having references to -// it in many structures would require synchronization. We should likely pass it as -// an argument to exactly the functionality that uses it. pub fn create_initial_options( initial_path: GraphPath>, initial_type: &QueryGraphNodeType, @@ -3564,6 +3608,7 @@ pub fn create_initial_options( condition_resolver: &mut impl ConditionResolver, excluded_edges: ExcludedDestinations, excluded_conditions: ExcludedConditions, + override_conditions: &EnabledOverrideConditions, ) -> Result, FederationError> { let initial_paths = SimultaneousPaths::from(initial_path); let mut lazy_initial_path = SimultaneousPathsWithLazyIndirectPaths::new( @@ -3574,8 +3619,12 @@ pub fn create_initial_options( ); if initial_type.is_federated_root_type() { - let initial_options = - lazy_initial_path.indirect_options(&initial_context, 0, condition_resolver)?; + let initial_options = lazy_initial_path.indirect_options( + &initial_context, + 0, + condition_resolver, + override_conditions, + )?; let options = initial_options .paths .iter() diff --git a/apollo-federation/src/query_graph/mod.rs b/apollo-federation/src/query_graph/mod.rs index f95588446c..4060ffdbe6 100644 --- a/apollo-federation/src/query_graph/mod.rs +++ b/apollo-federation/src/query_graph/mod.rs @@ -44,6 +44,7 @@ use crate::query_graph::graph_path::ExcludedDestinations; use crate::query_graph::graph_path::OpGraphPathContext; use crate::query_graph::graph_path::OpGraphPathTrigger; use crate::query_graph::graph_path::OpPathElement; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::QueryPlanCost; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -147,6 +148,25 @@ pub(crate) struct QueryGraphEdge { /// /// Outside of keys, @requires edges also rely on conditions. pub(crate) conditions: Option>, + /// Edges can require that an override condition (provided during query + /// planning) be met in order to be taken. This is used for progressive + /// @override, where (at least) 2 subgraphs can resolve the same field, but + /// one of them has an @override with a label. If the override condition + /// matches the query plan parameters, this edge can be taken. + pub(crate) override_condition: Option, +} + +impl QueryGraphEdge { + fn satisfies_override_conditions( + &self, + conditions_to_check: &EnabledOverrideConditions, + ) -> bool { + if let Some(override_condition) = &self.override_condition { + override_condition.condition == conditions_to_check.contains(&override_condition.label) + } else { + true + } + } } impl Display for QueryGraphEdge { @@ -158,13 +178,32 @@ impl Display for QueryGraphEdge { { return Ok(()); } - if let Some(conditions) = &self.conditions { - write!(f, "{} ⊢ {}", conditions, self.transition) - } else { - self.transition.fmt(f) + + match (&self.override_condition, &self.conditions) { + (Some(override_condition), Some(conditions)) => write!( + f, + "{}, {} ⊢ {}", + conditions, override_condition, self.transition + ), + (Some(override_condition), None) => { + write!(f, "{} ⊢ {}", override_condition, self.transition) + } + (None, Some(conditions)) => write!(f, "{} ⊢ {}", conditions, self.transition), + _ => self.transition.fmt(f), } } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct OverrideCondition { + pub(crate) label: String, + pub(crate) condition: bool, +} + +impl Display for OverrideCondition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} = {}", self.label, self.condition) + } +} /// The type of query graph edge "transition". /// @@ -639,7 +678,12 @@ impl QueryGraph { .find_ok(|selection| !external_metadata.selects_any_external_field(selection)) } - pub(crate) fn edge_for_field(&self, node: NodeIndex, field: &Field) -> Option { + pub(crate) fn edge_for_field( + &self, + node: NodeIndex, + field: &Field, + override_conditions: &EnabledOverrideConditions, + ) -> Option { let mut candidates = self.out_edges(node).into_iter().filter_map(|edge_ref| { let edge_weight = edge_ref.weight(); let QueryGraphEdgeTransition::FieldCollection { @@ -649,6 +693,11 @@ impl QueryGraph { else { return None; }; + + if !edge_weight.satisfies_override_conditions(override_conditions) { + return None; + } + // We explicitly avoid comparing parent type's here, to allow interface object // fields to match operation fields with the same name but differing types. if field.field_position.field_name() == field_definition_position.field_name() { @@ -715,12 +764,15 @@ impl QueryGraph { &self, node: NodeIndex, op_graph_path_trigger: &OpGraphPathTrigger, + override_conditions: &EnabledOverrideConditions, ) -> Option> { let OpGraphPathTrigger::OpPathElement(op_path_element) = op_graph_path_trigger else { return None; }; match op_path_element { - OpPathElement::Field(field) => self.edge_for_field(node, field).map(Some), + OpPathElement::Field(field) => self + .edge_for_field(node, field, override_conditions) + .map(Some), OpPathElement::InlineFragment(inline_fragment) => { if inline_fragment.type_condition_position.is_some() { self.edge_for_inline_fragment(node, inline_fragment) diff --git a/apollo-federation/src/query_plan/query_planner.rs b/apollo-federation/src/query_plan/query_planner.rs index c2c2d75805..a64f79364c 100644 --- a/apollo-federation/src/query_plan/query_planner.rs +++ b/apollo-federation/src/query_plan/query_planner.rs @@ -1,10 +1,11 @@ use std::cell::Cell; use std::num::NonZeroU32; +use std::ops::Deref; use std::sync::Arc; +use apollo_compiler::collections::HashSet; use apollo_compiler::collections::IndexMap; use apollo_compiler::collections::IndexSet; -use apollo_compiler::schema::ExtendedType; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; @@ -48,7 +49,6 @@ use crate::utils::logging::snapshot; use crate::ApiSchemaOptions; use crate::Supergraph; -pub(crate) const OVERRIDE_LABEL_ARG_NAME: &str = "overrideLabel"; pub(crate) const CONTEXT_DIRECTIVE: &str = "context"; pub(crate) const JOIN_FIELD: &str = "join__field"; @@ -196,6 +196,28 @@ impl QueryPlannerConfig { } } +#[derive(Debug, Default, Clone)] +pub struct QueryPlanOptions { + /// A set of labels which will be used _during query planning_ to + /// enable/disable edges with a matching label in their override condition. + /// Edges with override conditions require their label to be present or absent + /// from this set in order to be traversable. These labels enable the + /// progressive @override feature. + // PORT_NOTE: In JS implementation this was a Map + pub override_conditions: Vec, +} + +#[derive(Debug, Default, Clone)] +pub(crate) struct EnabledOverrideConditions(HashSet); + +impl Deref for EnabledOverrideConditions { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + pub struct QueryPlanner { config: QueryPlannerConfig, federated_query_graph: Arc, @@ -308,11 +330,6 @@ impl QueryPlanner { .filter(|position| is_inconsistent(position.clone())) .collect::>(); - // PORT_NOTE: JS prepares a map of override conditions here, which is - // a map where the keys are all `@join__field(overrideLabel:)` argument values - // and the values are all initialised to `false`. Instead of doing that, we should - // be able to use a Set where presence means `true` and absence means `false`. - Ok(Self { config, federated_query_graph: Arc::new(query_graph), @@ -339,6 +356,7 @@ impl QueryPlanner { &self, document: &Valid, operation_name: Option, + options: QueryPlanOptions, ) -> Result { let operation = document .operations @@ -482,7 +500,9 @@ impl QueryPlanner { .clone() .into(), config: self.config.clone(), - // PORT_NOTE: JS provides `override_conditions` here: see port note in `QueryPlanner::new`. + override_conditions: EnabledOverrideConditions(HashSet::from_iter( + options.override_conditions, + )), }; let root_node = match defer_conditions { @@ -549,59 +569,6 @@ impl QueryPlanner { } fn check_unsupported_features(supergraph: &Supergraph) -> Result<(), FederationError> { - // We have a *progressive* override when `join__field` has a - // non-null value for `overrideLabel` field. - // - // This looks at object types' fields and their directive - // applications, looking specifically for `@join__field` - // arguments list. - let has_progressive_overrides = supergraph - .schema - .schema() - .types - .values() - .filter_map(|extended_type| { - // The override label args can be only on ObjectTypes - if let ExtendedType::Object(object_type) = extended_type { - Some(object_type) - } else { - None - } - }) - .flat_map(|object_type| &object_type.fields) - .flat_map(|(_, field)| { - field - .directives - .iter() - .filter(|d| d.name.as_str() == JOIN_FIELD) - }) - .any(|join_directive| { - if let Some(override_label_arg) = - join_directive.argument_by_name(OVERRIDE_LABEL_ARG_NAME) - { - // Any argument value for `overrideLabel` that's not - // null can be considered as progressive override usage - if !override_label_arg.is_null() { - return true; - } - return false; - } - false - }); - if has_progressive_overrides { - let message = "\ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.\ - "; - return Err(SingleFederationError::UnsupportedFeature { - message: message.to_owned(), - kind: crate::error::UnsupportedFeatureKind::ProgressiveOverrides, - } - .into()); - } - // We will only check for `@context` direcive, since // `@fromContext` can only be used if `@context` is already // applied, and we assume a correctly composed supergraph. @@ -1102,7 +1069,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1134,7 +1103,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Sequence { @@ -1223,7 +1194,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Parallel { @@ -1334,7 +1307,9 @@ type User let mut config = QueryPlannerConfig::default(); config.debug.bypass_planner_for_single_subgraph = true; let planner = QueryPlanner::new(&supergraph, config).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "A") { @@ -1378,7 +1353,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1437,7 +1414,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "accounts") { @@ -1497,7 +1476,9 @@ type User .unwrap(); let planner = QueryPlanner::new(&supergraph, Default::default()).unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); // Make sure `fragment F2` contains `...F1`. insta::assert_snapshot!(plan, @r###" QueryPlan { @@ -1554,7 +1535,9 @@ type User "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); insta::assert_snapshot!(plan, @r###" QueryPlan { Fetch(service: "Subgraph1") { diff --git a/apollo-federation/src/query_plan/query_planning_traversal.rs b/apollo-federation/src/query_plan/query_planning_traversal.rs index c54a81ba66..cb5e6bb9f1 100644 --- a/apollo-federation/src/query_plan/query_planning_traversal.rs +++ b/apollo-federation/src/query_plan/query_planning_traversal.rs @@ -36,6 +36,7 @@ use crate::query_plan::fetch_dependency_graph_processor::FetchDependencyGraphToC use crate::query_plan::generate::generate_all_plans_and_find_best; use crate::query_plan::generate::PlanBuilder; use crate::query_plan::query_planner::compute_root_fetch_groups; +use crate::query_plan::query_planner::EnabledOverrideConditions; use crate::query_plan::query_planner::QueryPlannerConfig; use crate::query_plan::query_planner::QueryPlanningStatistics; use crate::query_plan::QueryPlanCost; @@ -72,6 +73,7 @@ pub(crate) struct QueryPlanningParameters<'a> { /// The configuration for the query planner. pub(crate) config: QueryPlannerConfig, pub(crate) statistics: &'a QueryPlanningStatistics, + pub(crate) override_conditions: EnabledOverrideConditions, } pub(crate) struct QueryPlanningTraversal<'a, 'b> { @@ -247,6 +249,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { &mut traversal, excluded_destinations, excluded_conditions, + ¶meters.override_conditions, )?; traversal.open_branches = map_options_to_selections(selection_set, initial_options); @@ -337,6 +340,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { self.parameters.supergraph_schema.clone(), &operation_element, /*resolver*/ self, + &self.parameters.override_conditions, )?; let Some(followups_for_option) = followups_for_option else { // There is no valid way to advance the current operation element from this option @@ -404,7 +408,9 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { let mut new_simultaneous_paths = vec![]; for simultaneous_path in &option.paths.0 { new_simultaneous_paths.push(Arc::new( - simultaneous_path.terminate_with_non_requested_typename_field()?, + simultaneous_path.terminate_with_non_requested_typename_field( + &self.parameters.override_conditions, + )?, )); } closed_paths.push(Arc::new(ClosedPath { @@ -1054,6 +1060,7 @@ impl<'a: 'b, 'b> QueryPlanningTraversal<'a, 'b> { .clone(), config: self.parameters.config.clone(), statistics: self.parameters.statistics, + override_conditions: self.parameters.override_conditions.clone(), }; let best_plan_opt = QueryPlanningTraversal::new_inner( ¶meters, diff --git a/apollo-federation/tests/query_plan/build_query_plan_support.rs b/apollo-federation/tests/query_plan/build_query_plan_support.rs index f73c3117f7..b73596590b 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_support.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_support.rs @@ -59,6 +59,18 @@ macro_rules! subgraph_name { /// formatted query plan string. /// Run `cargo insta review` to diff and accept changes to the generated query plan. macro_rules! assert_plan { + ($api_schema_and_planner: expr, $operation: expr, $options: expr, @$expected: literal) => {{ + let (api_schema, planner) = $api_schema_and_planner; + let document = apollo_compiler::ExecutableDocument::parse_and_validate( + api_schema.schema(), + $operation, + "operation.graphql", + ) + .unwrap(); + let plan = planner.build_query_plan(&document, None, $options).unwrap(); + insta::assert_snapshot!(plan, @$expected); + plan + }}; ($api_schema_and_planner: expr, $operation: expr, @$expected: literal) => {{ let (api_schema, planner) = $api_schema_and_planner; let document = apollo_compiler::ExecutableDocument::parse_and_validate( @@ -67,7 +79,7 @@ macro_rules! assert_plan { "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner.build_query_plan(&document, None, Default::default()).unwrap(); insta::assert_snapshot!(plan, @$expected); plan }}; diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests.rs b/apollo-federation/tests/query_plan/build_query_plan_tests.rs index 0d618a6d0f..acef092c2c 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests.rs @@ -44,11 +44,11 @@ mod merged_abstract_types_handling; mod mutations; mod named_fragments; mod named_fragments_preservation; +mod overrides; mod provides; mod requires; mod shareable_root_fields; mod subscriptions; - // TODO: port the rest of query-planner-js/src/__tests__/buildPlan.test.ts #[test] diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs index a18565aed0..884c10e7e1 100644 --- a/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/fetch_operation_names.rs @@ -253,7 +253,9 @@ fn correctly_handle_case_where_there_is_too_many_plans_to_consider() { "operation.graphql", ) .unwrap(); - let plan = planner.build_query_plan(&document, None).unwrap(); + let plan = planner + .build_query_plan(&document, None, Default::default()) + .unwrap(); // Note: The way the code that handle multiple plans currently work, it mess up the order of fields a bit. It's not a // big deal in practice cause everything gets re-order in practice during actual execution, but this means it's a tad diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs new file mode 100644 index 0000000000..c0f1b91a31 --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides.rs @@ -0,0 +1,375 @@ +use apollo_federation::query_plan::query_planner::QueryPlanOptions; + +mod shareable; + +#[test] +fn it_handles_progressive_override_on_root_fields() { + let planner = planner!( + s1: r#" + type Query { + hello: String + } + "#, + s2: r#" + type Query { + hello: String @override(from: "s1", label: "test") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + hello + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Fetch(service: "s2") { + { + hello + } + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_root_fields() { + let planner = planner!( + s1: r#" + type Query { + hello: String + } + "#, + s2: r#" + type Query { + hello: String @override(from: "s1", label: "test") + } + "#, + ); + assert_plan!( + &planner, + r#" + { + hello + } + "#, + + @r###" + QueryPlan { + Fetch(service: "s1") { + { + hello + } + }, + } + "### + ); +} + +#[test] +fn it_handles_progressive_override_on_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f1 + f2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_handles_progressive_override_on_nested_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t2 { + t { + f1 + } + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + t2 { + t { + __typename + id + } + } + } + }, + Flatten(path: "t2.t") { + Fetch(service: "s2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_does_not_override_unset_labels_on_nested_entity_fields() { + let planner = planner!( + s1: r#" + type Query { + t: T + t2: T2 + } + + type T @key(fields: "id") { + id: ID! + f1: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String @override(from: "s2", label: "test2") + t: T + } + "#, + s2: r#" + type T @key(fields: "id") { + id: ID! + f1: String @override(from: "s1", label: "test") + f2: String + } + + type T2 @key(fields: "id") { + id: ID! + f1: String + f2: String + } + "#, + ); + assert_plan!( + &planner, + r#" + { + t2 { + t { + f1 + } + } + } + "#, + + @r###" + QueryPlan { + Fetch(service: "s1") { + { + t2 { + t { + f1 + } + } + } + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs new file mode 100644 index 0000000000..103017912e --- /dev/null +++ b/apollo-federation/tests/query_plan/build_query_plan_tests/overrides/shareable.rs @@ -0,0 +1,244 @@ +use apollo_federation::query_plan::query_planner::QueryPlanOptions; + +const S1: &str = r#" + type Query { + t: T + } + + type T @key(fields: "id") { + id: ID! + f1: String @shareable + } +"#; + +const S2: &str = r#" + type T @key(fields: "id") { + id: ID! + f1: String @shareable @override(from: "S1", label: "test") + f2: String + } +"#; + +const S3: &str = r#" + type T @key(fields: "id") { + id: ID! + f1: String @shareable + f3: String + } +"#; + +#[test] +fn it_overrides_to_s2_when_label_is_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "S2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_resolves_in_s1_when_label_is_not_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f2 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "S2") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f2 + } + } + }, + }, + }, + } + "### + ); +} + +// This is very similar to the S2 example. The fact that the @override in S2 +// specifies _from_ S1 actually affects all T.f1 fields the same way (except +// S1). That is to say, it's functionally equivalent to have the `@override` +// exist in either S2 or S3 from S2/S3/Sn's perspective. It's helpful to +// test here that the QP will take a path through _either_ S2 or S3 when +// appropriate to do so. In these tests and the previous S2 tests, +// "appropriate" is determined by the other fields being selected in the +// query. +#[test] +fn it_overrides_f1_to_s3_when_label_is_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f3 + } + } + "#, + QueryPlanOptions { + override_conditions: vec!["test".to_string()] + }, + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + } + } + }, + Flatten(path: "t") { + Fetch(service: "S3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f3 + f1 + } + } + }, + }, + }, + } + "### + ); +} + +#[test] +fn it_resolves_f1_in_s1_when_label_is_not_provided() { + let planner = planner!( + S1: S1, + S2: S2, + S3: S3, + ); + assert_plan!( + &planner, + r#" + { + t { + f1 + f3 + } + } + "#, + + @r###" + QueryPlan { + Sequence { + Fetch(service: "S1") { + { + t { + __typename + id + f1 + } + } + }, + Flatten(path: "t") { + Fetch(service: "S3") { + { + ... on T { + __typename + id + } + } => + { + ... on T { + f3 + } + } + }, + }, + }, + } + "### + ); +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_nested_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql new file mode 100644 index 0000000000..206487ca49 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_does_not_override_unset_labels_on_root_fields.graphql @@ -0,0 +1,53 @@ +# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + hello: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql new file mode 100644 index 0000000000..cda69fb905 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_nested_entity_fields.graphql @@ -0,0 +1,73 @@ +# Composed from subgraphs with hash: ea1fd23b849053c0b7cfbf9a192453c71e70f889 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + t: T @join__field(graph: S1) + t2: T2 @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") + f2: String @join__field(graph: S2) +} + +type T2 + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, override: "s2", overrideLabel: "test2") @join__field(graph: S2, overrideLabel: "test2") + t: T @join__field(graph: S1) + f2: String @join__field(graph: S2) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql new file mode 100644 index 0000000000..206487ca49 --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_handles_progressive_override_on_root_fields.graphql @@ -0,0 +1,53 @@ +# Composed from subgraphs with hash: b409284c35003f62c7c83675734478b1970effb2 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "s1", url: "none") + S2 @join__graph(name: "s2", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) +{ + hello: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "s1", overrideLabel: "test") +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_f1_to_s3_when_label_is_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_overrides_to_s2_when_label_is_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_f1_in_s1_when_label_is_not_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql new file mode 100644 index 0000000000..8ee0521f3b --- /dev/null +++ b/apollo-federation/tests/query_plan/supergraphs/it_resolves_in_s1_when_label_is_not_provided.graphql @@ -0,0 +1,66 @@ +# Composed from subgraphs with hash: 5f73656f77e5320839ceba43507ea13060eea5e1 +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) +{ + query: Query +} + +directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE + +directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION + +directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + S1 @join__graph(name: "S1", url: "none") + S2 @join__graph(name: "S2", url: "none") + S3 @join__graph(name: "S3", url: "none") +} + +scalar link__Import + +enum link__Purpose { + """ + `SECURITY` features provide metadata necessary to securely resolve fields. + """ + SECURITY + + """ + `EXECUTION` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query + @join__type(graph: S1) + @join__type(graph: S2) + @join__type(graph: S3) +{ + t: T @join__field(graph: S1) +} + +type T + @join__type(graph: S1, key: "id") + @join__type(graph: S2, key: "id") + @join__type(graph: S3, key: "id") +{ + id: ID! + f1: String @join__field(graph: S1, overrideLabel: "test") @join__field(graph: S2, override: "S1", overrideLabel: "test") @join__field(graph: S3) + f2: String @join__field(graph: S2) + f3: String @join__field(graph: S3) +} diff --git a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs index 7cf8b1ba4a..752479b256 100644 --- a/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs +++ b/apollo-router/src/plugins/demand_control/cost_calculator/static_cost.rs @@ -725,7 +725,9 @@ mod tests { let planner = QueryPlanner::new(schema.federation_supergraph(), Default::default()).unwrap(); - let query_plan = planner.build_query_plan(&query.executable, None).unwrap(); + let query_plan = planner + .build_query_plan(&query.executable, None, Default::default()) + .unwrap(); let schema = DemandControlledSchema::new(Arc::new(schema.supergraph_schema().clone())).unwrap(); diff --git a/apollo-router/src/query_planner/bridge_query_planner.rs b/apollo-router/src/query_planner/bridge_query_planner.rs index 24ad85ee17..9cab43aab8 100644 --- a/apollo-router/src/query_planner/bridge_query_planner.rs +++ b/apollo-router/src/query_planner/bridge_query_planner.rs @@ -12,6 +12,7 @@ use apollo_compiler::validation::Valid; use apollo_compiler::Name; use apollo_federation::error::FederationError; use apollo_federation::error::SingleFederationError; +use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use futures::future::BoxFuture; use opentelemetry_api::metrics::MeterProvider as _; @@ -68,7 +69,6 @@ use crate::Configuration; pub(crate) const RUST_QP_MODE: &str = "rust"; pub(crate) const JS_QP_MODE: &str = "js"; const UNSUPPORTED_CONTEXT: &str = "context"; -const UNSUPPORTED_OVERRIDES: &str = "overrides"; const UNSUPPORTED_FED1: &str = "fed1"; const INTERNAL_INIT_ERROR: &str = "internal"; @@ -203,9 +203,6 @@ impl PlannerMode { metric_rust_qp_init(Some(UNSUPPORTED_FED1)); } SingleFederationError::UnsupportedFeature { message: _, kind } => match kind { - apollo_federation::error::UnsupportedFeatureKind::ProgressiveOverrides => { - metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)) - } apollo_federation::error::UnsupportedFeatureKind::Context => { metric_rust_qp_init(Some(UNSUPPORTED_CONTEXT)) } @@ -298,12 +295,20 @@ impl PlannerMode { let (plan, mut root_node) = tokio::task::spawn_blocking(move || { let start = Instant::now(); + let query_plan_options = QueryPlanOptions { + override_conditions: plan_options.override_conditions, + }; + let result = operation .as_deref() .map(|n| Name::new(n).map_err(FederationError::from)) .transpose() .and_then(|operation| { - rust_planner.build_query_plan(&doc.executable, operation) + rust_planner.build_query_plan( + &doc.executable, + operation, + query_plan_options, + ) }) .map_err(|e| QueryPlannerError::FederationError(e.to_string())); @@ -346,7 +351,7 @@ impl PlannerMode { let start = Instant::now(); let result = js - .plan(filtered_query, operation.clone(), plan_options) + .plan(filtered_query, operation.clone(), plan_options.clone()) .await; let elapsed = start.elapsed().as_secs_f64(); @@ -365,6 +370,9 @@ impl PlannerMode { } } + let query_plan_options = QueryPlanOptions { + override_conditions: plan_options.override_conditions, + }; BothModeComparisonJob { rust_planner: rust.clone(), js_duration: elapsed, @@ -375,6 +383,7 @@ impl PlannerMode { .as_ref() .map(|success| success.data.clone()) .map_err(|e| e.errors.clone()), + plan_options: query_plan_options, } .schedule(); @@ -941,9 +950,9 @@ impl BridgeQueryPlanner { if has_schema_introspection { if has_other_root_fields { let error = graphql::Error::builder() - .message("Mixed queries with both schema introspection and concrete fields are not supported") - .extension_code("MIXED_INTROSPECTION") - .build(); + .message("Mixed queries with both schema introspection and concrete fields are not supported") + .extension_code("MIXED_INTROSPECTION") + .build(); return Ok(QueryPlannerContent::Response { response: Box::new(graphql::Response::builder().error(error).build()), }); @@ -1242,8 +1251,8 @@ mod tests { &doc, query_metrics ) - .await - .unwrap_err(); + .await + .unwrap_err(); match err { QueryPlannerError::EmptyPlan(usage_reporting) => { @@ -1347,7 +1356,7 @@ mod tests { } }}"#); // Aliases - // FIXME: uncomment myName alias when this is fixed: + // FIXME: uncomment myName alias when this is fixed: // https://github.com/apollographql/router/issues/3263 s!(r#"query Q { me { username @@ -1797,13 +1806,6 @@ mod tests { "init.error_kind" = "context", "init.is_success" = false ); - metric_rust_qp_init(Some(UNSUPPORTED_OVERRIDES)); - assert_counter!( - "apollo.router.lifecycle.query_planner.init", - 1, - "init.error_kind" = "overrides", - "init.is_success" = false - ); metric_rust_qp_init(Some(UNSUPPORTED_FED1)); assert_counter!( "apollo.router.lifecycle.query_planner.init", diff --git a/apollo-router/src/query_planner/dual_query_planner.rs b/apollo-router/src/query_planner/dual_query_planner.rs index e368582a97..9952eb9782 100644 --- a/apollo-router/src/query_planner/dual_query_planner.rs +++ b/apollo-router/src/query_planner/dual_query_planner.rs @@ -12,6 +12,7 @@ use apollo_compiler::ast; use apollo_compiler::validation::Valid; use apollo_compiler::ExecutableDocument; use apollo_compiler::Name; +use apollo_federation::query_plan::query_planner::QueryPlanOptions; use apollo_federation::query_plan::query_planner::QueryPlanner; use apollo_federation::query_plan::QueryPlan; @@ -43,6 +44,7 @@ pub(crate) struct BothModeComparisonJob { pub(crate) document: Arc>, pub(crate) operation_name: Option, pub(crate) js_result: Result>>, + pub(crate) plan_options: QueryPlanOptions, } type Queue = crossbeam_channel::Sender; @@ -88,7 +90,9 @@ impl BothModeComparisonJob { let start = Instant::now(); // No question mark operator or macro from here … - let result = self.rust_planner.build_query_plan(&self.document, name); + let result = + self.rust_planner + .build_query_plan(&self.document, name, self.plan_options); let elapsed = start.elapsed().as_secs_f64(); metric_query_planning_plan_duration(RUST_QP_MODE, elapsed); diff --git a/apollo-router/tests/integration/query_planner.rs b/apollo-router/tests/integration/query_planner.rs index 88ace09147..03056ab47d 100644 --- a/apollo-router/tests/integration/query_planner.rs +++ b/apollo-router/tests/integration/query_planner.rs @@ -158,112 +158,6 @@ async fn fed2_schema_with_new_qp() { router.graceful_shutdown().await; } -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp() { - if !graph_os_enabled() { - return; - } - let mut router = IntegrationTest::builder() - .config(LEGACY_QP) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_new_qp() { - if !graph_os_enabled() { - return; - } - let mut router = IntegrationTest::builder() - .config(NEW_QP) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router - .assert_log_contains( - "could not create router: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router.assert_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp_change_to_new_qp_keeps_old_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{NEW_QP}"); - router.update_config(&config).await; - router - .assert_log_contains("error while reloading, continuing with previous configuration") - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - -#[tokio::test(flavor = "multi_thread")] -async fn progressive_override_with_legacy_qp_reload_to_both_best_effort_keep_previous_config() { - if !graph_os_enabled() { - return; - } - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{LEGACY_QP}"); - let mut router = IntegrationTest::builder() - .config(config) - .supergraph("src/plugins/progressive_override/testdata/supergraph.graphql") - .build() - .await; - router.start().await; - router.assert_started().await; - router.execute_default_query().await; - - let config = format!("{PROMETHEUS_METRICS_CONFIG}\n{BOTH_BEST_EFFORT_QP}"); - router.update_config(&config).await; - router - .assert_log_contains( - "Falling back to the legacy query planner: \ - failed to initialize the query planner: \ - `experimental_query_planner_mode: new` or `both` cannot yet \ - be used with progressive overrides. \ - Remove uses of progressive overrides to try the experimental query planner, \ - otherwise switch back to `legacy` or `both_best_effort`.", - ) - .await; - router - .assert_metrics_contains( - r#"apollo_router_lifecycle_query_planner_init_total{init_error_kind="overrides",init_is_success="false",otel_scope_name="apollo/router"} 1"#, - None, - ) - .await; - router.execute_default_query().await; - router.graceful_shutdown().await; -} - #[tokio::test(flavor = "multi_thread")] async fn context_with_legacy_qp() { if !graph_os_enabled() {