use crate::query_planner::{
ast::{
merge_path::{Condition, MergePath, Segment},
minification::minify_operation,
operation::{OperationDefinition, SubgraphFetchOperation, VariableDefinition},
selection_item::SelectionItem,
selection_set::{FieldSelection, SelectionSet},
value::Value,
},
planner::fetch::{
fetch_step_data::FetchStepData, selections::FetchStepSelections, state::MultiTypeFetchStep,
},
state::supergraph_state::{OperationKind, SupergraphState, TypeNode},
utils::pretty_display::{get_indent, PrettyDisplay},
};
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet},
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
hash::{Hash, Hasher},
};
use xxhash_rust::xxh3::Xxh3;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QueryPlan {
pub kind: &'static str, #[serde(skip_serializing_if = "Option::is_none")]
pub node: Option<PlanNode>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "kind")]
pub enum PlanNode {
Fetch(FetchNode),
BatchFetch(BatchFetchNode),
Sequence(SequenceNode),
Parallel(ParallelNode),
Flatten(FlattenNode),
Condition(ConditionNode),
Subscription(SubscriptionNode),
Defer(DeferNode),
}
impl PlanNode {
pub fn as_fetch(&self) -> Option<&FetchNode> {
match self {
PlanNode::Fetch(node) => Some(node),
_ => None,
}
}
pub fn as_batch_fetch(&self) -> Option<&BatchFetchNode> {
match self {
PlanNode::BatchFetch(node) => Some(node),
_ => None,
}
}
pub fn as_sequence(&self) -> Option<&SequenceNode> {
match self {
PlanNode::Sequence(node) => Some(node),
_ => None,
}
}
pub fn as_parallel(&self) -> Option<&ParallelNode> {
match self {
PlanNode::Parallel(node) => Some(node),
_ => None,
}
}
pub fn as_flatten(&self) -> Option<&FlattenNode> {
match self {
PlanNode::Flatten(node) => Some(node),
_ => None,
}
}
pub fn as_condition(&self) -> Option<&ConditionNode> {
match self {
PlanNode::Condition(node) => Some(node),
_ => None,
}
}
pub fn as_subscription(&self) -> Option<&SubscriptionNode> {
match self {
PlanNode::Subscription(node) => Some(node),
_ => None,
}
}
pub fn as_defer(&self) -> Option<&DeferNode> {
match self {
PlanNode::Defer(node) => Some(node),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchNode {
#[serde(skip_serializing)]
pub id: i64,
pub service_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub variable_usages: Option<BTreeSet<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operation_kind: Option<OperationKind>,
pub operation: SubgraphFetchOperation,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_scalar_paths: Option<CustomScalarPaths>,
#[serde(skip_serializing_if = "Option::is_none")]
pub requires: Option<SelectionSet>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_rewrites: Option<Vec<FetchRewrite>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_rewrites: Option<Vec<FetchRewrite>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BatchFetchNode {
#[serde(skip_serializing)]
pub id: i64,
pub service_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub variable_usages: Option<BTreeSet<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operation_kind: Option<OperationKind>,
pub operation: SubgraphFetchOperation,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_scalar_paths: Option<CustomScalarPaths>,
pub entity_batch: EntityBatch,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CustomScalarPaths {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub children: BTreeMap<String, CustomScalarPaths>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub terminal: bool,
}
impl CustomScalarPaths {
pub fn insert_path<I, S>(&mut self, path: I)
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut node = self;
for segment in path {
node = node
.children
.entry(segment.as_ref().to_string())
.or_default();
}
node.terminal = true;
}
pub fn is_empty(&self) -> bool {
!self.terminal && self.children.is_empty()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct CustomScalarPathState {
children: BTreeMap<String, CustomScalarPathState>,
custom_scalar_seen: bool,
standard_scalar_seen: bool,
}
impl CustomScalarPathState {
fn mark_custom_scalar<I, S>(&mut self, path: I)
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut node = self;
for segment in path {
node = node
.children
.entry(segment.as_ref().to_string())
.or_default();
}
node.custom_scalar_seen = true;
}
fn mark_standard_scalar<I, S>(&mut self, path: I)
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut node = self;
for segment in path {
node = node
.children
.entry(segment.as_ref().to_string())
.or_default();
}
node.standard_scalar_seen = true;
}
fn into_custom_scalar_paths(self) -> Option<CustomScalarPaths> {
let mut children = BTreeMap::new();
for (key, child) in self.children {
if let Some(paths) = child.into_custom_scalar_paths() {
children.insert(key, paths);
}
}
let terminal = self.custom_scalar_seen && !self.standard_scalar_seen && children.is_empty();
let paths = CustomScalarPaths { children, terminal };
(!paths.is_empty()).then_some(paths)
}
}
fn collect_custom_scalar_path_state(
path_kinds: &mut CustomScalarPathState,
response_path: &mut Vec<String>,
parent_type_name: &str,
selections: &SelectionSet,
supergraph: &SupergraphState,
) {
let Some(parent_def) = supergraph.definitions.get(parent_type_name) else {
return;
};
let parent_fields = parent_def.fields();
for item in &selections.items {
match item {
SelectionItem::Field(field) => {
let Some(field_def) = parent_fields.get(&field.name) else {
continue;
};
let response_key = field.selection_identifier();
let field_type_name = field_def.field_type.inner_type();
response_path.push(response_key.to_string());
if supergraph.is_custom_scalar_type(field_type_name) {
path_kinds.mark_custom_scalar(response_path.iter().map(String::as_str));
} else {
path_kinds.mark_standard_scalar(response_path.iter().map(String::as_str));
if !field.selections.is_empty() {
collect_custom_scalar_path_state(
path_kinds,
response_path,
field_type_name,
&field.selections,
supergraph,
);
}
}
response_path.pop();
}
SelectionItem::InlineFragment(fragment) => {
collect_custom_scalar_path_state(
path_kinds,
response_path,
&fragment.type_condition,
&fragment.selections,
supergraph,
);
}
SelectionItem::FragmentSpread(_) => {}
}
}
}
fn custom_scalar_paths_from_fetch_output(
output: &FetchStepSelections<MultiTypeFetchStep>,
supergraph: &SupergraphState,
entities_root_key: Option<&str>,
) -> Option<CustomScalarPaths> {
let mut state = CustomScalarPathState::default();
for (type_name, selection_set) in output.iter_selections() {
let mut response_path = entities_root_key
.map(|root| vec![root.to_string()])
.unwrap_or_default();
collect_custom_scalar_path_state(
&mut state,
&mut response_path,
type_name,
selection_set,
supergraph,
);
}
state.into_custom_scalar_paths()
}
pub fn custom_scalar_paths_for_entities_selection(
selection_set: &SelectionSet,
supergraph: &SupergraphState,
) -> Option<CustomScalarPaths> {
let mut state = CustomScalarPathState::default();
let mut response_path = Vec::new();
for item in &selection_set.items {
if let SelectionItem::InlineFragment(fragment) = item {
response_path.clear();
collect_custom_scalar_path_state(
&mut state,
&mut response_path,
&fragment.type_condition,
&fragment.selections,
supergraph,
);
}
}
state.into_custom_scalar_paths()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EntityBatch {
pub aliases: Vec<EntityBatchAlias>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EntityBatchAlias {
pub alias: String,
pub representations_variable_name: String,
#[serde(rename = "paths")]
pub merge_paths: Vec<FlattenNodePath>,
pub requires: SelectionSet,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_rewrites: Option<Vec<FetchRewrite>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_rewrites: Option<Vec<FetchRewrite>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FlattenNode {
pub path: FlattenNodePath,
pub node: Box<PlanNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SequenceNode {
pub nodes: Vec<PlanNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParallelNode {
pub nodes: Vec<PlanNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConditionNode {
pub condition: String, pub if_clause: Option<Box<PlanNode>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub else_clause: Option<Box<PlanNode>>,
}
impl ConditionNode {
pub fn can_merge_with(&self, other: &Self) -> bool {
if self.condition != other.condition {
return false;
}
let both_if = self.else_clause.is_none() && other.else_clause.is_none();
let both_else = self.if_clause.is_none() && other.if_clause.is_none();
both_if || both_else
}
pub fn merge(&mut self, mut other: Self) {
let merge_into_if_clause = self.if_clause.is_some();
let mut nodes = self.take_inner_nodes();
nodes.extend(other.take_inner_nodes());
let merged_body = PlanNode::sequence(nodes);
if merge_into_if_clause {
self.if_clause = Some(Box::new(merged_body));
} else {
self.else_clause = Some(Box::new(merged_body));
}
}
fn take_inner_nodes(&mut self) -> Vec<PlanNode> {
self.if_clause
.take()
.or_else(|| self.else_clause.take())
.map(|n| n.flatten_sequence())
.unwrap_or_default()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct KeyRenamer {
pub path: Vec<FetchNodePathSegment>,
pub rename_key_to: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub enum FetchNodePathSegment {
Key(String),
TypenameEquals(BTreeSet<String>),
}
impl FetchNodePathSegment {
pub fn typename_equals_from_type(type_name: String) -> Self {
Self::TypenameEquals(BTreeSet::from_iter([type_name]))
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub enum FetchRewrite {
ValueSetter(ValueSetter),
KeyRenamer(KeyRenamer),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub enum FlattenNodePathSegment {
Field(String),
TypeCondition(BTreeSet<String>),
#[serde(rename = "@")]
List,
}
impl From<&MergePath> for Vec<FetchNodePathSegment> {
fn from(value: &MergePath) -> Self {
value
.inner
.iter()
.filter_map(|path_segment| match path_segment {
Segment::TypeCondition(type_names, _) => {
Some(FetchNodePathSegment::TypenameEquals(type_names.clone()))
}
Segment::Field(field_seg, _args_hash, _) => Some(FetchNodePathSegment::Key(
field_seg.response_key().to_string(),
)),
Segment::List => None,
})
.collect()
}
}
impl FlattenNodePathSegment {
pub fn to_field(&self) -> Option<&String> {
match self {
FlattenNodePathSegment::Field(field_name) => Some(field_name),
_ => None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct FlattenNodePath(Vec<FlattenNodePathSegment>);
impl FlattenNodePath {
pub fn as_slice(&self) -> &[FlattenNodePathSegment] {
&self.0
}
}
impl Display for FlattenNodePathSegment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FlattenNodePathSegment::Field(field_name) => write!(f, "{}", field_name),
FlattenNodePathSegment::TypeCondition(type_names) => {
write!(
f,
"|[{}]",
type_names.iter().cloned().collect::<Vec<_>>().join("|")
)
}
FlattenNodePathSegment::List => write!(f, "@"),
}
}
}
impl Display for FlattenNodePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut segments_iter = self.0.iter().peekable();
while let Some(segment) = segments_iter.next() {
write!(f, "{}", segment)?;
if let Some(peeked) = segments_iter.peek() {
match peeked {
FlattenNodePathSegment::TypeCondition(_) => {
}
_ => write!(f, ".")?,
}
}
}
Ok(())
}
}
impl From<&MergePath> for FlattenNodePath {
fn from(path: &MergePath) -> Self {
FlattenNodePath(
path.inner
.iter()
.map(|seg| match seg {
Segment::TypeCondition(type_names, _) => {
FlattenNodePathSegment::TypeCondition(type_names.clone())
}
Segment::Field(field_seg, _args_hash, _) => {
FlattenNodePathSegment::Field(field_seg.response_key().to_string())
}
Segment::List => FlattenNodePathSegment::List,
})
.collect(),
)
}
}
impl From<MergePath> for FlattenNodePath {
fn from(path: MergePath) -> Self {
(&path).into()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct ValueSetter {
pub path: Vec<FetchNodePathSegment>,
pub set_value_to: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SubscriptionNode {
pub primary: FetchNode,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DeferNode {
pub primary: DeferPrimary,
pub deferred: Vec<DeferredNode>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DeferPrimary {
pub subselection: Option<String>,
pub node: Option<Box<PlanNode>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DeferredNode {
pub depends: Vec<DeferDependency>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
pub query_path: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subselection: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub node: Option<Box<PlanNode>>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DeferDependency {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub defer_label: Option<String>,
}
impl PlanNode {
pub fn into_nodes(self) -> Vec<PlanNode> {
match self {
PlanNode::Sequence(node) => node.nodes,
PlanNode::Parallel(node) => node.nodes,
other => vec![other],
}
}
pub fn flatten_sequence(self) -> Vec<PlanNode> {
match self {
PlanNode::Sequence(node) => node.nodes,
other => vec![other],
}
}
pub fn flatten_parallel(nodes: Vec<PlanNode>) -> Vec<PlanNode> {
let mut flattened = Vec::with_capacity(nodes.len());
for node in nodes {
match node {
PlanNode::Parallel(p) => flattened.extend(p.nodes),
other => flattened.push(other),
}
}
flattened
}
pub fn sequence(mut nodes: Vec<PlanNode>) -> PlanNode {
if nodes.len() == 1 {
nodes.remove(0)
} else {
PlanNode::Sequence(SequenceNode { nodes })
}
}
pub fn parallel(mut nodes: Vec<PlanNode>) -> PlanNode {
if nodes.len() == 1 {
nodes.remove(0)
} else {
PlanNode::Parallel(ParallelNode { nodes })
}
}
pub fn is_fetching_node(&self) -> bool {
match self {
PlanNode::Fetch(_) | PlanNode::BatchFetch(_) => true,
PlanNode::Flatten(flatten_node) => {
matches!(flatten_node.node.as_ref(), PlanNode::Fetch(_))
}
_ => false,
}
}
}
fn create_input_selection_set(
input_selections: &FetchStepSelections<MultiTypeFetchStep>,
) -> SelectionSet {
let selection_set = input_selections.to_non_root_selection_set();
selection_set.strip_for_plan_input()
}
fn create_output_operation(
step: &FetchStepData<MultiTypeFetchStep>,
supergraph: &SupergraphState,
) -> SubgraphFetchOperation {
let mut variables = vec![VariableDefinition {
name: "representations".to_string(),
variable_type: TypeNode::NonNull(Box::new(TypeNode::List(Box::new(TypeNode::NonNull(
Box::new(TypeNode::Named("_Any".to_string())),
))))),
default_value: None,
}];
if let Some(additional_vars) = &step.variable_definitions {
variables.extend(additional_vars.clone());
}
let operation_def = OperationDefinition {
name: None,
operation_kind: Some(OperationKind::Query),
variable_definitions: Some(variables),
selection_set: SelectionSet {
items: vec![SelectionItem::Field(FieldSelection {
name: "_entities".to_string(),
selections: step.output.to_non_root_selection_set(),
alias: None,
arguments: Some(
(
"representations".to_string(),
Value::Variable("representations".to_string()),
)
.into(),
),
skip_if: None,
include_if: None,
omit_from_response: false,
})],
},
};
let document = minify_operation(operation_def, supergraph).expect("Failed to minify");
SubgraphFetchOperation::from_anonymous_operation(document)
}
impl FetchNode {
pub fn from_fetch_step(
step: &FetchStepData<MultiTypeFetchStep>,
supergraph: &SupergraphState,
) -> Self {
match step.is_entity_call() {
true => FetchNode {
id: step.id,
service_name: step.service_name.0.clone(),
variable_usages: step.variable_usages.clone(),
operation_kind: Some(OperationKind::Query),
operation: create_output_operation(step, supergraph),
custom_scalar_paths: custom_scalar_paths_from_fetch_output(
&step.output,
supergraph,
Some("_entities"),
),
requires: Some(create_input_selection_set(&step.input)),
input_rewrites: step.input_rewrites.clone(),
output_rewrites: step.output_rewrites.clone(),
},
false => {
let root_type_name = supergraph.expect_root_type_name(Some(&step.operation_kind));
let operation_def = OperationDefinition {
name: None,
operation_kind: Some(step.operation_kind.clone()),
selection_set: step.output.to_root_selection_set(root_type_name),
variable_definitions: step.variable_definitions.clone(),
};
let document =
minify_operation(operation_def, supergraph).expect("Failed to minify");
FetchNode {
id: step.id,
service_name: step.service_name.0.clone(),
variable_usages: step.variable_usages.clone(),
operation_kind: Some(step.operation_kind.clone()),
operation: SubgraphFetchOperation::from_anonymous_operation(document),
custom_scalar_paths: custom_scalar_paths_from_fetch_output(
&step.output,
supergraph,
None,
),
requires: None,
input_rewrites: step.input_rewrites.clone(),
output_rewrites: step.output_rewrites.clone(),
}
}
}
}
}
impl PlanNode {
pub fn from_fetch_step(
step: &FetchStepData<MultiTypeFetchStep>,
supergraph: &SupergraphState,
) -> Self {
let fetch = FetchNode::from_fetch_step(step, supergraph);
let node = if !step.response_path.is_empty() {
PlanNode::Flatten(FlattenNode {
path: step.response_path.clone().into(),
node: Box::new(PlanNode::Fetch(fetch)),
})
} else if matches!(fetch.operation_kind, Some(OperationKind::Subscription)) {
PlanNode::Subscription(SubscriptionNode { primary: fetch })
} else {
PlanNode::Fetch(fetch)
};
match step.condition.as_ref() {
Some(condition) => match condition {
Condition::Include(var_name) => PlanNode::Condition(ConditionNode {
condition: var_name.clone(),
if_clause: Some(Box::new(node)),
else_clause: None,
}),
Condition::Skip(var_name) => PlanNode::Condition(ConditionNode {
condition: var_name.clone(),
if_clause: None,
else_clause: Some(Box::new(node)),
}),
Condition::SkipAndInclude { skip, include } => {
let include_node = PlanNode::Condition(ConditionNode {
condition: include.clone(),
if_clause: Some(Box::new(node)),
else_clause: None,
});
PlanNode::Condition(ConditionNode {
condition: skip.clone(),
if_clause: None,
else_clause: Some(Box::new(include_node)),
})
}
},
None => node,
}
}
}
impl Display for QueryPlan {
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
self.pretty_fmt(f, 0)
}
}
impl Display for PlanNode {
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
self.pretty_fmt(f, 0)
}
}
impl Display for FetchNode {
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
self.pretty_fmt(f, 0)
}
}
impl Display for BatchFetchNode {
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
self.pretty_fmt(f, 0)
}
}
impl Display for FlattenNode {
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
self.pretty_fmt(f, 0)
}
}
impl Display for SubscriptionNode {
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
self.pretty_fmt(f, 0)
}
}
impl PrettyDisplay for QueryPlan {
fn pretty_fmt(&self, f: &mut FmtFormatter<'_>, depth: usize) -> FmtResult {
let indent = get_indent(depth);
writeln!(f, "{indent}QueryPlan {{",)?;
if let Some(node) = &self.node {
node.pretty_fmt(f, depth + 1)?;
} else {
writeln!(f, "{indent} None,")?;
}
writeln!(f, "{indent}}},")?;
Ok(())
}
}
impl PrettyDisplay for FetchNode {
fn pretty_fmt(&self, f: &mut FmtFormatter<'_>, depth: usize) -> FmtResult {
let indent = get_indent(depth);
writeln!(f, "{indent}Fetch(service: \"{}\") {{", self.service_name)?;
if let Some(requires) = &self.requires {
writeln!(f, "{indent} {{")?;
requires.pretty_fmt(f, depth + 2)?;
writeln!(f, "{indent} }} =>")?;
}
self.operation.pretty_fmt(f, depth)?;
writeln!(f, "{indent}}},")?;
Ok(())
}
}
impl PrettyDisplay for BatchFetchNode {
fn pretty_fmt(&self, f: &mut FmtFormatter<'_>, depth: usize) -> FmtResult {
let indent = get_indent(depth);
writeln!(
f,
"{indent}BatchFetch(service: \"{}\") {{",
self.service_name
)?;
writeln!(f, "{indent} {{")?;
for alias in &self.entity_batch.aliases {
writeln!(f, "{indent} {} {{", alias.alias)?;
writeln!(f, "{indent} paths: [")?;
for merge_path in &alias.merge_paths {
writeln!(f, "{indent} \"{}\"", merge_path)?;
}
writeln!(f, "{indent} ]")?;
writeln!(f, "{indent} {{")?;
alias.requires.pretty_fmt(f, depth + 4)?;
writeln!(f, "{indent} }}")?;
writeln!(f, "{indent} }}")?;
}
writeln!(f, "{indent} }}")?;
self.operation.pretty_fmt(f, depth)?;
writeln!(f, "{indent}}},")?;
Ok(())
}
}
impl PrettyDisplay for FlattenNode {
fn pretty_fmt(&self, f: &mut FmtFormatter<'_>, depth: usize) -> FmtResult {
let indent = get_indent(depth);
writeln!(f, "{indent}Flatten(path: \"{}\") {{", self.path)?;
self.node.pretty_fmt(f, depth + 1)?;
writeln!(f, "{indent}}},")?;
Ok(())
}
}
impl PrettyDisplay for SequenceNode {
fn pretty_fmt(&self, f: &mut FmtFormatter<'_>, depth: usize) -> FmtResult {
let indent = get_indent(depth);
writeln!(f, "{indent}Sequence {{")?;
for node in &self.nodes {
node.pretty_fmt(f, depth + 1)?;
}
writeln!(f, "{indent}}},")?;
Ok(())
}
}
impl PrettyDisplay for ParallelNode {
fn pretty_fmt(&self, f: &mut FmtFormatter<'_>, depth: usize) -> FmtResult {
let indent = get_indent(depth);
writeln!(f, "{indent}Parallel {{")?;
for node in &self.nodes {
node.pretty_fmt(f, depth + 1)?;
}
writeln!(f, "{indent}}},")?;
Ok(())
}
}
impl PrettyDisplay for ConditionNode {
fn pretty_fmt(&self, f: &mut FmtFormatter<'_>, depth: usize) -> FmtResult {
let indent = get_indent(depth);
match (self.if_clause.as_ref(), self.else_clause.as_ref()) {
(Some(if_clause), None) => {
writeln!(f, "{indent}Include(if: ${}) {{", self.condition)?;
if_clause.pretty_fmt(f, depth + 1)?;
writeln!(f, "{indent}}},")?;
}
(None, Some(else_clause)) => {
writeln!(f, "{indent}Skip(if: ${}) {{", self.condition)?;
else_clause.pretty_fmt(f, depth + 1)?;
writeln!(f, "{indent}}},")?;
}
(Some(_if_clause), Some(_else_clause)) => {
todo!("Implement pretty_fmt for ConditionNode with both if and else clauses");
}
_ => panic!("Invalid condition node"),
}
Ok(())
}
}
impl PrettyDisplay for SubscriptionNode {
fn pretty_fmt(&self, f: &mut FmtFormatter<'_>, depth: usize) -> FmtResult {
let indent = get_indent(depth);
writeln!(f, "{indent}Subscription {{")?;
self.primary.pretty_fmt(f, depth + 1)?;
writeln!(f, "{indent}}},")?;
Ok(())
}
}
impl PrettyDisplay for PlanNode {
fn pretty_fmt(&self, f: &mut FmtFormatter<'_>, depth: usize) -> FmtResult {
match self {
PlanNode::Fetch(node) => node.pretty_fmt(f, depth),
PlanNode::BatchFetch(node) => node.pretty_fmt(f, depth),
PlanNode::Flatten(node) => node.pretty_fmt(f, depth),
PlanNode::Sequence(node) => node.pretty_fmt(f, depth),
PlanNode::Parallel(node) => node.pretty_fmt(f, depth),
PlanNode::Condition(node) => node.pretty_fmt(f, depth),
PlanNode::Subscription(node) => node.pretty_fmt(f, depth),
_ => Ok(()),
}
}
}
pub fn hash_minified_query(minified_query: &str) -> u64 {
let mut hasher = Xxh3::new();
minified_query.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use crate::query_planner::{
planner::fetch::{selections::FetchStepSelections, state::SingleTypeFetchStep},
state::supergraph_state::SupergraphState,
utils::parsing::parse_schema,
};
use super::{custom_scalar_paths_from_fetch_output, SelectionSet};
fn selection_set_for_field(field_name: &str) -> SelectionSet {
let selection = format!("{{ {field_name} }}");
graphql_tools::parser::parse_query(&selection)
.unwrap()
.into_static()
.definitions
.into_iter()
.find_map(|definition| match definition {
graphql_tools::parser::query::Definition::Operation(
graphql_tools::parser::query::OperationDefinition::SelectionSet(selection_set),
) => Some(selection_set.into()),
_ => None,
})
.expect("selection set")
}
#[test]
fn custom_scalar_paths_do_not_mark_custom_scalar_vs_builtin_scalar_collision() {
let schema = parse_schema(
r#"
scalar JSONBlob
type TypeA {
meta: JSONBlob
}
type TypeB {
meta: String
}
type Query {
root: String
}
"#,
);
let supergraph = SupergraphState::new(&schema);
let mut output = FetchStepSelections::<SingleTypeFetchStep>::new_empty().into_multi_type();
output.declare_known_type("TypeA");
output.declare_known_type("TypeB");
*output.selections_for_definition_mut("TypeA").unwrap() = selection_set_for_field("meta");
*output.selections_for_definition_mut("TypeB").unwrap() = selection_set_for_field("meta");
let paths = custom_scalar_paths_from_fetch_output(&output, &supergraph, Some("_entities"));
assert!(
paths.is_none(),
"custom-scalar vs built-in-scalar collision must not emit a terminal custom scalar path"
);
}
}