Skip to content

Commit 4eaf9ec

Browse files
fix: support @include and @skip in initial fetch node (#591)
Fixed an issue where `@skip` and `@include` directives were incorrectly removed from the initial Fetch of the Query Plan. References #579 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 247c1ce commit 4eaf9ec

File tree

18 files changed

+660
-65
lines changed

18 files changed

+660
-65
lines changed

.changeset/fix-cond.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
node-addon: patch
3+
router: patch
4+
query-planner: patch
5+
---
6+
7+
Fixed an issue where `@skip` and `@include` directives were incorrectly removed from the initial Fetch of the Query Plan.

lib/query-planner/src/ast/merge_path.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ pub enum Condition {
1313
Include(String),
1414
}
1515

16+
impl Condition {
17+
pub fn to_skip_if(&self) -> Option<String> {
18+
match self {
19+
Condition::Skip(var) => Some(var.clone()),
20+
_ => None,
21+
}
22+
}
23+
pub fn to_include_if(&self) -> Option<String> {
24+
match self {
25+
Condition::Include(var) => Some(var.clone()),
26+
_ => None,
27+
}
28+
}
29+
}
30+
1631
impl Display for Condition {
1732
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1833
match self {

lib/query-planner/src/ast/selection_item.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{
2-
ast::normalization::utils::extract_type_condition, utils::pretty_display::PrettyDisplay,
2+
ast::normalization::utils::extract_type_condition,
3+
utils::pretty_display::{get_indent, PrettyDisplay},
34
};
45
use graphql_parser::query as query_ast;
56

@@ -49,7 +50,10 @@ impl PrettyDisplay for SelectionItem {
4950
SelectionItem::InlineFragment(fragment_selection) => {
5051
fragment_selection.pretty_fmt(f, depth)?
5152
}
52-
SelectionItem::FragmentSpread(name) => write!(f, "...{}", name)?,
53+
SelectionItem::FragmentSpread(name) => {
54+
let indent = get_indent(depth);
55+
writeln!(f, "{indent}...{}", name)?
56+
}
5357
}
5458

5559
Ok(())

lib/query-planner/src/ast/selection_set.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ impl PrettyDisplay for FieldSelection {
303303
None => String::new(),
304304
};
305305

306+
write!(f, "{indent}{}{}{}", alias_str, self.name, args_str)?;
307+
306308
if let Some(skip_if) = &self.skip_if {
307309
write!(f, " @skip(if: ${})", skip_if)?;
308310
}
@@ -312,10 +314,10 @@ impl PrettyDisplay for FieldSelection {
312314
}
313315

314316
if self.is_leaf() {
315-
return writeln!(f, "{indent}{}{}{}", alias_str, self.name, args_str);
317+
return writeln!(f);
316318
}
317319

318-
writeln!(f, "{indent}{}{}{} {{", alias_str, self.name, args_str)?;
320+
writeln!(f, " {{")?;
319321
self.selections.pretty_fmt(f, depth + 1)?;
320322
writeln!(f, "{indent}}}")
321323
}

lib/query-planner/src/graph/edge.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ pub enum Edge {
159159
EntityMove(EntityMove),
160160
/// join__implements
161161
AbstractMove(String),
162+
/// A special edge representing a case where an inline fragment is used with a condition,
163+
/// and we don't really move anywhere, but we need to ensure that
164+
/// the condition is preserved when planning the query.
165+
Selfie(String),
162166
/// Represents a special case where going from @interfaceObject
163167
/// to an object type due to the `__typename` field usage,
164168
/// or usage of a type condition (fragment),
@@ -227,6 +231,7 @@ impl Edge {
227231
Self::FieldMove(fm) => &fm.name,
228232
Self::EntityMove(EntityMove { key, .. }) => key,
229233
Self::AbstractMove(id) => id,
234+
Self::Selfie(_) => "selfie",
230235
Self::SubgraphEntrypoint { name, .. } => &name.0,
231236
Self::InterfaceObjectTypeMove(InterfaceObjectTypeMove {
232237
object_type_name, ..
@@ -264,6 +269,7 @@ impl Display for Edge {
264269
Edge::SubgraphEntrypoint { name, .. } => write!(f, "{}", name.0),
265270
Edge::EntityMove(EntityMove { .. }) => write!(f, "🔑"),
266271
Edge::AbstractMove(_) => write!(f, "🔮"),
272+
Edge::Selfie(_) => write!(f, "🤳"),
267273
Edge::FieldMove(field_move) => write!(f, "{}", field_move.name),
268274
Edge::InterfaceObjectTypeMove(m) => write!(f, "🔎 {}", m.object_type_name),
269275
}?;
@@ -318,6 +324,7 @@ impl Debug for Edge {
318324
write!(f, "🔑 {}", key)
319325
}
320326
Edge::AbstractMove(name) => write!(f, "🔮 {}", name),
327+
Edge::Selfie(_) => write!(f, "🤳"),
321328
Edge::InterfaceObjectTypeMove(m) => write!(f, "🔎 {}", m.object_type_name),
322329
}
323330
}

lib/query-planner/src/graph/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,18 @@ impl Graph {
643643
);
644644
}
645645

646+
trace!(
647+
"[x] Creating self-referencing edge for '{}/{}'",
648+
def_name,
649+
graph_id
650+
);
651+
let head = self.upsert_node(Node::new_node(
652+
def_name,
653+
state.resolve_graph_id(graph_id)?,
654+
state.is_interface_object_in_subgraph(def_name, graph_id),
655+
));
656+
self.upsert_edge(head, head, Edge::Selfie(def_name.clone()));
657+
646658
for (field_name, field_definition) in definition.fields().iter() {
647659
let (is_available, maybe_join_field) =
648660
FederationRules::check_field_subgraph_availability(

lib/query-planner/src/graph/tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@ mod graph_tests {
287287
.assert_field_edge("createdBy", "User/products")
288288
.assert_field_edge("__typename", "String/products")
289289
.no_field_edge("reviews");
290-
assert_eq!(incoming.edges.len(), 2);
291-
assert_eq!(outgoing.edges.len(), 11);
290+
assert_eq!(incoming.edges.len(), 3);
291+
assert_eq!(outgoing.edges.len(), 12);
292292

293293
// requires preserves selection set in the graph
294294
let outgoing = find_node(&graph, "Product/inventory").1;
@@ -315,8 +315,8 @@ mod graph_tests {
315315
assert_eq!(graph.root_subscription_node(), None);
316316

317317
let (incoming, outgoing) = find_node(&graph, "Product/products");
318-
assert_eq!(incoming.edges.len(), 11);
319-
assert_eq!(outgoing.edges.len(), 15);
318+
assert_eq!(incoming.edges.len(), 14);
319+
assert_eq!(outgoing.edges.len(), 18);
320320

321321
incoming
322322
.assert_key_edge("id", "Product/inventory")

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

Lines changed: 158 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,26 @@ impl FetchGraph {
186186

187187
Ok(())
188188
}
189+
190+
/// Checks whether the given step is an ancestor of a step that has the given condition
191+
pub fn is_ancestor_of_condition(&self, step_index: NodeIndex, condition: &Condition) -> bool {
192+
let mut bfs = Bfs::new(&self.graph, step_index);
193+
194+
while let Some(current_index) = bfs.next(&self.graph) {
195+
let current_step = match self.get_step_data(current_index) {
196+
Ok(s) => s,
197+
Err(_) => continue,
198+
};
199+
200+
if let Some(step_condition) = &current_step.condition {
201+
if step_condition == condition {
202+
return true;
203+
}
204+
}
205+
}
206+
207+
false
208+
}
189209
}
190210

191211
impl Display for FetchGraph {
@@ -1004,6 +1024,92 @@ fn process_subgraph_entrypoint_edge(
10041024
)
10051025
}
10061026

1027+
// TODO: simplfy args
1028+
#[allow(clippy::too_many_arguments)]
1029+
#[instrument(level = "trace",skip_all, fields(
1030+
parent_fetch_step_index = parent_fetch_step_index.index(),
1031+
requiring_fetch_step_index = requiring_fetch_step_index.map(|f| f.index()),
1032+
type_name = target_type_name,
1033+
response_path = response_path.to_string(),
1034+
fetch_path = fetch_path.to_string()
1035+
))]
1036+
fn process_selfie_edge(
1037+
graph: &Graph,
1038+
fetch_graph: &mut FetchGraph,
1039+
override_context: &PlannerOverrideContext,
1040+
query_node: &QueryTreeNode,
1041+
parent_fetch_step_index: NodeIndex,
1042+
requiring_fetch_step_index: Option<NodeIndex>,
1043+
response_path: &MergePath,
1044+
fetch_path: &MergePath,
1045+
target_type_name: &String,
1046+
condition: Option<&Condition>,
1047+
) -> Result<Vec<NodeIndex>, FetchGraphError> {
1048+
let is_ancestor_of_condition = match condition {
1049+
Some(c) => fetch_graph.is_ancestor_of_condition(parent_fetch_step_index, c),
1050+
None => false,
1051+
};
1052+
1053+
let parent_fetch_step = fetch_graph.get_step_data_mut(parent_fetch_step_index)?;
1054+
trace!(
1055+
"starting an inline fragment for type '{}' to fetch step [{}]",
1056+
parent_fetch_step_index.index(),
1057+
target_type_name,
1058+
);
1059+
parent_fetch_step.output.add_at_path(
1060+
&TypeAwareSelection {
1061+
selection_set: SelectionSet {
1062+
items: vec![SelectionItem::InlineFragment(InlineFragmentSelection {
1063+
type_condition: target_type_name.clone(),
1064+
selections: SelectionSet::default(),
1065+
skip_if: condition.and_then(|c| {
1066+
if is_ancestor_of_condition {
1067+
None
1068+
} else {
1069+
c.to_skip_if()
1070+
}
1071+
}),
1072+
include_if: condition.and_then(|c| {
1073+
if is_ancestor_of_condition {
1074+
None
1075+
} else {
1076+
c.to_include_if()
1077+
}
1078+
}),
1079+
})],
1080+
},
1081+
type_name: target_type_name.clone(),
1082+
},
1083+
fetch_path.clone(),
1084+
false,
1085+
)?;
1086+
1087+
let segment_condition = if is_ancestor_of_condition {
1088+
None
1089+
} else {
1090+
condition.cloned()
1091+
};
1092+
let child_response_path = response_path.push(Segment::Cast(
1093+
target_type_name.clone(),
1094+
segment_condition.clone(),
1095+
));
1096+
let child_fetch_path =
1097+
fetch_path.push(Segment::Cast(target_type_name.clone(), segment_condition));
1098+
1099+
process_children_for_fetch_steps(
1100+
graph,
1101+
fetch_graph,
1102+
override_context,
1103+
query_node,
1104+
parent_fetch_step_index,
1105+
&child_response_path,
1106+
&child_fetch_path,
1107+
requiring_fetch_step_index,
1108+
condition,
1109+
false,
1110+
)
1111+
}
1112+
10071113
// TODO: simplfy args
10081114
#[allow(clippy::too_many_arguments)]
10091115
#[instrument(level = "trace",skip_all, fields(
@@ -1111,6 +1217,25 @@ fn process_plain_field_edge(
11111217
fetch_graph.connect(parent_fetch_step_index, requiring_fetch_step_index);
11121218
}
11131219

1220+
// If we are inserting into "... | [Product] @include(if: $bool)", we don't need the condition on the field.
1221+
let condition_in_path = if let Some(condition) = condition {
1222+
fetch_path.inner.iter().any(|segment| {
1223+
matches!(
1224+
segment,
1225+
Segment::Cast(_, Some(c)) | Segment::Field(_, _, Some(c)) if c == condition
1226+
)
1227+
})
1228+
} else {
1229+
false
1230+
};
1231+
1232+
let ancestor_of_condition = match condition {
1233+
Some(c) => fetch_graph.is_ancestor_of_condition(parent_fetch_step_index, c),
1234+
None => false,
1235+
};
1236+
1237+
let should_strip_condition = condition_in_path || ancestor_of_condition;
1238+
11141239
let parent_fetch_step = fetch_graph.get_step_data_mut(parent_fetch_step_index)?;
11151240
trace!(
11161241
"adding output field '{}' to fetch step [{}]",
@@ -1126,8 +1251,20 @@ fn process_plain_field_edge(
11261251
alias: query_node.selection_alias().map(|a| a.to_string()),
11271252
selections: SelectionSet::default(),
11281253
arguments: query_node.selection_arguments().cloned(),
1129-
skip_if: None,
1130-
include_if: None,
1254+
skip_if: condition.and_then(|c| {
1255+
if should_strip_condition {
1256+
None
1257+
} else {
1258+
c.to_skip_if()
1259+
}
1260+
}),
1261+
include_if: condition.and_then(|c| {
1262+
if should_strip_condition {
1263+
None
1264+
} else {
1265+
c.to_include_if()
1266+
}
1267+
}),
11311268
})],
11321269
},
11331270
type_name: field_move.type_name.to_string(),
@@ -1137,19 +1274,24 @@ fn process_plain_field_edge(
11371274
)?;
11381275

11391276
let child_segment = query_node.selection_alias().unwrap_or(&field_move.name);
1277+
let segment_condition = if ancestor_of_condition {
1278+
None
1279+
} else {
1280+
condition.cloned()
1281+
};
11401282
let segment_args_hash = query_node
11411283
.selection_arguments()
11421284
.map(|a| a.hash_u64())
11431285
.unwrap_or(0);
11441286
let mut child_response_path = response_path.push(Segment::Field(
11451287
child_segment.to_string(),
11461288
segment_args_hash,
1147-
None,
1289+
segment_condition.clone(),
11481290
));
11491291
let mut child_fetch_path = fetch_path.push(Segment::Field(
11501292
child_segment.to_string(),
11511293
segment_args_hash,
1152-
None,
1294+
segment_condition,
11531295
));
11541296

11551297
if field_move.is_list {
@@ -1595,6 +1737,18 @@ fn process_query_node(
15951737
created_from_requires,
15961738
),
15971739
},
1740+
Edge::Selfie(type_name) => process_selfie_edge(
1741+
graph,
1742+
fetch_graph,
1743+
override_context,
1744+
query_node,
1745+
parent_fetch_step_index,
1746+
requiring_fetch_step_index,
1747+
response_path,
1748+
fetch_path,
1749+
type_name,
1750+
condition,
1751+
),
15981752
Edge::AbstractMove(type_name) => process_abstract_edge(
15991753
graph,
16001754
fetch_graph,

0 commit comments

Comments
 (0)