Skip to content

Commit 24e5e6b

Browse files
authored
Fix: Avoid infinite loop when merging siblings (#282)
1 parent 1940cbb commit 24e5e6b

File tree

5 files changed

+243
-0
lines changed

5 files changed

+243
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
schema
2+
@link(url: "https://specs.apollo.dev/link/v1.0")
3+
@link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
4+
query: Query
5+
}
6+
7+
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
8+
9+
directive @join__field(
10+
graph: join__Graph
11+
requires: join__FieldSet
12+
provides: join__FieldSet
13+
type: String
14+
external: Boolean
15+
override: String
16+
usedOverridden: Boolean
17+
) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
18+
19+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
20+
21+
directive @join__implements(
22+
graph: join__Graph!
23+
interface: String!
24+
) repeatable on OBJECT | INTERFACE
25+
26+
directive @join__type(
27+
graph: join__Graph!
28+
key: join__FieldSet
29+
extension: Boolean! = false
30+
resolvable: Boolean! = true
31+
isInterfaceObject: Boolean! = false
32+
) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
33+
34+
directive @join__unionMember(
35+
graph: join__Graph!
36+
member: String!
37+
) repeatable on UNION
38+
39+
directive @link(
40+
url: String
41+
as: String
42+
for: link__Purpose
43+
import: [link__Import]
44+
) repeatable on SCHEMA
45+
46+
scalar join__FieldSet
47+
48+
enum join__Graph {
49+
A @join__graph(name: "a", url: "http://localhost/a")
50+
B @join__graph(name: "b", url: "http://localhost/b")
51+
C @join__graph(name: "c", url: "http://localhost/c")
52+
D @join__graph(name: "d", url: "http://localhost/d")
53+
}
54+
55+
scalar link__Import
56+
57+
enum link__Purpose {
58+
SECURITY
59+
EXECUTION
60+
}
61+
62+
type Product
63+
@join__type(graph: A, key: "id")
64+
@join__type(graph: B, key: "id")
65+
@join__type(graph: B, key: "pid")
66+
@join__type(graph: C, key: "pid")
67+
@join__type(graph: D, key: "pid") {
68+
id: ID! @join__field(graph: A) @join__field(graph: B)
69+
pid: ID! @join__field(graph: B) @join__field(graph: C) @join__field(graph: D)
70+
b: String! @join__field(graph: B)
71+
c: String! @join__field(graph: C)
72+
d: String! @join__field(graph: D)
73+
}
74+
75+
type Query
76+
@join__type(graph: A)
77+
@join__type(graph: B)
78+
@join__type(graph: C)
79+
@join__type(graph: D) {
80+
viewer: Viewer! @join__field(graph: A)
81+
}
82+
83+
union Review
84+
@join__type(graph: A)
85+
@join__unionMember(graph: A, member: "AnonymousReview")
86+
@join__unionMember(graph: A, member: "UserReview") =
87+
| AnonymousReview
88+
| UserReview
89+
90+
type UserReview @join__type(graph: A) {
91+
# user_body: String!
92+
product: Product
93+
}
94+
95+
type AnonymousReview @join__type(graph: A) {
96+
# anonymous_body: String!
97+
product: Product
98+
}
99+
100+
type Viewer @join__type(graph: A) {
101+
review: Review
102+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ impl FetchGraph {
6363
has_path_connecting(&self.graph, ancestor, descendant, None)
6464
}
6565

66+
/// Checks if one is ancestor of the other and vice versa
67+
pub fn is_ancestor_or_descendant(&self, a: NodeIndex, b: NodeIndex) -> bool {
68+
self.is_descendant_of(a, b) || self.is_descendant_of(b, a)
69+
}
70+
6671
pub fn step_indices(&self) -> NodeIndices<FetchStepData> {
6772
self.graph.node_indices()
6873
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ impl FetchStepData {
128128
}
129129
}
130130

131+
if fetch_graph.is_ancestor_or_descendant(self_index, other_index) {
132+
// Looks like they depend on each other
133+
return false;
134+
}
135+
131136
can_merge_base
132137
}
133138
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use crate::{
2+
tests::testkit::{build_query_plan, init_logger},
3+
utils::parsing::parse_operation,
4+
};
5+
use std::error::Error;
6+
7+
#[test]
8+
fn issue_281_test() -> Result<(), Box<dyn Error>> {
9+
init_logger();
10+
let document = parse_operation(
11+
r#"
12+
{
13+
viewer {
14+
review {
15+
... on AnonymousReview {
16+
__typename
17+
product {
18+
b
19+
}
20+
}
21+
... on UserReview {
22+
__typename
23+
product {
24+
c
25+
d
26+
}
27+
}
28+
}
29+
}
30+
}
31+
32+
"#,
33+
);
34+
let query_plan = build_query_plan("fixture/issues/281.supergraph.graphql", document)?;
35+
36+
insta::assert_snapshot!(format!("{}", query_plan), @r#"
37+
QueryPlan {
38+
Sequence {
39+
Fetch(service: "a") {
40+
{
41+
viewer {
42+
review {
43+
__typename
44+
... on AnonymousReview {
45+
__typename
46+
product {
47+
...a }
48+
}
49+
... on UserReview {
50+
__typename
51+
product {
52+
...a }
53+
}
54+
}
55+
}
56+
}
57+
fragment a on Product {
58+
__typename
59+
id
60+
}
61+
},
62+
Parallel {
63+
Flatten(path: "viewer.review|[UserReview].product") {
64+
Fetch(service: "b") {
65+
{
66+
... on Product {
67+
__typename
68+
id
69+
}
70+
} =>
71+
{
72+
... on Product {
73+
pid
74+
}
75+
}
76+
},
77+
},
78+
Flatten(path: "viewer.review|[AnonymousReview].product") {
79+
Fetch(service: "b") {
80+
{
81+
... on Product {
82+
__typename
83+
id
84+
}
85+
} =>
86+
{
87+
... on Product {
88+
b
89+
}
90+
}
91+
},
92+
},
93+
},
94+
Flatten(path: "viewer.review|[UserReview].product") {
95+
Fetch(service: "c") {
96+
{
97+
... on Product {
98+
__typename
99+
pid
100+
}
101+
} =>
102+
{
103+
... on Product {
104+
c
105+
pid
106+
}
107+
}
108+
},
109+
},
110+
Flatten(path: "viewer.review|[UserReview].product") {
111+
Fetch(service: "d") {
112+
{
113+
... on Product {
114+
__typename
115+
pid
116+
}
117+
} =>
118+
{
119+
... on Product {
120+
d
121+
}
122+
}
123+
},
124+
},
125+
},
126+
},
127+
"#);
128+
129+
Ok(())
130+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod include_skip;
55
mod interface;
66
mod interface_object;
77
mod interface_object_with_requires;
8+
mod issues;
89
mod mutations;
910
mod object_entities;
1011
mod override_requires;

0 commit comments

Comments
 (0)