From 295e0d5c46389b1dab6f484bd374e75f99179d1c Mon Sep 17 00:00:00 2001 From: Sandipsinh Dilipsinh Rathod <62684960+ssddOnTop@users.noreply.github.com> Date: Sun, 20 Oct 2024 03:01:04 -0400 Subject: [PATCH] perf: Cache OperationPlan creation (#2951) Co-authored-by: Tushar Mathur Co-authored-by: laststylebender --- .gitignore | 1 + Cargo.lock | 1 + Cargo.toml | 1 + ...impl_path_string_for_evaluation_context.rs | 2 +- examples/jsonplaceholder.graphql | 1 + lint.sh | 10 +- src/cli/runtime/mod.rs | 2 +- src/core/app_context.rs | 4 + src/core/cache/cache.rs | 9 +- src/core/ir/resolver_context_like.rs | 10 +- src/core/jit/builder.rs | 127 +++------ src/core/jit/common/jp.rs | 15 +- src/core/jit/context.rs | 24 +- src/core/jit/exec.rs | 4 +- src/core/jit/exec_const.rs | 65 ++++- src/core/jit/graphql_executor.rs | 42 ++- src/core/jit/mod.rs | 2 +- src/core/jit/model.rs | 268 +++++------------- src/core/jit/request.rs | 19 +- src/core/jit/response.rs | 2 +- ...ore__jit__builder__tests__alias_query.snap | 48 ++-- ...e__jit__builder__tests__default_value.snap | 35 +-- ...core__jit__builder__tests__directives.snap | 78 +++-- ..._core__jit__builder__tests__fragments.snap | 110 ++++--- ...e__jit__builder__tests__from_document.snap | 66 ++--- ...__builder__tests__multiple_operations.snap | 98 +++---- ...builder__tests__resolving_operation-2.snap | 95 +++---- ...__builder__tests__resolving_operation.snap | 70 +++-- ..._jit__builder__tests__simple_mutation.snap | 130 ++++----- ...re__jit__builder__tests__simple_query.snap | 48 ++-- ...ll__core__jit__builder__tests__unions.snap | 50 ++-- ..._core__jit__builder__tests__variables.snap | 56 ++-- src/core/jit/synth/synth.rs | 28 +- src/core/jit/transform/check_const.rs | 46 +++ src/core/jit/transform/check_dedupe.rs | 47 +++ .../jit/{ => transform}/input_resolver.rs | 58 ++-- src/core/jit/transform/mod.rs | 9 + src/core/jit/transform/skip.rs | 42 +++ src/core/runtime.rs | 2 +- tailcall-aws-lambda/src/runtime.rs | 2 +- tailcall-wasm/src/runtime.rs | 2 +- tests/core/parse.rs | 2 +- tests/core/runtime.rs | 2 +- .../graphql-conformance-001.md_5.snap | 8 +- .../graphql-conformance-015.md_10.snap | 8 +- .../graphql-conformance-015.md_9.snap | 8 +- .../graphql-conformance-http-001.md_5.snap | 8 +- .../graphql-conformance-http-015.md_10.snap | 8 +- .../graphql-conformance-http-015.md_9.snap | 8 +- tests/jit_spec.rs | 68 ++++- tests/server_spec.rs | 2 +- 51 files changed, 982 insertions(+), 869 deletions(-) create mode 100644 src/core/jit/transform/check_const.rs create mode 100644 src/core/jit/transform/check_dedupe.rs rename src/core/jit/{ => transform}/input_resolver.rs (80%) create mode 100644 src/core/jit/transform/mod.rs create mode 100644 src/core/jit/transform/skip.rs diff --git a/.gitignore b/.gitignore index 09c880ada4..91ef6dbccd 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ node_modules/ tailcall-/cloudflare/build *.snap.new +flamegraph.svg diff --git a/Cargo.lock b/Cargo.lock index 16975e81a4..6c1bcbe8e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5395,6 +5395,7 @@ dependencies = [ "colored", "convert_case 0.6.0", "criterion", + "dashmap", "datatest-stable", "derive-getters", "derive_more 0.99.18", diff --git a/Cargo.toml b/Cargo.toml index 77941f7749..42d9c17e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,6 +169,7 @@ num = "0.4.3" indenter = "0.3.3" derive_more = { workspace = true } strum = "0.26.2" +dashmap = "6.1.0" [dev-dependencies] datatest-stable = "0.2.9" diff --git a/benches/impl_path_string_for_evaluation_context.rs b/benches/impl_path_string_for_evaluation_context.rs index 8fd725209a..e4eb603f67 100644 --- a/benches/impl_path_string_for_evaluation_context.rs +++ b/benches/impl_path_string_for_evaluation_context.rs @@ -247,7 +247,7 @@ fn request_context() -> RequestContext { http, env: Arc::new(Env {}), file: Arc::new(File {}), - cache: Arc::new(InMemoryCache::new()), + cache: Arc::new(InMemoryCache::default()), extensions: Arc::new(vec![]), cmd_worker: None, worker: None, diff --git a/examples/jsonplaceholder.graphql b/examples/jsonplaceholder.graphql index b514aad597..3dbe8d313a 100644 --- a/examples/jsonplaceholder.graphql +++ b/examples/jsonplaceholder.graphql @@ -8,6 +8,7 @@ type Query { posts: [Post] @http(path: "/posts") users: [User] @http(path: "/users") user(id: Int!): User @http(path: "/users/{{.args.id}}") + greet: String @expr(body: "Hello World!") } type User { diff --git a/lint.sh b/lint.sh index fac6abfe41..0a02e76c7c 100755 --- a/lint.sh +++ b/lint.sh @@ -27,9 +27,9 @@ run_cargo_clippy() { run_prettier() { MODE=$1 if [ "$MODE" == "check" ]; then - prettier -c .prettierrc --check "**/*.$FILE_TYPES" + npx prettier -c .prettierrc --check "**/*.$FILE_TYPES" else - prettier -c .prettierrc --write "**/*.$FILE_TYPES" + npx prettier -c .prettierrc --write "**/*.$FILE_TYPES" fi return $? } @@ -54,6 +54,9 @@ case $MODE in run_autogen_schema $MODE AUTOGEN_SCHEMA_EXIT_CODE=$? + run_prettier $MODE + PRETTIER_EXIT_CODE=$? + # Commands that uses nightly toolchains are run from `.nightly` directory # to read the nightly version from `rust-toolchain.toml` file pushd .nightly @@ -62,9 +65,6 @@ case $MODE in run_cargo_clippy $MODE CLIPPY_EXIT_CODE=$? popd - - run_prettier $MODE - PRETTIER_EXIT_CODE=$? ;; *) echo "Invalid mode. Please use --mode=check or --mode=fix" diff --git a/src/cli/runtime/mod.rs b/src/cli/runtime/mod.rs index 5e80533508..d57bf1be31 100644 --- a/src/cli/runtime/mod.rs +++ b/src/cli/runtime/mod.rs @@ -66,7 +66,7 @@ fn init_http2_only(blueprint: &Blueprint) -> Arc { } fn init_in_memory_cache() -> InMemoryCache { - InMemoryCache::new() + InMemoryCache::default() } pub fn init(blueprint: &Blueprint) -> TargetRuntime { diff --git a/src/core/app_context.rs b/src/core/app_context.rs index 1f718e19d7..fa9b255a44 100644 --- a/src/core/app_context.rs +++ b/src/core/app_context.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use async_graphql::dynamic::{self, DynamicRequest}; use async_graphql_value::ConstValue; +use dashmap::DashMap; use super::lift::Lift; use crate::core::async_graphql_hyper::OperationId; @@ -14,6 +15,7 @@ use crate::core::grpc::data_loader::GrpcDataLoader; use crate::core::http::{DataLoaderRequest, HttpDataLoader}; use crate::core::ir::model::{DataLoaderId, IoId, IO, IR}; use crate::core::ir::Error; +use crate::core::jit::{OPHash, OperationPlan}; use crate::core::rest::{Checked, EndpointSet}; use crate::core::runtime::TargetRuntime; @@ -28,6 +30,7 @@ pub struct AppContext { pub auth_ctx: Arc, pub dedupe_handler: Arc>, pub dedupe_operation_handler: DedupeResult, Error>, + pub operation_plans: DashMap>, } impl AppContext { @@ -148,6 +151,7 @@ impl AppContext { auth_ctx: Arc::new(auth_ctx), dedupe_handler: Arc::new(DedupeResult::new(false)), dedupe_operation_handler: DedupeResult::new(false), + operation_plans: DashMap::new(), } } diff --git a/src/core/cache/cache.rs b/src/core/cache/cache.rs index 82b9576858..0f5313d2a7 100644 --- a/src/core/cache/cache.rs +++ b/src/core/cache/cache.rs @@ -14,19 +14,16 @@ pub struct InMemoryCache { miss: AtomicUsize, } -// TODO: take this from the user instead of hardcoding it -const CACHE_CAPACITY: usize = 100000; - impl Default for InMemoryCache { fn default() -> Self { - Self::new() + Self::new(100000) } } impl InMemoryCache { - pub fn new() -> Self { + pub fn new(capacity: usize) -> Self { InMemoryCache { - data: Arc::new(RwLock::new(TtlCache::new(CACHE_CAPACITY))), + data: Arc::new(RwLock::new(TtlCache::new(capacity))), hits: AtomicUsize::new(0), miss: AtomicUsize::new(0), } diff --git a/src/core/ir/resolver_context_like.rs b/src/core/ir/resolver_context_like.rs index 4ee1dafc78..3d5c2bc9cf 100644 --- a/src/core/ir/resolver_context_like.rs +++ b/src/core/ir/resolver_context_like.rs @@ -5,8 +5,6 @@ use async_graphql::{Name, ServerError, Value}; use async_graphql_value::ConstValue; use indexmap::IndexMap; -use crate::core::jit::Nested; - pub trait ResolverContextLike: Clone { fn value(&self) -> Option<&Value>; fn args(&self) -> Option<&IndexMap>; @@ -85,16 +83,14 @@ impl From> for SelectionField { } } -impl<'a> From<&'a crate::core::jit::Field, ConstValue>> for SelectionField { - fn from(value: &'a crate::core::jit::Field, ConstValue>) -> Self { +impl<'a> From<&'a crate::core::jit::Field> for SelectionField { + fn from(value: &'a crate::core::jit::Field) -> Self { Self::from_jit_field(value) } } impl SelectionField { - fn from_jit_field( - field: &crate::core::jit::Field, ConstValue>, - ) -> SelectionField { + fn from_jit_field(field: &crate::core::jit::Field) -> SelectionField { let name = field.output_name.to_string(); let type_name = field.type_of.name(); let selection_set = field diff --git a/src/core/jit/builder.rs b/src/core/jit/builder.rs index d7d9a2e080..f1cfd70e7a 100644 --- a/src/core/jit/builder.rs +++ b/src/core/jit/builder.rs @@ -7,15 +7,13 @@ use async_graphql::parser::types::{ OperationType, Selection, SelectionSet, }; use async_graphql::Positioned; -use async_graphql_value::{ConstValue, Value}; +use async_graphql_value::Value; -use super::input_resolver::InputResolver; use super::model::{Directive as JitDirective, *}; use super::BuildError; use crate::core::blueprint::{Blueprint, Index, QueryField}; use crate::core::counter::{Count, Counter}; use crate::core::jit::model::OperationPlan; -use crate::core::merge_right::MergeRight; use crate::core::Type; #[derive(PartialEq, strum_macros::Display)] @@ -132,17 +130,16 @@ impl Builder { &self, selection: &SelectionSet, type_condition: &str, - exts: Option, fragments: &HashMap<&str, &FragmentDefinition>, - ) -> Vec> { + ) -> Vec> { let mut fields = vec![]; + for selection in &selection.items { match &selection.node { Selection::Field(Positioned { node: gql_field, .. }) => { let conditions = self.include(&gql_field.directives); - // if include is always false xor skip is always true, - // then we can skip the field from the plan + // Skip fields based on GraphQL's skip/include conditions if conditions.is_const_skip() { continue; } @@ -164,7 +161,6 @@ impl Builder { } let (include, skip) = conditions.into_variable_tuple(); - let field_name = gql_field.name.node.as_str(); let request_args = gql_field .arguments @@ -172,6 +168,7 @@ impl Builder { .map(|(k, v)| (k.node.as_str().to_string(), v.node.to_owned())) .collect::>(); + // Check if the field is present in the schema index if let Some(field_def) = self.index.get_field(type_condition, field_name) { let mut args = Vec::with_capacity(request_args.len()); if let QueryField::Field((_, schema_args)) = field_def { @@ -187,8 +184,6 @@ impl Builder { id, name, type_of, - // TODO: handle errors for non existing request_args without the - // default value: request_args.get(arg_name).cloned(), default_value, }); @@ -201,18 +196,20 @@ impl Builder { }; let id = FieldId::new(self.field_id.next()); - let child_fields = self.iter( - &gql_field.selection_set.node, - type_of.name(), - Some(Flat::new(id.clone())), - fragments, - ); + + // Recursively gather child fields for the selection set + let child_fields = + self.iter(&gql_field.selection_set.node, type_of.name(), fragments); + let ir = match field_def { QueryField::Field((field_def, _)) => field_def.resolver.clone(), _ => None, }; - let flat_field = Field { + + // Create the field with its child fields in `selection` + let field = Field { id, + selection: child_fields, name: field_name.to_string(), output_name: gql_field .alias @@ -226,31 +223,27 @@ impl Builder { include, args, pos: selection.pos.into(), - extensions: exts.clone(), directives, }; - fields.push(flat_field); - fields = fields.merge_right(child_fields); + fields.push(field); } else if field_name == "__typename" { - let flat_field = Field { + let typename_field = Field { id: FieldId::new(self.field_id.next()), name: field_name.to_string(), output_name: field_name.to_string(), ir: None, type_of: Type::Named { name: "String".to_owned(), non_null: true }, - // __typename has a special meaning and could be applied - // to any type type_condition: None, skip, include, args: Vec::new(), pos: selection.pos.into(), - extensions: exts.clone(), + selection: vec![], // __typename has no child selection directives, }; - fields.push(flat_field); + fields.push(typename_field); } } Selection::FragmentSpread(Positioned { node: fragment_spread, .. }) => { @@ -260,7 +253,6 @@ impl Builder { fields.extend(self.iter( &fragment.selection_set.node, fragment.type_condition.node.on.node.as_str(), - exts.clone(), fragments, )); } @@ -272,19 +264,13 @@ impl Builder { .map(|cond| cond.node.on.node.as_str()) .unwrap_or(type_condition); - fields.extend(self.iter( - &fragment.selection_set.node, - type_of, - exts.clone(), - fragments, - )); + fields.extend(self.iter(&fragment.selection_set.node, type_of, fragments)); } } } fields } - #[inline(always)] fn get_type(&self, ty: OperationType) -> Option<&str> { match ty { @@ -322,12 +308,7 @@ impl Builder { } #[inline(always)] - pub fn build( - &self, - variables: &Variables, - operation_name: Option<&str>, - ) -> Result, BuildError> { - let mut fields = Vec::new(); + pub fn build(&self, operation_name: Option<&str>) -> Result, BuildError> { let mut fragments: HashMap<&str, &FragmentDefinition> = HashMap::new(); for (name, fragment) in self.document.fragments.iter() { @@ -339,10 +320,7 @@ impl Builder { let name = self .get_type(operation.ty) .ok_or(BuildError::RootOperationTypeNotDefined { operation: operation.ty })?; - fields.extend(self.iter(&operation.selection_set.node, name, None, &fragments)); - - // skip the fields depending on variables. - fields.retain(|f| !f.skip(variables)); + let fields = self.iter(&operation.selection_set.node, name, &fragments); let is_introspection_query = operation.selection_set.node.items.iter().any(|f| { if let Selection::Field(Positioned { node: gql_field, .. }) = &f.node { @@ -360,13 +338,7 @@ impl Builder { self.index.clone(), is_introspection_query, ); - - // TODO: operation from [ExecutableDocument] could contain definitions for - // default values of arguments. That info should be passed to - // [InputResolver] to resolve defaults properly - let input_resolver = InputResolver::new(plan); - - Ok(input_resolver.resolve_input(variables)?) + Ok(plan) } } @@ -382,16 +354,11 @@ mod tests { const CONFIG: &str = include_str!("./fixtures/jsonplaceholder-mutation.graphql"); - fn plan( - query: impl AsRef, - variables: &Variables, - ) -> OperationPlan { + fn plan(query: impl AsRef) -> OperationPlan { let config = Config::from_sdl(CONFIG).to_result().unwrap(); let blueprint = Blueprint::try_from(&config.into()).unwrap(); let document = async_graphql::parser::parse_query(query).unwrap(); - Builder::new(&blueprint, document) - .build(variables, None) - .unwrap() + Builder::new(&blueprint, document).build(None).unwrap() } #[tokio::test] @@ -402,10 +369,9 @@ mod tests { posts { user { id name } } } "#, - &Variables::new(), ); assert!(plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[tokio::test] @@ -416,7 +382,6 @@ mod tests { posts { user { id name } } } "#, - &Variables::new(), ); assert!(plan.is_query()); @@ -431,11 +396,10 @@ mod tests { posts { user { id } } } "#, - &Variables::new(), ); assert!(plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[test] @@ -446,11 +410,10 @@ mod tests { articles: posts { author: user { identifier: id } } } "#, - &Variables::new(), ); assert!(plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[test] @@ -475,11 +438,10 @@ mod tests { } } "#, - &Variables::new(), ); assert!(!plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[test] @@ -504,11 +466,10 @@ mod tests { } } "#, - &Variables::new(), ); assert!(plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[test] @@ -526,11 +487,10 @@ mod tests { } } "#, - &Variables::new(), ); assert!(plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[test] @@ -544,11 +504,10 @@ mod tests { } } "#, - &Variables::from_iter([("id".into(), ConstValue::from(1))]), ); assert!(plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[test] @@ -566,11 +525,10 @@ mod tests { } } "#, - &Variables::new(), ); assert!(plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[test] @@ -587,11 +545,10 @@ mod tests { } } "#, - &Variables::new(), ); assert!(!plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[test] @@ -670,35 +627,32 @@ mod tests { let blueprint = Blueprint::try_from(&config.into()).unwrap(); let document = async_graphql::parser::parse_query(query).unwrap(); let error = Builder::new(&blueprint, document.clone()) - .build(&Variables::new(), None) + .build(None) .unwrap_err(); assert_eq!(error, BuildError::OperationNameRequired); let error = Builder::new(&blueprint, document.clone()) - .build(&Variables::new(), Some("unknown")) + .build(Some("unknown")) .unwrap_err(); assert_eq!(error, BuildError::OperationNotFound("unknown".to_string())); let plan = Builder::new(&blueprint, document.clone()) - .build(&Variables::new(), Some("GetPosts")) + .build(Some("GetPosts")) .unwrap(); assert!(plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); let plan = Builder::new(&blueprint, document.clone()) - .build(&Variables::new(), Some("CreateNewPost")) + .build(Some("CreateNewPost")) .unwrap(); assert!(!plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } #[test] fn test_directives() { - let mut variables = Variables::new(); - variables.insert("includeName".to_string(), ConstValue::Boolean(true)); - let plan = plan( r#" query($includeName: Boolean! = true) { @@ -708,10 +662,9 @@ mod tests { } } "#, - &variables, ); assert!(plan.is_query()); - insta::assert_debug_snapshot!(plan.into_nested()); + insta::assert_debug_snapshot!(plan.selection); } } diff --git a/src/core/jit/common/jp.rs b/src/core/jit/common/jp.rs index 80d912d082..c57e9a2ece 100644 --- a/src/core/jit/common/jp.rs +++ b/src/core/jit/common/jp.rs @@ -7,9 +7,11 @@ use crate::core::config::{Config, ConfigModule}; use crate::core::jit::builder::Builder; use crate::core::jit::store::Store; use crate::core::jit::synth::Synth; -use crate::core::jit::{OperationPlan, Variables}; +use crate::core::jit::transform::InputResolver; +use crate::core::jit::{transform, OperationPlan, Variables}; use crate::core::json::{JsonLike, JsonObjectLike}; use crate::core::valid::Validator; +use crate::core::Transform; /// NOTE: This is a bit of a boilerplate reducing module that is used in tests /// and benchmarks. @@ -81,7 +83,7 @@ impl<'a, Value: JsonLike<'a> + Deserialize<'a> + Clone + 'a> TestData { } } -impl<'a, Value: Deserialize<'a> + Clone + 'a + JsonLike<'a>> JP { +impl<'a, Value: Deserialize<'a> + Clone + 'a + JsonLike<'a> + std::fmt::Debug> JP { const CONFIG: &'static str = include_str!("../fixtures/jsonplaceholder-mutation.graphql"); fn plan(query: &str, variables: &Variables) -> OperationPlan { @@ -91,7 +93,14 @@ impl<'a, Value: Deserialize<'a> + Clone + 'a + JsonLike<'a>> JP { async_graphql::parser::parse_query(query).unwrap(), ); - let plan = builder.build(variables, None).unwrap(); + let plan = builder.build(None).unwrap(); + let plan = transform::Skip::new(variables) + .transform(plan) + .to_result() + .unwrap(); + + let input_resolver = InputResolver::new(plan); + let plan = input_resolver.resolve_input(variables).unwrap(); plan.try_map(Deserialize::deserialize).unwrap() } diff --git a/src/core/jit/context.rs b/src/core/jit/context.rs index 2ff3a54792..5c865b9369 100644 --- a/src/core/jit/context.rs +++ b/src/core/jit/context.rs @@ -5,7 +5,7 @@ use async_graphql_value::ConstValue; use indexmap::IndexMap; use super::error::*; -use super::{Field, Nested, OperationPlan, Positioned}; +use super::{Field, OperationPlan, Positioned}; use crate::core::ir::{ResolverContextLike, SelectionField}; #[derive(Debug, Clone)] @@ -36,11 +36,11 @@ pub struct Context<'a, Input, Output> { args: Option>, // TODO: remove the args, since they're already present inside the fields and add support for // default values. - field: &'a Field, Input>, + field: &'a Field, request: &'a RequestContext, } impl<'a, Input: Clone, Output> Context<'a, Input, Output> { - pub fn new(field: &'a Field, Input>, request: &'a RequestContext) -> Self { + pub fn new(field: &'a Field, request: &'a RequestContext) -> Self { Self { request, value: None, args: Self::build_args(field), field } } @@ -54,11 +54,7 @@ impl<'a, Input: Clone, Output> Context<'a, Input, Output> { } } - pub fn with_value_and_field( - &self, - value: &'a Output, - field: &'a Field, Input>, - ) -> Self { + pub fn with_value_and_field(&self, value: &'a Output, field: &'a Field) -> Self { Self { request: self.request, args: Self::build_args(field), @@ -71,11 +67,11 @@ impl<'a, Input: Clone, Output> Context<'a, Input, Output> { self.value } - pub fn field(&self) -> &Field, Input> { + pub fn field(&self) -> &Field { self.field } - fn build_args(field: &Field, Input>) -> Option> { + fn build_args(field: &Field) -> Option> { let mut arg_map = IndexMap::new(); for arg in field.args.iter() { @@ -120,6 +116,7 @@ mod test { use crate::core::blueprint::Blueprint; use crate::core::config::{Config, ConfigModule}; use crate::core::ir::ResolverContextLike; + use crate::core::jit::transform::InputResolver; use crate::core::jit::{OperationPlan, Request}; use crate::core::valid::Validator; @@ -129,13 +126,16 @@ mod test { let blueprint = Blueprint::try_from(&ConfigModule::from(config))?; let request = Request::new(query); let plan = request.clone().create_plan(&blueprint)?; + let input_resolver = InputResolver::new(plan); + let plan = input_resolver.resolve_input(&Default::default()).unwrap(); + Ok(plan) } #[test] fn test_field() { let plan = setup("query {posts {id title}}").unwrap(); - let field = plan.as_nested(); + let field = &plan.selection; let env = RequestContext::new(plan.clone()); let ctx = Context::::new(&field[0], &env); let expected = as ResolverContextLike>::field(&ctx).unwrap(); @@ -146,7 +146,7 @@ mod test { fn test_is_query() { let plan = setup("query {posts {id title}}").unwrap(); let env = RequestContext::new(plan.clone()); - let ctx = Context::new(&plan.as_nested()[0], &env); + let ctx = Context::new(&plan.selection[0], &env); assert!(ctx.is_query()); } } diff --git a/src/core/jit/exec.rs b/src/core/jit/exec.rs index cfff9a948d..7dcb2f7371 100644 --- a/src/core/jit/exec.rs +++ b/src/core/jit/exec.rs @@ -30,7 +30,7 @@ where Exec: IRExecutor, { pub fn new(plan: OperationPlan, exec: Exec) -> Self { - Self { exec, ctx: RequestContext::new(plan.clone()) } + Self { exec, ctx: RequestContext::new(plan) } } pub async fn store(&self) -> Store>> { @@ -71,7 +71,7 @@ where } async fn init(&mut self) { - join_all(self.request.plan().as_nested().iter().map(|field| async { + join_all(self.request.plan().selection.iter().map(|field| async { let ctx = Context::new(field, self.request); // TODO: with_args should be called on inside iter_field on any level, not only // for root fields diff --git a/src/core/jit/exec_const.rs b/src/core/jit/exec_const.rs index 213825a05a..aa2bd0a75f 100644 --- a/src/core/jit/exec_const.rs +++ b/src/core/jit/exec_const.rs @@ -1,41 +1,90 @@ use std::sync::Arc; -use async_graphql_value::ConstValue; +use async_graphql_value::{ConstValue, Value}; use futures_util::future::join_all; use super::context::Context; use super::exec::{Executor, IRExecutor}; -use super::{Error, OperationPlan, Request, Response, Result}; +use super::{ + transform, BuildError, Error, OperationPlan, Pos, Positioned, Request, Response, Result, +}; use crate::core::app_context::AppContext; use crate::core::http::RequestContext; use crate::core::ir::model::IR; use crate::core::ir::{self, EvalContext}; use crate::core::jit::synth::Synth; +use crate::core::jit::transform::InputResolver; use crate::core::json::{JsonLike, JsonLikeList}; +use crate::core::valid::Validator; +use crate::core::Transform; /// A specialized executor that executes with async_graphql::Value pub struct ConstValueExecutor { - pub plan: OperationPlan, + pub plan: OperationPlan, + pub response: Option>, +} + +impl From> for ConstValueExecutor { + fn from(plan: OperationPlan) -> Self { + Self { plan, response: None } + } } impl ConstValueExecutor { - pub fn new(request: &Request, app_ctx: &Arc) -> Result { - Ok(Self { plan: request.create_plan(&app_ctx.blueprint)? }) + pub fn try_new(request: &Request, app_ctx: &Arc) -> Result { + let plan = request.create_plan(&app_ctx.blueprint)?; + Ok(Self::from(plan)) } pub async fn execute( - self, + mut self, req_ctx: &RequestContext, request: &Request, ) -> Response { - let plan = self.plan; + let variables = &request.variables; + let is_const = self.plan.is_const; + + // Attempt to skip unnecessary fields + let plan = transform::Skip::new(variables) + .transform(self.plan.clone()) + .to_result() + .unwrap_or(self.plan); + + // Attempt to replace variables in the plan with the actual values + // TODO: operation from [ExecutableDocument] could contain definitions for + // default values of arguments. That info should be passed to + // [InputResolver] to resolve defaults properly + let result = InputResolver::new(plan).resolve_input(variables); + + let plan = match result { + Ok(plan) => plan, + Err(err) => { + return Response { + data: None, + // TODO: Position shouldn't be 0, 0 + errors: vec![Positioned::new( + BuildError::from(err).into(), + Pos { line: 0, column: 0 }, + )], + extensions: Default::default(), + }; + } + }; + // TODO: drop the clones in plan let exec = ConstValueExec::new(plan.clone(), req_ctx); let vars = request.variables.clone(); let exe = Executor::new(plan.clone(), exec); let store = exe.store().await; let synth = Synth::new(plan, store, vars); - exe.execute(synth).await + let response = exe.execute(synth).await; + + // Cache the response if we know the output is always the same + if is_const { + self.response = Some(response.clone()); + } + + response } } diff --git a/src/core/jit/graphql_executor.rs b/src/core/jit/graphql_executor.rs index 3ce3bad8fc..9249580aae 100644 --- a/src/core/jit/graphql_executor.rs +++ b/src/core/jit/graphql_executor.rs @@ -1,16 +1,17 @@ use std::collections::BTreeMap; use std::future::Future; +use std::hash::{Hash, Hasher}; use std::sync::Arc; use async_graphql::{Data, Executor, Response, Value}; use async_graphql_value::{ConstValue, Extensions}; use futures_util::stream::BoxStream; +use tailcall_hasher::TailcallHasher; use crate::core::app_context::AppContext; use crate::core::async_graphql_hyper::OperationId; use crate::core::http::RequestContext; -use crate::core::jit; -use crate::core::jit::ConstValueExecutor; +use crate::core::jit::{self, ConstValueExecutor, OPHash}; use crate::core::lift::{CanLift, Lift}; use crate::core::merge_right::MergeRight; @@ -72,6 +73,14 @@ impl JITExecutor { out.map(|response| response.take()).unwrap_or_default() } + + #[inline(always)] + fn req_hash(request: &async_graphql::Request) -> OPHash { + let mut hasher = TailcallHasher::default(); + request.query.hash(&mut hasher); + + OPHash::new(hasher.finish()) + } } impl Clone for Lift { @@ -104,18 +113,27 @@ impl From> for async_graphql::Request { impl Executor for JITExecutor { fn execute(&self, request: async_graphql::Request) -> impl Future + Send { - let jit_request = jit::Request::from(request); + let hash = Self::req_hash(&request); async move { - match ConstValueExecutor::new(&jit_request, &self.app_ctx) { - Ok(exec) => { - if self.is_query && exec.plan.dedupe { - self.dedupe_and_exec(exec, jit_request).await - } else { - self.exec(exec, jit_request).await - } - } - Err(error) => Response::from_errors(vec![error.into()]), + let jit_request = jit::Request::from(request); + let exec = if let Some(op) = self.app_ctx.operation_plans.get(&hash) { + ConstValueExecutor::from(op.value().clone()) + } else { + let exec = match ConstValueExecutor::try_new(&jit_request, &self.app_ctx) { + Ok(exec) => exec, + Err(error) => return Response::from_errors(vec![error.into()]), + }; + self.app_ctx.operation_plans.insert(hash, exec.plan.clone()); + exec + }; + + if let Some(ref response) = exec.response { + response.clone().into_async_graphql() + } else if self.is_query && exec.plan.is_dedupe { + self.dedupe_and_exec(exec, jit_request).await + } else { + self.exec(exec, jit_request).await } } } diff --git a/src/core/jit/mod.rs b/src/core/jit/mod.rs index e2b281bc6c..8c90bb5d21 100644 --- a/src/core/jit/mod.rs +++ b/src/core/jit/mod.rs @@ -2,13 +2,13 @@ mod exec; mod model; mod store; mod synth; +mod transform; use builder::*; use store::*; mod context; mod error; mod exec_const; -mod input_resolver; mod request; mod response; diff --git a/src/core/jit/model.rs b/src/core/jit/model.rs index 3842d683d5..3e11aba357 100644 --- a/src/core/jit/model.rs +++ b/src/core/jit/model.rs @@ -54,7 +54,7 @@ impl FromIterator<(String, V)> for Variables { } } -impl Field { +impl Field { #[inline(always)] pub fn skip<'json, Value: JsonLike<'json>>(&self, variables: &Variables) -> bool { let eval = @@ -78,6 +78,10 @@ impl Field { { value.get_type_name().unwrap_or(self.type_of.name()) } + + pub fn iter(&self) -> impl Iterator> { + self.selection.iter() + } } #[derive(Debug, Clone)] @@ -92,14 +96,14 @@ pub struct Arg { impl Arg { pub fn try_map( self, - map: impl Fn(Input) -> Result, + map: &impl Fn(Input) -> Result, ) -> Result, Error> { Ok(Arg { id: self.id, name: self.name, type_of: self.type_of, - value: self.value.map(&map).transpose()?, - default_value: self.default_value.map(&map).transpose()?, + value: self.value.map(map).transpose()?, + default_value: self.default_value.map(map).transpose()?, }) } } @@ -138,7 +142,7 @@ impl FieldId { } #[derive(Clone)] -pub struct Field { +pub struct Field { pub id: FieldId, /// Name of key in the value object for this field pub name: String, @@ -155,11 +159,31 @@ pub struct Field { pub skip: Option, pub include: Option, pub args: Vec>, - pub extensions: Option, + pub selection: Vec>, pub pos: Pos, pub directives: Vec>, } +pub struct DFS<'a, Input> { + stack: Vec>>, +} + +impl<'a, Input> Iterator for DFS<'a, Input> { + type Item = &'a Field; + + fn next(&mut self) -> Option { + while let Some(iter) = self.stack.last_mut() { + if let Some(field) = iter.next() { + self.stack.push(field.selection.iter()); + return Some(field); + } else { + self.stack.pop(); + } + } + None + } +} + #[derive(Clone, Debug, PartialEq)] pub struct Variable(String); @@ -175,22 +199,11 @@ impl Variable { } } -impl Field, Input> { +impl Field { pub fn try_map( self, map: &impl Fn(Input) -> Result, - ) -> Result, Output>, Error> { - let mut extensions = None; - - if let Some(nested) = self.extensions { - let nested = nested - .0 - .into_iter() - .map(|v| v.try_map(map)) - .collect::>()?; - extensions = Some(Nested(nested)); - } - + ) -> Result, Error> { Ok(Field { id: self.id, name: self.name, @@ -198,107 +211,29 @@ impl Field, Input> { ir: self.ir, type_of: self.type_of, type_condition: self.type_condition, - extensions, - pos: self.pos, - skip: self.skip, - include: self.include, - args: self - .args + selection: self + .selection .into_iter() - .map(|arg| arg.try_map(map)) - .collect::>()?, - directives: self - .directives - .into_iter() - .map(|directive| directive.try_map(map)) - .collect::>()?, - }) - } -} - -impl Field { - pub fn try_map( - self, - map: impl Fn(Input) -> Result, - ) -> Result, Error> { - Ok(Field { - id: self.id, - name: self.name, - output_name: self.output_name, - ir: self.ir, - type_of: self.type_of, - type_condition: self.type_condition, - extensions: self.extensions, + .map(|f| f.try_map(map)) + .collect::>, Error>>()?, skip: self.skip, include: self.include, pos: self.pos, args: self .args .into_iter() - .map(|arg| arg.try_map(&map)) + .map(|arg| arg.try_map(map)) .collect::>()?, directives: self .directives .into_iter() - .map(|directive| directive.try_map(&map)) + .map(|directive| directive.try_map(map)) .collect::>()?, }) } } -impl Field, Input> { - /// iters over children fields - pub fn iter(&self) -> impl Iterator, Input>> { - self.extensions - .as_ref() - .map(move |nested| nested.0.iter()) - .into_iter() - .flatten() - } -} - -impl Field { - pub fn parent(&self) -> Option<&FieldId> { - self.extensions.as_ref().map(|flat| &flat.0) - } - - fn into_nested(self, fields: &[Field]) -> Field, Input> - where - Input: Clone, - { - let mut children = Vec::new(); - for field in fields.iter() { - if let Some(id) = field.parent() { - if *id == self.id { - children.push(field.to_owned().into_nested(fields)); - } - } - } - - let extensions = if children.is_empty() { - None - } else { - Some(Nested(children)) - }; - - Field { - id: self.id, - name: self.name, - output_name: self.output_name, - ir: self.ir, - type_of: self.type_of, - type_condition: self.type_condition, - skip: self.skip, - include: self.include, - args: self.args, - pos: self.pos, - extensions, - directives: self.directives, - } - } -} - -impl Debug for Field { +impl Debug for Field { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut debug_struct = f.debug_struct("Field"); debug_struct.field("id", &self.id); @@ -312,8 +247,8 @@ impl Debug for Field { if !self.args.is_empty() { debug_struct.field("args", &self.args); } - if self.extensions.is_some() { - debug_struct.field("extensions", &self.extensions); + if !self.selection.is_empty() { + debug_struct.field("selection", &self.selection); } if self.skip.is_some() { debug_struct.field("skip", &self.skip); @@ -327,32 +262,25 @@ impl Debug for Field { } } -/// Stores field relationships in a flat structure where each field links to its -/// parent. -#[derive(Clone, Debug)] -pub struct Flat(FieldId); +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct OPHash(u64); -impl Flat { - pub fn new(parent_id: FieldId) -> Self { - Flat(parent_id) +impl OPHash { + pub fn new(hash: u64) -> Self { + OPHash(hash) } } -/// Store field relationships in a nested structure like a tree where each field -/// links to its children. -#[derive(Clone, Debug)] -pub struct Nested(Vec, Input>>); - #[derive(Clone)] pub struct OperationPlan { - root_name: String, - flat: Vec>, - operation_type: OperationType, - nested: Vec, Input>>, + pub root_name: String, + pub operation_type: OperationType, // TODO: drop index from here. Embed all the necessary information in each field of the plan. pub index: Arc, pub is_introspection_query: bool, - pub dedupe: bool, + pub is_dedupe: bool, + pub is_const: bool, + pub selection: Vec>, } impl std::fmt::Debug for OperationPlan { @@ -368,26 +296,20 @@ impl OperationPlan { self, map: impl Fn(Input) -> Result, ) -> Result, Error> { - let mut flat = vec![]; + let mut selection = vec![]; - for f in self.flat { - flat.push(f.try_map(&map)?); - } - - let mut nested = vec![]; - - for n in self.nested { - nested.push(n.try_map(&map)?); + for n in self.selection { + selection.push(n.try_map(&map)?); } Ok(OperationPlan { root_name: self.root_name, - flat, operation_type: self.operation_type, - nested, + selection, index: self.index, is_introspection_query: self.is_introspection_query, - dedupe: self.dedupe, + is_dedupe: self.is_dedupe, + is_const: self.is_const, }) } } @@ -396,7 +318,7 @@ impl OperationPlan { #[allow(clippy::too_many_arguments)] pub fn new( root_name: &str, - fields: Vec>, + selection: Vec>, operation_type: OperationType, index: Arc, is_introspection_query: bool, @@ -404,32 +326,14 @@ impl OperationPlan { where Input: Clone, { - let nested = fields - .clone() - .into_iter() - .filter(|f| f.extensions.is_none()) - .map(|f| f.into_nested(&fields)) - .collect::>(); - - let dedupe = fields - .iter() - .map(|field| { - if let Some(IR::IO(io)) = field.ir.as_ref() { - io.dedupe() - } else { - true - } - }) - .all(|a| a); - Self { root_name: root_name.to_string(), - flat: fields, - nested, + selection, operation_type, index, is_introspection_query, - dedupe, + is_dedupe: false, + is_const: false, } } @@ -448,32 +352,17 @@ impl OperationPlan { self.operation_type == OperationType::Query } - /// Returns a nested [Field] representation - pub fn as_nested(&self) -> &[Field, Input>] { - &self.nested - } - - /// Returns an owned version of [Field] representation - pub fn into_nested(self) -> Vec, Input>> { - self.nested - } - /// Returns a flat [Field] representation - pub fn as_parent(&self) -> &[Field] { - &self.flat - } - - /// Search for a field with a specified [FieldId] - pub fn find_field(&self, id: FieldId) -> Option<&Field> { - self.flat.iter().find(|field| field.id == id) + pub fn iter_dfs(&self) -> DFS { + DFS { stack: vec![self.selection.iter()] } } /// Search for a field by specified path of nested fields - pub fn find_field_path>(&self, path: &[S]) -> Option<&Field> { + pub fn find_field_path>(&self, path: &[S]) -> Option<&Field> { match path.split_first() { None => None, Some((name, path)) => { - let field = self.flat.iter().find(|field| field.name == name.as_ref())?; + let field = self.iter_dfs().find(|field| field.name == name.as_ref())?; if path.is_empty() { Some(field) } else { @@ -485,31 +374,30 @@ impl OperationPlan { /// Returns number of fields in plan pub fn size(&self) -> usize { - self.flat.len() + fn count(field: &Field) -> usize { + 1 + field.selection.iter().map(count).sum::() + } + self.selection.iter().map(count).sum() } /// Check if the field is of scalar type - pub fn field_is_scalar(&self, field: &Field) -> bool { + pub fn field_is_scalar(&self, field: &Field) -> bool { self.index.type_is_scalar(field.type_of.name()) } /// Check if the field is of enum type - pub fn field_is_enum(&self, field: &Field) -> bool { + pub fn field_is_enum(&self, field: &Field) -> bool { self.index.type_is_enum(field.type_of.name()) } /// Validate the value against enum variants of the field - pub fn field_validate_enum_value( - &self, - field: &Field, - value: &str, - ) -> bool { + pub fn field_validate_enum_value(&self, field: &Field, value: &str) -> bool { self.index.validate_enum_value(field.type_of.name(), value) } pub fn field_is_part_of_value<'a, Output>( &'a self, - field: &'a Field, Input>, + field: &'a Field, value: &'a Output, ) -> bool where @@ -538,7 +426,7 @@ pub struct Directive { impl Directive { pub fn try_map( self, - map: impl Fn(Input) -> Result, + map: &impl Fn(Input) -> Result, ) -> Result, Error> { Ok(Directive { name: self.name, @@ -701,7 +589,7 @@ mod test { use crate::core::jit; use crate::include_config; - fn plan(query: &str) -> OperationPlan { + fn plan(query: &str) -> OperationPlan { let config = include_config!("./fixtures/dedupe.graphql").unwrap(); let module = ConfigModule::from(config); let bp = Blueprint::try_from(&module).unwrap(); @@ -726,20 +614,20 @@ mod test { fn test_operation_plan_dedupe() { let actual = plan(r#"{ posts { id } }"#); - assert!(!actual.dedupe); + assert!(!actual.is_dedupe); } #[test] fn test_operation_plan_dedupe_nested() { let actual = plan(r#"{ posts { id users { id } } }"#); - assert!(!actual.dedupe); + assert!(!actual.is_dedupe); } #[test] fn test_operation_plan_dedupe_false() { let actual = plan(r#"{ users { id comments {body} } }"#); - assert!(actual.dedupe); + assert!(actual.is_dedupe); } } diff --git a/src/core/jit/request.rs b/src/core/jit/request.rs index e352164744..7a4d7d030f 100644 --- a/src/core/jit/request.rs +++ b/src/core/jit/request.rs @@ -4,8 +4,11 @@ use std::ops::DerefMut; use async_graphql_value::ConstValue; use serde::Deserialize; -use super::{Builder, OperationPlan, Result, Variables}; +use super::{transform, Builder, OperationPlan, Result, Variables}; use crate::core::blueprint::Blueprint; +use crate::core::transform::TransformerOps; +use crate::core::valid::Validator; +use crate::core::Transform; #[derive(Debug, Deserialize, Clone)] pub struct Request { @@ -34,12 +37,20 @@ impl From for Request { } impl Request { - pub fn create_plan(&self, blueprint: &Blueprint) -> Result> { + pub fn create_plan( + &self, + blueprint: &Blueprint, + ) -> Result> { let doc = async_graphql::parser::parse_query(&self.query)?; let builder = Builder::new(blueprint, doc); - let plan = builder.build(&self.variables, self.operation_name.as_deref())?; + let plan = builder.build(self.operation_name.as_deref())?; - Ok(plan) + Ok(transform::CheckConst::new() + .pipe(transform::CheckDedupe::new()) + .transform(plan) + .to_result() + // NOTE: Unwrapping because these transformations fail with () error + .unwrap()) } } diff --git a/src/core/jit/response.rs b/src/core/jit/response.rs index f489d25fc5..fda519e9ef 100644 --- a/src/core/jit/response.rs +++ b/src/core/jit/response.rs @@ -7,7 +7,7 @@ use super::Positioned; use crate::core::jit; use crate::core::merge_right::MergeRight; -#[derive(Setters, Serialize)] +#[derive(Clone, Setters, Serialize)] pub struct Response { #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__alias_query.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__alias_query.snap index 70ebd89e69..55a962e53f 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__alias_query.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__alias_query.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -12,39 +12,31 @@ expression: plan.into_nested() type_condition: Some( "Query", ), - extensions: Some( - Nested( - [ + selection: [ + Field { + id: 1, + name: "user", + output_name: "author", + ir: "Some(..)", + type_of: User, + type_condition: Some( + "Post", + ), + selection: [ Field { - id: 1, - name: "user", - output_name: "author", - ir: "Some(..)", - type_of: User, + id: 2, + name: "id", + output_name: "identifier", + type_of: ID!, type_condition: Some( - "Post", - ), - extensions: Some( - Nested( - [ - Field { - id: 2, - name: "id", - output_name: "identifier", - type_of: ID!, - type_condition: Some( - "User", - ), - directives: [], - }, - ], - ), + "User", ), directives: [], }, ], - ), - ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__default_value.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__default_value.snap index c1bdc1b33d..783937ff61 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__default_value.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__default_value.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -35,33 +35,24 @@ expression: plan.into_nested() ): String( "tailcall test", ), - Name( - "id", - ): Number( - Number(101), - ), }, ), ), default_value: None, }, ], - extensions: Some( - Nested( - [ - Field { - id: 1, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "Post", - ), - directives: [], - }, - ], - ), - ), + selection: [ + Field { + id: 1, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "Post", + ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__directives.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__directives.snap index b35eadcd1d..4ae3b7cc67 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__directives.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__directives.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -12,49 +12,47 @@ expression: plan.into_nested() type_condition: Some( "Query", ), - extensions: Some( - Nested( - [ - Field { - id: 1, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "User", - ), - directives: [ - Directive { - name: "options", - arguments: [ - ( - "paging", - Boolean( - true, - ), + selection: [ + Field { + id: 1, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "User", + ), + directives: [ + Directive { + name: "options", + arguments: [ + ( + "paging", + Variable( + Name( + "includeName", ), - ], - }, - ], - }, - Field { - id: 2, - name: "name", - output_name: "name", - type_of: String!, - type_condition: Some( - "User", - ), - include: Some( - Variable( - "includeName", + ), ), - ), - directives: [], + ], }, ], - ), - ), + }, + Field { + id: 2, + name: "name", + output_name: "name", + type_of: String!, + type_condition: Some( + "User", + ), + include: Some( + Variable( + "includeName", + ), + ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__fragments.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__fragments.snap index b8201fa695..43558fa415 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__fragments.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__fragments.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -25,62 +25,58 @@ expression: plan.into_nested() default_value: None, }, ], - extensions: Some( - Nested( - [ - Field { - id: 1, - name: "name", - output_name: "name", - type_of: String!, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 2, - name: "email", - output_name: "email", - type_of: String!, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 3, - name: "phone", - output_name: "phone", - type_of: String, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 4, - name: "title", - output_name: "title", - type_of: String!, - type_condition: Some( - "Post", - ), - directives: [], - }, - Field { - id: 5, - name: "body", - output_name: "body", - type_of: String!, - type_condition: Some( - "Post", - ), - directives: [], - }, - ], - ), - ), + selection: [ + Field { + id: 1, + name: "name", + output_name: "name", + type_of: String!, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 2, + name: "email", + output_name: "email", + type_of: String!, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 3, + name: "phone", + output_name: "phone", + type_of: String, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 4, + name: "title", + output_name: "title", + type_of: String!, + type_condition: Some( + "Post", + ), + directives: [], + }, + Field { + id: 5, + name: "body", + output_name: "body", + type_of: String!, + type_condition: Some( + "Post", + ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__from_document.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__from_document.snap index a66c07dc6e..dfe66ad529 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__from_document.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__from_document.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -12,49 +12,41 @@ expression: plan.into_nested() type_condition: Some( "Query", ), - extensions: Some( - Nested( - [ + selection: [ + Field { + id: 1, + name: "user", + output_name: "user", + ir: "Some(..)", + type_of: User, + type_condition: Some( + "Post", + ), + selection: [ Field { - id: 1, - name: "user", - output_name: "user", - ir: "Some(..)", - type_of: User, + id: 2, + name: "id", + output_name: "id", + type_of: ID!, type_condition: Some( - "Post", + "User", ), - extensions: Some( - Nested( - [ - Field { - id: 2, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 3, - name: "name", - output_name: "name", - type_of: String!, - type_condition: Some( - "User", - ), - directives: [], - }, - ], - ), + directives: [], + }, + Field { + id: 3, + name: "name", + output_name: "name", + type_of: String!, + type_condition: Some( + "User", ), directives: [], }, ], - ), - ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__multiple_operations.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__multiple_operations.snap index 999533bf3e..efd0911f59 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__multiple_operations.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__multiple_operations.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -25,32 +25,28 @@ expression: plan.into_nested() default_value: None, }, ], - extensions: Some( - Nested( - [ - Field { - id: 1, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 2, - name: "username", - output_name: "username", - type_of: String!, - type_condition: Some( - "User", - ), - directives: [], - }, - ], - ), - ), + selection: [ + Field { + id: 1, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 2, + name: "username", + output_name: "username", + type_of: String!, + type_condition: Some( + "User", + ), + directives: [], + }, + ], directives: [], }, Field { @@ -62,32 +58,28 @@ expression: plan.into_nested() type_condition: Some( "Query", ), - extensions: Some( - Nested( - [ - Field { - id: 4, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "Post", - ), - directives: [], - }, - Field { - id: 5, - name: "title", - output_name: "title", - type_of: String!, - type_condition: Some( - "Post", - ), - directives: [], - }, - ], - ), - ), + selection: [ + Field { + id: 4, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "Post", + ), + directives: [], + }, + Field { + id: 5, + name: "title", + output_name: "title", + type_of: String!, + type_condition: Some( + "Post", + ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation-2.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation-2.snap index 08c1237e9b..85bc18599b 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation-2.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation-2.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -35,63 +35,54 @@ expression: plan.into_nested() ): String( "test-12", ), - Name( - "id", - ): Number( - Number(101), - ), }, ), ), default_value: None, }, ], - extensions: Some( - Nested( - [ - Field { - id: 1, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "Post", - ), - directives: [], - }, - Field { - id: 2, - name: "userId", - output_name: "userId", - type_of: ID!, - type_condition: Some( - "Post", - ), - directives: [], - }, - Field { - id: 3, - name: "title", - output_name: "title", - type_of: String!, - type_condition: Some( - "Post", - ), - directives: [], - }, - Field { - id: 4, - name: "body", - output_name: "body", - type_of: String!, - type_condition: Some( - "Post", - ), - directives: [], - }, - ], - ), - ), + selection: [ + Field { + id: 1, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "Post", + ), + directives: [], + }, + Field { + id: 2, + name: "userId", + output_name: "userId", + type_of: ID!, + type_condition: Some( + "Post", + ), + directives: [], + }, + Field { + id: 3, + name: "title", + output_name: "title", + type_of: String!, + type_condition: Some( + "Post", + ), + directives: [], + }, + Field { + id: 4, + name: "body", + output_name: "body", + type_of: String!, + type_condition: Some( + "Post", + ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation.snap index 8c61134907..5a27256443 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__resolving_operation.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -12,42 +12,38 @@ expression: plan.into_nested() type_condition: Some( "Query", ), - extensions: Some( - Nested( - [ - Field { - id: 1, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "Post", - ), - directives: [], - }, - Field { - id: 2, - name: "userId", - output_name: "userId", - type_of: ID!, - type_condition: Some( - "Post", - ), - directives: [], - }, - Field { - id: 3, - name: "title", - output_name: "title", - type_of: String!, - type_condition: Some( - "Post", - ), - directives: [], - }, - ], - ), - ), + selection: [ + Field { + id: 1, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "Post", + ), + directives: [], + }, + Field { + id: 2, + name: "userId", + output_name: "userId", + type_of: ID!, + type_condition: Some( + "Post", + ), + directives: [], + }, + Field { + id: 3, + name: "title", + output_name: "title", + type_of: String!, + type_condition: Some( + "Post", + ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_mutation.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_mutation.snap index 242fca4dfd..8899a472d8 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_mutation.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_mutation.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -56,72 +56,68 @@ expression: plan.into_nested() default_value: None, }, ], - extensions: Some( - Nested( - [ - Field { - id: 1, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 2, - name: "name", - output_name: "name", - type_of: String!, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 3, - name: "email", - output_name: "email", - type_of: String!, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 4, - name: "phone", - output_name: "phone", - type_of: String, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 5, - name: "website", - output_name: "website", - type_of: String, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 6, - name: "username", - output_name: "username", - type_of: String!, - type_condition: Some( - "User", - ), - directives: [], - }, - ], - ), - ), + selection: [ + Field { + id: 1, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 2, + name: "name", + output_name: "name", + type_of: String!, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 3, + name: "email", + output_name: "email", + type_of: String!, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 4, + name: "phone", + output_name: "phone", + type_of: String, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 5, + name: "website", + output_name: "website", + type_of: String, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 6, + name: "username", + output_name: "username", + type_of: String!, + type_condition: Some( + "User", + ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_query.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_query.snap index b4109ee4ac..3e08218120 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_query.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__simple_query.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -12,39 +12,31 @@ expression: plan.into_nested() type_condition: Some( "Query", ), - extensions: Some( - Nested( - [ + selection: [ + Field { + id: 1, + name: "user", + output_name: "user", + ir: "Some(..)", + type_of: User, + type_condition: Some( + "Post", + ), + selection: [ Field { - id: 1, - name: "user", - output_name: "user", - ir: "Some(..)", - type_of: User, + id: 2, + name: "id", + output_name: "id", + type_of: ID!, type_condition: Some( - "Post", - ), - extensions: Some( - Nested( - [ - Field { - id: 2, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "User", - ), - directives: [], - }, - ], - ), + "User", ), directives: [], }, ], - ), - ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__unions.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__unions.snap index 46b9485a94..6d3590237c 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__unions.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__unions.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -25,32 +25,28 @@ expression: plan.into_nested() default_value: None, }, ], - extensions: Some( - Nested( - [ - Field { - id: 1, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "UserId", - ), - directives: [], - }, - Field { - id: 2, - name: "email", - output_name: "email", - type_of: String!, - type_condition: Some( - "UserEmail", - ), - directives: [], - }, - ], - ), - ), + selection: [ + Field { + id: 1, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "UserId", + ), + directives: [], + }, + Field { + id: 2, + name: "email", + output_name: "email", + type_of: String!, + type_condition: Some( + "UserEmail", + ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__variables.snap b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__variables.snap index 0c29d55b1f..c2d15d78bf 100644 --- a/src/core/jit/snapshots/tailcall__core__jit__builder__tests__variables.snap +++ b/src/core/jit/snapshots/tailcall__core__jit__builder__tests__variables.snap @@ -1,6 +1,6 @@ --- source: src/core/jit/builder.rs -expression: plan.into_nested() +expression: plan.selection --- [ Field { @@ -18,39 +18,37 @@ expression: plan.into_nested() name: "id", type_of: ID!, value: Some( - Number( - Number(1), + Variable( + Name( + "id", + ), ), ), default_value: None, }, ], - extensions: Some( - Nested( - [ - Field { - id: 1, - name: "id", - output_name: "id", - type_of: ID!, - type_condition: Some( - "User", - ), - directives: [], - }, - Field { - id: 2, - name: "name", - output_name: "name", - type_of: String!, - type_condition: Some( - "User", - ), - directives: [], - }, - ], - ), - ), + selection: [ + Field { + id: 1, + name: "id", + output_name: "id", + type_of: ID!, + type_condition: Some( + "User", + ), + directives: [], + }, + Field { + id: 2, + name: "name", + output_name: "name", + type_of: String!, + type_condition: Some( + "User", + ), + directives: [], + }, + ], directives: [], }, ] diff --git a/src/core/jit/synth/synth.rs b/src/core/jit/synth/synth.rs index 73b1ae7484..f2c63d681b 100644 --- a/src/core/jit/synth/synth.rs +++ b/src/core/jit/synth/synth.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use crate::core::jit::model::{Field, Nested, OperationPlan, Variables}; +use crate::core::jit::model::{Field, OperationPlan, Variables}; use crate::core::jit::store::{DataPath, Store}; use crate::core::jit::{Error, PathSegment, Positioned, ValidationError}; use crate::core::json::{JsonLike, JsonObjectLike}; @@ -30,7 +30,7 @@ where Value: JsonLike<'a> + Clone + std::fmt::Debug, { #[inline(always)] - fn include(&self, field: &Field) -> bool { + fn include(&self, field: &Field) -> bool { !field.skip(&self.variables) } @@ -40,7 +40,7 @@ where let mut path = Vec::new(); let root_name = self.plan.root_name(); - for child in self.plan.as_nested().iter() { + for child in self.plan.selection.iter() { if !self.include(child) { continue; } @@ -57,7 +57,7 @@ where #[inline(always)] fn iter( &'a self, - node: &'a Field, Value>, + node: &'a Field, value: Option<&'a Value>, data_path: &DataPath, path: &mut Vec, @@ -96,7 +96,7 @@ where /// case it does not it throws an Error fn node_nullable_guard( &'a self, - node: &'a Field, Value>, + node: &'a Field, path: &[PathSegment], root_name: Option<&'a str>, ) -> Result> { @@ -118,7 +118,7 @@ where #[inline(always)] fn iter_inner( &'a self, - node: &'a Field, Value>, + node: &'a Field, value: &'a Value, data_path: &DataPath, path: &mut Vec, @@ -221,7 +221,7 @@ where fn to_location_error( &'a self, error: Error, - node: &'a Field, Value>, + node: &'a Field, path: &[PathSegment], ) -> Positioned { Positioned::new(error, node.pos).with_path(path.to_vec()) @@ -322,8 +322,18 @@ mod tests { let config = ConfigModule::from(config); let builder = Builder::new(&Blueprint::try_from(&config).unwrap(), doc); - let plan = builder.build(&Variables::new(), None).unwrap(); - let plan = plan.try_map(Deserialize::deserialize).unwrap(); + let plan = builder.build(None).unwrap(); + let plan = plan + .try_map(|v| { + // Earlier we hard OperationPlan which has impl Deserialize + // but now InputResolver takes OperationPlan + // and returns OperationPlan. + // So we need to map Plan to some other value before being able to deserialize + // it. + let serde = v.into_json().unwrap(); + Deserialize::deserialize(serde) + }) + .unwrap(); let store = store .into_iter() diff --git a/src/core/jit/transform/check_const.rs b/src/core/jit/transform/check_const.rs new file mode 100644 index 0000000000..6b1d928e3e --- /dev/null +++ b/src/core/jit/transform/check_const.rs @@ -0,0 +1,46 @@ +use std::marker::PhantomData; + +use crate::core::ir::model::IR; +use crate::core::jit::OperationPlan; +use crate::core::valid::Valid; +use crate::core::Transform; + +pub struct CheckConst(PhantomData); +impl CheckConst { + pub fn new() -> Self { + Self(PhantomData) + } +} + +/// Checks if the IR will always evaluate to a Constant Value +pub fn is_const(ir: &IR) -> bool { + match ir { + IR::Dynamic(dynamic_value) => dynamic_value.is_const(), + IR::IO(_) => false, + IR::Cache(_) => false, + IR::Path(ir, _) => is_const(ir), + IR::ContextPath(_) => false, + IR::Protect(ir) => is_const(ir), + IR::Map(map) => is_const(&map.input), + IR::Pipe(ir, ir1) => is_const(ir) && is_const(ir1), + IR::Discriminate(_, ir) => is_const(ir), + IR::Entity(hash_map) => hash_map.values().all(is_const), + IR::Service(_) => true, + } +} + +impl Transform for CheckConst { + type Value = OperationPlan; + type Error = (); + + fn transform(&self, mut plan: Self::Value) -> Valid { + let is_const = plan.iter_dfs().all(|field| match field.ir { + Some(ref ir) => is_const(ir), + None => true, + }); + + plan.is_const = is_const; + + Valid::succeed(plan) + } +} diff --git a/src/core/jit/transform/check_dedupe.rs b/src/core/jit/transform/check_dedupe.rs new file mode 100644 index 0000000000..878847042a --- /dev/null +++ b/src/core/jit/transform/check_dedupe.rs @@ -0,0 +1,47 @@ +use crate::core::ir::model::IR; +use crate::core::jit::OperationPlan; +use crate::core::valid::Valid; +use crate::core::Transform; + +pub struct CheckDedupe(std::marker::PhantomData); +impl CheckDedupe { + pub fn new() -> Self { + Self(std::marker::PhantomData) + } +} + +#[inline] +fn check_dedupe(ir: &IR) -> bool { + match ir { + IR::IO(io) => io.dedupe(), + IR::Cache(cache) => cache.io.dedupe(), + IR::Path(ir, _) => check_dedupe(ir), + IR::Protect(ir) => check_dedupe(ir), + IR::Pipe(ir, ir1) => check_dedupe(ir) && check_dedupe(ir1), + IR::Discriminate(_, ir) => check_dedupe(ir), + IR::Entity(hash_map) => hash_map.values().all(check_dedupe), + IR::Dynamic(_) => true, + IR::ContextPath(_) => true, + IR::Map(_) => true, + IR::Service(_) => true, + } +} + +impl Transform for CheckDedupe { + type Value = OperationPlan; + type Error = (); + + fn transform(&self, mut plan: Self::Value) -> Valid { + let dedupe = plan.selection.iter().all(|field| { + if let Some(ir) = field.ir.as_ref() { + check_dedupe(ir) + } else { + true + } + }); + + plan.is_dedupe = dedupe; + + Valid::succeed(plan) + } +} diff --git a/src/core/jit/input_resolver.rs b/src/core/jit/transform/input_resolver.rs similarity index 80% rename from src/core/jit/input_resolver.rs rename to src/core/jit/transform/input_resolver.rs index 31bc8b498b..8c5ab03771 100644 --- a/src/core/jit/input_resolver.rs +++ b/src/core/jit/transform/input_resolver.rs @@ -1,6 +1,6 @@ use async_graphql_value::{ConstValue, Value}; -use super::{Arg, Field, OperationPlan, ResolveInputError, Variables}; +use super::super::{Field, OperationPlan, ResolveInputError, Variables}; use crate::core::json::{JsonLikeOwned, JsonObjectLike}; use crate::core::Type; @@ -44,8 +44,8 @@ impl InputResolver { impl InputResolver where - Input: Clone, - Output: Clone + JsonLikeOwned + TryFrom, + Input: Clone + std::fmt::Debug, + Output: Clone + JsonLikeOwned + TryFrom + std::fmt::Debug, Input: InputResolvable, >::Error: std::fmt::Debug, { @@ -55,32 +55,17 @@ where ) -> Result, ResolveInputError> { let new_fields = self .plan - .as_parent() + .selection .iter() - .map(|field| field.clone().try_map(|value| value.resolve(variables))) + .map(|field| (*field).clone().try_map(&|value| value.resolve(variables))) .map(|field| match field { Ok(field) => { - let args = field - .args - .into_iter() - .map(|arg| { - let value = self.recursive_parse_arg( - &field.name, - &arg.name, - &arg.type_of, - &arg.default_value, - arg.value, - )?; - Ok(Arg { value, ..arg }) - }) - .collect::>()?; - - Ok(Field { args, ..field }) + let field = self.iter_args(field)?; + Ok(field) } Err(err) => Err(err), }) .collect::, _>>()?; - Ok(OperationPlan::new( self.plan.root_name(), new_fields, @@ -90,6 +75,35 @@ where )) } + #[inline(always)] + fn iter_args(&self, mut field: Field) -> Result, ResolveInputError> { + let args = field + .args + .into_iter() + .map(|mut arg| { + let value = self.recursive_parse_arg( + &field.name, + &arg.name, + &arg.type_of, + &arg.default_value, + arg.value, + )?; + arg.value = value; + Ok(arg) + }) + .collect::>()?; + + field.args = args; + + field.selection = field + .selection + .into_iter() + .map(|val| self.iter_args(val)) + .collect::>()?; + + Ok(field) + } + #[allow(clippy::too_many_arguments)] fn recursive_parse_arg( &self, diff --git a/src/core/jit/transform/mod.rs b/src/core/jit/transform/mod.rs new file mode 100644 index 0000000000..aeb68bd8f9 --- /dev/null +++ b/src/core/jit/transform/mod.rs @@ -0,0 +1,9 @@ +mod check_const; +mod check_dedupe; +mod input_resolver; +mod skip; + +pub use check_const::*; +pub use check_dedupe::*; +pub use input_resolver::*; +pub use skip::*; diff --git a/src/core/jit/transform/skip.rs b/src/core/jit/transform/skip.rs new file mode 100644 index 0000000000..84298e106b --- /dev/null +++ b/src/core/jit/transform/skip.rs @@ -0,0 +1,42 @@ +use std::marker::PhantomData; + +use crate::core::jit::{Error, Field, OperationPlan, Variables}; +use crate::core::json::JsonLike; +use crate::core::valid::Valid; +use crate::core::Transform; + +pub struct Skip<'a, Var, Value> { + variables: &'a Variables, + _value: PhantomData, +} + +impl<'a, Var, Value> Skip<'a, Var, Value> { + pub fn new(variables: &'a Variables) -> Self { + Self { variables, _value: PhantomData } + } +} + +impl<'a, Var, Value: Clone> Transform for Skip<'a, Var, Value> +where + Var: for<'b> JsonLike<'b> + Clone, +{ + type Value = OperationPlan; + + type Error = Error; + + fn transform(&self, plan: Self::Value) -> Valid { + let mut plan = plan; + let variables: &Variables = self.variables; + skip(&mut plan.selection, variables); + + Valid::succeed(plan) + } +} + +/// Drops all the fields that are not needed based on the set variables +fn skip JsonLike<'b>>(fields: &mut Vec>, vars: &Variables) { + fields.retain(|f| !f.skip(vars)); + for field in fields { + skip(&mut field.selection, vars); + } +} diff --git a/src/core/runtime.rs b/src/core/runtime.rs index a1599dc0de..1caf1d5e3b 100644 --- a/src/core/runtime.rs +++ b/src/core/runtime.rs @@ -186,7 +186,7 @@ pub mod test { http2_only: http2, env: Arc::new(env), file: Arc::new(file), - cache: Arc::new(InMemoryCache::new()), + cache: Arc::new(InMemoryCache::default()), extensions: Arc::new(vec![]), cmd_worker: match &script { Some(script) => Some(init_worker_io::(script.to_owned())), diff --git a/tailcall-aws-lambda/src/runtime.rs b/tailcall-aws-lambda/src/runtime.rs index 53b9d58bec..9fccef6d6f 100644 --- a/tailcall-aws-lambda/src/runtime.rs +++ b/tailcall-aws-lambda/src/runtime.rs @@ -48,7 +48,7 @@ pub fn init_file() -> Arc { } pub fn init_cache() -> Arc { - Arc::new(InMemoryCache::new()) + Arc::new(InMemoryCache::default()) } pub fn init_runtime() -> TargetRuntime { diff --git a/tailcall-wasm/src/runtime.rs b/tailcall-wasm/src/runtime.rs index 25ca9c418e..437cfa807d 100644 --- a/tailcall-wasm/src/runtime.rs +++ b/tailcall-wasm/src/runtime.rs @@ -23,7 +23,7 @@ fn init_env() -> Arc { } fn init_cache() -> Arc> { - Arc::new(InMemoryCache::new()) + Arc::new(InMemoryCache::default()) } pub fn init_rt() -> TargetRuntime { diff --git a/tests/core/parse.rs b/tests/core/parse.rs index 6cb7b90521..00744100cc 100644 --- a/tests/core/parse.rs +++ b/tests/core/parse.rs @@ -302,7 +302,7 @@ impl ExecutionSpec { http2_only, file: Arc::new(File::new(self.clone())), env: Arc::new(Env::init(env)), - cache: Arc::new(InMemoryCache::new()), + cache: Arc::new(InMemoryCache::default()), extensions: Arc::new(vec![]), cmd_worker: http_worker, worker, diff --git a/tests/core/runtime.rs b/tests/core/runtime.rs index cb0da3d72b..0961593cc3 100644 --- a/tests/core/runtime.rs +++ b/tests/core/runtime.rs @@ -79,7 +79,7 @@ pub fn create_runtime( http2_only: http2, env: Arc::new(env), file: Arc::new(file), - cache: Arc::new(InMemoryCache::new()), + cache: Arc::new(InMemoryCache::default()), extensions: Arc::new(vec![]), cmd_worker: match &script { Some(script) => Some(init_worker_io::(script.to_owned())), diff --git a/tests/core/snapshots/graphql-conformance-001.md_5.snap b/tests/core/snapshots/graphql-conformance-001.md_5.snap index 92f6de675e..028246ac51 100644 --- a/tests/core/snapshots/graphql-conformance-001.md_5.snap +++ b/tests/core/snapshots/graphql-conformance-001.md_5.snap @@ -11,7 +11,13 @@ expression: response "data": null, "errors": [ { - "message": "Build error: ResolveInputError: Argument `id` for field `user` is required" + "message": "Build error: ResolveInputError: Argument `id` for field `user` is required", + "locations": [ + { + "line": 0, + "column": 0 + } + ] } ] } diff --git a/tests/core/snapshots/graphql-conformance-015.md_10.snap b/tests/core/snapshots/graphql-conformance-015.md_10.snap index 1431a65d5e..40dcdfd94b 100644 --- a/tests/core/snapshots/graphql-conformance-015.md_10.snap +++ b/tests/core/snapshots/graphql-conformance-015.md_10.snap @@ -11,7 +11,13 @@ expression: response "data": null, "errors": [ { - "message": "Build error: ResolveInputError: Argument `size` for field `profilePic` is required" + "message": "Build error: ResolveInputError: Argument `size` for field `profilePic` is required", + "locations": [ + { + "line": 0, + "column": 0 + } + ] } ] } diff --git a/tests/core/snapshots/graphql-conformance-015.md_9.snap b/tests/core/snapshots/graphql-conformance-015.md_9.snap index d1fd985eaf..6d14fcb37a 100644 --- a/tests/core/snapshots/graphql-conformance-015.md_9.snap +++ b/tests/core/snapshots/graphql-conformance-015.md_9.snap @@ -11,7 +11,13 @@ expression: response "data": null, "errors": [ { - "message": "Build error: ResolveInputError: Argument `height` for field `featuredVideoPreview.video` is required" + "message": "Build error: ResolveInputError: Argument `height` for field `featuredVideoPreview.video` is required", + "locations": [ + { + "line": 0, + "column": 0 + } + ] } ] } diff --git a/tests/core/snapshots/graphql-conformance-http-001.md_5.snap b/tests/core/snapshots/graphql-conformance-http-001.md_5.snap index 92f6de675e..028246ac51 100644 --- a/tests/core/snapshots/graphql-conformance-http-001.md_5.snap +++ b/tests/core/snapshots/graphql-conformance-http-001.md_5.snap @@ -11,7 +11,13 @@ expression: response "data": null, "errors": [ { - "message": "Build error: ResolveInputError: Argument `id` for field `user` is required" + "message": "Build error: ResolveInputError: Argument `id` for field `user` is required", + "locations": [ + { + "line": 0, + "column": 0 + } + ] } ] } diff --git a/tests/core/snapshots/graphql-conformance-http-015.md_10.snap b/tests/core/snapshots/graphql-conformance-http-015.md_10.snap index 1431a65d5e..40dcdfd94b 100644 --- a/tests/core/snapshots/graphql-conformance-http-015.md_10.snap +++ b/tests/core/snapshots/graphql-conformance-http-015.md_10.snap @@ -11,7 +11,13 @@ expression: response "data": null, "errors": [ { - "message": "Build error: ResolveInputError: Argument `size` for field `profilePic` is required" + "message": "Build error: ResolveInputError: Argument `size` for field `profilePic` is required", + "locations": [ + { + "line": 0, + "column": 0 + } + ] } ] } diff --git a/tests/core/snapshots/graphql-conformance-http-015.md_9.snap b/tests/core/snapshots/graphql-conformance-http-015.md_9.snap index d1fd985eaf..6d14fcb37a 100644 --- a/tests/core/snapshots/graphql-conformance-http-015.md_9.snap +++ b/tests/core/snapshots/graphql-conformance-http-015.md_9.snap @@ -11,7 +11,13 @@ expression: response "data": null, "errors": [ { - "message": "Build error: ResolveInputError: Argument `height` for field `featuredVideoPreview.video` is required" + "message": "Build error: ResolveInputError: Argument `height` for field `featuredVideoPreview.video` is required", + "locations": [ + { + "line": 0, + "column": 0 + } + ] } ] } diff --git a/tests/jit_spec.rs b/tests/jit_spec.rs index ae342112ee..0dd64bc9e6 100644 --- a/tests/jit_spec.rs +++ b/tests/jit_spec.rs @@ -2,12 +2,16 @@ mod tests { use std::sync::Arc; + use async_graphql::Pos; use async_graphql_value::ConstValue; use tailcall::core::app_context::AppContext; use tailcall::core::blueprint::Blueprint; use tailcall::core::config::{Config, ConfigModule}; use tailcall::core::http::RequestContext; - use tailcall::core::jit::{ConstValueExecutor, Error, Request, Response}; + use tailcall::core::jit::{ + BuildError, ConstValueExecutor, Error, Positioned, Request, ResolveInputError, Response, + }; + use tailcall::core::json::{JsonLike, JsonObjectLike}; use tailcall::core::rest::EndpointSet; use tailcall::core::valid::Validator; @@ -33,7 +37,7 @@ mod tests { &self, request: Request, ) -> anyhow::Result> { - let executor = ConstValueExecutor::new(&request, &self.app_ctx)?; + let executor = ConstValueExecutor::try_new(&request, &self.app_ctx)?; Ok(executor.execute(&self.req_ctx, &request).await) } @@ -167,13 +171,14 @@ mod tests { let request = Request::new(query); let executor = TestExecutor::try_new().await.unwrap(); - match executor.run(request).await { - Ok(_) => panic!("Should fail with unresolved variable"), - Err(err) => assert_eq!( - err.to_string(), - "Build error: ResolveInputError: Variable `id` is not defined" - ), - }; + let resp = executor.run(request).await.unwrap(); + let errs = vec![Positioned::new( + Error::BuildError(BuildError::ResolveInputError( + ResolveInputError::VariableIsNotFound("id".to_string()), + )), + Pos::default().into(), + )]; + assert_eq!(format!("{:?}", resp.errors), format!("{:?}", errs)); let request = Request::new(query); let request = request.variables([("id".into(), ConstValue::from(1))]); @@ -183,6 +188,51 @@ mod tests { insta::assert_json_snapshot!(data); } + #[tokio::test] + async fn test_operation_plan_cache() { + fn get_id_value(data: ConstValue) -> Option { + data.as_object() + .and_then(|v| v.get_key("user")) + .and_then(|v| v.get_key("id")) + .and_then(|u| u.as_i64()) + } + + // NOTE: This test makes a real HTTP call + let query = r#" + query user($id: Int!) { + user(id: $id) { + id + name + } + } + "#; + let request = Request::new(query); + let executor = TestExecutor::try_new().await.unwrap(); + + let resp = executor.run(request).await.unwrap(); + let errs = vec![Positioned::new( + Error::BuildError(BuildError::ResolveInputError( + ResolveInputError::VariableIsNotFound("id".to_string()), + )), + Pos::default().into(), + )]; + assert_eq!(format!("{:?}", resp.errors), format!("{:?}", errs)); + + let request = Request::new(query); + let request = request.variables([("id".into(), ConstValue::from(1))]); + let response = executor.run(request).await.unwrap(); + let data = response.data; + + assert_eq!(data.and_then(get_id_value).unwrap(), 1); + + let request = Request::new(query); + let request = request.variables([("id".into(), ConstValue::from(2))]); + let response = executor.run(request).await.unwrap(); + let data = response.data; + + assert_eq!(data.and_then(get_id_value).unwrap(), 2); + } + #[tokio::test] async fn test_query_alias() { // NOTE: This test makes a real HTTP call diff --git a/tests/server_spec.rs b/tests/server_spec.rs index 8c1e865196..a024dad739 100644 --- a/tests/server_spec.rs +++ b/tests/server_spec.rs @@ -143,7 +143,7 @@ pub mod test { http2_only: http2, env: Arc::new(env), file: Arc::new(file), - cache: Arc::new(InMemoryCache::new()), + cache: Arc::new(InMemoryCache::default()), extensions: Arc::new(vec![]), cmd_worker: match &script { Some(script) => Some(init_worker_io::(script.to_owned())),