Skip to content

Commit b078df1

Browse files
authored
Fix: Improve merge passthrough child optimization (#272)
The optimization now checks if the child's input contains the output instead of being identical. The optimizer now runs in a loop to ensure that all possible optimizations are applied. Closes #278
1 parent 7e1c0f6 commit b078df1

File tree

17 files changed

+280
-156
lines changed

17 files changed

+280
-156
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/query-planner/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ serde_json = { version = "1.0.140", features = ["preserve_order"] }
1616
tracing = { version = "0.1.41" }
1717
# Data Structures
1818
petgraph = "0.8.0"
19+
bitflags = "2.9.1"
1920
# Utils
2021
lazy-init = "0.5.1"
2122
thiserror = "2.0.12"

lib/query-planner/src/ast/minification/selection_id.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ use std::hash::{Hash, Hasher};
44
use crate::ast::hash::ASTHash;
55
use crate::ast::selection_set::SelectionSet;
66

7-
pub type SelectionId = u64;
7+
#[derive(Hash, PartialEq, Eq, Copy, Clone)]
8+
pub struct SelectionId(u64);
89

910
pub fn generate_selection_id(type_name: &str, selection_set: &SelectionSet) -> SelectionId {
1011
let mut hasher = DefaultHasher::new();
1112
type_name.hash(&mut hasher);
1213
selection_set.ast_hash::<_, true>(&mut hasher);
13-
hasher.finish()
14+
SelectionId(hasher.finish())
1415
}

lib/query-planner/src/ast/minification/stats.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use std::collections::HashMap;
22

33
use crate::ast::minification::error::MinificationError;
4-
use crate::ast::minification::selection_id::generate_selection_id;
4+
use crate::ast::minification::selection_id::{generate_selection_id, SelectionId};
55
use crate::ast::selection_item::SelectionItem;
66
use crate::ast::selection_set::SelectionSet;
77
use crate::state::supergraph_state::SupergraphState;
88

99
pub struct Stats {
10-
state: HashMap<u64, usize>,
10+
state: HashMap<SelectionId, usize>,
1111
contains_duplicates: bool,
1212
}
1313

@@ -30,11 +30,11 @@ impl Stats {
3030
self.contains_duplicates
3131
}
3232

33-
pub fn is_duplicated(&self, key: &u64) -> bool {
33+
pub fn is_duplicated(&self, key: &SelectionId) -> bool {
3434
self.state.get(key).unwrap_or(&0) > &1
3535
}
3636

37-
pub fn increase(&mut self, key: u64) -> &usize {
37+
pub fn increase(&mut self, key: SelectionId) -> &usize {
3838
let occurrences = self.state.entry(key).or_insert_with(|| 0);
3939
*occurrences += 1;
4040

lib/query-planner/src/ast/minification/transform.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ use std::mem::take;
44
use crate::ast::document::Document;
55
use crate::ast::fragment::FragmentDefinition;
66
use crate::ast::minification::error::MinificationError;
7-
use crate::ast::minification::selection_id::generate_selection_id;
7+
use crate::ast::minification::selection_id::{generate_selection_id, SelectionId};
88
use crate::ast::minification::stats::Stats;
99
use crate::ast::operation::OperationDefinition;
1010
use crate::ast::selection_item::SelectionItem;
1111
use crate::ast::selection_set::{FieldSelection, InlineFragmentSelection, SelectionSet};
1212
use crate::state::supergraph_state::SupergraphState;
1313

14-
type Fragments = HashMap<u64, FragmentDefinition>;
14+
type Fragments = HashMap<SelectionId, FragmentDefinition>;
1515

1616
pub fn transform_operation(
1717
supergraph: &SupergraphState,
@@ -253,7 +253,7 @@ pub fn get_or_create_fragment(
253253
stats: &Stats,
254254
fragments: &mut Fragments,
255255
next_fragment_name_idx: &mut usize,
256-
id: &u64,
256+
id: &SelectionId,
257257
type_name: &str,
258258
selection_set: &mut SelectionSet,
259259
) -> Result<String, MinificationError> {

lib/query-planner/src/ast/normalization/pipeline/flatten_fragments.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,34 @@ fn build_possible_types_map<'a>(ctx: &NormalizationContext<'a>) -> PossibleTypes
6868
let mut possible_types = PossibleTypesMap::new();
6969
let maybe_subgraph_name = ctx.subgraph_name.as_ref();
7070

71-
let relevant_definitions = ctx.supergraph.definitions.iter().filter(|(_, def)| {
72-
if let Some(subgraph_name) = maybe_subgraph_name {
73-
def.is_defined_in_subgraph(subgraph_name.as_str())
74-
} else {
75-
true
71+
let mut object_types_list = Vec::new();
72+
let mut abstract_types_list = Vec::new();
73+
74+
for (name, def) in &ctx.supergraph.definitions {
75+
match def {
76+
SupergraphDefinition::Union(_) | SupergraphDefinition::Interface(_) => {
77+
if maybe_subgraph_name.is_none()
78+
|| maybe_subgraph_name.is_some_and(|subgraph_name| {
79+
def.is_defined_in_subgraph(subgraph_name.as_str())
80+
})
81+
{
82+
abstract_types_list.push((name, def));
83+
}
84+
}
85+
SupergraphDefinition::Object(_) => {
86+
if maybe_subgraph_name.is_none()
87+
|| maybe_subgraph_name.is_some_and(|subgraph_name| {
88+
def.is_defined_in_subgraph(subgraph_name.as_str())
89+
})
90+
{
91+
object_types_list.push((name, def));
92+
}
93+
}
94+
_ => {}
7695
}
77-
});
96+
}
7897

79-
for (type_name, type_def) in relevant_definitions.clone() {
98+
for (type_name, type_def) in &abstract_types_list {
8099
match type_def {
81100
SupergraphDefinition::Union(union_type) => {
82101
let members = union_type
@@ -95,14 +114,14 @@ fn build_possible_types_map<'a>(ctx: &NormalizationContext<'a>) -> PossibleTypes
95114
}
96115
SupergraphDefinition::Interface(_) => {
97116
let mut object_types: HashSet<&str> = HashSet::new();
98-
for (obj_type_name, obj_type_def) in relevant_definitions.clone() {
117+
for (obj_type_name, obj_type_def) in &object_types_list {
99118
if let SupergraphDefinition::Object(object_type) = obj_type_def {
100119
if object_type.join_implements.iter().any(|j| {
101120
let belongs = match maybe_subgraph_name {
102121
Some(subgraph_name) => &j.graph_id == *subgraph_name,
103122
None => true,
104123
};
105-
belongs && &j.interface == type_name
124+
belongs && &j.interface == *type_name
106125
}) {
107126
object_types.insert(obj_type_name.as_str());
108127
}

lib/query-planner/src/planner/fetch/fetch_graph.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::ast::type_aware_selection::TypeAwareSelection;
55
use crate::graph::edge::{Edge, FieldMove, InterfaceObjectTypeMove, PlannerOverrideContext};
66
use crate::graph::node::Node;
77
use crate::graph::Graph;
8-
use crate::planner::fetch::fetch_step_data::{FetchStepData, FetchStepKind};
8+
use crate::planner::fetch::fetch_step_data::{FetchStepData, FetchStepFlags, FetchStepKind};
99
use crate::planner::plan_nodes::{FetchNodePathSegment, FetchRewrite, ValueSetter};
1010
use crate::planner::tree::query_tree::QueryTree;
1111
use crate::planner::tree::query_tree_node::{MutationFieldPosition, QueryTreeNode};
@@ -236,6 +236,11 @@ impl Display for FetchGraph {
236236
}
237237

238238
fn create_noop_fetch_step(fetch_graph: &mut FetchGraph, created_from_requires: bool) -> NodeIndex {
239+
let flags = if created_from_requires {
240+
FetchStepFlags::USED_FOR_REQUIRES
241+
} else {
242+
FetchStepFlags::empty()
243+
};
239244
fetch_graph.add_step(FetchStepData {
240245
service_name: SubgraphName::any(),
241246
response_path: MergePath::default(),
@@ -247,7 +252,7 @@ fn create_noop_fetch_step(fetch_graph: &mut FetchGraph, created_from_requires: b
247252
selection_set: SelectionSet::default(),
248253
type_name: "*".to_string(),
249254
},
250-
used_for_requires: created_from_requires,
255+
flags,
251256
condition: None,
252257
kind: FetchStepKind::Root,
253258
input_rewrites: None,
@@ -268,6 +273,11 @@ fn create_fetch_step_for_entity_call(
268273
condition: Option<&Condition>,
269274
used_for_requires: bool,
270275
) -> NodeIndex {
276+
let flags = if used_for_requires {
277+
FetchStepFlags::USED_FOR_REQUIRES
278+
} else {
279+
FetchStepFlags::empty()
280+
};
271281
fetch_graph.add_step(FetchStepData {
272282
service_name: subgraph_name.clone(),
273283
response_path: response_path.clone(),
@@ -281,7 +291,7 @@ fn create_fetch_step_for_entity_call(
281291
selection_set: SelectionSet::default(),
282292
type_name: output_type_name.to_string(),
283293
},
284-
used_for_requires,
294+
flags,
285295
condition: condition.cloned(),
286296
kind: FetchStepKind::Entity,
287297
input_rewrites: None,
@@ -311,7 +321,7 @@ fn create_fetch_step_for_root_move(
311321
selection_set: SelectionSet::default(),
312322
type_name: type_name.to_string(),
313323
},
314-
used_for_requires: false,
324+
flags: FetchStepFlags::empty(),
315325
condition: None,
316326
kind: FetchStepKind::Root,
317327
variable_usages: None,
@@ -373,7 +383,9 @@ fn ensure_fetch_step_for_subgraph(
373383

374384
// If there are requirements, then we do not re-use
375385
// optimizations will try to re-use the existing step later, if possible.
376-
if fetch_step.used_for_requires || requires.is_some() {
386+
if fetch_step.flags.contains(FetchStepFlags::USED_FOR_REQUIRES)
387+
|| requires.is_some()
388+
{
377389
return None;
378390
}
379391

@@ -713,13 +725,18 @@ fn process_entity_move_edge(
713725
output_type_name,
714726
fetch_step_index.index()
715727
);
728+
716729
fetch_step.add_input_rewrite(FetchRewrite::ValueSetter(ValueSetter {
717730
path: vec![
718731
FetchNodePathSegment::TypenameEquals(output_type_name.to_string()),
719732
FetchNodePathSegment::Key("__typename".to_string()),
720733
],
721734
set_value_to: output_type_name.clone().into(),
722735
}));
736+
737+
fetch_step
738+
.flags
739+
.insert(FetchStepFlags::USED_FOR_TYPE_CONDITION);
723740
}
724741

725742
let parent_fetch_step = fetch_graph.get_step_data_mut(parent_fetch_step_index)?;

lib/query-planner/src/planner/fetch/fetch_step_data.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{collections::BTreeSet, fmt::Display};
22

3+
use bitflags::bitflags;
34
use petgraph::{graph::NodeIndex, visit::EdgeRef};
45
use tracing::trace;
56

@@ -17,14 +18,24 @@ use crate::{
1718
state::supergraph_state::SubgraphName,
1819
};
1920

21+
bitflags! {
22+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
23+
pub struct FetchStepFlags: u8 {
24+
/// This fetch is for resolving a @requires directive.
25+
const USED_FOR_REQUIRES = 1 << 0;
26+
/// This fetch is for resolving a type condition on an interface.
27+
const USED_FOR_TYPE_CONDITION = 1 << 1;
28+
}
29+
}
30+
2031
#[derive(Debug, Clone)]
2132
pub struct FetchStepData {
2233
pub service_name: SubgraphName,
2334
pub response_path: MergePath,
2435
pub input: TypeAwareSelection,
2536
pub output: TypeAwareSelection,
2637
pub kind: FetchStepKind,
27-
pub used_for_requires: bool,
38+
pub flags: FetchStepFlags,
2839
pub condition: Option<Condition>,
2940
pub variable_usages: Option<BTreeSet<String>>,
3041
pub variable_definitions: Option<Vec<VariableDefinition>>,
@@ -52,10 +63,14 @@ impl Display for FetchStepData {
5263
self.response_path.join("."),
5364
)?;
5465

55-
if self.used_for_requires {
66+
if self.flags.contains(FetchStepFlags::USED_FOR_REQUIRES) {
5667
write!(f, " [@requires]")?;
5768
}
5869

70+
if self.flags.contains(FetchStepFlags::USED_FOR_TYPE_CONDITION) {
71+
write!(f, " [no_pass_through]")?;
72+
}
73+
5974
if let Some(condition) = &self.condition {
6075
match condition {
6176
Condition::Include(var_name) => write!(f, " [@include(if: ${})]", var_name)?,

lib/query-planner/src/planner/fetch/optimize/merge_passthrough_child.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ use petgraph::{
88
use tracing::{instrument, trace};
99

1010
use crate::planner::fetch::{
11-
error::FetchGraphError, fetch_graph::FetchGraph, fetch_step_data::FetchStepData,
11+
error::FetchGraphError,
12+
fetch_graph::FetchGraph,
13+
fetch_step_data::{FetchStepData, FetchStepFlags},
1214
};
1315

1416
impl FetchGraph {
15-
/// When a child has the input identical as the output,
17+
/// When a child's input contains its output,
1618
/// it gets squashed into its parent.
1719
/// Its children becomes children of the parent.
1820
#[instrument(level = "trace", skip_all)]
@@ -94,6 +96,13 @@ impl FetchStepData {
9496
return false;
9597
}
9698

99+
if other
100+
.flags
101+
.contains(FetchStepFlags::USED_FOR_TYPE_CONDITION)
102+
{
103+
return false;
104+
}
105+
97106
// if the `other` FetchStep has a single parent and it's `this` FetchStep
98107
if fetch_graph.parents_of(other_index).count() != 1 {
99108
return false;
@@ -103,7 +112,7 @@ impl FetchStepData {
103112
return false;
104113
}
105114

106-
other.input.eq(&other.output)
115+
other.input.contains(&other.output)
107116
}
108117
}
109118

lib/query-planner/src/planner/fetch/optimize/mod.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,25 @@ use crate::{
1818
impl FetchGraph {
1919
#[instrument(level = "trace", skip_all)]
2020
pub fn optimize(&mut self, supergraph_state: &SupergraphState) -> Result<(), FetchGraphError> {
21-
self.merge_passthrough_child()?;
22-
self.merge_children_with_parents()?;
23-
self.merge_siblings()?;
24-
self.merge_leafs()?;
25-
self.deduplicate_and_prune_fetch_steps()?;
21+
// Run optimization passes repeatedly until the graph stabilizes, as one optimization can create
22+
// opportunities for others.
23+
loop {
24+
let node_count_before = self.graph.node_count();
25+
let edge_count_before = self.graph.edge_count();
26+
27+
self.merge_passthrough_child()?;
28+
self.merge_children_with_parents()?;
29+
self.merge_siblings()?;
30+
self.merge_leafs()?;
31+
self.deduplicate_and_prune_fetch_steps()?;
32+
33+
let node_count_after = self.graph.node_count();
34+
let edge_count_after = self.graph.edge_count();
35+
36+
if node_count_before == node_count_after && edge_count_before == edge_count_after {
37+
break;
38+
}
39+
}
2640
self.turn_mutations_into_sequence()?;
2741
self.fix_conflicting_type_mismatches(supergraph_state)?;
2842

0 commit comments

Comments
 (0)