use std::{
collections::{BTreeSet, HashMap, HashSet},
hash::{Hash, Hasher},
};
use xxhash_rust::xxh3::Xxh3;
use crate::query_planner::{
ast::{
hash::{ASTHash, SemanticShapeHashContext},
minification::minify_operation,
operation::{OperationDefinition, SubgraphFetchOperation, VariableDefinition},
selection_item::SelectionItem,
selection_set::{FieldSelection, SelectionSet},
value::Value,
},
planner::error::QueryPlanError,
planner::plan_nodes::{
custom_scalar_paths_for_entities_selection, BatchFetchNode, CustomScalarPaths, EntityBatch,
EntityBatchAlias, FetchRewrite, FlattenNodePath, PlanNode,
},
state::supergraph_state::{OperationKind, SupergraphState, TypeNode},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct ShapeKey {
requires_hash: u64,
entities_selection_hash: u64,
input_rewrites_hash: u64,
output_rewrites_hash: u64,
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct RepresentationsInputKey {
requires: SelectionSet,
input_rewrites: Option<Vec<FetchRewrite>>,
merge_paths: Vec<FlattenNodePath>,
}
struct BatchFetchBuilder<'a> {
merged_non_representation_variables: &'a [VariableDefinition],
operation_variable_definitions: Vec<VariableDefinition>,
operation_selection_items: Vec<SelectionItem>,
batched_aliases: Vec<EntityBatchAlias>,
used_variable_names: HashSet<String>,
representations_var_index: usize,
variable_usages: BTreeSet<String>,
representations_var_by_input_key: HashMap<RepresentationsInputKey, String>,
custom_scalar_paths: CustomScalarPaths,
}
impl<'a> BatchFetchBuilder<'a> {
fn new(
merged_non_representation_variables: &'a [VariableDefinition],
alias_count: usize,
) -> Self {
Self {
merged_non_representation_variables,
operation_variable_definitions: Vec::with_capacity(
alias_count + merged_non_representation_variables.len(),
),
operation_selection_items: Vec::with_capacity(alias_count),
batched_aliases: Vec::with_capacity(alias_count),
used_variable_names: merged_non_representation_variables
.iter()
.map(|var| var.name.clone())
.collect(),
representations_var_index: 0,
variable_usages: BTreeSet::new(),
representations_var_by_input_key: HashMap::new(),
custom_scalar_paths: CustomScalarPaths::default(),
}
}
fn add_shape_group(
&mut self,
alias_index: usize,
shape_group: &[EntityFetch],
supergraph: &SupergraphState,
) -> Result<(), QueryPlanError> {
let representative = shape_group.first().ok_or_else(|| {
QueryPlanError::Internal("Batched entities shape group cannot be empty".to_string())
})?;
for candidate in shape_group {
if let Some(candidate_variable_usages) = &candidate.variable_usages {
self.variable_usages
.extend(candidate_variable_usages.iter().cloned());
}
}
let alias = format!("_e{alias_index}");
let merge_paths = Self::collect_merge_paths(shape_group);
let representations_variable_name =
self.get_or_create_representations_var(representative, &merge_paths);
self.operation_selection_items
.push(SelectionItem::Field(FieldSelection {
name: "_entities".to_string(),
selections: representative.entities_selection.clone(),
alias: Some(alias.clone()),
arguments: Some(
(
"representations".to_string(),
Value::Variable(representations_variable_name.clone()),
)
.into(),
),
skip_if: None,
include_if: None,
omit_from_response: false,
}));
if let Some(alias_paths) = custom_scalar_paths_for_entities_selection(
&representative.entities_selection,
supergraph,
) {
self.custom_scalar_paths
.children
.insert(alias.clone(), alias_paths);
}
self.batched_aliases.push(EntityBatchAlias {
alias,
representations_variable_name,
merge_paths,
requires: representative.requires.clone(),
input_rewrites: representative.input_rewrites.clone(),
output_rewrites: representative.output_rewrites.clone(),
});
Ok(())
}
fn collect_merge_paths(shape_group: &[EntityFetch]) -> Vec<FlattenNodePath> {
let mut merge_paths = Vec::with_capacity(shape_group.len());
let mut seen_merge_paths = HashSet::with_capacity(shape_group.len());
for candidate in shape_group {
let path = candidate.flatten_path.clone();
if seen_merge_paths.insert(path.clone()) {
merge_paths.push(path);
}
}
merge_paths
}
fn get_or_create_representations_var(
&mut self,
representative: &EntityFetch,
merge_paths: &[FlattenNodePath],
) -> String {
let representations_input_key = RepresentationsInputKey {
requires: representative.requires.clone(),
input_rewrites: representative.input_rewrites.clone(),
merge_paths: merge_paths.to_vec(),
};
if let Some(existing_name) = self
.representations_var_by_input_key
.get(&representations_input_key)
{
return existing_name.clone();
}
let name = next_unique_representations_var_name(
&mut self.used_variable_names,
&mut self.representations_var_index,
);
self.operation_variable_definitions
.push(VariableDefinition {
name: name.clone(),
variable_type: TypeNode::NonNull(
Box::new(TypeNode::List(
Box::new(TypeNode::NonNull(
Box::new(TypeNode::Named("_Any".to_string())),
)),
)), ), default_value: None,
});
self.representations_var_by_input_key
.insert(representations_input_key, name.clone());
name
}
fn finish(
mut self,
first_candidate: &EntityFetch,
supergraph: &SupergraphState,
) -> Result<BatchFetchNode, QueryPlanError> {
self.operation_variable_definitions
.extend(self.merged_non_representation_variables.iter().cloned());
let operation_definition = OperationDefinition {
name: None,
operation_kind: Some(OperationKind::Query),
selection_set: SelectionSet {
items: self.operation_selection_items,
},
variable_definitions: Some(self.operation_variable_definitions),
};
let document = minify_operation(operation_definition, supergraph).map_err(|error| {
QueryPlanError::Internal(format!(
"Failed to minify batched entities operation: {error}"
))
})?;
Ok(BatchFetchNode {
id: first_candidate.fetch_node_id,
service_name: first_candidate.service_name.clone(),
variable_usages: if self.variable_usages.is_empty() {
None
} else {
Some(self.variable_usages)
},
operation_kind: Some(OperationKind::Query),
operation: SubgraphFetchOperation::from_anonymous_operation(document),
custom_scalar_paths: (!self.custom_scalar_paths.is_empty())
.then_some(self.custom_scalar_paths),
entity_batch: EntityBatch {
aliases: self.batched_aliases,
},
})
}
}
#[derive(Clone)]
struct EntityFetch {
index: usize,
fetch_node_id: i64,
service_name: String,
flatten_path: FlattenNodePath,
variable_usages: Option<BTreeSet<String>>,
requires: SelectionSet,
entities_selection: SelectionSet,
input_rewrites: Option<Vec<FetchRewrite>>,
output_rewrites: Option<Vec<FetchRewrite>>,
shape_key: ShapeKey,
non_representations_variable_definitions: Vec<VariableDefinition>,
}
impl EntityFetch {
fn eq_shape(&self, right: &EntityFetch) -> bool {
self.shape_key == right.shape_key
&& self.requires == right.requires
&& self.entities_selection == right.entities_selection
&& self.input_rewrites == right.input_rewrites
&& self.output_rewrites == right.output_rewrites
}
fn from_node(index: usize, node: &PlanNode) -> Result<Option<Self>, QueryPlanError> {
let PlanNode::Flatten(flatten_node) = node else {
return Ok(None);
};
let PlanNode::Fetch(fetch_node) = flatten_node.node.as_ref() else {
return Ok(None);
};
let Some(entities_field) = fetch_node
.operation
.document
.operation
.selection_set
.entities_field()
else {
return Ok(None);
};
let Some(representations_var) = entities_field.representations_variable_name() else {
return Ok(None);
};
let Some(requires) = fetch_node.requires.clone() else {
return Ok(None);
};
let requires =
requires.inline_fragment_spreads(&fetch_node.operation.document.fragments)?;
let entities_selection = entities_field
.selections
.inline_fragment_spreads(&fetch_node.operation.document.fragments)?;
let input_rewrites = fetch_node.input_rewrites.clone();
let output_rewrites = fetch_node.output_rewrites.clone();
let non_representations_variable_definitions = fetch_node
.operation
.document
.operation
.variable_definitions
.clone()
.unwrap_or_default()
.into_iter()
.filter(|var| var.name != representations_var)
.collect::<Vec<_>>();
let fragments = &fetch_node.operation.document.fragments;
let mut hasher = Xxh3::new();
let shape_context = SemanticShapeHashContext::new(fragments);
requires.semantic_shape_hash(&mut hasher, &shape_context);
let requires_hash = hasher.finish();
let mut hasher = Xxh3::new();
let shape_context = SemanticShapeHashContext::new(fragments);
entities_selection.semantic_shape_hash(&mut hasher, &shape_context);
let entities_selection_hash = hasher.finish();
let shape_key = ShapeKey {
requires_hash,
entities_selection_hash,
input_rewrites_hash: fetch_rewrites_hash(input_rewrites.as_deref()),
output_rewrites_hash: fetch_rewrites_hash(output_rewrites.as_deref()),
};
Ok(Some(EntityFetch {
index,
fetch_node_id: fetch_node.id,
service_name: fetch_node.service_name.clone(),
flatten_path: flatten_node.path.clone(),
variable_usages: fetch_node.variable_usages.clone(),
requires,
entities_selection,
input_rewrites,
output_rewrites,
shape_key,
non_representations_variable_definitions,
}))
}
}
#[derive(Clone)]
struct VariableCompatibleGroup {
candidates: Vec<EntityFetch>,
variables: Vec<VariableDefinition>,
}
pub(super) fn optimize_top_level_sequence(nodes: Vec<PlanNode>) -> Vec<PlanNode> {
optimize_plan_sequence(nodes)
}
pub(super) fn optimize_root_node(
node: PlanNode,
supergraph: &SupergraphState,
) -> Result<PlanNode, QueryPlanError> {
PlanOptimizer { supergraph }.optimize_node(node)
}
struct PlanOptimizer<'a> {
supergraph: &'a SupergraphState,
}
impl PlanOptimizer<'_> {
fn optimize_node(&self, node: PlanNode) -> Result<PlanNode, QueryPlanError> {
match node {
PlanNode::Fetch(_) | PlanNode::BatchFetch(_) => Ok(node),
PlanNode::Flatten(flatten_node) => {
if !matches!(flatten_node.node.as_ref(), PlanNode::Fetch(_)) {
return Err(QueryPlanError::Internal(format!(
"FlattenNode is expected to wrap a FetchNode, got {:?}",
flatten_node.node.as_ref()
)));
}
Ok(PlanNode::Flatten(flatten_node))
}
PlanNode::Sequence(mut sequence_node) => {
sequence_node.nodes = self.optimize_children(sequence_node.nodes)?;
sequence_node.nodes = optimize_plan_sequence(sequence_node.nodes);
Ok(PlanNode::sequence(sequence_node.nodes))
}
PlanNode::Parallel(parallel_node) => {
let optimized_nodes = if parallel_node
.nodes
.iter()
.all(|node| node.is_fetching_node())
{
parallel_node.nodes
} else {
self.optimize_children(parallel_node.nodes)?
};
let optimized_nodes = PlanNode::flatten_parallel(optimized_nodes);
let optimized_nodes = optimize_parallel_node(optimized_nodes, self.supergraph)?;
Ok(PlanNode::parallel(optimized_nodes))
}
PlanNode::Condition(mut condition_node) => {
self.optimize_optional_child(&mut condition_node.if_clause)?;
self.optimize_optional_child(&mut condition_node.else_clause)?;
Ok(PlanNode::Condition(condition_node))
}
PlanNode::Subscription(subscription_node) => {
Ok(PlanNode::Subscription(subscription_node))
}
PlanNode::Defer(mut defer_node) => {
self.optimize_optional_child(&mut defer_node.primary.node)?;
for deferred in defer_node.deferred.iter_mut() {
self.optimize_optional_child(&mut deferred.node)?;
}
Ok(PlanNode::Defer(defer_node))
}
}
}
fn optimize_children(&self, nodes: Vec<PlanNode>) -> Result<Vec<PlanNode>, QueryPlanError> {
nodes
.into_iter()
.map(|node| self.optimize_node(node))
.collect()
}
fn optimize_optional_child(
&self,
node: &mut Option<Box<PlanNode>>,
) -> Result<(), QueryPlanError> {
if let Some(current_node) = node.take() {
*node = Some(Box::new(self.optimize_node(*current_node)?));
}
Ok(())
}
}
fn optimize_parallel_node(
nodes: Vec<PlanNode>,
supergraph: &SupergraphState,
) -> Result<Vec<PlanNode>, QueryPlanError> {
let candidates_by_subgraph = partition_by_subgraph(&nodes)?;
let mut batch_node_replacements: HashMap<usize, PlanNode> = HashMap::new();
let mut removed_indices: HashSet<usize> = HashSet::new();
for candidates in candidates_by_subgraph.into_values() {
if candidates.len() < 2 {
continue;
}
for variable_group in partition_by_variables_compatibility(candidates) {
if variable_group.candidates.len() < 2 {
continue;
}
let shape_groups = partition_by_shape(variable_group.candidates);
let Some(first_group) = shape_groups.first() else {
continue;
};
let Some(first_candidate) = first_group.first() else {
continue;
};
let first_index = first_candidate.index;
let batch_fetch_node =
build_batched_fetch_node(&shape_groups, &variable_group.variables, supergraph)?;
batch_node_replacements.insert(first_index, PlanNode::BatchFetch(batch_fetch_node));
for group in &shape_groups {
for candidate in group {
if candidate.index != first_index {
removed_indices.insert(candidate.index);
}
}
}
}
}
if batch_node_replacements.is_empty() {
return Ok(nodes);
}
let mut optimized_nodes = Vec::with_capacity(nodes.len() - removed_indices.len());
for (index, node) in nodes.into_iter().enumerate() {
if let Some(replacement) = batch_node_replacements.remove(&index) {
optimized_nodes.push(replacement);
continue;
}
if removed_indices.contains(&index) {
continue;
}
optimized_nodes.push(node);
}
Ok(optimized_nodes)
}
fn partition_by_subgraph(
nodes: &[PlanNode],
) -> Result<HashMap<String, Vec<EntityFetch>>, QueryPlanError> {
let mut candidates_by_subgraph: HashMap<String, Vec<EntityFetch>> = HashMap::new();
for (index, node) in nodes.iter().enumerate() {
if let Some(candidate) = EntityFetch::from_node(index, node)? {
candidates_by_subgraph
.entry(candidate.service_name.clone())
.or_default()
.push(candidate);
}
}
Ok(candidates_by_subgraph)
}
fn fetch_rewrites_hash(rewrites: Option<&[FetchRewrite]>) -> u64 {
let mut hasher = Xxh3::new();
if let Some(rewrites) = rewrites {
true.hash(&mut hasher);
rewrites.hash(&mut hasher);
} else {
false.hash(&mut hasher);
}
hasher.finish()
}
fn next_unique_representations_var_name(
used_variable_names: &mut HashSet<String>,
next_index: &mut usize,
) -> String {
loop {
let name = format!("__batch_reps_{next_index}");
*next_index += 1;
if used_variable_names.insert(name.clone()) {
return name;
}
}
}
fn partition_by_shape(candidates: Vec<EntityFetch>) -> Vec<Vec<EntityFetch>> {
let mut groups: Vec<Vec<EntityFetch>> = Vec::new();
let mut by_key: HashMap<ShapeKey, Vec<usize>> = HashMap::new();
'candidates_loop: for candidate in candidates {
if let Some(indices) = by_key.get(&candidate.shape_key) {
for &group_index in indices {
if groups[group_index][0].eq_shape(&candidate) {
groups[group_index].push(candidate);
continue 'candidates_loop;
}
}
}
let group_index = groups.len();
groups.push(vec![candidate]);
by_key
.entry(groups[group_index][0].shape_key)
.or_default()
.push(group_index);
}
groups
}
fn partition_by_variables_compatibility(
candidates: Vec<EntityFetch>,
) -> Vec<VariableCompatibleGroup> {
let mut groups: Vec<VariableCompatibleGroup> = Vec::new();
for candidate in candidates {
let candidate_variables = &candidate.non_representations_variable_definitions;
let chosen_group = groups.iter().position(|group| {
can_merge_variable_definitions(&group.variables, candidate_variables)
});
if let Some(group_index) = chosen_group {
let group = &mut groups[group_index];
merge_variable_definitions(&mut group.variables, candidate_variables);
group.candidates.push(candidate);
continue;
}
groups.push(VariableCompatibleGroup {
variables: candidate_variables.clone(),
candidates: vec![candidate],
});
}
groups
}
fn can_merge_variable_definitions(
merged_variables: &[VariableDefinition],
other_variables: &[VariableDefinition],
) -> bool {
for other_variable in other_variables {
let existing_variable = merged_variables
.iter()
.find(|v| v.name == other_variable.name);
let Some(existing_variable) = existing_variable else {
continue;
};
if !existing_variable.can_merge(other_variable) {
return false;
}
}
true
}
fn merge_variable_definitions(
merged_variables: &mut Vec<VariableDefinition>,
other_variables: &[VariableDefinition],
) {
for variable in other_variables {
if merged_variables
.iter()
.any(|existing| existing.name == variable.name)
{
continue;
}
merged_variables.push(variable.clone());
}
}
fn build_batched_fetch_node(
shape_groups: &[Vec<EntityFetch>],
merged_non_representation_variables: &[VariableDefinition],
supergraph: &SupergraphState,
) -> Result<BatchFetchNode, QueryPlanError> {
let first_candidate = shape_groups
.first()
.and_then(|group| group.first())
.ok_or_else(|| {
QueryPlanError::Internal("Batched entities candidates were empty".to_string())
})?;
let mut builder =
BatchFetchBuilder::new(merged_non_representation_variables, shape_groups.len());
for (index, shape_group) in shape_groups.iter().enumerate() {
builder.add_shape_group(index, shape_group, supergraph)?;
}
builder.finish(first_candidate, supergraph)
}
fn optimize_plan_sequence(nodes: Vec<PlanNode>) -> Vec<PlanNode> {
let mut flattened_nodes = Vec::with_capacity(nodes.len());
for node in nodes {
match node {
PlanNode::Sequence(sequence_node) => {
flattened_nodes.extend(sequence_node.nodes);
}
other => flattened_nodes.push(other),
}
}
flattened_nodes
.into_iter()
.fold(Vec::new(), |mut acc, current_node| {
match (acc.last_mut(), current_node) {
(Some(PlanNode::Condition(last_cond)), PlanNode::Condition(current_cond))
if last_cond.can_merge_with(¤t_cond) =>
{
last_cond.merge(current_cond);
}
(_, current_node) => {
acc.push(current_node);
}
}
acc
})
}
#[cfg(test)]
mod tests {
use std::{
collections::{BTreeSet, HashSet},
fs,
path::PathBuf,
};
use graphql_tools::parser::query as query_ast;
use crate::query_planner::{
ast::{
document::Document,
merge_path::{FieldPathSegment, MergePath, Segment},
operation::SubgraphFetchOperation,
},
planner::plan_nodes::{
FetchNode, FetchNodePathSegment, FetchRewrite, FlattenNode, FlattenNodePath, PlanNode,
QueryPlan, ValueSetter,
},
state::supergraph_state::{OperationKind, SupergraphState},
utils::parsing::{parse_operation, parse_schema},
};
use super::{next_unique_representations_var_name, optimize_root_node};
#[test]
fn next_unique_representations_variable_name_skips_used_values() {
let mut used = HashSet::from(["__batch_reps_0".to_string(), "__batch_reps_2".to_string()]);
let mut next_index = 0;
let first = next_unique_representations_var_name(&mut used, &mut next_index);
let second = next_unique_representations_var_name(&mut used, &mut next_index);
assert_eq!(first, "__batch_reps_1");
assert_eq!(second, "__batch_reps_3");
}
#[test]
fn optimize_parallel_node_batches_compatible_fetches_and_keeps_order() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let passthrough = PlanNode::Fetch(non_entity_fetch_node(100, "products"));
let candidate_a =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, &entities_query);
let candidate_b =
flatten_entity_fetch_node(2, "inventory", "products", requires_query, &entities_query);
let nodes = vec![passthrough.clone(), candidate_a, candidate_b];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
Parallel {
Fetch(service: "products") {
{
products {
upc
}
}
},
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
shippingEstimate
}
}
}
},
},
},
"#);
}
#[test]
fn optimize_parallel_node_does_not_batch_incompatible_variables() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query_a = "
query($representations:[_Any!]!, $locale: String) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let entities_query_b = "
query($representations:[_Any!]!, $locale: String!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let candidate_a = flatten_entity_fetch_node(
1,
"inventory",
"products",
requires_query,
&entities_query_a,
);
let candidate_b = flatten_entity_fetch_node(
2,
"inventory",
"products",
requires_query,
&entities_query_b,
);
let nodes = vec![candidate_a, candidate_b];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
Parallel {
Flatten(path: "products.@") {
Fetch(service: "inventory") {
{
... on Product {
upc
}
} =>
($locale:String) {
... on Product {
shippingEstimate
}
}
},
},
Flatten(path: "products.@") {
Fetch(service: "inventory") {
{
... on Product {
upc
}
} =>
($locale:String!) {
... on Product {
shippingEstimate
}
}
},
},
},
},
"#);
}
#[test]
fn optimize_parallel_node_avoids_representations_variable_collision() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!, $__batch_reps_0: String) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let candidate_a =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, &entities_query);
let candidate_b =
flatten_entity_fetch_node(2, "inventory", "products", requires_query, &entities_query);
let nodes = vec![candidate_a, candidate_b];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
}
($__batch_reps_0:String) {
_e0: _entities(representations: $__batch_reps_1) {
... on Product {
shippingEstimate
}
}
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_does_not_batch_across_subgraphs() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let inventory_candidate = flatten_entity_fetch_node(
1,
"inventory", "products",
requires_query,
entities_query,
);
let products_candidate = flatten_entity_fetch_node(
2,
"products", "products",
requires_query,
entities_query,
);
let nodes = vec![inventory_candidate, products_candidate];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
Parallel {
Flatten(path: "products.@") {
Fetch(service: "inventory") {
{
... on Product {
upc
}
} =>
{
... on Product {
shippingEstimate
}
}
},
},
Flatten(path: "products.@") {
Fetch(service: "products") {
{
... on Product {
upc
}
} =>
{
... on Product {
shippingEstimate
}
}
},
},
},
},
"#);
}
#[test]
fn optimize_parallel_node_batches_same_shape_with_different_merge_paths() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let products_candidate = flatten_entity_fetch_node(
1,
"inventory",
"products", requires_query,
entities_query,
);
let top_products_candidate = flatten_entity_fetch_node(
2,
"inventory",
"topProducts", requires_query,
entities_query,
);
let nodes = vec![products_candidate, top_products_candidate];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
"topProducts.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
shippingEstimate
}
}
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_splits_aliases_when_entities_query_differs() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let shipping_estimate_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let in_stock_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { inStock }
}
}
";
let products_candidate = flatten_entity_fetch_node(
1,
"inventory",
"products",
requires_query,
shipping_estimate_query,
);
let top_products_candidate = flatten_entity_fetch_node(
2,
"inventory",
"topProducts",
requires_query,
in_stock_query,
);
let nodes = vec![products_candidate, top_products_candidate];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
_e1 {
paths: [
"topProducts.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
shippingEstimate
}
}
_e1: _entities(representations: $__batch_reps_1) {
... on Product {
inStock
}
}
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_shares_representations_variable_when_input_and_paths_match() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let shipping_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let in_stock_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { inStock }
}
}
";
let a =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, shipping_query);
let b =
flatten_entity_fetch_node(2, "inventory", "products", requires_query, in_stock_query);
let optimized = optimize_root_node(PlanNode::parallel(vec![a, b]), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
_e1 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
shippingEstimate
}
}
_e1: _entities(representations: $__batch_reps_0) {
... on Product {
inStock
}
}
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_does_not_share_representations_variable_when_requires_differs() {
let supergraph = test_supergraph_state();
let requires_upc = "query { ... on Product { upc } }";
let requires_name = "query { ... on Product { name } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let a = flatten_entity_fetch_node(1, "inventory", "products", requires_upc, entities_query);
let b =
flatten_entity_fetch_node(2, "inventory", "products", requires_name, entities_query);
let optimized = optimize_root_node(PlanNode::parallel(vec![a, b]), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
_e1 {
paths: [
"products.@"
]
{
... on Product {
name
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
...a
}
}
_e1: _entities(representations: $__batch_reps_1) {
... on Product {
...a
}
}
}
fragment a on Product {
shippingEstimate
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_does_not_share_representations_variable_when_input_rewrites_differ() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let base =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, entities_query);
let with_rewrite = with_input_rewrite(flatten_entity_fetch_node(
2,
"inventory",
"products",
requires_query,
entities_query,
));
let optimized =
optimize_root_node(PlanNode::parallel(vec![base, with_rewrite]), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
_e1 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
...a
}
}
_e1: _entities(representations: $__batch_reps_1) {
... on Product {
...a
}
}
}
fragment a on Product {
shippingEstimate
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_splits_aliases_when_requires_differs() {
let supergraph = test_supergraph_state();
let requires_upc = "query { ... on Product { upc } }";
let requires_name = "query { ... on Product { name } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let a = flatten_entity_fetch_node(1, "inventory", "products", requires_upc, entities_query);
let b =
flatten_entity_fetch_node(2, "inventory", "topProducts", requires_name, entities_query);
let nodes = vec![a, b];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
_e1 {
paths: [
"topProducts.@"
]
{
... on Product {
name
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
...a
}
}
_e1: _entities(representations: $__batch_reps_1) {
... on Product {
...a
}
}
}
fragment a on Product {
shippingEstimate
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_splits_aliases_when_input_rewrites_differ() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let base =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, entities_query);
let with_rewrite = with_input_rewrite(flatten_entity_fetch_node(
2,
"inventory",
"topProducts",
requires_query,
entities_query,
));
let nodes = vec![base, with_rewrite];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let batch_node = optimized
.as_batch_fetch()
.expect("optimized should be BatchFetch node");
let base_node = batch_node
.entity_batch
.aliases
.get(0)
.expect("BatchFetch node should have two aliases");
let rewrite_node = batch_node
.entity_batch
.aliases
.get(1)
.expect("BatchFetch node should have two aliases");
assert!(
base_node.input_rewrites.is_none(),
"base node should not have input rewrites"
);
assert!(
rewrite_node.input_rewrites.is_some(),
"rewrite node should have input rewrites"
);
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
_e1 {
paths: [
"topProducts.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
...a
}
}
_e1: _entities(representations: $__batch_reps_1) {
... on Product {
...a
}
}
}
fragment a on Product {
shippingEstimate
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_splits_aliases_when_output_rewrites_differ() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let base =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, entities_query);
let with_rewrite = with_output_rewrite(flatten_entity_fetch_node(
2,
"inventory",
"topProducts",
requires_query,
entities_query,
));
let nodes = vec![base, with_rewrite];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
_e1 {
paths: [
"topProducts.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
...a
}
}
_e1: _entities(representations: $__batch_reps_1) {
... on Product {
...a
}
}
}
fragment a on Product {
shippingEstimate
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_shares_representations_variable_when_only_output_rewrites_differ() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let base =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, entities_query);
let with_output_rewrite = with_output_rewrite(flatten_entity_fetch_node(
2,
"inventory",
"products",
requires_query,
entities_query,
));
let nodes = vec![base, with_output_rewrite];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
_e1 {
paths: [
"products.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
...a
}
}
_e1: _entities(representations: $__batch_reps_0) {
... on Product {
...a
}
}
}
fragment a on Product {
shippingEstimate
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_alias_order_is_deterministic() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let shipping_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let in_stock_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { inStock }
}
}
";
let shape_a_first =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, shipping_query);
let shape_b = flatten_entity_fetch_node(
2,
"inventory",
"topProducts",
requires_query,
in_stock_query,
);
let shape_a_second =
flatten_entity_fetch_node(3, "inventory", "products2", requires_query, shipping_query);
let nodes = vec![shape_a_first, shape_b, shape_a_second];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
"products2.@"
]
{
... on Product {
upc
}
}
}
_e1 {
paths: [
"topProducts.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
shippingEstimate
}
}
_e1: _entities(representations: $__batch_reps_1) {
... on Product {
inStock
}
}
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_batches_entities_query_with_fragment_spread() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query_a = "
fragment A on Product { shippingEstimate }
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { ...A }
}
}
";
let entities_query_b = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { ...B }
}
}
fragment B on Product { shippingEstimate }
";
let with_fragment_a =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, entities_query_a);
let with_fragment_b = flatten_entity_fetch_node(
2,
"inventory",
"topProducts",
requires_query,
entities_query_b,
);
let nodes = vec![with_fragment_a, with_fragment_b];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
"topProducts.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
... on Product {
shippingEstimate
}
}
}
}
},
},
"#);
}
#[test]
fn optimize_parallel_node_returns_original_when_no_entity_candidates() {
let supergraph = test_supergraph_state();
let first = PlanNode::Fetch(non_entity_fetch_node(1, "products"));
let second = PlanNode::Fetch(non_entity_fetch_node(2, "inventory"));
let nodes = vec![first, second];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
Parallel {
Fetch(service: "products") {
{
products {
upc
}
}
},
Fetch(service: "inventory") {
{
products {
upc
}
}
},
},
},
"#);
}
#[test]
fn optimize_parallel_node_does_not_batch_single_candidate() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let candidate =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, entities_query);
let nodes = vec![candidate];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
Flatten(path: "products.@") {
Fetch(service: "inventory") {
{
... on Product {
upc
}
} =>
{
... on Product {
shippingEstimate
}
}
},
},
},
"#);
}
#[test]
fn optimize_parallel_node_creates_multiple_batch_nodes_for_multiple_services() {
let supergraph = test_supergraph_state();
let requires_query = "query { ... on Product { upc } }";
let entities_query = "
query($representations:[_Any!]!) {
_entities(representations: $representations) {
... on Product { shippingEstimate }
}
}
";
let inventory_a =
flatten_entity_fetch_node(1, "inventory", "products", requires_query, entities_query);
let inventory_b = flatten_entity_fetch_node(
2,
"inventory",
"topProducts",
requires_query,
entities_query,
);
let products_a =
flatten_entity_fetch_node(3, "products", "products", requires_query, entities_query);
let products_b =
flatten_entity_fetch_node(4, "products", "topProducts", requires_query, entities_query);
let nodes = vec![inventory_a, inventory_b, products_a, products_b];
let optimized = optimize_root_node(PlanNode::parallel(nodes), &supergraph)
.expect("optimize should work");
let query_plan = QueryPlan {
kind: "QueryPlan",
node: Some(optimized),
};
insta::assert_snapshot!(format!("{query_plan}"), @r#"
QueryPlan {
Parallel {
BatchFetch(service: "inventory") {
{
_e0 {
paths: [
"products.@"
"topProducts.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
shippingEstimate
}
}
}
},
BatchFetch(service: "products") {
{
_e0 {
paths: [
"products.@"
"topProducts.@"
]
{
... on Product {
upc
}
}
}
}
{
_e0: _entities(representations: $__batch_reps_0) {
... on Product {
shippingEstimate
}
}
}
},
},
},
"#);
}
fn test_supergraph_state() -> SupergraphState {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("fixture/tests/simple-requires.supergraph.graphql");
let sdl = fs::read_to_string(path).expect("fixture should be readable");
let schema = parse_schema(&sdl);
SupergraphState::new(&schema)
}
fn flatten_entity_fetch_node(
id: i64,
service_name: &str,
root_path_field: &str,
requires_query: &str,
entities_query: &str,
) -> PlanNode {
let requires = parse_document(requires_query).operation.selection_set;
let entities_document = parse_document(entities_query);
let non_representation_variable_names = {
let representations_var = entities_document
.operation
.selection_set
.entities_field()
.and_then(|field| field.representations_variable_name())
.expect("entities query should define representations variable");
let usages: BTreeSet<String> = entities_document
.operation
.variable_definitions
.clone()
.unwrap_or_default()
.into_iter()
.filter(|var| var.name != representations_var)
.map(|var| var.name)
.collect();
if usages.is_empty() {
None
} else {
Some(usages)
}
};
let operation = SubgraphFetchOperation::from_anonymous_operation(entities_document);
let fetch_node = FetchNode {
id,
service_name: service_name.to_string(),
variable_usages: non_representation_variable_names,
operation_kind: Some(OperationKind::Query),
operation,
custom_scalar_paths: None,
requires: Some(requires),
input_rewrites: None,
output_rewrites: None,
};
let path = FlattenNodePath::from(&MergePath::new(vec![
Segment::Field(
FieldPathSegment::named(root_path_field.to_string()),
0,
None,
),
Segment::List,
]));
PlanNode::Flatten(FlattenNode {
path,
node: Box::new(PlanNode::Fetch(fetch_node)),
})
}
fn with_input_rewrite(node: PlanNode) -> PlanNode {
with_fetch_rewrite(node, RewriteTarget::Input)
}
fn with_output_rewrite(node: PlanNode) -> PlanNode {
with_fetch_rewrite(node, RewriteTarget::Output)
}
enum RewriteTarget {
Input,
Output,
}
fn with_fetch_rewrite(node: PlanNode, target: RewriteTarget) -> PlanNode {
let PlanNode::Flatten(mut flatten_node) = node else {
panic!("expected Flatten node")
};
let PlanNode::Fetch(mut fetch_node) = *flatten_node.node else {
panic!("expected Flatten(Fetch)")
};
let rewrite = FetchRewrite::ValueSetter(ValueSetter {
path: vec![FetchNodePathSegment::Key("upc".to_string())],
set_value_to: "constant".to_string(),
});
match target {
RewriteTarget::Input => fetch_node.input_rewrites = Some(vec![rewrite]),
RewriteTarget::Output => fetch_node.output_rewrites = Some(vec![rewrite]),
}
flatten_node.node = Box::new(PlanNode::Fetch(fetch_node));
PlanNode::Flatten(flatten_node)
}
fn non_entity_fetch_node(id: i64, service_name: &str) -> FetchNode {
let operation = SubgraphFetchOperation::from_anonymous_operation(parse_document(
"query { products { upc } }",
));
FetchNode {
id,
service_name: service_name.to_string(),
variable_usages: None,
operation_kind: Some(OperationKind::Query),
operation,
custom_scalar_paths: None,
requires: None,
input_rewrites: None,
output_rewrites: None,
}
}
fn parse_document(query: &str) -> Document {
let document = parse_operation(query);
let mut operation = None;
let mut fragments = Vec::new();
for definition in document.definitions {
match definition {
query_ast::Definition::Operation(current_operation) => {
if operation.is_none() {
operation = Some(current_operation.into());
}
}
query_ast::Definition::Fragment(fragment) => {
fragments.push(fragment.into());
}
}
}
Document {
operation: operation.expect("operation definition should exist"),
fragments,
}
}
}