From fac9c8c064fd659010a16b747969c44192baf20f Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Wed, 8 Nov 2023 15:07:22 -0800 Subject: [PATCH 1/2] Implement the per-subgraph logic of query graph creation --- Cargo.toml | 1 + src/link/argument.rs | 1 - src/link/federation_spec_definition.rs | 107 +- src/query_graph/build_query_graph.rs | 937 ++++++++++++++++++ .../extract_subgraphs_from_supergraph.rs | 21 +- src/query_graph/mod.rs | 328 +++++- src/schema/mod.rs | 27 +- 7 files changed, 1413 insertions(+), 9 deletions(-) create mode 100644 src/query_graph/build_query_graph.rs diff --git a/Cargo.toml b/Cargo.toml index c83a891e..8012fe3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ indexmap = "2.0.2" thiserror = "1.0" url = "2" lazy_static = "1.4.0" +petgraph = "0.6.4" strum = "0.25.0" strum_macros = "0.25.2" diff --git a/src/link/argument.rs b/src/link/argument.rs index a22d1f0d..f6fb3041 100644 --- a/src/link/argument.rs +++ b/src/link/argument.rs @@ -92,7 +92,6 @@ pub(crate) fn directive_optional_fieldset_argument( } } -#[allow(dead_code)] pub(crate) fn directive_required_fieldset_argument( application: &Node, name: &Name, diff --git a/src/link/federation_spec_definition.rs b/src/link/federation_spec_definition.rs index 893933ec..04401bb5 100644 --- a/src/link/federation_spec_definition.rs +++ b/src/link/federation_spec_definition.rs @@ -1,12 +1,18 @@ use crate::error::{FederationError, SingleFederationError}; +use crate::link::argument::{ + directive_optional_boolean_argument, directive_required_fieldset_argument, +}; use crate::link::spec::{Identity, Url, Version}; use crate::link::spec_definition::{SpecDefinition, SpecDefinitions}; use crate::schema::FederationSchema; use apollo_compiler::ast::Argument; -use apollo_compiler::schema::{Directive, DirectiveDefinition, Name, Value}; +use apollo_compiler::schema::{ + Directive, DirectiveDefinition, ExtendedType, Name, UnionType, Value, +}; use apollo_compiler::{name, Node, NodeStr}; use lazy_static::lazy_static; +pub(crate) const FEDERATION_ENTITY_TYPE_NAME_IN_SPEC: Name = name!("_Entity"); pub(crate) const FEDERATION_KEY_DIRECTIVE_NAME_IN_SPEC: Name = name!("key"); pub(crate) const FEDERATION_INTERFACEOBJECT_DIRECTIVE_NAME_IN_SPEC: Name = name!("interfaceObject"); pub(crate) const FEDERATION_EXTERNAL_DIRECTIVE_NAME_IN_SPEC: Name = name!("external"); @@ -20,6 +26,11 @@ pub(crate) const FEDERATION_RESOLVABLE_ARGUMENT_NAME: Name = name!("resolvable") pub(crate) const FEDERATION_REASON_ARGUMENT_NAME: Name = name!("reason"); pub(crate) const FEDERATION_FROM_ARGUMENT_NAME: Name = name!("from"); +pub(crate) struct KeyDirectiveArguments { + pub(crate) fields: NodeStr, + pub(crate) resolvable: bool, +} + pub(crate) struct FederationSpecDefinition { url: Url, } @@ -34,6 +45,31 @@ impl FederationSpecDefinition { } } + pub(crate) fn entity_type_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result>, FederationError> { + // Note that the _Entity type is special in that: + // 1. Spec renaming doesn't take place for it (there's no prefixing or importing needed), + // in order to maintain backwards compatibility with Fed 1. + // 2. Its presence is optional; if absent, it means the subgraph has no resolvable keys. + match schema + .schema() + .types + .get(&FEDERATION_ENTITY_TYPE_NAME_IN_SPEC) + { + Some(ExtendedType::Union(type_)) => Ok(Some(type_)), + None => Ok(None), + _ => Err(SingleFederationError::Internal { + message: format!( + "Unexpectedly found non-union for federation spec's \"{}\" type definition", + FEDERATION_ENTITY_TYPE_NAME_IN_SPEC + ), + } + .into()), + } + } + pub(crate) fn key_directive_definition<'schema>( &self, schema: &'schema FederationSchema, @@ -76,6 +112,43 @@ impl FederationSpecDefinition { }) } + pub(crate) fn key_directive_arguments( + &self, + application: &Node, + ) -> Result { + Ok(KeyDirectiveArguments { + fields: directive_required_fieldset_argument( + application, + &FEDERATION_FIELDS_ARGUMENT_NAME, + )?, + resolvable: directive_optional_boolean_argument( + application, + &FEDERATION_RESOLVABLE_ARGUMENT_NAME, + )? + .unwrap_or(false), + }) + } + + pub(crate) fn interface_object_directive_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result>, FederationError> { + if *self.version() < (Version { major: 2, minor: 3 }) { + return Ok(None); + } + self.directive_definition(schema, &FEDERATION_INTERFACEOBJECT_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| { + SingleFederationError::Internal { + message: format!( + "Unexpectedly could not find federation spec's \"@{}\" directive definition", + FEDERATION_INTERFACEOBJECT_DIRECTIVE_NAME_IN_SPEC + ), + } + .into() + }) + .map(Some) + } + pub(crate) fn interface_object_directive( &self, schema: &FederationSchema, @@ -97,6 +170,22 @@ impl FederationSpecDefinition { }) } + pub(crate) fn external_directive_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result<&'schema Node, FederationError> { + self.directive_definition(schema, &FEDERATION_EXTERNAL_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| { + SingleFederationError::Internal { + message: format!( + "Unexpectedly could not find federation spec's \"@{}\" directive definition", + FEDERATION_EXTERNAL_DIRECTIVE_NAME_IN_SPEC + ), + } + .into() + }) + } + pub(crate) fn external_directive( &self, schema: &FederationSchema, @@ -120,6 +209,22 @@ impl FederationSpecDefinition { }) } + pub(crate) fn requires_directive_definition<'schema>( + &self, + schema: &'schema FederationSchema, + ) -> Result<&'schema Node, FederationError> { + self.directive_definition(schema, &FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC)? + .ok_or_else(|| { + SingleFederationError::Internal { + message: format!( + "Unexpectedly could not find federation spec's \"@{}\" directive definition", + FEDERATION_REQUIRES_DIRECTIVE_NAME_IN_SPEC + ), + } + .into() + }) + } + pub(crate) fn requires_directive( &self, schema: &FederationSchema, diff --git a/src/query_graph/build_query_graph.rs b/src/query_graph/build_query_graph.rs new file mode 100644 index 00000000..ac985ec8 --- /dev/null +++ b/src/query_graph/build_query_graph.rs @@ -0,0 +1,937 @@ +use crate::error::{FederationError, SingleFederationError}; +use crate::link::federation_spec_definition::{ + FederationSpecDefinition, KeyDirectiveArguments, FEDERATION_VERSIONS, +}; +use crate::link::spec::Identity; +use crate::link::spec_definition::spec_definitions; +use crate::query_graph::extract_subgraphs_from_supergraph::extract_subgraphs_from_supergraph; +use crate::query_graph::{QueryGraph, QueryGraphEdge, QueryGraphEdgeTransition, QueryGraphNode}; +use crate::schema::position::{ + AbstractTypeDefinitionPosition, FieldDefinitionPosition, InterfaceTypeDefinitionPosition, + ObjectFieldDefinitionPosition, ObjectTypeDefinitionPosition, SchemaRootDefinitionKind, + SchemaRootDefinitionPosition, UnionTypeDefinitionPosition, +}; +use crate::schema::FederationSchema; +use apollo_compiler::executable::SelectionSet; +use apollo_compiler::schema::{Component, Directive, ExtendedType, Name, NamedType}; +use apollo_compiler::{NodeStr, Schema}; +use indexmap::{IndexMap, IndexSet}; +use petgraph::graph::NodeIndex; +use petgraph::Direction; +use std::ops::Deref; +use std::sync::Arc; +use strum::IntoEnumIterator; + +// Builds a "federated" query graph based on the provided supergraph and API schema. +// +// A federated query graph is one that is used to reason about queries made by a router against a +// set of federated subgraph services. +// +// Assumes the given schemas have been validated. +pub fn build_federated_query_graph( + supergraph_schema: Schema, + api_schema: Arc, + validate_extracted_subgraphs: Option, + for_query_planning: Option, +) -> Result { + let for_query_planning = for_query_planning.unwrap_or(true); + let mut query_graph = QueryGraph { + // Note this name is a dummy initial name that gets overridden as we build the query graph. + name: NodeStr::new(""), + graph: Default::default(), + sources: Default::default(), + types_to_nodes_by_source: Default::default(), + root_kinds_to_nodes_by_source: Default::default(), + non_trivial_followup_edges: Default::default(), + }; + let subgraphs = + extract_subgraphs_from_supergraph(supergraph_schema, validate_extracted_subgraphs)?; + for (subgraph_name, subgraph) in subgraphs { + let federation_link = &subgraph + .schema + .metadata() + .as_ref() + .and_then(|metadata| metadata.for_identity(&Identity::federation_identity())) + .ok_or_else(|| SingleFederationError::Internal { + message: "Subgraph unexpectedly does not use federation spec".to_owned(), + })?; + let federation_spec_definition = spec_definitions(FEDERATION_VERSIONS.deref())? + .find(&federation_link.url.version) + .ok_or_else(|| SingleFederationError::Internal { + message: "Subgraph unexpectedly does not use a supported federation spec version" + .to_owned(), + })?; + let builder = SchemaQueryGraphBuilder::new( + query_graph, + NodeStr::new(&subgraph_name), + subgraph.schema, + Some(SchemaQueryGraphBuilderSubgraphData { + federation_spec_definition, + api_schema: api_schema.clone(), + }), + for_query_planning, + ); + query_graph = builder.build()?; + } + + Ok(query_graph) +} +struct BaseQueryGraphBuilder { + source: NodeStr, + query_graph: QueryGraph, +} + +impl BaseQueryGraphBuilder { + fn new(mut query_graph: QueryGraph, source: NodeStr, schema: FederationSchema) -> Self { + query_graph.name = source.clone(); + query_graph.sources.insert(source.clone(), schema); + query_graph + .types_to_nodes_by_source + .insert(source.clone(), IndexMap::new()); + query_graph + .root_kinds_to_nodes_by_source + .insert(source.clone(), IndexMap::new()); + Self { + source, + query_graph, + } + } + + fn build(self) -> QueryGraph { + self.query_graph + } + + fn add_edge( + &mut self, + head: NodeIndex, + tail: NodeIndex, + transition: QueryGraphEdgeTransition, + conditions: Option, + ) -> Result<(), FederationError> { + self.query_graph.graph.add_edge( + head, + tail, + QueryGraphEdge { + transition, + conditions, + }, + ); + let head_weight = self.query_graph.node_weight(head)?; + let tail_weight = self.query_graph.node_weight(tail)?; + if head_weight.source != tail_weight.source { + self.mark_has_reachable_cross_subgraph_edges_for_ancestors(head)?; + } + Ok(()) + } + + fn mark_has_reachable_cross_subgraph_edges_for_ancestors( + &mut self, + from: NodeIndex, + ) -> Result<(), FederationError> { + let from_weight = self.query_graph.node_weight(from)?; + // When we mark a node, we mark all of its "ancestor" nodes, so if we get a node already + // marked, there is nothing more to do. + if from_weight.has_reachable_cross_subgraph_edges { + return Ok(()); + } + let mut stack = vec![from]; + while let Some(next) = stack.pop() { + let next_weight = self.query_graph.node_weight_mut(next)?; + next_weight.has_reachable_cross_subgraph_edges = true; + let next_weight = self.query_graph.node_weight(next)?; + for head in self + .query_graph + .graph + .neighbors_directed(next, Direction::Incoming) + { + let head_weight = self.query_graph.node_weight(head)?; + // Again, no point in redoing work as soon as we read an already-marked node. We + // also only follow in-edges within the same subgraph, as nodes on other subgraphs + // will have been marked with their own cross-subgraph edges. + if head_weight.source == next_weight.source + && !head_weight.has_reachable_cross_subgraph_edges + { + stack.push(head); + } + } + } + Ok(()) + } + + fn create_new_node(&mut self, type_name: NamedType) -> Result { + let node = self.query_graph.graph.add_node(QueryGraphNode { + type_: type_name.clone(), + source: self.source.clone(), + has_reachable_cross_subgraph_edges: false, + provide_id: None, + root_kind: None, + }); + let types_to_nodes = self.query_graph.types_to_nodes_mut()?; + if !types_to_nodes.contains_key(&type_name) { + types_to_nodes.insert(type_name.clone(), IndexSet::new()); + } + let nodes = + types_to_nodes + .get_mut(&type_name) + .ok_or_else(|| SingleFederationError::Internal { + message: "Type's node set unexpectedly missing when adding node".to_owned(), + })?; + nodes.insert(node); + Ok(node) + } + + fn create_root_node( + &mut self, + type_name: NamedType, + root_kind: SchemaRootDefinitionKind, + ) -> Result { + let node = self.create_new_node(type_name)?; + self.set_as_root(node, root_kind)?; + Ok(node) + } + + fn set_as_root( + &mut self, + node: NodeIndex, + root_kind: SchemaRootDefinitionKind, + ) -> Result<(), FederationError> { + let node_weight = self.query_graph.node_weight_mut(node)?; + node_weight.root_kind = Some(root_kind.clone()); + let root_kinds_to_nodes = self.query_graph.root_kinds_to_nodes_mut()?; + root_kinds_to_nodes.insert(root_kind, node); + Ok(()) + } +} + +struct SchemaQueryGraphBuilder { + base: BaseQueryGraphBuilder, + subgraph: Option, + for_query_planning: bool, +} + +struct SchemaQueryGraphBuilderSubgraphData { + federation_spec_definition: &'static FederationSpecDefinition, + api_schema: Arc, +} + +impl SchemaQueryGraphBuilder { + fn new( + query_graph: QueryGraph, + source: NodeStr, + schema: FederationSchema, + subgraph: Option, + for_query_planning: bool, + ) -> Self { + let base = BaseQueryGraphBuilder::new(query_graph, source, schema); + SchemaQueryGraphBuilder { + base, + subgraph, + for_query_planning, + } + } + + fn build(mut self) -> Result { + // PORT_NOTE: Note that most of the JS code's buildGraphInternal() logic was moved into this + // build() method. + for root_kind in SchemaRootDefinitionKind::iter() { + let pos = SchemaRootDefinitionPosition { + root_kind: root_kind.clone(), + }; + if pos + .try_get(self.base.query_graph.schema()?.schema()) + .is_some() + { + self.add_recursively_from_root(pos)?; + } + } + if self.subgraph.is_some() { + self.add_interface_entity_edges()?; + } + if self.for_query_planning { + self.add_additional_abstract_type_edges()?; + } + Ok(self.base.query_graph) + } + + fn is_external( + &self, + field_definition_position: FieldDefinitionPosition, + ) -> Result { + // TODO: Should port JS ExternalTester for this, as we're missing some fields which are + // effectively external. + Ok(if let Some(subgraph) = &self.subgraph { + let external_directive_definition = subgraph + .federation_spec_definition + .external_directive_definition(self.base.query_graph.schema()?)?; + field_definition_position + .get(self.base.query_graph.schema()?.schema())? + .directives + .iter() + .any(|d| d.name == external_directive_definition.name) + } else { + false + }) + } + + // Adds a node for the provided root object type (marking that node as a root node for the + // provided `kind`) and recursively descends into the type definition to add the related nodes + // and edges. + // + // In other words, calling this method on, say, the root query type of a schema will add nodes + // and edges for all the types reachable from that root query type. + fn add_recursively_from_root( + &mut self, + root: SchemaRootDefinitionPosition, + ) -> Result<(), FederationError> { + let root_type_name = root.get(self.base.query_graph.schema()?.schema())?.clone(); + let node = self.add_type_recursively(&root_type_name)?; + self.base.set_as_root(node, root.root_kind.clone()) + } + + // Adds in a node for the provided type in the in-building query graph, and recursively adds + // edges and nodes corresponding to the type definition (so for object types, it will add edges + // for each field and recursively add nodes for each field's type, etc...). + fn add_type_recursively( + &mut self, + type_name: &NamedType, + ) -> Result { + if let Some(existing) = self.base.query_graph.types_to_nodes()?.get(type_name) { + if let Some(first_node) = existing.first() { + return if existing.len() == 1 { + Ok(*first_node) + } else { + Err(SingleFederationError::Internal { + message: format!( + "Only one node should have been created for type \"{}\", got {}", + type_name, + existing.len(), + ), + } + .into()) + }; + } + } + let node = self.base.create_new_node(type_name.clone())?; + let type_ = self + .base + .query_graph + .schema()? + .schema() + .types + .get(type_name) + .ok_or_else(|| SingleFederationError::Internal { + message: format!( + "Type \"{}\" unexpectedly did not exist in the schema", + type_name + ), + })?; + match type_ { + ExtendedType::Object(_) => { + let pos = ObjectTypeDefinitionPosition { + type_name: type_name.clone(), + }; + self.add_object_type_edges(pos, node)?; + } + ExtendedType::Interface(_) => { + let pos = InterfaceTypeDefinitionPosition { + type_name: type_name.clone(), + }; + // For interfaces, we generally don't add direct edges for their fields. Because in + // general, the subgraph where a particular field can be fetched from may depend on + // the runtime implementation. However, if the subgraph we're currently including + // "provides" a particular interface field locally *for all the supergraph + // interface's implementations* (in other words, we know we can always ask the field + // to that subgraph directly on the interface and will never miss anything), then we + // can add a direct edge to the field for the interface in that subgraph (which + // avoids unnecessary type exploding in practice). + if self.subgraph.is_some() { + self.maybe_add_interface_fields_edges(pos.clone(), node)?; + } + self.add_abstract_type_edges(pos.clone().into(), node)?; + } + ExtendedType::Union(_) => { + let pos = UnionTypeDefinitionPosition { + type_name: type_name.clone(), + }; + // Add the special-case __typename edge for unions. + self.add_edge_for_field(pos.introspection_typename_field().into(), node, false)?; + self.add_abstract_type_edges(pos.clone().into(), node)?; + } + // Any other case (scalar or enum; input objects are not possible here) is terminal and + // has no edges to consider. + _ => {} + } + Ok(node) + } + + fn add_object_type_edges( + &mut self, + object_type_definition_position: ObjectTypeDefinitionPosition, + head: NodeIndex, + ) -> Result<(), FederationError> { + let type_ = + object_type_definition_position.get(self.base.query_graph.schema()?.schema())?; + let is_interface_object = if let Some(subgraph) = &self.subgraph { + let interface_object_directive_definition = subgraph + .federation_spec_definition + .interface_object_directive_definition(self.base.query_graph.schema()?)? + .ok_or_else(|| SingleFederationError::Internal { + message: "Interface object directive definition unexpectedly missing" + .to_owned(), + })?; + type_ + .directives + .iter() + .any(|d| d.name == interface_object_directive_definition.name) + } else { + false + }; + + // Add edges to the query graph for each field. Note subgraph extraction adds the _entities + // field to subgraphs, so when we recursively handle that field on the root query type, we + // ensure that all entities are part of the graph (even if they are not reachable by any + // other user operations). + // + // Note that FederationSchema ensures there are no introspection fields in "fields", but + // we do handle __typename as a special case below. + let fields = type_.fields.keys().cloned().collect::>(); + for field_name in fields { + // Fields marked @external only exist to ensure subgraph schemas are valid GraphQL, but + // they don't create actual edges. However, even if we don't add an edge, we still want + // to add the field's type. The reason is that while we don't add a "general" edge for + // an external field, we may later add path-specific edges for the field due to a + // `@provides`. When we do so, we need the node corresponding to that field type to + // exist, and in rare cases a type could be only mentioned in this external field, so if + // we don't add the type here, we never do and get issues later when we add @provides + // edges. + let pos = object_type_definition_position.field(field_name); + let is_external = self.is_external(pos.clone().into())?; + self.add_edge_for_field(pos.into(), head, is_external)?; + } + // We add an edge for the built-in __typename field. For instance, it's perfectly valid to + // query __typename manually, so we want to have an edge for it. + // + // However, note that @interfaceObject types are an exception to the rule of "it's perfectly + // valid to query __typename". More precisely, a query can ask for the `__typename` of + // anything, but it shouldn't be answered by an @interfaceObject and so we don't add an + // edge in that case, ensuring the query planner has to get it from another subgraph (than + // the one with said @interfaceObject). + if !is_interface_object { + let pos = object_type_definition_position.introspection_typename_field(); + self.add_edge_for_field(pos.into(), head, false)?; + } + + Ok(()) + } + + fn add_edge_for_field( + &mut self, + field_definition_position: FieldDefinitionPosition, + head: NodeIndex, + skip_edge: bool, + ) -> Result<(), FederationError> { + let field = field_definition_position.get(self.base.query_graph.schema()?.schema())?; + let field_base_type = field.ty.inner_named_type().clone(); + let tail = self.add_type_recursively(&field_base_type)?; + let transition = QueryGraphEdgeTransition::FieldCollection { + source: self.base.source.clone(), + field_definition_position, + is_part_of_provide: false, + }; + if !skip_edge { + self.base.add_edge(head, tail, transition, None)?; + } + Ok(()) + } + + fn maybe_add_interface_fields_edges( + &mut self, + interface_type_definition_position: InterfaceTypeDefinitionPosition, + head: NodeIndex, + ) -> Result<(), FederationError> { + let Some(subgraph) = &self.subgraph else { + return Err(SingleFederationError::Internal { + message: "Missing subgraph data when building subgraph query graph".to_owned(), + } + .into()); + }; + // In theory, the interface might have been marked inaccessible and not be in the API + // schema. If that's the case, we just don't add direct edges at all (adding interface edges + // is an optimization and if the interface is inaccessible, it probably doesn't play any + // role in query planning anyway, so it doesn't matter). + if interface_type_definition_position + .try_get(subgraph.api_schema.schema()) + .is_none() + { + return Ok(()); + } + + let api_runtime_type_positions = subgraph + .api_schema + .referencers() + .get_interface_type(&interface_type_definition_position.type_name)? + .object_types + .clone(); + // Note that it's possible that the current subgraph does not even know some of the possible + // runtime types of the API schema. But as edges to interfaces can only come from the + // current subgraph, it does mean that whatever field led to this interface was resolved in + // this subgraph and can never return one of those unknown runtime types. So we can ignore + // them. + // + // TODO: We *must* revisit this once we fully add @key for interfaces as it will invalidate + // the "edges to interfaces can only come from the current subgraph". Most likely, _if_ an + // interface has a key, then we should return early from this function (add no field edges + // at all) if the subgraph doesn't know of at least one implementation. + let mut local_runtime_type_positions = Vec::new(); + for api_runtime_type_position in &api_runtime_type_positions { + if api_runtime_type_position + .try_get(self.base.query_graph.schema()?.schema()) + .is_some() + { + local_runtime_type_positions.push(api_runtime_type_position) + } + } + let type_ = + interface_type_definition_position.get(self.base.query_graph.schema()?.schema())?; + + // Same as for objects, we add edges to the query graph for each field. + // + // Note that FederationSchema ensures there are no introspection fields in "fields", but + // we do handle __typename as a special case below. + let fields = type_.fields.keys().cloned().collect::>(); + for field_name in fields { + // To include the field, it must not be external itself, and it must be provided on + // all of the local runtime types. + let pos = interface_type_definition_position.field(field_name.clone()); + let is_external = self.is_external(pos.clone().into())?; + let mut is_provided_by_all_local_types = true; + for local_runtime_type in &local_runtime_type_positions { + if !self + .is_directly_provided_by_type(local_runtime_type.field(field_name.clone()))? + { + is_provided_by_all_local_types = false; + } + } + if is_external || !is_provided_by_all_local_types { + continue; + } + self.add_edge_for_field(pos.into(), head, false)?; + } + // Same as for objects, we add an edge for the built-in __typename field. + // + // Note that __typename will never be external and will always provided by all local runtime + // types, so we unconditionally add the edge here. + self.add_edge_for_field( + interface_type_definition_position + .introspection_typename_field() + .into(), + head, + false, + ) + } + + fn is_directly_provided_by_type( + &self, + object_field_definition_position: ObjectFieldDefinitionPosition, + ) -> Result { + // The field is directly provided if: + // 1) the type does have it. + // 2) it is not external. + // 3) it does not have a @requires (essentially, this method is called on type + // implementations of an interface to decide if we can avoid type-explosion, but if the + // field has a @requires on an implementation, then we need to type-explode to make + // sure we handle that @requires). + if object_field_definition_position + .try_get(self.base.query_graph.schema()?.schema()) + .is_none() + { + return Ok(false); + } + let is_external = self.is_external(object_field_definition_position.clone().into())?; + let has_requires = if let Some(subgraph) = &self.subgraph { + let requires_directive_definition = subgraph + .federation_spec_definition + .requires_directive_definition(self.base.query_graph.schema()?)?; + object_field_definition_position + .get(self.base.query_graph.schema()?.schema())? + .directives + .iter() + .any(|d| d.name == requires_directive_definition.name) + } else { + false + }; + Ok(!is_external && !has_requires) + } + + fn add_abstract_type_edges( + &mut self, + abstract_type_definition_position: AbstractTypeDefinitionPosition, + head: NodeIndex, + ) -> Result<(), FederationError> { + let implementations = self + .base + .query_graph + .schema()? + .possible_runtime_types(abstract_type_definition_position.clone().into())?; + for pos in implementations { + let tail = self.add_type_recursively(&pos.type_name)?; + let transition = QueryGraphEdgeTransition::Downcast { + source: self.base.source.clone(), + from_type_position: abstract_type_definition_position.clone().into(), + to_type_position: pos.into(), + }; + self.base.add_edge(head, tail, transition, None)?; + } + Ok(()) + } + + // We've added edges that avoid type-explosion _directly_ from an interface, but it means that + // so far we always type-explode unions to all their implementation types, and always + // type-explode when we go through 2 unrelated interfaces. For instance, say we have + // ``` + // type Query { + // i1: I1 + // i2: I2 + // u: U + // } + // + // interface I1 { + // x: Int + // } + // + // interface I2 { + // y: Int + // } + // + // type A implements I1 & I2 { + // x: Int + // y: Int + // } + // + // type B implements I1 & I2 { + // x: Int + // y: Int + // } + // + // union U = A | B + // ``` + // If we query: + // ``` + // { + // u { + // ... on I1 { + // x + // } + // } + // } + // ``` + // then we currently have no edge between `U` and `I1` whatsoever, so query planning would have + // to type-explode `U` even though that's not necessary (assuming everything is in the same + // subgraph, we'd want to send the query "as-is"). + // Same thing for: + // ``` + // { + // i1 { + // x + // ... on I2 { + // y + // } + // } + // } + // ``` + // due to not having edges from `I1` to `I2` (granted, in that example, type-exploding is not + // all that worse, but it gets worse with more implementations/fields). + // + // And so this method is about adding such edges. Essentially, every time 2 abstract types have + // an intersection of runtime types > 1, we add an edge. + // + // Do note that in practice we only add those edges when we build a query graph for query + // planning purposes, because not type-exploding is only an optimization but type-exploding will + // always "work" and for composition validation, we don't care about being optimal, while + // limiting edges make validation faster by limiting the choices to explore. Also, query + // planning is careful, as it walks those edges, to compute the actual possible runtime types we + // could have to avoid later type-exploding in impossible runtime types. + fn add_additional_abstract_type_edges(&mut self) -> Result<(), FederationError> { + // As mentioned above, we only care about this on subgraph query graphs during query + // planning. But if this ever gets called in some other code path, ignore this. + let Some(subgraph) = &self.subgraph else { + return Ok(()); + }; + + // For each abstract type in the schema, compute its runtime types. + let mut abstract_types_with_runtime_types = Vec::new(); + for (type_name, type_) in &self.base.query_graph.schema()?.schema().types { + let pos: AbstractTypeDefinitionPosition = match type_ { + ExtendedType::Interface(_) => InterfaceTypeDefinitionPosition { + type_name: type_name.clone(), + } + .into(), + ExtendedType::Union(_) => UnionTypeDefinitionPosition { + type_name: type_name.clone(), + } + .into(), + _ => continue, + }; + // All "normal" types from subgraphs should be in the API schema, but there are a + // couple exceptions: + // - Subgraphs have the `_Entity` type, which is not in the API schema. + // - Types marked @inaccessible also won't be in the API schema. + // In those cases, we don't create any additional edges for those types. For + // inaccessible types, we could theoretically try to add them, but we would need the + // full supergraph while we currently only have access to the API schema, and besides, + // inaccessible types can only be part of the query execution in indirect ways (e.g. + // through some @requires), and you'd need pretty weird @requires for the + // optimization here to ever matter. + match subgraph.api_schema.schema().types.get(type_name) { + Some(ExtendedType::Interface(_)) => {} + Some(ExtendedType::Union(_)) => {} + None => continue, + _ => { + return Err(SingleFederationError::Internal { + message: format!( + "Type \"{}\" was abstract in subgraph but not in API schema", + type_name, + ), + } + .into()); + } + } + abstract_types_with_runtime_types.push(AbstractTypeWithRuntimeTypes { + abstract_type_definition_position: pos.clone(), + subgraph_runtime_type_positions: self + .base + .query_graph + .schema()? + .possible_runtime_types(pos.clone().into())?, + api_runtime_type_positions: subgraph + .api_schema + .possible_runtime_types(pos.clone().into())?, + }); + } + + // Check every pair of abstract types that intersect on at least 2 runtime types to see if + // we have edges to add. Note that in practice, we only care about 'Union -> Interface' and + // 'Interface -> Interface'. + for (i, t1) in abstract_types_with_runtime_types.iter().enumerate() { + // Note that in general, t1 is already part of the graph, so `add_type_recursively()` + // doesn't really add anything, it just returns the existing node. That said, if t1 is + // returned by no field (at least no field reachable from a root type), that type will + // not be part of the graph. And in that case, we do add it. And it's actually + // possible that we don't create any edge to that created node, so we may be creating a + // disconnected subset of the graph, a part that is not reachable from any root. It's + // not optimal, but it's a bit hard to avoid in the first place (we could also try to + // purge such subsets after this method, but it's probably not worth it in general) and + // it's not a big deal: it will just use a bit more memory than necessary, and it's + // probably pretty rare in the first place. + let t1_node = + self.add_type_recursively(t1.abstract_type_definition_position.type_name())?; + for (j, t2) in abstract_types_with_runtime_types.iter().enumerate() { + if j > i { + break; + } + + // PORT_NOTE: The JS code skipped cases where interfaces implemented other + // interfaces, claiming this was handled already by add_abstract_type_edges(). + // However, this was not actually handled by that function, so we fix the bug here + // by not early-returning for interfaces implementing interfaces. + + let mut add_t1_to_t2 = false; + let mut add_t2_to_t1 = false; + if t1.abstract_type_definition_position == t2.abstract_type_definition_position { + // We always add an edge from a type to itself. This is just saying that if + // we're type-casting to the type we're already on, it's doing nothing, and in + // particular it shouldn't force us to type-explode anymore that if we didn't + // have the cast in the first place. Note that we only set `add_t1_To_t2` to + // true, otherwise we'd be adding the same edge twice. + add_t1_to_t2 = true; + } else { + // Otherwise, there is 2 aspects to take into account: + // - It's only worth adding an edge between types, meaning that we might save + // type-exploding into the runtime types of the target/"to" one, if the local + // intersection (of runtime types, in the current subgraph) for the abstract + // types is more than 2. If it's just 1 type, then going to that type directly + // is not less efficient and is more precise in a sense. And if the + // intersection is empty, then no point in polluting the query graphs with + // edges we'll never take. + // - _But_ we can only save type-exploding if that local intersection does not + // exclude any runtime types that are local to the source/"from" type, not + // local to the target/"to" type, *but* are global to the target/"to" type, + // because such types should not be excluded and only type-explosion will + // achieve that (for some concrete examples, see the "merged abstract types + // handling" tests in `build_plan()` tests). In other words, we don't want to + // avoid the type explosion if there is a type in the intersection of the + // local source/"from" runtime types and global target/"to" runtime types that + // is not in the purely local runtime type intersection. + + let intersecting_local_runtime_type_positions = t1 + .subgraph_runtime_type_positions + .intersection(&t2.subgraph_runtime_type_positions) + .collect::>(); + if intersecting_local_runtime_type_positions.len() >= 2 { + let is_in_local_other_type_but_not_local_intersection = |type_pos: &ObjectTypeDefinitionPosition, other_type: &AbstractTypeWithRuntimeTypes| { + other_type.subgraph_runtime_type_positions.contains(type_pos) && + !intersecting_local_runtime_type_positions.contains(type_pos) + }; + // TODO: we're currently _never_ adding the edge if the target/"to" type is + // a union. We shouldn't be doing that, this will genuinely make some cases + // less efficient than they could be (though those cases are admittedly a + // bit convoluted), but this make sense *until* + // https://github.com/apollographql/federation/issues/2256 gets fixed. + // Because until then, we do not properly track unions through composition, + // and that means there is never a difference (in the query planner) between + // a local union definition and the supergraph one, even if that different + // actually exists. And so, never type-exploding in that case is somewhat + // safer, as not-type-exploding is ultimately an optimisation. Please note + // that this is *not* a fix for #2256, and most of the issues created by + // #2256 still needs fixing, but it avoids making it even worth for a few + // corner cases. We should remove the `isUnionType` below once the + // fix for #2256 is implemented. + if !(matches!( + t2.abstract_type_definition_position, + AbstractTypeDefinitionPosition::Union(_) + ) || t2 + .api_runtime_type_positions + .iter() + .any(|rt| is_in_local_other_type_but_not_local_intersection(rt, t1))) + { + add_t1_to_t2 = true; + } + if !(matches!( + t1.abstract_type_definition_position, + AbstractTypeDefinitionPosition::Union(_) + ) || t1 + .api_runtime_type_positions + .iter() + .any(|rt| is_in_local_other_type_but_not_local_intersection(rt, t2))) + { + add_t2_to_t1 = true; + } + } + } + + if add_t1_to_t2 || add_t2_to_t1 { + // Same remark as for t1 above. + let t2_node = self + .add_type_recursively(t2.abstract_type_definition_position.type_name())?; + if add_t1_to_t2 { + let transition = QueryGraphEdgeTransition::Downcast { + source: self.base.source.clone(), + from_type_position: t1.abstract_type_definition_position.clone().into(), + to_type_position: t2.abstract_type_definition_position.clone().into(), + }; + self.base.add_edge(t1_node, t2_node, transition, None)?; + } + if add_t2_to_t1 { + let transition = QueryGraphEdgeTransition::Downcast { + source: self.base.source.clone(), + from_type_position: t2.abstract_type_definition_position.clone().into(), + to_type_position: t1.abstract_type_definition_position.clone().into(), + }; + self.base.add_edge(t2_node, t1_node, transition, None)?; + } + } + } + } + + Ok(()) + } + + // In a subgraph, all entity object types will be "automatically" reachable (from the root query + // type) because of the `_entities` field (it returns `_Entity`, which is a union of all entity + // object types, making those reachable. + // + // However, we also want entity interface types (interfaces with an @key) to be reachable in a + // similar way, because the `_entities` field is also technically the one resolving them, and + // not having them reachable would break plenty of code that assume that by traversing a query + // graph from root, we get to everything that can be queried. + // + // But because GraphQL unions cannot have interface types, they are not part of the `_Entity` + // union (and cannot be). This is ok as far as the typing of the schema goes, because even when + // `_entities` is called to resolve an interface type, it technically returns a concrete object, + // and so, since every implementation of an entity interface is also an entity, this is captured + // by the `_Entity` union. + // + // But it does mean we want to manually add the corresponding edges now for interfaces, or + // @key on interfaces wouldn't work properly (at least, when the interface is not otherwise + // reachable by an operation on the subgraph). + fn add_interface_entity_edges(&mut self) -> Result<(), FederationError> { + let Some(subgraph) = &self.subgraph else { + return Err(SingleFederationError::Internal { + message: "Missing subgraph data when building subgraph query graph".to_owned(), + } + .into()); + }; + let federation_spec_definition = subgraph.federation_spec_definition; + let entity_type_definition = + federation_spec_definition.entity_type_definition(self.base.query_graph.schema()?)?; + // We can ignore this case because if the subgraph has an interface with an @key, then we + // force its implementations to be marked as entity too and so we know that if `_Entity` is + // undefined, then we have no need for entity edges. + let Some(entity_type_definition) = entity_type_definition else { + return Ok(()); + }; + let entity_type_name = entity_type_definition.name.clone(); + let entity_type_node = self.add_type_recursively(&entity_type_name)?; + let key_directive_definition = + federation_spec_definition.key_directive_definition(self.base.query_graph.schema()?)?; + let mut interface_type_names = Vec::new(); + for (type_name, type_) in &self.base.query_graph.schema()?.schema().types { + let ExtendedType::Interface(type_) = type_ else { + continue; + }; + if !resolvable_key_applications( + &type_.directives, + &key_directive_definition.name, + federation_spec_definition, + )? + .is_empty() + { + interface_type_names.push(type_name.clone()); + } + } + for interface_type_name in interface_type_names { + let interface_type_node = self.add_type_recursively(&interface_type_name)?; + let transition = QueryGraphEdgeTransition::Downcast { + source: self.base.source.clone(), + from_type_position: UnionTypeDefinitionPosition { + type_name: entity_type_name.clone(), + } + .into(), + to_type_position: InterfaceTypeDefinitionPosition { + type_name: interface_type_name, + } + .into(), + }; + self.base + .add_edge(entity_type_node, interface_type_node, transition, None)?; + } + + Ok(()) + } +} + +struct AbstractTypeWithRuntimeTypes { + abstract_type_definition_position: AbstractTypeDefinitionPosition, + subgraph_runtime_type_positions: IndexSet, + api_runtime_type_positions: IndexSet, +} + +fn resolvable_key_applications( + directives: &[Component], + key_directive_definition_name: &Name, + federation_spec_definition: &'static FederationSpecDefinition, +) -> Result, FederationError> { + let mut applications = Vec::new(); + for directive in directives { + if directive.name != *key_directive_definition_name { + continue; + } + let key_directive_application = + federation_spec_definition.key_directive_arguments(directive)?; + if !key_directive_application.resolvable { + continue; + } + applications.push(key_directive_application); + } + Ok(applications) +} diff --git a/src/query_graph/extract_subgraphs_from_supergraph.rs b/src/query_graph/extract_subgraphs_from_supergraph.rs index 9778627f..ae348ac3 100644 --- a/src/query_graph/extract_subgraphs_from_supergraph.rs +++ b/src/query_graph/extract_subgraphs_from_supergraph.rs @@ -31,7 +31,7 @@ use std::ops::Deref; // TODO: A lot of common data gets passed around in the functions called by this one, considering // making an e.g. ExtractSubgraphs struct to contain the data (which should also hopefully let us // elide more lifetime parameters). -fn extract_subgraphs_from_supergraph( +pub(super) fn extract_subgraphs_from_supergraph( supergraph_schema: Schema, validate_extracted_subgraphs: Option, ) -> Result { @@ -150,7 +150,8 @@ fn collect_empty_subgraphs<'schema>( join_spec_definition: &JoinSpecDefinition, ) -> Result, FederationError> { let mut subgraphs = FederationSubgraphs::new(); - let graph_directive = join_spec_definition.graph_directive_definition(supergraph_schema)?; + let graph_directive_definition = + join_spec_definition.graph_directive_definition(supergraph_schema)?; let graph_enum = join_spec_definition.graph_enum_definition(supergraph_schema)?; let mut federation_spec_definitions = IndexMap::new(); let mut graph_enum_value_name_to_subgraph_name = IndexMap::new(); @@ -158,7 +159,7 @@ fn collect_empty_subgraphs<'schema>( let graph_application = enum_value_definition .directives .iter() - .find(|d| d.name == graph_directive.name) + .find(|d| d.name == graph_directive_definition.name) .ok_or_else(|| SingleFederationError::InvalidFederationSupergraph { message: format!( "Value \"{}\" of join__Graph enum has no @join__graph directive", @@ -1575,6 +1576,15 @@ impl FederationSubgraphs { } } +impl IntoIterator for FederationSubgraphs { + type Item = as IntoIterator>::Item; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.subgraphs.into_iter() + } +} + lazy_static! { static ref EXECUTABLE_DIRECTIVE_LOCATIONS: IndexSet = { IndexSet::from([ @@ -1724,7 +1734,8 @@ fn add_federation_operations( fields: service_fields, }), )?; - let key_directive = federation_spec_definition.key_directive_definition(&subgraph.schema)?; + let key_directive_definition = + federation_spec_definition.key_directive_definition(&subgraph.schema)?; let entity_members = subgraph .schema .schema() @@ -1737,7 +1748,7 @@ fn add_federation_operations( if !type_ .directives .iter() - .any(|d| d.name == key_directive.name) + .any(|d| d.name == key_directive_definition.name) { return None; } diff --git a/src/query_graph/mod.rs b/src/query_graph/mod.rs index 6f3d83bb..bdc7a255 100644 --- a/src/query_graph/mod.rs +++ b/src/query_graph/mod.rs @@ -1 +1,327 @@ -mod extract_subgraphs_from_supergraph; +use crate::error::{FederationError, SingleFederationError}; +use crate::schema::position::{ + CompositeTypeDefinitionPosition, FieldDefinitionPosition, SchemaRootDefinitionKind, +}; +use crate::schema::FederationSchema; +use apollo_compiler::executable::SelectionSet; +use apollo_compiler::schema::{Name, NamedType}; +use apollo_compiler::NodeStr; +use indexmap::{IndexMap, IndexSet}; +use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex}; +use std::fmt::{Display, Formatter}; + +pub(crate) mod build_query_graph; +pub(crate) mod extract_subgraphs_from_supergraph; + +pub(crate) struct QueryGraphNode { + // The graphQL type this node points to. + pub(crate) type_: NamedType, + // An identifier of the underlying schema containing the `type_` this node points to. This is + // mainly used in federated query graphs, where the `source` is a subgraph name. + pub(crate) source: NodeStr, + // True if there is a cross-subgraph edge that is reachable from this node. + pub(crate) has_reachable_cross_subgraph_edges: bool, + // @provides works by creating duplicates of the node/type involved in the provides and adding + // the provided edges only to those copies. This means that with @provides, you can have more + // than one node per-type-and-subgraph in a query graph. Which is fine, but this `provide_id` + // allows distinguishing if a node was created as part of this @provides duplication or not. The + // value of this field has no other meaning than to be unique per-@provide, and so all the nodes + // copied for a given @provides application will have the same `provide_id`. Overall, this + // mostly exists for debugging visualization. + pub(crate) provide_id: Option, + // If present, this node represents a root node of the corresponding kind. + pub(crate) root_kind: Option, +} + +impl Display for QueryGraphNode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}({})", self.type_, self.source)?; + if let Some(provide_id) = self.provide_id { + write!(f, "-{}", provide_id)?; + } + if self.root_kind.is_some() { + write!(f, "*")?; + } + Ok(()) + } +} + +pub(crate) struct QueryGraphEdge { + // Indicates what kind of edge this is and what the edge does/represents. For instance, if the + // edge represents a field, the `transition` will be a `FieldCollection` transition and will + // link to the definition of the field it represents. + pub(crate) transition: QueryGraphEdgeTransition, + // Optional conditions on an edge. + // + // Conditions are a select of selections (in the GraphQL sense) that the traversal of a query + // graph needs to "collect" (traverse edges with transitions corresponding to those selections) + // in order to be able to collect that edge. + // + // Conditions are primarily used for edges corresponding to @key, in which case they correspond + // to the fields composing the @key. In other words, for an @key edge, conditions basically + // represent the fact that you need the key to be able to use an @key edge. + // + // Outside of keys, @requires edges also rely on conditions. + pub(crate) conditions: Option, +} + +// The type of query graph edge "transition". +// +// An edge transition encodes what the edge corresponds to, in the underlying GraphQL schema. +pub(crate) enum QueryGraphEdgeTransition { + // A field edge, going from (a node for) the field parent type to the field's (base) type. + FieldCollection { + // The name of the schema containing the field. + source: NodeStr, + // The object/interface field being collected. + field_definition_position: FieldDefinitionPosition, + // Whether this field is part of an @provides. + is_part_of_provide: bool, + }, + // A downcast edge, going from a composite type (object, interface, or union) to another + // composite type that intersects that type (i.e. has at least one possible runtime object type + // in common with it). + Downcast { + // The name of the schema containing the from/to types. + source: NodeStr, + // The parent type of the type condition, i.e. the type of the selection set containing + // the type condition. + from_type_position: CompositeTypeDefinitionPosition, + // The type of the type condition, i.e. the type coming after "... on". + to_type_position: CompositeTypeDefinitionPosition, + }, + // A key edge (only found in federated query graphs) going from an entity type in a particular + // subgraph to the same entity type but in another subgraph. Key transition edges _must_ have + // `conditions` corresponding to the key fields. + KeyResolution, + // A root type edge (only found in federated query graphs) going from a root type (query, + // mutation or subscription) of a subgraph to the (same) root type of another subgraph. It + // encodes the fact that if a subgraph field returns a root type, any subgraph can be queried + // from there. + RootTypeResolution { + // The kind of schema root resolved. + root_kind: SchemaRootDefinitionKind, + }, + // A subgraph-entering edge, which is a special case only used for edges coming out of the root + // nodes of "federated" query graphs. It does not correspond to any physical GraphQL elements + // but can be understood as the fact that the router is always free to start querying any of the + // subgraph services as needed. + SubgraphEnteringTransition, + // A "fake" downcast edge (only found in federated query graphs) going from an @interfaceObject + // type to an implementation. This encodes the fact that an @interfaceObject type "stands-in" + // for any possible implementations (in the supergraph) of the corresponding interface. It is + // "fake" because the corresponding edge stays on the @interfaceObject type (this is also why + // the "to type" is only a name: that to/casted type does not actually exist in the subgraph + // in which the corresponding edge will be found). + InterfaceObjectFakeDownCast { + // The name of the schema containing the from type. + source: NodeStr, + // The parent type of the type condition, i.e. the type of the selection set containing + // the type condition. + from_type_position: CompositeTypeDefinitionPosition, + // The type of the type condition, i.e. the type coming after "... on". + to_type_name: Name, + }, +} + +impl QueryGraphEdgeTransition { + pub(crate) fn collect_operation_elements(&self) -> bool { + match self { + QueryGraphEdgeTransition::FieldCollection { .. } => true, + QueryGraphEdgeTransition::Downcast { .. } => true, + QueryGraphEdgeTransition::KeyResolution => false, + QueryGraphEdgeTransition::RootTypeResolution { .. } => false, + QueryGraphEdgeTransition::SubgraphEnteringTransition => false, + QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { .. } => true, + } + } +} + +impl Display for QueryGraphEdgeTransition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + QueryGraphEdgeTransition::FieldCollection { + field_definition_position, + .. + } => { + write!(f, "{}", field_definition_position.field_name()) + } + QueryGraphEdgeTransition::Downcast { + to_type_position, .. + } => { + write!(f, "... on {}", to_type_position.type_name()) + } + QueryGraphEdgeTransition::KeyResolution => { + write!(f, "key()") + } + QueryGraphEdgeTransition::RootTypeResolution { root_kind } => { + write!(f, "{}()", root_kind) + } + QueryGraphEdgeTransition::SubgraphEnteringTransition => { + write!(f, "∅") + } + QueryGraphEdgeTransition::InterfaceObjectFakeDownCast { to_type_name, .. } => { + write!(f, "... on {}", to_type_name) + } + } + } +} + +pub struct QueryGraph { + // The "current" source of the query graph. For query graphs representing a single source graph, + // this will only ever be one value, but it will change for "federated" query graphs while + // they're being built (and after construction, will become FEDERATED_GRAPH_ROOT_SOURCE, which + // is a reserved placeholder value). + name: NodeStr, + // The nodes/edges of the query graph. Note that nodes/edges should never be removed, so indexes + // are immutable when a node/edge is created. + graph: DiGraph, + // The sources on which the query graph was built, which is a set (potentially of size 1) of + // GraphQL schema keyed by the name identifying them. Note that the `source` strings in the + // nodes/edges of a query graph are guaranteed to be valid key in this map. + sources: IndexMap, + // A map (keyed by source) that associates type names of the underlying schema on which this + // query graph was built to each of the nodes that points to a type of that name. Note that for + // a "federated" query graph source, each type name will only map to a single node. + types_to_nodes_by_source: IndexMap>>, + // A map (keyed by source) that associates schema root kinds to root nodes. + root_kinds_to_nodes_by_source: IndexMap>, + // Maps an edge to the possible edges that can follow it "productively", that is without + // creating a trivially inefficient path. + // + // More precisely, this map is equivalent calling to looking at the out edges of a given edge's + // tail node and filtering those edges that "never make sense" after the given edge, which + // mainly amounts to avoiding chaining @key edges when we know there is guaranteed to be a + // better option. As an example, suppose we have 3 subgraphs A, B and C which all defined an + // `@key(fields: "id")` on some entity type `T`. Then it is never interesting to take that @key + // edge from B -> C after A -> B because if we're in A and want to get to C, we can always do + // A -> C (of course, this is only true because it's the "same" key). + // + // See `precompute_non_trivial_followup_edges` for more details on which exact edges are + // filtered. + // + // Lastly, note that the main reason for having this field is that its result is pre-computed. + // Which in turn is done for performance reasons: having the same key defined in multiple + // subgraphs is _the_ most common pattern, and while our later algorithms (composition + // validation and query planning) would know to not select those trivially inefficient + // "detours", they might have to redo those checks many times and pre-computing once it is + // significantly faster (and pretty easy). FWIW, when originally introduced, this optimization + // lowered composition validation on a big composition (100+ subgraphs) from ~4 minutes to + // ~10 seconds. + non_trivial_followup_edges: IndexMap>, +} + +impl QueryGraph { + pub(crate) fn name(&self) -> &str { + &self.name + } + + pub(crate) fn graph(&self) -> &DiGraph { + &self.graph + } + + pub(crate) fn node_weight(&self, node: NodeIndex) -> Result<&QueryGraphNode, FederationError> { + self.graph.node_weight(node).ok_or_else(|| { + SingleFederationError::Internal { + message: "Node unexpectedly missing".to_owned(), + } + .into() + }) + } + + fn node_weight_mut(&mut self, node: NodeIndex) -> Result<&mut QueryGraphNode, FederationError> { + self.graph.node_weight_mut(node).ok_or_else(|| { + SingleFederationError::Internal { + message: "Node unexpectedly missing".to_owned(), + } + .into() + }) + } + + pub(crate) fn edge_weight(&self, edge: EdgeIndex) -> Result<&QueryGraphEdge, FederationError> { + self.graph.edge_weight(edge).ok_or_else(|| { + SingleFederationError::Internal { + message: "Edge unexpectedly missing".to_owned(), + } + .into() + }) + } + + fn edge_weight_mut(&mut self, edge: EdgeIndex) -> Result<&mut QueryGraphEdge, FederationError> { + self.graph.edge_weight_mut(edge).ok_or_else(|| { + SingleFederationError::Internal { + message: "Edge unexpectedly missing".to_owned(), + } + .into() + }) + } + + pub(crate) fn sources(&self) -> &IndexMap { + &self.sources + } + + pub(crate) fn schema(&self) -> Result<&FederationSchema, FederationError> { + self.sources.get(&self.name).ok_or_else(|| { + SingleFederationError::Internal { + message: "schema unexpectedly missing".to_owned(), + } + .into() + }) + } + + pub(crate) fn types_to_nodes( + &self, + ) -> Result<&IndexMap>, FederationError> { + self.types_to_nodes_by_source + .get(&self.name) + .ok_or_else(|| { + SingleFederationError::Internal { + message: "Type-to-nodes map unexpectedly missing".to_owned(), + } + .into() + }) + } + + fn types_to_nodes_mut( + &mut self, + ) -> Result<&mut IndexMap>, FederationError> { + self.types_to_nodes_by_source + .get_mut(&self.name) + .ok_or_else(|| { + SingleFederationError::Internal { + message: "Type-to-nodes map unexpectedly missing".to_owned(), + } + .into() + }) + } + + pub(crate) fn root_kinds_to_nodes( + &self, + ) -> Result<&IndexMap, FederationError> { + self.root_kinds_to_nodes_by_source + .get(&self.name) + .ok_or_else(|| { + SingleFederationError::Internal { + message: "root-kinds-to-nodes map unexpectedly missing".to_owned(), + } + .into() + }) + } + + fn root_kinds_to_nodes_mut( + &mut self, + ) -> Result<&mut IndexMap, FederationError> { + self.root_kinds_to_nodes_by_source + .get_mut(&self.name) + .ok_or_else(|| { + SingleFederationError::Internal { + message: "root-kinds-to-nodes map unexpectedly missing".to_owned(), + } + .into() + }) + } + + pub(crate) fn non_trivial_followup_edges(&self) -> &IndexMap> { + &self.non_trivial_followup_edges + } +} diff --git a/src/schema/mod.rs b/src/schema/mod.rs index ad2ea43f..046fcdcb 100644 --- a/src/schema/mod.rs +++ b/src/schema/mod.rs @@ -1,11 +1,14 @@ +use crate::error::FederationError; use crate::link::LinksMetadata; +use crate::schema::position::{CompositeTypeDefinitionPosition, ObjectTypeDefinitionPosition}; use apollo_compiler::Schema; +use indexmap::IndexSet; use referencer::Referencers; pub(crate) mod position; pub(crate) mod referencer; -pub(crate) struct FederationSchema { +pub struct FederationSchema { schema: Schema, metadata: Option, referencers: Referencers, @@ -23,4 +26,26 @@ impl FederationSchema { pub(crate) fn referencers(&self) -> &Referencers { &self.referencers } + + pub(crate) fn possible_runtime_types( + &self, + composite_type_definition_position: CompositeTypeDefinitionPosition, + ) -> Result, FederationError> { + Ok(match composite_type_definition_position { + CompositeTypeDefinitionPosition::Object(pos) => IndexSet::from([pos]), + CompositeTypeDefinitionPosition::Interface(pos) => self + .referencers() + .get_interface_type(&pos.type_name)? + .object_types + .clone(), + CompositeTypeDefinitionPosition::Union(pos) => pos + .get(self.schema())? + .members + .iter() + .map(|t| ObjectTypeDefinitionPosition { + type_name: t.name.clone(), + }) + .collect::>(), + }) + } } From 2fa79239adeae162efbf5615f07b0015844a91de Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Thu, 9 Nov 2023 14:33:41 -0800 Subject: [PATCH 2/2] Update to wrap Name usages in QueryGraph where needed, and update code to more generally use *_position (which removes a fair amount of lifetimes) --- src/query_graph/build_query_graph.rs | 151 +++-- .../extract_subgraphs_from_supergraph.rs | 583 ++++++++---------- src/query_graph/mod.rs | 40 +- src/schema/mod.rs | 56 +- src/schema/position.rs | 223 +++++++ 5 files changed, 645 insertions(+), 408 deletions(-) diff --git a/src/query_graph/build_query_graph.rs b/src/query_graph/build_query_graph.rs index ac985ec8..eb788e73 100644 --- a/src/query_graph/build_query_graph.rs +++ b/src/query_graph/build_query_graph.rs @@ -5,15 +5,18 @@ use crate::link::federation_spec_definition::{ use crate::link::spec::Identity; use crate::link::spec_definition::spec_definitions; use crate::query_graph::extract_subgraphs_from_supergraph::extract_subgraphs_from_supergraph; -use crate::query_graph::{QueryGraph, QueryGraphEdge, QueryGraphEdgeTransition, QueryGraphNode}; +use crate::query_graph::{ + QueryGraph, QueryGraphEdge, QueryGraphEdgeTransition, QueryGraphNode, QueryGraphNodeType, +}; use crate::schema::position::{ AbstractTypeDefinitionPosition, FieldDefinitionPosition, InterfaceTypeDefinitionPosition, - ObjectFieldDefinitionPosition, ObjectTypeDefinitionPosition, SchemaRootDefinitionKind, - SchemaRootDefinitionPosition, UnionTypeDefinitionPosition, + ObjectFieldDefinitionPosition, ObjectTypeDefinitionPosition, OutputTypeDefinitionPosition, + SchemaRootDefinitionKind, SchemaRootDefinitionPosition, TypeDefinitionPosition, + UnionTypeDefinitionPosition, }; use crate::schema::FederationSchema; use apollo_compiler::executable::SelectionSet; -use apollo_compiler::schema::{Component, Directive, ExtendedType, Name, NamedType}; +use apollo_compiler::schema::{Component, Directive, ExtendedType, Name}; use apollo_compiler::{NodeStr, Schema}; use indexmap::{IndexMap, IndexSet}; use petgraph::graph::NodeIndex; @@ -158,34 +161,30 @@ impl BaseQueryGraphBuilder { Ok(()) } - fn create_new_node(&mut self, type_name: NamedType) -> Result { + fn create_new_node(&mut self, type_: QueryGraphNodeType) -> Result { let node = self.query_graph.graph.add_node(QueryGraphNode { - type_: type_name.clone(), + type_: type_.clone(), source: self.source.clone(), has_reachable_cross_subgraph_edges: false, provide_id: None, root_kind: None, }); - let types_to_nodes = self.query_graph.types_to_nodes_mut()?; - if !types_to_nodes.contains_key(&type_name) { - types_to_nodes.insert(type_name.clone(), IndexSet::new()); + if let QueryGraphNodeType::SubgraphType(pos) = type_ { + self.query_graph + .types_to_nodes_mut()? + .entry(pos.type_name().clone()) + .or_insert_with(IndexSet::new) + .insert(node); } - let nodes = - types_to_nodes - .get_mut(&type_name) - .ok_or_else(|| SingleFederationError::Internal { - message: "Type's node set unexpectedly missing when adding node".to_owned(), - })?; - nodes.insert(node); Ok(node) } fn create_root_node( &mut self, - type_name: NamedType, + type_: QueryGraphNodeType, root_kind: SchemaRootDefinitionKind, ) -> Result { - let node = self.create_new_node(type_name)?; + let node = self.create_new_node(type_)?; self.set_as_root(node, root_kind)?; Ok(node) } @@ -283,8 +282,25 @@ impl SchemaQueryGraphBuilder { &mut self, root: SchemaRootDefinitionPosition, ) -> Result<(), FederationError> { - let root_type_name = root.get(self.base.query_graph.schema()?.schema())?.clone(); - let node = self.add_type_recursively(&root_type_name)?; + let root_type_name = root.get(self.base.query_graph.schema()?.schema())?; + let pos = match self + .base + .query_graph + .schema()? + .get_type(root_type_name.name.clone())? + { + TypeDefinitionPosition::Object(pos) => pos, + _ => { + return Err(SingleFederationError::Internal { + message: format!( + "Root type \"{}\" was unexpectedly not an object type", + root_type_name.name, + ), + } + .into()); + } + }; + let node = self.add_type_recursively(pos.into())?; self.base.set_as_root(node, root.root_kind.clone()) } @@ -293,9 +309,10 @@ impl SchemaQueryGraphBuilder { // for each field and recursively add nodes for each field's type, etc...). fn add_type_recursively( &mut self, - type_name: &NamedType, + output_type_definition_position: OutputTypeDefinitionPosition, ) -> Result { - if let Some(existing) = self.base.query_graph.types_to_nodes()?.get(type_name) { + let type_name = output_type_definition_position.type_name().clone(); + if let Some(existing) = self.base.query_graph.types_to_nodes()?.get(&type_name) { if let Some(first_node) = existing.first() { return if existing.len() == 1 { Ok(*first_node) @@ -311,31 +328,14 @@ impl SchemaQueryGraphBuilder { }; } } - let node = self.base.create_new_node(type_name.clone())?; - let type_ = self - .base - .query_graph - .schema()? - .schema() - .types - .get(type_name) - .ok_or_else(|| SingleFederationError::Internal { - message: format!( - "Type \"{}\" unexpectedly did not exist in the schema", - type_name - ), - })?; - match type_ { - ExtendedType::Object(_) => { - let pos = ObjectTypeDefinitionPosition { - type_name: type_name.clone(), - }; + let node = self.base.create_new_node(QueryGraphNodeType::SubgraphType( + output_type_definition_position.clone(), + ))?; + match output_type_definition_position { + OutputTypeDefinitionPosition::Object(pos) => { self.add_object_type_edges(pos, node)?; } - ExtendedType::Interface(_) => { - let pos = InterfaceTypeDefinitionPosition { - type_name: type_name.clone(), - }; + OutputTypeDefinitionPosition::Interface(pos) => { // For interfaces, we generally don't add direct edges for their fields. Because in // general, the subgraph where a particular field can be fetched from may depend on // the runtime implementation. However, if the subgraph we're currently including @@ -349,10 +349,7 @@ impl SchemaQueryGraphBuilder { } self.add_abstract_type_edges(pos.clone().into(), node)?; } - ExtendedType::Union(_) => { - let pos = UnionTypeDefinitionPosition { - type_name: type_name.clone(), - }; + OutputTypeDefinitionPosition::Union(pos) => { // Add the special-case __typename edge for unions. self.add_edge_for_field(pos.introspection_typename_field().into(), node, false)?; self.add_abstract_type_edges(pos.clone().into(), node)?; @@ -431,8 +428,28 @@ impl SchemaQueryGraphBuilder { skip_edge: bool, ) -> Result<(), FederationError> { let field = field_definition_position.get(self.base.query_graph.schema()?.schema())?; - let field_base_type = field.ty.inner_named_type().clone(); - let tail = self.add_type_recursively(&field_base_type)?; + let tail_pos = match self + .base + .query_graph + .schema()? + .get_type(field.ty.inner_named_type().clone())? + { + TypeDefinitionPosition::Scalar(pos) => pos.into(), + TypeDefinitionPosition::Object(pos) => pos.into(), + TypeDefinitionPosition::Interface(pos) => pos.into(), + TypeDefinitionPosition::Union(pos) => pos.into(), + TypeDefinitionPosition::Enum(pos) => pos.into(), + TypeDefinitionPosition::InputObject(pos) => { + return Err(SingleFederationError::Internal { + message: format!( + "Field \"{}\" has non-output type \"{}\"", + field_definition_position, pos, + ), + } + .into()); + } + }; + let tail = self.add_type_recursively(tail_pos)?; let transition = QueryGraphEdgeTransition::FieldCollection { source: self.base.source.clone(), field_definition_position, @@ -574,7 +591,7 @@ impl SchemaQueryGraphBuilder { .schema()? .possible_runtime_types(abstract_type_definition_position.clone().into())?; for pos in implementations { - let tail = self.add_type_recursively(&pos.type_name)?; + let tail = self.add_type_recursively(pos.clone().into())?; let transition = QueryGraphEdgeTransition::Downcast { source: self.base.source.clone(), from_type_position: abstract_type_definition_position.clone().into(), @@ -724,7 +741,7 @@ impl SchemaQueryGraphBuilder { // it's not a big deal: it will just use a bit more memory than necessary, and it's // probably pretty rare in the first place. let t1_node = - self.add_type_recursively(t1.abstract_type_definition_position.type_name())?; + self.add_type_recursively(t1.abstract_type_definition_position.clone().into())?; for (j, t2) in abstract_types_with_runtime_types.iter().enumerate() { if j > i { break; @@ -811,8 +828,9 @@ impl SchemaQueryGraphBuilder { if add_t1_to_t2 || add_t2_to_t1 { // Same remark as for t1 above. - let t2_node = self - .add_type_recursively(t2.abstract_type_definition_position.type_name())?; + let t2_node = self.add_type_recursively( + t2.abstract_type_definition_position.clone().into(), + )?; if add_t1_to_t2 { let transition = QueryGraphEdgeTransition::Downcast { source: self.base.source.clone(), @@ -871,10 +889,15 @@ impl SchemaQueryGraphBuilder { return Ok(()); }; let entity_type_name = entity_type_definition.name.clone(); - let entity_type_node = self.add_type_recursively(&entity_type_name)?; + let entity_type_node = self.add_type_recursively( + UnionTypeDefinitionPosition { + type_name: entity_type_name.clone(), + } + .into(), + )?; let key_directive_definition = federation_spec_definition.key_directive_definition(self.base.query_graph.schema()?)?; - let mut interface_type_names = Vec::new(); + let mut interface_type_definition_positions = Vec::new(); for (type_name, type_) in &self.base.query_graph.schema()?.schema().types { let ExtendedType::Interface(type_) = type_ else { continue; @@ -886,21 +909,21 @@ impl SchemaQueryGraphBuilder { )? .is_empty() { - interface_type_names.push(type_name.clone()); + interface_type_definition_positions.push(InterfaceTypeDefinitionPosition { + type_name: type_name.clone(), + }); } } - for interface_type_name in interface_type_names { - let interface_type_node = self.add_type_recursively(&interface_type_name)?; + for interface_type_definition_position in interface_type_definition_positions { + let interface_type_node = + self.add_type_recursively(interface_type_definition_position.clone().into())?; let transition = QueryGraphEdgeTransition::Downcast { source: self.base.source.clone(), from_type_position: UnionTypeDefinitionPosition { type_name: entity_type_name.clone(), } .into(), - to_type_position: InterfaceTypeDefinitionPosition { - type_name: interface_type_name, - } - .into(), + to_type_position: interface_type_definition_position.into(), }; self.base .add_edge(entity_type_node, interface_type_node, transition, None)?; diff --git a/src/query_graph/extract_subgraphs_from_supergraph.rs b/src/query_graph/extract_subgraphs_from_supergraph.rs index ae348ac3..b682e6d8 100644 --- a/src/query_graph/extract_subgraphs_from_supergraph.rs +++ b/src/query_graph/extract_subgraphs_from_supergraph.rs @@ -8,8 +8,9 @@ use crate::link::spec::{Identity, Version}; use crate::link::spec_definition::{spec_definitions, SpecDefinition}; use crate::schema::position::{ DirectiveDefinitionPosition, EnumTypeDefinitionPosition, InputObjectFieldDefinitionPosition, - InputObjectTypeDefinitionPosition, InterfaceFieldDefinitionPosition, - InterfaceTypeDefinitionPosition, ObjectFieldDefinitionPosition, ObjectTypeDefinitionPosition, + InputObjectTypeDefinitionPosition, InterfaceTypeDefinitionPosition, + ObjectFieldDefinitionPosition, ObjectOrInterfaceFieldDefinitionPosition, + ObjectOrInterfaceTypeDefinitionPosition, ObjectTypeDefinitionPosition, ScalarTypeDefinitionPosition, SchemaRootDefinitionKind, SchemaRootDefinitionPosition, TypeDefinitionPosition, UnionTypeDefinitionPosition, }; @@ -29,8 +30,7 @@ use std::ops::Deref; // Assumes the given schema has been validated. // // TODO: A lot of common data gets passed around in the functions called by this one, considering -// making an e.g. ExtractSubgraphs struct to contain the data (which should also hopefully let us -// elide more lifetime parameters). +// making an e.g. ExtractSubgraphs struct to contain the data. pub(super) fn extract_subgraphs_from_supergraph( supergraph_schema: Schema, validate_extracted_subgraphs: Option, @@ -42,12 +42,14 @@ pub(super) fn extract_subgraphs_from_supergraph( let (mut subgraphs, federation_spec_definitions, graph_enum_value_name_to_subgraph_name) = collect_empty_subgraphs(&supergraph_schema, join_spec_definition)?; - let mut filtered_types: Vec<&NamedType> = Vec::new(); - for type_name in supergraph_schema.schema().types.keys() { - if !join_spec_definition.is_spec_type_name(&supergraph_schema, type_name)? - && !link_spec_definition.is_spec_type_name(&supergraph_schema, type_name)? + let mut filtered_types = Vec::new(); + for type_definition_position in supergraph_schema.get_types() { + if !join_spec_definition + .is_spec_type_name(&supergraph_schema, type_definition_position.type_name())? + && !link_spec_definition + .is_spec_type_name(&supergraph_schema, type_definition_position.type_name())? { - filtered_types.push(type_name); + filtered_types.push(type_definition_position); } } if is_fed_1 { @@ -140,15 +142,15 @@ fn validate_supergraph(supergraph_schema: Schema) -> Result = ( +type CollectEmptySubgraphsOk = ( FederationSubgraphs, - IndexMap<&'schema Name, &'static FederationSpecDefinition>, - IndexMap<&'schema Name, NodeStr>, + IndexMap, + IndexMap, ); -fn collect_empty_subgraphs<'schema>( - supergraph_schema: &'schema FederationSchema, +fn collect_empty_subgraphs( + supergraph_schema: &FederationSchema, join_spec_definition: &JoinSpecDefinition, -) -> Result, FederationError> { +) -> Result { let mut subgraphs = FederationSubgraphs::new(); let graph_directive_definition = join_spec_definition.graph_directive_definition(supergraph_schema)?; @@ -187,8 +189,9 @@ fn collect_empty_subgraphs<'schema>( .to_owned(), })?; subgraphs.add(subgraph)?; - graph_enum_value_name_to_subgraph_name.insert(enum_value_name, graph_arguments.name); - federation_spec_definitions.insert(enum_value_name, federation_spec_definition); + graph_enum_value_name_to_subgraph_name + .insert(enum_value_name.clone(), graph_arguments.name); + federation_spec_definitions.insert(enum_value_name.clone(), federation_spec_definition); } Ok(( subgraphs, @@ -255,27 +258,27 @@ pub(crate) fn new_empty_fed_2_subgraph_schema() -> Result { - name: &'schema NamedType, +struct TypeInfo { + name: NamedType, // HashMap subgraph_info: IndexMap, } -struct TypeInfos<'schema> { - object_types: Vec>, - interface_types: Vec>, - union_types: Vec>, - enum_types: Vec>, - input_object_types: Vec>, +struct TypeInfos { + object_types: Vec, + interface_types: Vec, + union_types: Vec, + enum_types: Vec, + input_object_types: Vec, } -fn extract_subgraphs_from_fed_2_supergraph<'schema>( - supergraph_schema: &'schema FederationSchema, +fn extract_subgraphs_from_fed_2_supergraph( + supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, - graph_enum_value_name_to_subgraph_name: &IndexMap<&'schema Name, NodeStr>, - federation_spec_definitions: &IndexMap<&Name, &'static FederationSpecDefinition>, + graph_enum_value_name_to_subgraph_name: &IndexMap, + federation_spec_definitions: &IndexMap, join_spec_definition: &'static JoinSpecDefinition, - filtered_types: &Vec<&'schema NamedType>, + filtered_types: &Vec, ) -> Result<(), FederationError> { let TypeInfos { object_types, @@ -386,14 +389,14 @@ fn extract_subgraphs_from_fed_2_supergraph<'schema>( Ok(()) } -fn add_all_empty_subgraph_types<'schema>( - supergraph_schema: &'schema FederationSchema, +fn add_all_empty_subgraph_types( + supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, - graph_enum_value_name_to_subgraph_name: &IndexMap<&'schema Name, NodeStr>, - federation_spec_definitions: &IndexMap<&Name, &'static FederationSpecDefinition>, + graph_enum_value_name_to_subgraph_name: &IndexMap, + federation_spec_definitions: &IndexMap, join_spec_definition: &'static JoinSpecDefinition, - filtered_types: &Vec<&'schema NamedType>, -) -> Result, FederationError> { + filtered_types: &Vec, +) -> Result { let type_directive_definition = join_spec_definition.type_directive_definition(supergraph_schema)?; @@ -403,14 +406,8 @@ fn add_all_empty_subgraph_types<'schema>( let mut enum_types: Vec = Vec::new(); let mut input_object_types: Vec = Vec::new(); - for type_name in filtered_types { - let type_ = supergraph_schema - .schema() - .types - .get(*type_name) - .ok_or_else(|| SingleFederationError::Internal { - message: format!("Type \"{}\" missing from schema", type_name), - })?; + for type_definition_position in filtered_types { + let type_ = type_definition_position.get(supergraph_schema.schema())?; let mut type_directive_applications = Vec::new(); for directive in type_.directives().iter() { if directive.name != type_directive_definition.name { @@ -419,21 +416,18 @@ fn add_all_empty_subgraph_types<'schema>( type_directive_applications .push(join_spec_definition.type_directive_arguments(directive)?); } - match type_ { - ExtendedType::Scalar(_) => { + let types_mut = match &type_definition_position { + TypeDefinitionPosition::Scalar(pos) => { // Scalar are a bit special in that they don't have any sub-component, so we don't // track them beyond adding them to the proper subgraphs. It's also simple because // there is no possible key so there is exactly one @join__type application for each // subgraph having the scalar (and most arguments cannot be present). - for type_directive_application in type_directive_applications { + for type_directive_application in &type_directive_applications { let subgraph = get_subgraph( subgraphs, graph_enum_value_name_to_subgraph_name, &type_directive_application.graph, )?; - let pos = ScalarTypeDefinitionPosition { - type_name: (*type_name).clone(), - }; pos.pre_insert(&mut subgraph.schema)?; pos.insert( &mut subgraph.schema, @@ -444,57 +438,22 @@ fn add_all_empty_subgraph_types<'schema>( }), )?; } + None } - ExtendedType::Object(_) => { - object_types.push(add_empty_type( - type_name, - type_, - &type_directive_applications, - subgraphs, - graph_enum_value_name_to_subgraph_name, - federation_spec_definitions, - )?); - } - ExtendedType::Interface(_) => { - interface_types.push(add_empty_type( - type_name, - type_, - &type_directive_applications, - subgraphs, - graph_enum_value_name_to_subgraph_name, - federation_spec_definitions, - )?); - } - ExtendedType::Union(_) => { - union_types.push(add_empty_type( - type_name, - type_, - &type_directive_applications, - subgraphs, - graph_enum_value_name_to_subgraph_name, - federation_spec_definitions, - )?); - } - ExtendedType::Enum(_) => { - enum_types.push(add_empty_type( - type_name, - type_, - &type_directive_applications, - subgraphs, - graph_enum_value_name_to_subgraph_name, - federation_spec_definitions, - )?); - } - ExtendedType::InputObject(_) => { - input_object_types.push(add_empty_type( - type_name, - type_, - &type_directive_applications, - subgraphs, - graph_enum_value_name_to_subgraph_name, - federation_spec_definitions, - )?); - } + TypeDefinitionPosition::Object(_) => Some(&mut object_types), + TypeDefinitionPosition::Interface(_) => Some(&mut interface_types), + TypeDefinitionPosition::Union(_) => Some(&mut union_types), + TypeDefinitionPosition::Enum(_) => Some(&mut enum_types), + TypeDefinitionPosition::InputObject(_) => Some(&mut input_object_types), + }; + if let Some(types_mut) = types_mut { + types_mut.push(add_empty_type( + type_definition_position.clone(), + &type_directive_applications, + subgraphs, + graph_enum_value_name_to_subgraph_name, + federation_spec_definitions, + )?); } } @@ -507,23 +466,22 @@ fn add_all_empty_subgraph_types<'schema>( }) } -fn add_empty_type<'schema>( - type_name: &'schema NamedType, - type_: &'schema ExtendedType, +fn add_empty_type( + type_definition_position: TypeDefinitionPosition, type_directive_applications: &Vec, subgraphs: &mut FederationSubgraphs, - graph_enum_value_name_to_subgraph_name: &IndexMap<&'schema Name, NodeStr>, - federation_spec_definitions: &IndexMap<&Name, &'static FederationSpecDefinition>, -) -> Result, FederationError> { + graph_enum_value_name_to_subgraph_name: &IndexMap, + federation_spec_definitions: &IndexMap, +) -> Result { // In fed2, we always mark all types with `@join__type` but making sure. if type_directive_applications.is_empty() { return Err(SingleFederationError::InvalidFederationSupergraph { - message: format!("Missing @join__type on \"{}\"", type_name), + message: format!("Missing @join__type on \"{}\"", type_definition_position), } .into()); } let mut type_info = TypeInfo { - name: type_name, + name: type_definition_position.type_name().clone(), subgraph_info: IndexMap::new(), }; for type_directive_application in type_directive_applications { @@ -541,83 +499,19 @@ fn add_empty_type<'schema>( ), })?; - if type_info + if !type_info .subgraph_info .contains_key(&type_directive_application.graph) { - if let Some(key) = &type_directive_application.key { - let mut key_directive = Component::new(federation_spec_definition.key_directive( - &subgraph.schema, - key.clone(), - type_directive_application.resolvable, - )?); - if type_directive_application.extension { - key_directive.origin = - ComponentOrigin::Extension(ExtensionId::new(&key_directive.node)) - } - match subgraph - .schema - .schema() - .types - .get(type_name) - .ok_or_else(|| SingleFederationError::Internal { - message: format!( - "Missing type \"{}\" from subgraph despite it being in type_info", - type_name - ), - })? { - ExtendedType::Scalar(_) => { - return Err(SingleFederationError::Internal { - message: "\"add_empty_type()\" shouldn't be called for scalars" - .to_owned(), - } - .into()); - } - ExtendedType::Object(_) => { - ObjectTypeDefinitionPosition { - type_name: type_name.clone(), - } - .insert_directive(&mut subgraph.schema, key_directive)?; - } - ExtendedType::Interface(_) => { - InterfaceTypeDefinitionPosition { - type_name: type_name.clone(), - } - .insert_directive(&mut subgraph.schema, key_directive)?; - } - ExtendedType::Union(_) => { - UnionTypeDefinitionPosition { - type_name: type_name.clone(), - } - .insert_directive(&mut subgraph.schema, key_directive)?; - } - ExtendedType::Enum(_) => { - EnumTypeDefinitionPosition { - type_name: type_name.clone(), - } - .insert_directive(&mut subgraph.schema, key_directive)?; - } - ExtendedType::InputObject(_) => { - InputObjectTypeDefinitionPosition { - type_name: type_name.clone(), - } - .insert_directive(&mut subgraph.schema, key_directive)?; - } - }; - } - } else { let mut is_interface_object = false; - match type_ { - ExtendedType::Scalar(_) => { + match &type_definition_position { + TypeDefinitionPosition::Scalar(_) => { return Err(SingleFederationError::Internal { message: "\"add_empty_type()\" shouldn't be called for scalars".to_owned(), } .into()); } - ExtendedType::Object(_) => { - let pos = ObjectTypeDefinitionPosition { - type_name: type_name.clone(), - }; + TypeDefinitionPosition::Object(pos) => { pos.pre_insert(&mut subgraph.schema)?; pos.insert( &mut subgraph.schema, @@ -629,36 +523,45 @@ fn add_empty_type<'schema>( fields: Default::default(), }), )?; - if type_name == "Query" { - let pos = SchemaRootDefinitionPosition { + if pos.type_name == "Query" { + let root_pos = SchemaRootDefinitionPosition { root_kind: SchemaRootDefinitionKind::Query, }; - if pos.try_get(subgraph.schema.schema()).is_none() { - pos.insert(&mut subgraph.schema, ComponentName::from(type_name))?; + if root_pos.try_get(subgraph.schema.schema()).is_none() { + root_pos.insert( + &mut subgraph.schema, + ComponentName::from(&pos.type_name), + )?; } - } else if type_name == "Mutation" { - let pos = SchemaRootDefinitionPosition { + } else if pos.type_name == "Mutation" { + let root_pos = SchemaRootDefinitionPosition { root_kind: SchemaRootDefinitionKind::Mutation, }; - if pos.try_get(subgraph.schema.schema()).is_none() { - pos.insert(&mut subgraph.schema, ComponentName::from(type_name))?; + if root_pos.try_get(subgraph.schema.schema()).is_none() { + root_pos.insert( + &mut subgraph.schema, + ComponentName::from(&pos.type_name), + )?; } - } else if type_name == "Subscription" { - let pos = SchemaRootDefinitionPosition { + } else if pos.type_name == "Subscription" { + let root_pos = SchemaRootDefinitionPosition { root_kind: SchemaRootDefinitionKind::Subscription, }; - if pos.try_get(subgraph.schema.schema()).is_none() { - pos.insert(&mut subgraph.schema, ComponentName::from(type_name))?; + if root_pos.try_get(subgraph.schema.schema()).is_none() { + root_pos.insert( + &mut subgraph.schema, + ComponentName::from(&pos.type_name), + )?; } } } - ExtendedType::Interface(_) => { + TypeDefinitionPosition::Interface(pos) => { if type_directive_application.is_interface_object { is_interface_object = true; let interface_object_directive = federation_spec_definition .interface_object_directive(&subgraph.schema)?; let pos = ObjectTypeDefinitionPosition { - type_name: type_name.clone(), + type_name: pos.type_name.clone(), }; pos.pre_insert(&mut subgraph.schema)?; pos.insert( @@ -674,9 +577,6 @@ fn add_empty_type<'schema>( }), )?; } else { - let pos = InterfaceTypeDefinitionPosition { - type_name: type_name.clone(), - }; pos.pre_insert(&mut subgraph.schema)?; pos.insert( &mut subgraph.schema, @@ -690,10 +590,7 @@ fn add_empty_type<'schema>( )?; } } - ExtendedType::Union(_) => { - let pos = UnionTypeDefinitionPosition { - type_name: type_name.clone(), - }; + TypeDefinitionPosition::Union(pos) => { pos.pre_insert(&mut subgraph.schema)?; pos.insert( &mut subgraph.schema, @@ -705,10 +602,7 @@ fn add_empty_type<'schema>( }), )?; } - ExtendedType::Enum(_) => { - let pos = EnumTypeDefinitionPosition { - type_name: type_name.clone(), - }; + TypeDefinitionPosition::Enum(pos) => { pos.pre_insert(&mut subgraph.schema)?; pos.insert( &mut subgraph.schema, @@ -720,10 +614,7 @@ fn add_empty_type<'schema>( }), )?; } - ExtendedType::InputObject(_) => { - let pos = InputObjectTypeDefinitionPosition { - type_name: type_name.clone(), - }; + TypeDefinitionPosition::InputObject(pos) => { pos.pre_insert(&mut subgraph.schema)?; pos.insert( &mut subgraph.schema, @@ -740,19 +631,57 @@ fn add_empty_type<'schema>( type_directive_application.graph.clone(), is_interface_object, ); - }; + } + + if let Some(key) = &type_directive_application.key { + let mut key_directive = Component::new(federation_spec_definition.key_directive( + &subgraph.schema, + key.clone(), + type_directive_application.resolvable, + )?); + if type_directive_application.extension { + key_directive.origin = + ComponentOrigin::Extension(ExtensionId::new(&key_directive.node)) + } + let subgraph_type_definition_position = subgraph + .schema + .get_type(type_definition_position.type_name().clone())?; + match &subgraph_type_definition_position { + TypeDefinitionPosition::Scalar(_) => { + return Err(SingleFederationError::Internal { + message: "\"add_empty_type()\" shouldn't be called for scalars".to_owned(), + } + .into()); + } + TypeDefinitionPosition::Object(pos) => { + pos.insert_directive(&mut subgraph.schema, key_directive)?; + } + TypeDefinitionPosition::Interface(pos) => { + pos.insert_directive(&mut subgraph.schema, key_directive)?; + } + TypeDefinitionPosition::Union(pos) => { + pos.insert_directive(&mut subgraph.schema, key_directive)?; + } + TypeDefinitionPosition::Enum(pos) => { + pos.insert_directive(&mut subgraph.schema, key_directive)?; + } + TypeDefinitionPosition::InputObject(pos) => { + pos.insert_directive(&mut subgraph.schema, key_directive)?; + } + }; + } } Ok(type_info) } -fn extract_object_type_content<'schema>( - supergraph_schema: &'schema FederationSchema, +fn extract_object_type_content( + supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, - graph_enum_value_name_to_subgraph_name: &IndexMap<&'schema Name, NodeStr>, - federation_spec_definitions: &IndexMap<&Name, &'static FederationSpecDefinition>, + graph_enum_value_name_to_subgraph_name: &IndexMap, + federation_spec_definitions: &IndexMap, join_spec_definition: &JoinSpecDefinition, - info: &[TypeInfo<'schema>], + info: &[TypeInfo], ) -> Result<(), FederationError> { let field_directive_definition = join_spec_definition.field_directive_definition(supergraph_schema)?; @@ -803,6 +732,7 @@ fn extract_object_type_content<'schema>( } for (field_name, field) in type_.fields.iter() { + let field_pos = pos.field(field_name.clone()); let mut field_directive_applications = Vec::new(); for directive in field.directives.iter() { if directive.name != field_directive_definition.name { @@ -828,9 +758,8 @@ fn extract_object_type_content<'schema>( .to_owned(), })?; add_subgraph_field( - field_name, + field_pos.clone().into(), field, - type_name, subgraph, federation_spec_definition, is_shareable, @@ -878,9 +807,8 @@ fn extract_object_type_content<'schema>( ); } add_subgraph_field( - field_name, + field_pos.clone().into(), field, - type_name, subgraph, federation_spec_definition, is_shareable, @@ -894,13 +822,13 @@ fn extract_object_type_content<'schema>( Ok(()) } -fn extract_interface_type_content<'schema>( - supergraph_schema: &'schema FederationSchema, +fn extract_interface_type_content( + supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, - graph_enum_value_name_to_subgraph_name: &IndexMap<&'schema Name, NodeStr>, - federation_spec_definitions: &IndexMap<&Name, &'static FederationSpecDefinition>, + graph_enum_value_name_to_subgraph_name: &IndexMap, + federation_spec_definitions: &IndexMap, join_spec_definition: &JoinSpecDefinition, - info: &[TypeInfo<'schema>], + info: &[TypeInfo], ) -> Result<(), FederationError> { let field_directive_definition = join_spec_definition.field_directive_definition(supergraph_schema)?; @@ -917,43 +845,27 @@ fn extract_interface_type_content<'schema>( subgraph_info, } in info.iter() { - let pos = InterfaceTypeDefinitionPosition { + let type_ = InterfaceTypeDefinitionPosition { type_name: (*type_name).clone(), - }; - let type_ = pos.get(supergraph_schema.schema())?; - - for directive in type_.directives.iter() { - if directive.name != implements_directive_definition.name { - continue; - } - let implements_directive_application = - join_spec_definition.implements_directive_arguments(directive)?; - let is_interface_object = *subgraph_info.get(&implements_directive_application.graph).ok_or_else(|| { + } + .get(supergraph_schema.schema())?; + fn get_pos( + subgraph: &FederationSubgraph, + subgraph_info: &IndexMap, + graph_enum_value: &Name, + type_name: NamedType, + ) -> Result { + let is_interface_object = *subgraph_info.get(graph_enum_value).ok_or_else(|| { SingleFederationError::InvalidFederationSupergraph { message: format!( "@join__implements cannot exist on {} for subgraph {} without type-level @join__type", type_name, - implements_directive_application.graph, + graph_enum_value, ), } })?; - let subgraph = get_subgraph( - subgraphs, - graph_enum_value_name_to_subgraph_name, - &implements_directive_application.graph, - )?; - match subgraph - .schema - .schema() - .types - .get(*type_name) - .ok_or_else(|| SingleFederationError::Internal { - message: format!( - "Missing type \"{}\" from subgraph despite it being in type_info", - type_name - ), - })? { - ExtendedType::Object(_) => { + Ok(match subgraph.schema.get_type(type_name.clone())? { + TypeDefinitionPosition::Object(pos) => { if !is_interface_object { return Err( SingleFederationError::Internal { @@ -961,17 +873,9 @@ fn extract_interface_type_content<'schema>( }.into() ); } - let pos = ObjectTypeDefinitionPosition { - type_name: (*type_name).clone(), - }; - pos.insert_implements_interface( - &mut subgraph.schema, - ComponentName::from(graphql_name( - &implements_directive_application.interface, - )?), - )?; + pos.into() } - ExtendedType::Interface(_) => { + TypeDefinitionPosition::Interface(pos) => { if is_interface_object { return Err( SingleFederationError::Internal { @@ -979,12 +883,7 @@ fn extract_interface_type_content<'schema>( }.into() ); } - pos.insert_implements_interface( - &mut subgraph.schema, - ComponentName::from(graphql_name( - &implements_directive_application.interface, - )?), - )?; + pos.into() } _ => { return Err( @@ -993,7 +892,44 @@ fn extract_interface_type_content<'schema>( }.into() ); } - }; + }) + } + + for directive in type_.directives.iter() { + if directive.name != implements_directive_definition.name { + continue; + } + let implements_directive_application = + join_spec_definition.implements_directive_arguments(directive)?; + let subgraph = get_subgraph( + subgraphs, + graph_enum_value_name_to_subgraph_name, + &implements_directive_application.graph, + )?; + let pos = get_pos( + subgraph, + subgraph_info, + &implements_directive_application.graph, + type_name.clone(), + )?; + match pos { + ObjectOrInterfaceTypeDefinitionPosition::Object(pos) => { + pos.insert_implements_interface( + &mut subgraph.schema, + ComponentName::from(graphql_name( + &implements_directive_application.interface, + )?), + )?; + } + ObjectOrInterfaceTypeDefinitionPosition::Interface(pos) => { + pos.insert_implements_interface( + &mut subgraph.schema, + ComponentName::from(graphql_name( + &implements_directive_application.interface, + )?), + )?; + } + } } for (field_name, field) in type_.fields.iter() { @@ -1014,6 +950,8 @@ fn extract_interface_type_content<'schema>( graph_enum_value_name_to_subgraph_name, graph_enum_value, )?; + let pos = + get_pos(subgraph, subgraph_info, graph_enum_value, type_name.clone())?; let federation_spec_definition = federation_spec_definitions .get(graph_enum_value) .ok_or_else(|| SingleFederationError::InvalidFederationSupergraph { @@ -1021,9 +959,8 @@ fn extract_interface_type_content<'schema>( .to_owned(), })?; add_subgraph_field( - field_name, + pos.field(field_name.clone()), field, - type_name, subgraph, federation_spec_definition, false, @@ -1043,6 +980,8 @@ fn extract_interface_type_content<'schema>( graph_enum_value_name_to_subgraph_name, graph_enum_value, )?; + let pos = + get_pos(subgraph, subgraph_info, graph_enum_value, type_name.clone())?; let federation_spec_definition = federation_spec_definitions .get(graph_enum_value) .ok_or_else(|| SingleFederationError::InvalidFederationSupergraph { @@ -1062,9 +1001,8 @@ fn extract_interface_type_content<'schema>( ); } add_subgraph_field( - field_name, + pos.field(field_name.clone()), field, - type_name, subgraph, federation_spec_definition, false, @@ -1078,12 +1016,12 @@ fn extract_interface_type_content<'schema>( Ok(()) } -fn extract_union_type_content<'schema>( - supergraph_schema: &'schema FederationSchema, +fn extract_union_type_content( + supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, - graph_enum_value_name_to_subgraph_name: &IndexMap<&'schema Name, NodeStr>, + graph_enum_value_name_to_subgraph_name: &IndexMap, join_spec_definition: &JoinSpecDefinition, - info: &[TypeInfo<'schema>], + info: &[TypeInfo], ) -> Result<(), FederationError> { // This was added in join 0.3, so it can genuinely be None. let union_member_directive_definition = @@ -1170,12 +1108,12 @@ fn extract_union_type_content<'schema>( Ok(()) } -fn extract_enum_type_content<'schema>( - supergraph_schema: &'schema FederationSchema, +fn extract_enum_type_content( + supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, - graph_enum_value_name_to_subgraph_name: &IndexMap<&'schema Name, NodeStr>, + graph_enum_value_name_to_subgraph_name: &IndexMap, join_spec_definition: &JoinSpecDefinition, - info: &[TypeInfo<'schema>], + info: &[TypeInfo], ) -> Result<(), FederationError> { // This was added in join 0.3, so it can genuinely be None. let enum_value_directive_definition = @@ -1254,12 +1192,12 @@ fn extract_enum_type_content<'schema>( Ok(()) } -fn extract_input_object_type_content<'schema>( - supergraph_schema: &'schema FederationSchema, +fn extract_input_object_type_content( + supergraph_schema: &FederationSchema, subgraphs: &mut FederationSubgraphs, - graph_enum_value_name_to_subgraph_name: &IndexMap<&'schema Name, NodeStr>, + graph_enum_value_name_to_subgraph_name: &IndexMap, join_spec_definition: &JoinSpecDefinition, - info: &[TypeInfo<'schema>], + info: &[TypeInfo], ) -> Result<(), FederationError> { let field_directive_definition = join_spec_definition.field_directive_definition(supergraph_schema)?; @@ -1275,6 +1213,7 @@ fn extract_input_object_type_content<'schema>( let type_ = pos.get(supergraph_schema.schema())?; for (input_field_name, input_field) in type_.fields.iter() { + let input_field_pos = pos.field(input_field_name.clone()); let mut field_directive_applications = Vec::new(); for directive in input_field.directives.iter() { if directive.name != field_directive_definition.name { @@ -1290,13 +1229,7 @@ fn extract_input_object_type_content<'schema>( graph_enum_value_name_to_subgraph_name, graph_enum_value, )?; - add_subgraph_input_field( - input_field_name, - input_field, - type_name, - subgraph, - None, - )?; + add_subgraph_input_field(input_field_pos.clone(), input_field, subgraph, None)?; } } else { for field_directive_application in &field_directive_applications { @@ -1324,9 +1257,8 @@ fn extract_input_object_type_content<'schema>( ); } add_subgraph_input_field( - input_field_name, + input_field_pos.clone(), input_field, - type_name, subgraph, Some(field_directive_application), )?; @@ -1338,10 +1270,9 @@ fn extract_input_object_type_content<'schema>( Ok(()) } -fn add_subgraph_field<'schema>( - field_name: &'schema Name, - field: &'schema FieldDefinition, - type_name: &'schema NamedType, +fn add_subgraph_field( + object_or_interface_field_definition_position: ObjectOrInterfaceFieldDefinitionPosition, + field: &FieldDefinition, subgraph: &mut FederationSubgraph, federation_spec_definition: &'static FederationSpecDefinition, is_shareable: bool, @@ -1363,7 +1294,9 @@ fn add_subgraph_field<'schema>( }; let mut subgraph_field = FieldDefinition { description: None, - name: field_name.clone(), + name: object_or_interface_field_definition_position + .field_name() + .clone(), arguments: vec![], ty: subgraph_field_type, directives: Default::default(), @@ -1414,48 +1347,21 @@ fn add_subgraph_field<'schema>( )); } - match subgraph - .schema - .schema() - .types - .get(type_name) - .ok_or_else(|| SingleFederationError::Internal { - message: format!( - "Missing type \"{}\" from subgraph despite it being in type_info", - type_name - ), - })? { - ExtendedType::Object(_) => { - ObjectFieldDefinitionPosition { - type_name: type_name.clone(), - field_name: field_name.clone(), - } - .insert(&mut subgraph.schema, Component::from(subgraph_field))?; + match object_or_interface_field_definition_position { + ObjectOrInterfaceFieldDefinitionPosition::Object(pos) => { + pos.insert(&mut subgraph.schema, Component::from(subgraph_field))?; } - ExtendedType::Interface(_) => { - InterfaceFieldDefinitionPosition { - type_name: type_name.clone(), - field_name: field_name.clone(), - } - .insert(&mut subgraph.schema, Component::from(subgraph_field))?; - } - _ => { - return Err(SingleFederationError::Internal { - message: - "\"add_subgraph_field()\" encountered non-object/interface type in subgraph" - .to_owned(), - } - .into()); + ObjectOrInterfaceFieldDefinitionPosition::Interface(pos) => { + pos.insert(&mut subgraph.schema, Component::from(subgraph_field))?; } }; Ok(()) } -fn add_subgraph_input_field<'schema>( - input_field_name: &'schema Name, - input_field: &'schema InputValueDefinition, - type_name: &'schema NamedType, +fn add_subgraph_input_field( + input_object_field_definition_position: InputObjectFieldDefinitionPosition, + input_field: &InputValueDefinition, subgraph: &mut FederationSubgraph, field_directive_application: Option<&FieldDirectiveArguments>, ) -> Result<(), FederationError> { @@ -1475,17 +1381,14 @@ fn add_subgraph_input_field<'schema>( }; let subgraph_input_field = InputValueDefinition { description: None, - name: input_field_name.clone(), + name: input_object_field_definition_position.field_name.clone(), ty: subgraph_input_field_type, default_value: input_field.default_value.clone(), directives: Default::default(), }; - InputObjectFieldDefinitionPosition { - type_name: type_name.clone(), - field_name: input_field_name.clone(), - } - .insert(&mut subgraph.schema, Component::from(subgraph_input_field))?; + input_object_field_definition_position + .insert(&mut subgraph.schema, Component::from(subgraph_input_field))?; Ok(()) } @@ -1515,10 +1418,10 @@ fn decode_type(type_: &str) -> Result { Ok(dummy_field.ty.clone()) } -fn get_subgraph<'subgraph, 'schema>( +fn get_subgraph<'subgraph>( subgraphs: &'subgraph mut FederationSubgraphs, - graph_enum_value_name_to_subgraph_name: &IndexMap<&'schema Name, NodeStr>, - graph_enum_value: &'schema Name, + graph_enum_value_name_to_subgraph_name: &IndexMap, + graph_enum_value: &Name, ) -> Result<&'subgraph mut FederationSubgraph, FederationError> { let subgraph_name = graph_enum_value_name_to_subgraph_name .get(graph_enum_value) diff --git a/src/query_graph/mod.rs b/src/query_graph/mod.rs index bdc7a255..55992252 100644 --- a/src/query_graph/mod.rs +++ b/src/query_graph/mod.rs @@ -1,6 +1,7 @@ use crate::error::{FederationError, SingleFederationError}; use crate::schema::position::{ - CompositeTypeDefinitionPosition, FieldDefinitionPosition, SchemaRootDefinitionKind, + CompositeTypeDefinitionPosition, FieldDefinitionPosition, OutputTypeDefinitionPosition, + SchemaRootDefinitionKind, }; use crate::schema::FederationSchema; use apollo_compiler::executable::SelectionSet; @@ -15,7 +16,7 @@ pub(crate) mod extract_subgraphs_from_supergraph; pub(crate) struct QueryGraphNode { // The graphQL type this node points to. - pub(crate) type_: NamedType, + pub(crate) type_: QueryGraphNodeType, // An identifier of the underlying schema containing the `type_` this node points to. This is // mainly used in federated query graphs, where the `source` is a subgraph name. pub(crate) source: NodeStr, @@ -46,6 +47,23 @@ impl Display for QueryGraphNode { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum QueryGraphNodeType { + SubgraphType(OutputTypeDefinitionPosition), + FederatedRootType(SchemaRootDefinitionKind), +} + +impl Display for QueryGraphNodeType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + QueryGraphNodeType::SubgraphType(pos) => pos.fmt(f), + QueryGraphNodeType::FederatedRootType(root_kind) => { + write!(f, "[{}]", root_kind) + } + } + } +} + pub(crate) struct QueryGraphEdge { // Indicates what kind of edge this is and what the edge does/represents. For instance, if the // edge represents a field, the `transition` will be a `FieldCollection` transition and will @@ -65,6 +83,24 @@ pub(crate) struct QueryGraphEdge { pub(crate) conditions: Option, } +impl Display for QueryGraphEdge { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if matches!( + self.transition, + QueryGraphEdgeTransition::SubgraphEnteringTransition + ) && self.conditions.is_none() + { + return Ok(()); + } + if let Some(conditions) = &self.conditions { + // TODO: Use conditions.serialize.no_indent() when those changes land in apollo-rs. + write!(f, "{:#?} ⊢ {}", conditions, self.transition) + } else { + self.transition.fmt(f) + } + } +} + // The type of query graph edge "transition". // // An edge transition encodes what the edge corresponds to, in the underlying GraphQL schema. diff --git a/src/schema/mod.rs b/src/schema/mod.rs index 046fcdcb..8350958d 100644 --- a/src/schema/mod.rs +++ b/src/schema/mod.rs @@ -1,6 +1,11 @@ -use crate::error::FederationError; +use crate::error::{FederationError, SingleFederationError}; use crate::link::LinksMetadata; -use crate::schema::position::{CompositeTypeDefinitionPosition, ObjectTypeDefinitionPosition}; +use crate::schema::position::{ + CompositeTypeDefinitionPosition, EnumTypeDefinitionPosition, InputObjectTypeDefinitionPosition, + InterfaceTypeDefinitionPosition, ObjectTypeDefinitionPosition, ScalarTypeDefinitionPosition, + TypeDefinitionPosition, UnionTypeDefinitionPosition, +}; +use apollo_compiler::schema::{ExtendedType, Name}; use apollo_compiler::Schema; use indexmap::IndexSet; use referencer::Referencers; @@ -27,6 +32,53 @@ impl FederationSchema { &self.referencers } + pub(crate) fn get_types(&self) -> Vec { + self.schema + .types + .iter() + .map(|(type_name, type_)| { + let type_name = type_name.clone(); + match type_ { + ExtendedType::Scalar(_) => ScalarTypeDefinitionPosition { type_name }.into(), + ExtendedType::Object(_) => ObjectTypeDefinitionPosition { type_name }.into(), + ExtendedType::Interface(_) => { + InterfaceTypeDefinitionPosition { type_name }.into() + } + ExtendedType::Union(_) => UnionTypeDefinitionPosition { type_name }.into(), + ExtendedType::Enum(_) => EnumTypeDefinitionPosition { type_name }.into(), + ExtendedType::InputObject(_) => { + InputObjectTypeDefinitionPosition { type_name }.into() + } + } + }) + .collect() + } + + pub(crate) fn get_type( + &self, + type_name: Name, + ) -> Result { + let type_ = + self.schema + .types + .get(&type_name) + .ok_or_else(|| SingleFederationError::Internal { + message: format!("Schema has no type \"{}\"", type_name), + })?; + Ok(match type_ { + ExtendedType::Scalar(_) => ScalarTypeDefinitionPosition { type_name }.into(), + ExtendedType::Object(_) => ObjectTypeDefinitionPosition { type_name }.into(), + ExtendedType::Interface(_) => InterfaceTypeDefinitionPosition { type_name }.into(), + ExtendedType::Union(_) => UnionTypeDefinitionPosition { type_name }.into(), + ExtendedType::Enum(_) => EnumTypeDefinitionPosition { type_name }.into(), + ExtendedType::InputObject(_) => InputObjectTypeDefinitionPosition { type_name }.into(), + }) + } + + pub(crate) fn try_get_type(&self, type_name: Name) -> Option { + self.get_type(type_name).ok() + } + pub(crate) fn possible_runtime_types( &self, composite_type_definition_position: CompositeTypeDefinitionPosition, diff --git a/src/schema/position.rs b/src/schema/position.rs index 91814d17..9b1d2a75 100644 --- a/src/schema/position.rs +++ b/src/schema/position.rs @@ -30,6 +30,68 @@ pub(crate) enum TypeDefinitionPosition { InputObject(InputObjectTypeDefinitionPosition), } +impl TypeDefinitionPosition { + pub(crate) fn type_name(&self) -> &Name { + match self { + TypeDefinitionPosition::Scalar(type_) => &type_.type_name, + TypeDefinitionPosition::Object(type_) => &type_.type_name, + TypeDefinitionPosition::Interface(type_) => &type_.type_name, + TypeDefinitionPosition::Union(type_) => &type_.type_name, + TypeDefinitionPosition::Enum(type_) => &type_.type_name, + TypeDefinitionPosition::InputObject(type_) => &type_.type_name, + } + } + + pub(crate) fn get<'schema>( + &self, + schema: &'schema Schema, + ) -> Result<&'schema ExtendedType, FederationError> { + let type_name = self.type_name(); + let type_ = schema + .types + .get(type_name) + .ok_or_else(|| SingleFederationError::Internal { + message: format!("Schema has no type \"{}\"", self), + })?; + let type_matches = match type_ { + ExtendedType::Scalar(_) => matches!(self, TypeDefinitionPosition::Scalar(_)), + ExtendedType::Object(_) => matches!(self, TypeDefinitionPosition::Object(_)), + ExtendedType::Interface(_) => matches!(self, TypeDefinitionPosition::Interface(_)), + ExtendedType::Union(_) => matches!(self, TypeDefinitionPosition::Union(_)), + ExtendedType::Enum(_) => matches!(self, TypeDefinitionPosition::Enum(_)), + ExtendedType::InputObject(_) => matches!(self, TypeDefinitionPosition::InputObject(_)), + }; + if type_matches { + Ok(type_) + } else { + Err(SingleFederationError::Internal { + message: format!("Schema type \"{}\" is the wrong kind", self), + } + .into()) + } + } + + pub(crate) fn try_get<'schema>( + &self, + schema: &'schema Schema, + ) -> Option<&'schema ExtendedType> { + self.get(schema).ok() + } +} + +impl Display for TypeDefinitionPosition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TypeDefinitionPosition::Scalar(type_) => type_.fmt(f), + TypeDefinitionPosition::Object(type_) => type_.fmt(f), + TypeDefinitionPosition::Interface(type_) => type_.fmt(f), + TypeDefinitionPosition::Union(type_) => type_.fmt(f), + TypeDefinitionPosition::Enum(type_) => type_.fmt(f), + TypeDefinitionPosition::InputObject(type_) => type_.fmt(f), + } + } +} + impl From for TypeDefinitionPosition { fn from(value: ScalarTypeDefinitionPosition) -> Self { TypeDefinitionPosition::Scalar(value) @@ -66,6 +128,82 @@ impl From for TypeDefinitionPosition { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum OutputTypeDefinitionPosition { + Scalar(ScalarTypeDefinitionPosition), + Object(ObjectTypeDefinitionPosition), + Interface(InterfaceTypeDefinitionPosition), + Union(UnionTypeDefinitionPosition), + Enum(EnumTypeDefinitionPosition), +} + +impl OutputTypeDefinitionPosition { + pub(crate) fn type_name(&self) -> &Name { + match self { + OutputTypeDefinitionPosition::Scalar(type_) => &type_.type_name, + OutputTypeDefinitionPosition::Object(type_) => &type_.type_name, + OutputTypeDefinitionPosition::Interface(type_) => &type_.type_name, + OutputTypeDefinitionPosition::Union(type_) => &type_.type_name, + OutputTypeDefinitionPosition::Enum(type_) => &type_.type_name, + } + } +} + +impl Display for OutputTypeDefinitionPosition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + OutputTypeDefinitionPosition::Scalar(type_) => type_.fmt(f), + OutputTypeDefinitionPosition::Object(type_) => type_.fmt(f), + OutputTypeDefinitionPosition::Interface(type_) => type_.fmt(f), + OutputTypeDefinitionPosition::Union(type_) => type_.fmt(f), + OutputTypeDefinitionPosition::Enum(type_) => type_.fmt(f), + } + } +} + +impl From for OutputTypeDefinitionPosition { + fn from(value: ScalarTypeDefinitionPosition) -> Self { + OutputTypeDefinitionPosition::Scalar(value) + } +} + +impl From for OutputTypeDefinitionPosition { + fn from(value: ObjectTypeDefinitionPosition) -> Self { + OutputTypeDefinitionPosition::Object(value) + } +} + +impl From for OutputTypeDefinitionPosition { + fn from(value: InterfaceTypeDefinitionPosition) -> Self { + OutputTypeDefinitionPosition::Interface(value) + } +} + +impl From for OutputTypeDefinitionPosition { + fn from(value: UnionTypeDefinitionPosition) -> Self { + OutputTypeDefinitionPosition::Union(value) + } +} + +impl From for OutputTypeDefinitionPosition { + fn from(value: EnumTypeDefinitionPosition) -> Self { + OutputTypeDefinitionPosition::Enum(value) + } +} + +impl From for OutputTypeDefinitionPosition { + fn from(value: AbstractTypeDefinitionPosition) -> Self { + match value { + AbstractTypeDefinitionPosition::Interface(value) => { + OutputTypeDefinitionPosition::Interface(value) + } + AbstractTypeDefinitionPosition::Union(value) => { + OutputTypeDefinitionPosition::Union(value) + } + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) enum CompositeTypeDefinitionPosition { Object(ObjectTypeDefinitionPosition), @@ -137,6 +275,44 @@ impl From for AbstractTypeDefinitionPosition { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum ObjectOrInterfaceTypeDefinitionPosition { + Object(ObjectTypeDefinitionPosition), + Interface(InterfaceTypeDefinitionPosition), +} + +impl ObjectOrInterfaceTypeDefinitionPosition { + pub(crate) fn type_name(&self) -> &Name { + match self { + ObjectOrInterfaceTypeDefinitionPosition::Object(type_) => &type_.type_name, + ObjectOrInterfaceTypeDefinitionPosition::Interface(type_) => &type_.type_name, + } + } + + pub(crate) fn field(&self, field_name: Name) -> ObjectOrInterfaceFieldDefinitionPosition { + match self { + ObjectOrInterfaceTypeDefinitionPosition::Object(type_) => { + type_.field(field_name).into() + } + ObjectOrInterfaceTypeDefinitionPosition::Interface(type_) => { + type_.field(field_name).into() + } + } + } +} + +impl From for ObjectOrInterfaceTypeDefinitionPosition { + fn from(value: ObjectTypeDefinitionPosition) -> Self { + ObjectOrInterfaceTypeDefinitionPosition::Object(value) + } +} + +impl From for ObjectOrInterfaceTypeDefinitionPosition { + fn from(value: InterfaceTypeDefinitionPosition) -> Self { + ObjectOrInterfaceTypeDefinitionPosition::Interface(value) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) enum FieldDefinitionPosition { Object(ObjectFieldDefinitionPosition), @@ -165,6 +341,16 @@ impl FieldDefinitionPosition { } } +impl Display for FieldDefinitionPosition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FieldDefinitionPosition::Object(field) => field.fmt(f), + FieldDefinitionPosition::Interface(field) => field.fmt(f), + FieldDefinitionPosition::Union(field) => field.fmt(f), + } + } +} + impl From for FieldDefinitionPosition { fn from(value: ObjectFieldDefinitionPosition) -> Self { FieldDefinitionPosition::Object(value) @@ -183,6 +369,43 @@ impl From for FieldDefinitionPosition { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum ObjectOrInterfaceFieldDefinitionPosition { + Object(ObjectFieldDefinitionPosition), + Interface(InterfaceFieldDefinitionPosition), +} + +impl ObjectOrInterfaceFieldDefinitionPosition { + pub(crate) fn field_name(&self) -> &Name { + match self { + ObjectOrInterfaceFieldDefinitionPosition::Object(field) => &field.field_name, + ObjectOrInterfaceFieldDefinitionPosition::Interface(field) => &field.field_name, + } + } + + pub(crate) fn get<'schema>( + &self, + schema: &'schema Schema, + ) -> Result<&'schema Component, FederationError> { + match self { + ObjectOrInterfaceFieldDefinitionPosition::Object(field) => field.get(schema), + ObjectOrInterfaceFieldDefinitionPosition::Interface(field) => field.get(schema), + } + } +} + +impl From for ObjectOrInterfaceFieldDefinitionPosition { + fn from(value: ObjectFieldDefinitionPosition) -> Self { + ObjectOrInterfaceFieldDefinitionPosition::Object(value) + } +} + +impl From for ObjectOrInterfaceFieldDefinitionPosition { + fn from(value: InterfaceFieldDefinitionPosition) -> Self { + ObjectOrInterfaceFieldDefinitionPosition::Interface(value) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct SchemaDefinitionPosition;