Skip to content

Commit

Permalink
fix: query plan fragments (#2352)
Browse files Browse the repository at this point in the history
Co-authored-by: Tushar Mathur <[email protected]>
  • Loading branch information
shashitnak and tusharmath authored Jul 5, 2024
1 parent 0ce296f commit 1779753
Show file tree
Hide file tree
Showing 5 changed files with 1,242 additions and 59 deletions.
118 changes: 74 additions & 44 deletions src/core/jit/builder.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::collections::HashMap;

use async_graphql::parser::types::{
DocumentOperations, ExecutableDocument, OperationType, Selection, SelectionSet,
DocumentOperations, ExecutableDocument, FragmentDefinition, OperationType, Selection,
SelectionSet,
};
use async_graphql::Positioned;

use super::model::*;
use crate::core::blueprint::{Blueprint, Index, QueryField};
Expand All @@ -27,57 +29,80 @@ impl Builder {
}
}

#[allow(clippy::too_many_arguments)]
fn iter(
&self,
selection: &SelectionSet,
type_of: &str,
refs: Option<Parent>,
fragments: &HashMap<&str, &FragmentDefinition>,
) -> Vec<Field<Parent>> {
let mut fields = vec![];
for selection in &selection.items {
if let Selection::Field(gql_field) = &selection.node {
let field_name = gql_field.node.name.node.as_str();
let field_args = gql_field
.node
.arguments
.iter()
.map(|(k, v)| (k.node.as_str().to_string(), v.node.to_owned()))
.collect::<HashMap<_, _>>();
match &selection.node {
Selection::Field(Positioned { node: gql_field, .. }) => {
let field_name = gql_field.name.node.as_str();
let field_args = gql_field
.arguments
.iter()
.map(|(k, v)| (k.node.as_str().to_string(), v.node.to_owned()))
.collect::<HashMap<_, _>>();

if let Some(field_def) = self.index.get_field(type_of, field_name) {
let mut args = vec![];
for (arg_name, value) in field_args {
if let Some(arg) = field_def.get_arg(&arg_name) {
let type_of = arg.of_type.clone();
let id = ArgId::new(self.arg_id.next());
let name = arg_name.clone();
let default_value = arg
.default_value
.as_ref()
.and_then(|v| v.to_owned().try_into().ok());
args.push(Arg { id, name, type_of, value: Some(value), default_value });
if let Some(field_def) = self.index.get_field(type_of, field_name) {
let mut args = vec![];
for (arg_name, value) in field_args {
if let Some(arg) = field_def.get_arg(&arg_name) {
let type_of = arg.of_type.clone();
let id = ArgId::new(self.arg_id.next());
let name = arg_name.clone();
let default_value = arg
.default_value
.as_ref()
.and_then(|v| v.to_owned().try_into().ok());
args.push(Arg {
id,
name,
type_of,
value: Some(value),
default_value,
});
}
}
}

let type_of = match field_def {
QueryField::Field((field_def, _)) => field_def.of_type.clone(),
QueryField::InputField(field_def) => field_def.of_type.clone(),
};
let type_of = match field_def {
QueryField::Field((field_def, _)) => field_def.of_type.clone(),
QueryField::InputField(field_def) => field_def.of_type.clone(),
};

let id = FieldId::new(self.field_id.next());
let child_fields = self.iter(
&gql_field.node.selection_set.node,
type_of.name(),
Some(Parent::new(id.clone())),
);
let name = field_name.to_owned();
let ir = match field_def {
QueryField::Field((field_def, _)) => field_def.resolver.clone(),
_ => None,
};
fields.push(Field { id, name, ir, type_of, args, refs: refs.clone() });
fields = fields.merge_right(child_fields);
let id = FieldId::new(self.field_id.next());
let child_fields = self.iter(
&gql_field.selection_set.node,
type_of.name(),
Some(Parent::new(id.clone())),
fragments,
);
let name = field_name.to_owned();
let ir = match field_def {
QueryField::Field((field_def, _)) => field_def.resolver.clone(),
_ => None,
};
fields.push(Field { id, name, ir, type_of, args, refs: refs.clone() });
fields = fields.merge_right(child_fields);
}
}
Selection::FragmentSpread(Positioned { node: fragment_spread, .. }) => {
if let Some(fragment) =
fragments.get(fragment_spread.fragment_name.node.as_str())
{
fields.extend(self.iter(
&fragment.selection_set.node,
fragment.type_condition.node.on.node.as_str(),
refs.clone(),
fragments,
));
}
}
_ => {}
}
}

Expand All @@ -94,10 +119,10 @@ impl Builder {

pub fn build(&self) -> Result<ExecutionPlan, String> {
let mut fields = Vec::new();
let mut fragments: HashMap<&str, &FragmentDefinition> = HashMap::new();

for fragment in self.document.fragments.values() {
let on_type = fragment.node.type_condition.node.on.node.as_str();
fields.extend(self.iter(&fragment.node.selection_set.node, on_type, None));
for (name, fragment) in self.document.fragments.iter() {
fragments.insert(name.as_str(), &fragment.node);
}

match &self.document.operations {
Expand All @@ -106,15 +131,20 @@ impl Builder {
"Root Operation type not defined for {}",
single.node.ty
))?;
fields.extend(self.iter(&single.node.selection_set.node, name, None));
fields.extend(self.iter(&single.node.selection_set.node, name, None, &fragments));
}
DocumentOperations::Multiple(multiple) => {
for single in multiple.values() {
let name = self.get_type(single.node.ty).ok_or(format!(
"Root Operation type not defined for {}",
single.node.ty
))?;
fields.extend(self.iter(&single.node.selection_set.node, name, None));
fields.extend(self.iter(
&single.node.selection_set.node,
name,
None,
&fragments,
));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,6 @@ expression: plan
[
Field {
id: 0,
name: "name",
type_of: String!,
},
Field {
id: 1,
name: "email",
type_of: String!,
},
Field {
id: 2,
name: "phone",
type_of: String,
},
Field {
id: 3,
name: "user",
ir: "Some(..)",
type_of: User,
Expand All @@ -36,5 +21,26 @@ expression: plan
default_value: None,
},
],
refs: Some(
Children(
[
Field {
id: 1,
name: "name",
type_of: String!,
},
Field {
id: 2,
name: "email",
type_of: String!,
},
Field {
id: 3,
name: "phone",
type_of: String,
},
],
),
),
},
]
57 changes: 57 additions & 0 deletions tests/jit_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,61 @@ mod tests {

insta::assert_json_snapshot!(data);
}

#[tokio::test]
async fn test_executor_fragments() {
// NOTE: This test makes a real HTTP call
let request = Request::new(
r#"
fragment UserPII on User {
name
email
phone
}
query {
users {
id
...UserPII
username
}
}
"#,
);
let executor = new_executor(&request).await.unwrap();
let response = executor.execute(request).await;
let data = response.data;

insta::assert_json_snapshot!(data);
}

#[tokio::test]
async fn test_executor_fragments_nested() {
// NOTE: This test makes a real HTTP call
let request = Request::new(
r#"
fragment UserPII on User {
name
email
phone
}
query {
posts {
id
user {
id
...UserPII
username
}
}
}
"#,
);
let executor = new_executor(&request).await.unwrap();
let response = executor.execute(request).await;
let data = response.data;

insta::assert_json_snapshot!(data);
}
}
80 changes: 80 additions & 0 deletions tests/snapshots/jit_spec__tests__executor_fragments.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
source: tests/jit_spec.rs
expression: data
---
{
"data": {
"users": [
{
"id": 1,
"name": "Leanne Graham",
"email": "[email protected]",
"phone": "1-770-736-8031 x56442",
"username": "Bret"
},
{
"id": 2,
"name": "Ervin Howell",
"email": "[email protected]",
"phone": "010-692-6593 x09125",
"username": "Antonette"
},
{
"id": 3,
"name": "Clementine Bauch",
"email": "[email protected]",
"phone": "1-463-123-4447",
"username": "Samantha"
},
{
"id": 4,
"name": "Patricia Lebsack",
"email": "[email protected]",
"phone": "493-170-9623 x156",
"username": "Karianne"
},
{
"id": 5,
"name": "Chelsey Dietrich",
"email": "[email protected]",
"phone": "(254)954-1289",
"username": "Kamren"
},
{
"id": 6,
"name": "Mrs. Dennis Schulist",
"email": "[email protected]",
"phone": "1-477-935-8478 x6430",
"username": "Leopoldo_Corkery"
},
{
"id": 7,
"name": "Kurtis Weissnat",
"email": "[email protected]",
"phone": "210.067.6132",
"username": "Elwyn.Skiles"
},
{
"id": 8,
"name": "Nicholas Runolfsdottir V",
"email": "[email protected]",
"phone": "586.493.6943 x140",
"username": "Maxime_Nienow"
},
{
"id": 9,
"name": "Glenna Reichert",
"email": "[email protected]",
"phone": "(775)976-6794 x41206",
"username": "Delphine"
},
{
"id": 10,
"name": "Clementina DuBuque",
"email": "[email protected]",
"phone": "024-648-3804",
"username": "Moriah.Stanton"
}
]
}
}
Loading

1 comment on commit 1779753

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running 30s test @ http://localhost:8000/graphql

4 threads and 100 connections

Thread Stats Avg Stdev Max +/- Stdev
Latency 7.34ms 3.25ms 64.30ms 70.44%
Req/Sec 3.44k 145.74 3.72k 85.33%

410824 requests in 30.01s, 2.06GB read

Requests/sec: 13689.69

Transfer/sec: 70.26MB

Please sign in to comment.