diff --git a/src/core/app_context.rs b/src/core/app_context.rs index 03036584c6..c0dd7f146f 100644 --- a/src/core/app_context.rs +++ b/src/core/app_context.rs @@ -55,6 +55,7 @@ impl AppContext { http_filter, is_list, dedupe, + is_dependent, .. } => { let is_list = *is_list; @@ -73,6 +74,7 @@ impl AppContext { http_filter: http_filter.clone(), is_list, dedupe, + is_dependent: *is_dependent, })); http_data_loaders.push(data_loader); diff --git a/src/core/blueprint/operators/http.rs b/src/core/blueprint/operators/http.rs index 6518388845..534268ab49 100644 --- a/src/core/blueprint/operators/http.rs +++ b/src/core/blueprint/operators/http.rs @@ -63,6 +63,12 @@ pub fn compile_http( .or(config_module.upstream.on_request.clone()) .map(|on_request| HttpFilter { on_request }); + // check if the resolver is independent or not. + let is_dependent = http.query.iter().any(|q| q.value.contains("{{.value") || q.value.contains("{{value")) || http + .body + .as_ref() + .map_or(false, |b| b.contains("{{.value") || b.contains("{{value")); + let io = if !http.batch_key.is_empty() && http.method == Method::GET { // Find a query parameter that contains a reference to the {{.value}} key let key = http.query.iter().find_map(|q| { @@ -77,6 +83,7 @@ pub fn compile_http( http_filter, is_list, dedupe, + is_dependent, }) } else { IR::IO(IO::Http { @@ -86,6 +93,7 @@ pub fn compile_http( http_filter, is_list, dedupe, + is_dependent, }) }; (io, &http.select) diff --git a/src/core/ir/eval.rs b/src/core/ir/eval.rs index 840893c84f..886b212002 100644 --- a/src/core/ir/eval.rs +++ b/src/core/ir/eval.rs @@ -146,6 +146,7 @@ impl IR { Ok(ConstValue::object(obj)) } + IR::Deferred { ir, .. } => ir.eval(ctx).await, } }) } diff --git a/src/core/ir/model.rs b/src/core/ir/model.rs index 234e38beef..90546e3a49 100644 --- a/src/core/ir/model.rs +++ b/src/core/ir/model.rs @@ -13,6 +13,15 @@ use crate::core::graphql::{self}; use crate::core::http::HttpFilter; use crate::core::{grpc, http}; +#[derive(Clone, Debug)] +#[allow(dead_code)] +pub struct IrId(usize); +impl IrId { + pub fn new(id: usize) -> Self { + Self(id) + } +} + #[derive(Clone, Debug, Display)] pub enum IR { Dynamic(DynamicValue), @@ -30,6 +39,11 @@ pub enum IR { Entity(HashMap), /// Apollo Federation _service resolver Service(String), + Deferred { + id: IrId, + ir: Box, + path: Vec, + }, } #[derive(Clone, Debug)] @@ -48,6 +62,7 @@ pub enum IO { http_filter: Option, is_list: bool, dedupe: bool, + is_dependent: bool, }, GraphQL { req_template: graphql::RequestTemplate, @@ -68,6 +83,16 @@ pub enum IO { } impl IO { + // TODO: fix is_dependent for GraphQL and Grpc. + pub fn is_dependent(&self) -> bool { + match self { + IO::Http { is_dependent, .. } => *is_dependent, + IO::GraphQL { .. } => true, + IO::Grpc { .. } => true, + IO::Js { .. } => false, + } + } + pub fn dedupe(&self) -> bool { match self { IO::Http { dedupe, .. } => *dedupe, @@ -174,6 +199,9 @@ impl IR { .collect(), ), IR::Service(sdl) => IR::Service(sdl), + IR::Deferred { ir, path, id } => { + IR::Deferred { ir: Box::new(ir.modify(modifier)), path, id } + } } } } diff --git a/src/core/jit/builder.rs b/src/core/jit/builder.rs index 26ab6d1f66..6f63082f0a 100644 --- a/src/core/jit/builder.rs +++ b/src/core/jit/builder.rs @@ -278,7 +278,34 @@ impl Builder { .map(|cond| cond.node.on.node.as_str()) .unwrap_or(type_condition); - fields.extend(self.iter(&fragment.selection_set.node, type_of, fragments)); + // what if we've directive defined in the fragment? + let mut directives = Vec::with_capacity(fragment.directives.len()); + for directive in fragment.directives.iter() { + let directive = &directive.node; + if directive.name.node == "skip" || directive.name.node == "include" { + continue; + } + let arguments = directive + .arguments + .iter() + .map(|(k, v)| (k.node.to_string(), v.node.clone())) + .collect::>(); + + directives + .push(JitDirective { name: directive.name.to_string(), arguments }); + } + + if directives.iter().find(|d| d.name == "defer").is_some() { + let mut child_fields = + self.iter(&fragment.selection_set.node, type_of, fragments); + + child_fields.iter_mut().for_each(|f| { + f.directives.extend(directives.clone()); + }); + fields.extend(child_fields); + } else { + fields.extend(self.iter(&fragment.selection_set.node, type_of, fragments)); + } } } } diff --git a/src/core/jit/exec_const.rs b/src/core/jit/exec_const.rs index 1606f05631..11b0a0f650 100644 --- a/src/core/jit/exec_const.rs +++ b/src/core/jit/exec_const.rs @@ -98,6 +98,9 @@ impl ConstValueExecutor { let resp: Response = exe.execute(&synth).await; + + // add the pending to response. + if is_introspection_query { let async_req = async_graphql::Request::from(request).only_introspection(); let async_resp = app_ctx.execute(async_req).await; diff --git a/src/core/jit/model.rs b/src/core/jit/model.rs index 1b833b0ed0..c9767257b0 100644 --- a/src/core/jit/model.rs +++ b/src/core/jit/model.rs @@ -291,6 +291,7 @@ pub struct OperationPlan { pub min_cache_ttl: Option, pub selection: Vec>, pub before: Option, + pub deferred_fields: Vec>, } impl OperationPlan { @@ -298,12 +299,16 @@ impl OperationPlan { self, map: impl Fn(Input) -> Result, ) -> Result, Error> { - let mut selection = vec![]; - + let mut selection = Vec::with_capacity(self.selection.len()); for n in self.selection { selection.push(n.try_map(&map)?); } + let mut deferred_selection = Vec::with_capacity(self.deferred_fields.len()); + for n in self.deferred_fields { + deferred_selection.push(n.try_map(&map)?); + } + Ok(OperationPlan { selection, root_name: self.root_name, @@ -315,6 +320,7 @@ impl OperationPlan { is_protected: self.is_protected, min_cache_ttl: self.min_cache_ttl, before: self.before, + deferred_fields: deferred_selection, }) } } @@ -342,6 +348,7 @@ impl OperationPlan { is_protected: false, min_cache_ttl: None, before: Default::default(), + deferred_fields: Default::default(), } } @@ -601,4 +608,10 @@ mod test { assert!(actual.is_dedupe); } + + #[test] + fn test_defer(){ + let proposed_plan = plan(r#"{ users { id ... @defer(label: "comment-defer") { comments { body } } } }"#); + insta::assert_debug_snapshot!(proposed_plan); + } } diff --git a/src/core/jit/request.rs b/src/core/jit/request.rs index dc08ee53d7..d2cbb93f0a 100644 --- a/src/core/jit/request.rs +++ b/src/core/jit/request.rs @@ -50,6 +50,8 @@ impl Request { .pipe(transform::AuthPlanner::new()) .pipe(transform::CheckDedupe::new()) .pipe(transform::CheckCache::new()) + .pipe(transform::WrapDefer::new()) + .pipe(transform::DeferPlanner::new()) .transform(plan) .to_result() // both transformers are infallible right now diff --git a/src/core/jit/response.rs b/src/core/jit/response.rs index aabe67dd65..12e414a82f 100644 --- a/src/core/jit/response.rs +++ b/src/core/jit/response.rs @@ -20,6 +20,9 @@ pub struct Response { #[serde(skip)] pub cache_control: CacheControl, + + #[serde(skip_serializing_if = "Vec::is_empty")] + pub pending: Vec } impl Default for Response { @@ -29,6 +32,7 @@ impl Default for Response { errors: Default::default(), extensions: Default::default(), cache_control: Default::default(), + pending: Default::default(), } } } diff --git a/src/core/jit/snapshots/tailcall__core__jit__model__test__defer.snap b/src/core/jit/snapshots/tailcall__core__jit__model__test__defer.snap new file mode 100644 index 0000000000..b8d8e61966 --- /dev/null +++ b/src/core/jit/snapshots/tailcall__core__jit__model__test__defer.snap @@ -0,0 +1,913 @@ +--- +source: src/core/jit/model.rs +expression: proposed_plan +--- +OperationPlan { + root_name: "Query", + operation_type: Query, + index: Index { + map: { + "Comment": ( + Object( + ObjectTypeDefinition { + name: "Comment", + fields: [ + FieldDefinition { + name: "body", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "email", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "id", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "name", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "postId", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + ], + description: None, + implements: {}, + directives: [], + }, + ), + { + "body": Field( + ( + FieldDefinition { + name: "body", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "email": Field( + ( + FieldDefinition { + name: "email", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "id": Field( + ( + FieldDefinition { + name: "id", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "name": Field( + ( + FieldDefinition { + name: "name", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "postId": Field( + ( + FieldDefinition { + name: "postId", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + }, + ), + "Post": ( + Object( + ObjectTypeDefinition { + name: "Post", + fields: [ + FieldDefinition { + name: "body", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "id", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "title", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "user", + args: [], + of_type: User, + resolver: Some( + IO( + Http { + req_template: RequestTemplate { + root_url: Mustache( + [ + Literal( + "http://jsonplaceholder.typicode.com/users/", + ), + Expression( + [ + "value", + "userId", + ], + ), + ], + ), + query: [], + method: GET, + headers: [], + body_path: None, + endpoint: Endpoint { + path: "http://jsonplaceholder.typicode.com/users/{{.value.userId}}", + query: [], + method: GET, + input: Obj( + {}, + ), + output: Obj( + {}, + ), + headers: {}, + body: None, + description: None, + encoding: ApplicationJson, + }, + encoding: ApplicationJson, + query_encoder: RepeatedKey, + }, + group_by: None, + dl_id: None, + http_filter: None, + is_list: false, + dedupe: true, + }, + ), + ), + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "userId", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + ], + description: None, + implements: {}, + directives: [], + }, + ), + { + "body": Field( + ( + FieldDefinition { + name: "body", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "id": Field( + ( + FieldDefinition { + name: "id", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "title": Field( + ( + FieldDefinition { + name: "title", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "user": Field( + ( + FieldDefinition { + name: "user", + args: [], + of_type: User, + resolver: Some( + IO( + Http { + req_template: RequestTemplate { + root_url: Mustache( + [ + Literal( + "http://jsonplaceholder.typicode.com/users/", + ), + Expression( + [ + "value", + "userId", + ], + ), + ], + ), + query: [], + method: GET, + headers: [], + body_path: None, + endpoint: Endpoint { + path: "http://jsonplaceholder.typicode.com/users/{{.value.userId}}", + query: [], + method: GET, + input: Obj( + {}, + ), + output: Obj( + {}, + ), + headers: {}, + body: None, + description: None, + encoding: ApplicationJson, + }, + encoding: ApplicationJson, + query_encoder: RepeatedKey, + }, + group_by: None, + dl_id: None, + http_filter: None, + is_list: false, + dedupe: true, + }, + ), + ), + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "userId": Field( + ( + FieldDefinition { + name: "userId", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + }, + ), + "Query": ( + Object( + ObjectTypeDefinition { + name: "Query", + fields: [ + FieldDefinition { + name: "posts", + args: [], + of_type: [Post], + resolver: Some( + IO( + Http { + req_template: RequestTemplate { + root_url: Mustache( + [ + Literal( + "http://jsonplaceholder.typicode.com/posts", + ), + ], + ), + query: [], + method: GET, + headers: [], + body_path: None, + endpoint: Endpoint { + path: "http://jsonplaceholder.typicode.com/posts", + query: [], + method: GET, + input: Obj( + {}, + ), + output: Obj( + {}, + ), + headers: {}, + body: None, + description: None, + encoding: ApplicationJson, + }, + encoding: ApplicationJson, + query_encoder: RepeatedKey, + }, + group_by: None, + dl_id: None, + http_filter: None, + is_list: true, + dedupe: false, + }, + ), + ), + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "users", + args: [], + of_type: [User], + resolver: Some( + IO( + Http { + req_template: RequestTemplate { + root_url: Mustache( + [ + Literal( + "http://jsonplaceholder.typicode.com/users", + ), + ], + ), + query: [], + method: GET, + headers: [], + body_path: None, + endpoint: Endpoint { + path: "http://jsonplaceholder.typicode.com/users", + query: [], + method: GET, + input: Obj( + {}, + ), + output: Obj( + {}, + ), + headers: {}, + body: None, + description: None, + encoding: ApplicationJson, + }, + encoding: ApplicationJson, + query_encoder: RepeatedKey, + }, + group_by: None, + dl_id: None, + http_filter: None, + is_list: true, + dedupe: true, + }, + ), + ), + directives: [], + description: None, + default_value: None, + }, + ], + description: None, + implements: {}, + directives: [], + }, + ), + { + "posts": Field( + ( + FieldDefinition { + name: "posts", + args: [], + of_type: [Post], + resolver: Some( + IO( + Http { + req_template: RequestTemplate { + root_url: Mustache( + [ + Literal( + "http://jsonplaceholder.typicode.com/posts", + ), + ], + ), + query: [], + method: GET, + headers: [], + body_path: None, + endpoint: Endpoint { + path: "http://jsonplaceholder.typicode.com/posts", + query: [], + method: GET, + input: Obj( + {}, + ), + output: Obj( + {}, + ), + headers: {}, + body: None, + description: None, + encoding: ApplicationJson, + }, + encoding: ApplicationJson, + query_encoder: RepeatedKey, + }, + group_by: None, + dl_id: None, + http_filter: None, + is_list: true, + dedupe: false, + }, + ), + ), + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "users": Field( + ( + FieldDefinition { + name: "users", + args: [], + of_type: [User], + resolver: Some( + IO( + Http { + req_template: RequestTemplate { + root_url: Mustache( + [ + Literal( + "http://jsonplaceholder.typicode.com/users", + ), + ], + ), + query: [], + method: GET, + headers: [], + body_path: None, + endpoint: Endpoint { + path: "http://jsonplaceholder.typicode.com/users", + query: [], + method: GET, + input: Obj( + {}, + ), + output: Obj( + {}, + ), + headers: {}, + body: None, + description: None, + encoding: ApplicationJson, + }, + encoding: ApplicationJson, + query_encoder: RepeatedKey, + }, + group_by: None, + dl_id: None, + http_filter: None, + is_list: true, + dedupe: true, + }, + ), + ), + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + }, + ), + "User": ( + Object( + ObjectTypeDefinition { + name: "User", + fields: [ + FieldDefinition { + name: "comments", + args: [], + of_type: [Comment], + resolver: Some( + IO( + Http { + req_template: RequestTemplate { + root_url: Mustache( + [ + Literal( + "http://jsonplaceholder.typicode.com/users/", + ), + Expression( + [ + "value", + "id", + ], + ), + Literal( + "/comments", + ), + ], + ), + query: [], + method: GET, + headers: [], + body_path: None, + endpoint: Endpoint { + path: "http://jsonplaceholder.typicode.com/users/{{.value.id}}/comments", + query: [], + method: GET, + input: Obj( + {}, + ), + output: Obj( + {}, + ), + headers: {}, + body: None, + description: None, + encoding: ApplicationJson, + }, + encoding: ApplicationJson, + query_encoder: RepeatedKey, + }, + group_by: None, + dl_id: None, + http_filter: None, + is_list: true, + dedupe: true, + }, + ), + ), + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "email", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "id", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "name", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "phone", + args: [], + of_type: String, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + FieldDefinition { + name: "username", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + ], + description: None, + implements: {}, + directives: [], + }, + ), + { + "comments": Field( + ( + FieldDefinition { + name: "comments", + args: [], + of_type: [Comment], + resolver: Some( + IO( + Http { + req_template: RequestTemplate { + root_url: Mustache( + [ + Literal( + "http://jsonplaceholder.typicode.com/users/", + ), + Expression( + [ + "value", + "id", + ], + ), + Literal( + "/comments", + ), + ], + ), + query: [], + method: GET, + headers: [], + body_path: None, + endpoint: Endpoint { + path: "http://jsonplaceholder.typicode.com/users/{{.value.id}}/comments", + query: [], + method: GET, + input: Obj( + {}, + ), + output: Obj( + {}, + ), + headers: {}, + body: None, + description: None, + encoding: ApplicationJson, + }, + encoding: ApplicationJson, + query_encoder: RepeatedKey, + }, + group_by: None, + dl_id: None, + http_filter: None, + is_list: true, + dedupe: true, + }, + ), + ), + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "email": Field( + ( + FieldDefinition { + name: "email", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "id": Field( + ( + FieldDefinition { + name: "id", + args: [], + of_type: ID!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "name": Field( + ( + FieldDefinition { + name: "name", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "phone": Field( + ( + FieldDefinition { + name: "phone", + args: [], + of_type: String, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + "username": Field( + ( + FieldDefinition { + name: "username", + args: [], + of_type: String!, + resolver: None, + directives: [], + description: None, + default_value: None, + }, + {}, + ), + ), + }, + ), + }, + schema: SchemaDefinition { + query: "Query", + mutation: None, + directives: [ + Directive { + name: "server", + arguments: { + "hostname": String("0.0.0.0"), + "port": Number(8000), + }, + }, + ], + }, + }, + is_introspection_query: false, + is_dedupe: true, + is_const: false, + is_protected: false, + min_cache_ttl: None, + selection: [ + Field { + id: 0, + name: "users", + output_name: "users", + ir: "Some(..)", + type_of: [User], + type_condition: Some( + "Query", + ), + selection: [ + Field { + id: 1, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "User", + ), + directives: [], + }, + ], + directives: [], + }, + ], + before: None, + deferred_fields: [ + Field { + id: 2, + name: "comments", + output_name: "comments", + ir: "Some(..)", + type_of: [Comment], + type_condition: Some( + "User", + ), + selection: [ + Field { + id: 3, + name: "body", + output_name: "body", + type_of: String!, + type_condition: Some( + "Comment", + ), + directives: [], + }, + ], + directives: [ + Directive { + name: "defer", + arguments: [ + ( + "label", + String( + "comment-defer", + ), + ), + ], + }, + ], + }, + ], +} diff --git a/src/core/jit/transform/auth_planner.rs b/src/core/jit/transform/auth_planner.rs index e372c4daf2..47083cfbe4 100644 --- a/src/core/jit/transform/auth_planner.rs +++ b/src/core/jit/transform/auth_planner.rs @@ -78,5 +78,8 @@ pub fn update_ir(ir: &mut IR, vec: &mut Vec) { IR::Discriminate(_, ir) => { update_ir(ir, vec); } + IR::Deferred { ir, .. } => { + update_ir(ir, vec); + } } } diff --git a/src/core/jit/transform/check_cache.rs b/src/core/jit/transform/check_cache.rs index 83a7dc202c..8e49547221 100644 --- a/src/core/jit/transform/check_cache.rs +++ b/src/core/jit/transform/check_cache.rs @@ -35,6 +35,7 @@ fn check_cache(ir: &IR) -> Option { } ttl } + IR::Deferred{ir, ..} => check_cache(ir), IR::Dynamic(_) | IR::ContextPath(_) | IR::Map(_) | IR::Service(_) => None, } } diff --git a/src/core/jit/transform/check_const.rs b/src/core/jit/transform/check_const.rs index 6ecdd4e599..156eced8e1 100644 --- a/src/core/jit/transform/check_const.rs +++ b/src/core/jit/transform/check_const.rs @@ -28,6 +28,7 @@ pub fn is_const(ir: &IR) -> bool { IR::Discriminate(_, ir) => is_const(ir), IR::Entity(hash_map) => hash_map.values().all(is_const), IR::Service(_) => true, + IR::Deferred{ir, ..} => is_const(ir), } } diff --git a/src/core/jit/transform/check_dedupe.rs b/src/core/jit/transform/check_dedupe.rs index 296038a6f3..5f78201a12 100644 --- a/src/core/jit/transform/check_dedupe.rs +++ b/src/core/jit/transform/check_dedupe.rs @@ -27,6 +27,7 @@ fn check_dedupe(ir: &IR) -> bool { IR::ContextPath(_) => true, IR::Map(_) => true, IR::Service(_) => true, + IR::Deferred{ir, ..} => check_dedupe(ir) } } diff --git a/src/core/jit/transform/check_protected.rs b/src/core/jit/transform/check_protected.rs index 2b16557e28..3973048880 100644 --- a/src/core/jit/transform/check_protected.rs +++ b/src/core/jit/transform/check_protected.rs @@ -28,6 +28,7 @@ pub fn is_protected(ir: &IR) -> bool { IR::Discriminate(_, ir) => is_protected(ir), IR::Entity(hash_map) => hash_map.values().any(is_protected), IR::Service(_) => false, + IR::Deferred{ir, ..} => is_protected(ir), } } diff --git a/src/core/jit/transform/defer_planner.rs b/src/core/jit/transform/defer_planner.rs new file mode 100644 index 0000000000..a1253803fc --- /dev/null +++ b/src/core/jit/transform/defer_planner.rs @@ -0,0 +1,68 @@ +use std::{convert::Infallible, fmt::Debug, marker::PhantomData}; + +use tailcall_valid::Valid; + +use crate::core::{ + ir::model::IR, + jit::{Field, OperationPlan}, + Transform, +}; + +pub struct DeferPlanner(PhantomData); + +impl DeferPlanner { + pub fn new() -> Self { + Self(PhantomData) + } +} + +// collect the fields that has IR type of Deferred and return back. +fn move_deferred_fields(field: &mut Field) -> Vec> { + let mut deferred_fields = Vec::new(); + for selection in field.selection.iter_mut() { + match selection.ir { + Some(IR::Deferred { .. }) => { + deferred_fields.push(selection.clone()); + } + _ => {} + } + deferred_fields.extend(move_deferred_fields(selection)); + } + + field.selection.retain(|f| { + f.ir.as_ref() + .map_or(true, |ir| !matches!(ir, IR::Deferred { .. })) + }); + + deferred_fields +} + +impl Transform for DeferPlanner { + type Value = OperationPlan; + type Error = Infallible; + + fn transform(&self, mut plan: Self::Value) -> Valid { + let mut deferred_fields = Vec::new(); + for field in plan.selection.iter_mut() { + match field.ir { + Some(IR::Deferred { .. }) => { + deferred_fields.push(field.clone()); + } + _ => {} + } + + deferred_fields.extend(move_deferred_fields(field)); + } + + plan.selection.retain(|f| { + f.ir.as_ref() + .map_or(true, |ir| !matches!(ir, IR::Deferred { .. })) + }); + + plan.deferred_fields = deferred_fields; + + println!("{:?}", plan.deferred_fields); + + Valid::succeed(plan) + } +} diff --git a/src/core/jit/transform/input_resolver.rs b/src/core/jit/transform/input_resolver.rs index 920bcfdae1..4354a9f77c 100644 --- a/src/core/jit/transform/input_resolver.rs +++ b/src/core/jit/transform/input_resolver.rs @@ -68,6 +68,14 @@ where .map(|field| Self::resolve_field(&index, field?)) .collect::, _>>()?; + let deferred_fields = self + .plan + .deferred_fields + .into_iter() + .map(|field| field.try_map(&|value| value.resolve(variables))) + .map(|field| Self::resolve_field(&index, field?)) + .collect::, _>>()?; + Ok(OperationPlan { root_name: self.plan.root_name.to_string(), operation_type: self.plan.operation_type, @@ -79,6 +87,7 @@ where min_cache_ttl: self.plan.min_cache_ttl, selection, before: self.plan.before, + deferred_fields: deferred_fields, }) } diff --git a/src/core/jit/transform/mod.rs b/src/core/jit/transform/mod.rs index 71928ddbfd..ae0531858a 100644 --- a/src/core/jit/transform/mod.rs +++ b/src/core/jit/transform/mod.rs @@ -4,7 +4,9 @@ mod check_const; mod check_dedupe; mod check_protected; mod input_resolver; +mod wrap_defer; mod skip; +mod defer_planner; pub use auth_planner::*; pub use check_cache::*; @@ -12,4 +14,6 @@ pub use check_const::*; pub use check_dedupe::*; pub use check_protected::*; pub use input_resolver::*; +pub use wrap_defer::*; +pub use defer_planner::*; pub use skip::*; diff --git a/src/core/jit/transform/wrap_defer.rs b/src/core/jit/transform/wrap_defer.rs new file mode 100644 index 0000000000..5a6fa911f3 --- /dev/null +++ b/src/core/jit/transform/wrap_defer.rs @@ -0,0 +1,89 @@ +use std::{convert::Infallible, marker::PhantomData}; + +use tailcall_valid::Valid; + +use crate::core::{ + counter::{Count, Counter}, + ir::model::{IrId, IR}, + jit::{Field, OperationPlan}, + Transform, +}; + +pub struct WrapDefer { + _marker: PhantomData, + defer_id: Counter, +} + +fn check_dependent_irs(ir: &IR) -> bool { + match ir { + IR::IO(io) => io.is_dependent(), + IR::Cache(cache) => cache.io.is_dependent(), + IR::Deferred { .. } | IR::Service(_) | IR::ContextPath(_) | IR::Dynamic(_) => false, + IR::Path(ir, _) => check_dependent_irs(ir), + IR::Map(map) => check_dependent_irs(&map.input), + IR::Pipe(l, r) => check_dependent_irs(l) || check_dependent_irs(r), + IR::Discriminate(_, ir) => check_dependent_irs(ir), + IR::Entity(map) => map.values().any(check_dependent_irs), + IR::Protect(_, ir) => check_dependent_irs(ir), + } +} + +impl WrapDefer { + pub fn new() -> Self { + Self { _marker: PhantomData, defer_id: Counter::new(0) } + } + /// goes through selection and finds out IR's that needs to be deferred. + #[inline] + fn detect_and_wrap(&self, field: &mut Field, path: &mut Vec) { + path.push(field.output_name.clone()); + for selection in field.selection.iter_mut() { + if let Some(ir) = std::mem::take(&mut selection.ir) { + let ir = if selection + .directives + .iter() + .find(|d| d.name == "defer") + .is_some() + && !check_dependent_irs(&ir) + { + IR::Deferred { + ir: Box::new(ir), + path: path.clone(), + id: IrId::new(self.defer_id.next()), + } + } else { + ir + }; + selection.ir = Some(ir); + } + + self.detect_and_wrap(selection, path); + } + + path.pop(); + } +} + +impl Transform for WrapDefer { + type Value = OperationPlan; + type Error = Infallible; + fn transform(&self, mut plan: Self::Value) -> Valid { + plan.selection.iter_mut().for_each(|f| { + if let Some(ir) = std::mem::take(&mut f.ir) { + let ir = if f.directives.iter().find(|d| d.name == "defer").is_some() + && !check_dependent_irs(&ir) + { + IR::Deferred { + ir: Box::new(ir), + path: vec![], + id: IrId::new(self.defer_id.next()), + } + } else { + ir + }; + f.ir = Some(ir); + } + self.detect_and_wrap(f, &mut vec![]) + }); + Valid::succeed(plan) + } +}