From f35cb087d784f3c51a4b540e0000ebca253c18d9 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Thu, 26 Jun 2025 00:58:29 +0300 Subject: [PATCH 01/35] No preserve order, serialize the response using selection set in order --- Cargo.lock | 1 - bin/dev-cli/Cargo.toml | 2 +- bin/gateway/Cargo.toml | 2 +- bin/gateway/src/pipeline/execution_service.rs | 51 +- lib/query-plan-executor/Cargo.toml | 2 +- .../benches/executor_benches.rs | 9 +- lib/query-plan-executor/src/lib.rs | 322 +-- lib/query-plan-executor/src/projection.rs | 235 ++ lib/query-plan-executor/src/tests/mod.rs | 6 +- ...ests__query_executor_pipeline_locally.snap | 2335 +---------------- lib/query-planner/Cargo.toml | 3 +- 11 files changed, 284 insertions(+), 2684 deletions(-) create mode 100644 lib/query-plan-executor/src/projection.rs diff --git a/Cargo.lock b/Cargo.lock index 8d23438ed..4da8ab491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2342,7 +2342,6 @@ version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ - "indexmap 2.10.0", "itoa", "memchr", "ryu", diff --git a/bin/dev-cli/Cargo.toml b/bin/dev-cli/Cargo.toml index 3e5f73875..7e4680a0a 100644 --- a/bin/dev-cli/Cargo.toml +++ b/bin/dev-cli/Cargo.toml @@ -12,4 +12,4 @@ query-planner = { path = "../../lib/query-planner" } graphql-parser = "0.4.1" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-tree = "0.4.0" -serde_json = { version = "1.0.120", features = ["preserve_order"] } +serde_json = "1.0.140" diff --git a/bin/gateway/Cargo.toml b/bin/gateway/Cargo.toml index 8a61841aa..b5a8356e0 100644 --- a/bin/gateway/Cargo.toml +++ b/bin/gateway/Cargo.toml @@ -20,7 +20,7 @@ graphql-tools = "0.4.0" # Using version from original file # Serialization serde = { version = "1.0.203", features = ["derive"] } -serde_json = { version = "1.0.120", features = ["preserve_order"] } +serde_json = "1.0.140" # HTTP client and caching moka = { version = "0.12.8", features = ["future"] } diff --git a/bin/gateway/src/pipeline/execution_service.rs b/bin/gateway/src/pipeline/execution_service.rs index e0cd3cc3d..b91480045 100644 --- a/bin/gateway/src/pipeline/execution_service.rs +++ b/bin/gateway/src/pipeline/execution_service.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::convert::Infallible; use std::future::Future; use std::pin::Pin; @@ -11,12 +10,9 @@ use crate::pipeline::normalize_service::GraphQLNormalizationPayload; use crate::pipeline::query_plan_service::QueryPlanPayload; use crate::shared_state::GatewaySharedState; use axum::body::Body; -use axum::response::IntoResponse; -use axum::Json; use http::header::CONTENT_TYPE; -use http::{HeaderName, Request, Response}; -use query_plan_executor::{execute_query_plan, ExecutionResult}; -use serde_json::{json, to_value}; +use http::{Request, Response}; +use query_plan_executor::execute_query_plan; use tower::Service; #[derive(Clone, Debug, Default)] @@ -90,37 +86,18 @@ impl Service> for ExecutionService { .get::() .expect("HttpRequestParams missing"); - let mut execution_result = match expose_query_plan { - ExposeQueryPlanMode::DryRun => ExecutionResult { - data: Some(json!({})), - errors: None, - extensions: Some(HashMap::new()), - }, - _ => { - execute_query_plan( - &query_plan_payload.query_plan, - &app_state.subgraph_executor_map, - &variable_payload.variables_map, - &app_state.schema_metadata, - &normalized_payload.normalized_document.operation, - normalized_payload.has_introspection, - ) - .await - } - }; - - if expose_query_plan == ExposeQueryPlanMode::Yes - || expose_query_plan == ExposeQueryPlanMode::DryRun - { - let plan_value = to_value(query_plan_payload.query_plan.as_ref()).unwrap(); - - execution_result - .extensions - .get_or_insert_with(HashMap::new) - .insert("queryPlan".to_string(), plan_value); - } - - let mut response = Json(execution_result).into_response(); + let execution_result = execute_query_plan( + &query_plan_payload.query_plan, + &app_state.subgraph_executor_map, + &variable_payload.variables_map, + &app_state.schema_metadata, + &normalized_payload.normalized_document.operation, + normalized_payload.has_introspection, + expose_query_plan, + ) + .await; + + let mut response = Response::new(Body::from(execution_result)); response.headers_mut().insert( CONTENT_TYPE, http_request_params.response_content_type.clone(), diff --git a/lib/query-plan-executor/Cargo.toml b/lib/query-plan-executor/Cargo.toml index 1412c4ac7..bdacd1ad3 100644 --- a/lib/query-plan-executor/Cargo.toml +++ b/lib/query-plan-executor/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] async-trait = "0.1" serde = "1.0.219" -serde_json = { version = "1.0.140", features = ["preserve_order"] } +serde_json = "1.0.140" futures = "0.3" reqwest = { version = "0.12", features = ["json"] } query-planner = { path = "../query-planner" } diff --git a/lib/query-plan-executor/benches/executor_benches.rs b/lib/query-plan-executor/benches/executor_benches.rs index ec3294991..a07e8bb29 100644 --- a/lib/query-plan-executor/benches/executor_benches.rs +++ b/lib/query-plan-executor/benches/executor_benches.rs @@ -57,6 +57,7 @@ fn query_plan_executor_pipeline_via_http(c: &mut Criterion) { schema_metadata, operation, has_introspection, + false, ) .await; black_box(result) @@ -148,6 +149,7 @@ fn query_plan_executor_pipeline_locally(c: &mut Criterion) { schema_metadata, operation, has_introspection, + false, ) .await; black_box(result) @@ -227,16 +229,19 @@ fn project_data_by_operation(c: &mut Criterion) { let data = black_box(&mut data); let mut errors = vec![]; let errors = black_box(&mut errors); + let extensions = HashMap::new(); + let extensions = black_box(&extensions); let operation = black_box(&operation); let schema_metadata = black_box(&schema_metadata); - query_plan_executor::project_data_by_operation( + let result = query_plan_executor::projection::project_by_operation( data, errors, + extensions, operation, schema_metadata, &None, ); - black_box(()); + black_box(result); }); }); } diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index b72d54a4e..d370e653d 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -1,28 +1,23 @@ use async_trait::async_trait; use futures::future::BoxFuture; use query_planner::{ - ast::{ - operation::OperationDefinition, selection_item::SelectionItem, selection_set::SelectionSet, - }, + ast::{operation::OperationDefinition, selection_item::SelectionItem}, planner::plan_nodes::{ ConditionNode, FetchNode, FetchNodePathSegment, FetchRewrite, FlattenNode, FlattenNodePath, FlattenNodePathSegment, KeyRenamer, ParallelNode, PlanNode, QueryPlan, SequenceNode, ValueSetter, }, - state::supergraph_state::OperationKind, }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::HashMap; use tracing::{instrument, trace, warn}; // For reading file in main -use crate::{ - deep_merge::deep_merge_objects, executors::map::SubgraphExecutorMap, - schema_metadata::SchemaMetadata, -}; +use crate::{executors::map::SubgraphExecutorMap, schema_metadata::SchemaMetadata}; pub mod deep_merge; pub mod executors; pub mod introspection; +pub mod projection; pub mod schema_metadata; pub mod validation; mod value_from_ast; @@ -1122,271 +1117,6 @@ fn contains_typename(value: &Value, type_name: &str, default_for_missing: bool) .map_or(default_for_missing, |s| s == type_name) } -// --- Helper Functions --- - -// --- Main Function (for testing) --- - -#[instrument( - level = "trace", - skip_all, - fields( - type_name = %type_name, - selection_set = ?selection_set.items.iter().map(|s| s.to_string()).collect::>(), - obj = ?obj - ) -)] -fn project_selection_set_with_map( - obj: &mut Map, - errors: &mut Vec, - selection_set: &SelectionSet, - type_name: &str, - schema_metadata: &SchemaMetadata, - variable_values: &Option>, -) -> Option> { - let type_name = match obj.get(TYPENAME_FIELD) { - Some(Value::String(type_name)) => type_name, - _ => type_name, - } - .to_string(); - let field_map = schema_metadata.type_fields.get(&type_name)?; - let mut new_obj = Map::new(); - for selection in &selection_set.items { - match selection { - SelectionItem::Field(field) => { - // Get the type fields for the current type - // Type is not found in the schema - if let Some(ref skip_variable) = field.skip_if { - let variable_value = variable_values - .as_ref() - .and_then(|vars| vars.get(skip_variable)); - if variable_value == Some(&Value::Bool(true)) { - continue; // Skip this field if the variable is true - } - } - if let Some(ref include_variable) = field.include_if { - let variable_value = variable_values - .as_ref() - .and_then(|vars| vars.get(include_variable)); - if variable_value != Some(&Value::Bool(true)) { - continue; // Skip this field if the variable is not true - } - } - let response_key = field.alias.as_ref().unwrap_or(&field.name).to_string(); - if field.name == TYPENAME_FIELD { - new_obj.insert(response_key, Value::String(type_name.to_string())); - continue; - } - let field_type = field_map.get(&field.name); - if field.name == "__schema" && type_name == "Query" { - obj.insert( - response_key.to_string(), - schema_metadata.introspection_schema_root_json.clone(), - ); - } - let field_val = obj.get_mut(&response_key); - match (field_type, field_val) { - (Some(field_type), Some(field_val)) => { - match field_val { - Value::Object(field_val_map) => { - let new_field_val_map = project_selection_set_with_map( - field_val_map, - errors, - &field.selections, - field_type, - schema_metadata, - variable_values, - ); - match new_field_val_map { - Some(new_field_val_map) => { - // If the field is an object, merge the projected values - new_obj - .insert(response_key, Value::Object(new_field_val_map)); - } - None => { - new_obj.insert(response_key, Value::Null); - } - } - } - field_val => { - project_selection_set( - field_val, - errors, - &field.selections, - field_type, - schema_metadata, - variable_values, - ); - new_obj.insert( - response_key, - field_val.clone(), // Clone the value to insert - ); - } - } - } - (Some(_field_type), None) => { - // If the field is not found in the object, set it to Null - new_obj.insert(response_key, Value::Null); - } - (None, _) => { - // It won't reach here already, as the selection should be validated before projection - warn!( - "Field {} not found in type {}. Skipping projection.", - field.name, type_name - ); - } - } - } - SelectionItem::InlineFragment(inline_fragment) => { - if entity_satisfies_type_condition( - &schema_metadata.possible_types, - &type_name, - &inline_fragment.type_condition, - ) { - if let Some(ref skip_variable) = inline_fragment.skip_if { - let variable_value = variable_values - .as_ref() - .and_then(|vars| vars.get(skip_variable)); - if variable_value == Some(&Value::Bool(true)) { - continue; // Skip this selection set if the variable is not true - } - } - if let Some(ref include_variable) = inline_fragment.include_if { - let variable_value = variable_values - .as_ref() - .and_then(|vars| vars.get(include_variable)); - if variable_value != Some(&Value::Bool(true)) { - continue; // Skip this selection set if the variable is not true - } - } - - let sub_new_obj = project_selection_set_with_map( - obj, - errors, - &inline_fragment.selections, - &type_name, - schema_metadata, - variable_values, - ); - if let Some(sub_new_obj) = sub_new_obj { - // If the inline fragment projection returns a new object, merge it - deep_merge_objects(&mut new_obj, sub_new_obj) - } else { - // If the inline fragment projection returns None, skip it - continue; - } - } - } - SelectionItem::FragmentSpread(_name_ref) => { - // We only minify the queries to subgraphs, so we never have fragment spreads here. - // In this projection, we expect only inline fragments and fields - // as it's the query produced by the ast normalization process. - unreachable!("Fragment spreads should not exist in the final response projection."); - } - } - } - Some(new_obj) -} - -#[instrument( - level = "trace", - skip_all, - fields( - type_name = %type_name, - selection_set = ?selection_set.items.iter().map(|s| s.to_string()).collect::>(), - data = ?data - ) -)] -fn project_selection_set( - data: &mut Value, - errors: &mut Vec, - selection_set: &SelectionSet, - type_name: &str, - schema_metadata: &SchemaMetadata, - variable_values: &Option>, -) { - match data { - Value::Null => { - // If data is Null, no need to project further - } - Value::String(value) => { - if let Some(enum_values) = schema_metadata.enum_values.get(type_name) { - if !enum_values.contains(value) { - // If the value is not a valid enum value, add an error - // and set data to Null - *data = Value::Null; // Set data to Null if the value is not valid - errors.push(GraphQLError { - message: format!( - "Value is not a valid enum value for type '{}'", - type_name - ), - locations: None, - path: None, - extensions: None, - }); - } - } // No further processing needed for strings - } - Value::Array(arr) => { - // If data is an array, project each item in the array - for item in arr { - project_selection_set( - item, - errors, - selection_set, - type_name, - schema_metadata, - variable_values, - ); - } // No further processing needed for arrays - } - Value::Object(obj) => { - match project_selection_set_with_map( - obj, - errors, - selection_set, - type_name, - schema_metadata, - variable_values, - ) { - Some(new_obj) => { - // If the projection returns a new object, replace the old one - *obj = new_obj; - } - None => { - // If the projection returns None, set data to Null - *data = Value::Null; - } - } - } - _ => {} - } -} - -#[instrument(level = "trace", skip_all)] -pub fn project_data_by_operation( - data: &mut Value, - errors: &mut Vec, - operation: &OperationDefinition, - schema_metadata: &SchemaMetadata, - variable_values: &Option>, -) { - let root_type_name = match operation.operation_kind { - Some(OperationKind::Query) => "Query", - Some(OperationKind::Mutation) => "Mutation", - Some(OperationKind::Subscription) => "Subscription", - None => "Query", - }; - // Project the data based on the selection set - project_selection_set( - data, - errors, - &operation.selection_set, - root_type_name, - schema_metadata, - variable_values, - ) -} - #[instrument( level = "trace", skip_all, @@ -1403,7 +1133,8 @@ pub async fn execute_query_plan( schema_metadata: &SchemaMetadata, operation: &OperationDefinition, has_introspection: bool, -) -> ExecutionResult { + expose_query_plan: bool, +) -> String { let mut result_data = Value::Null; // Initialize data as Null let mut result_errors = vec![]; // Initial errors are empty #[allow(unused_mut)] @@ -1420,36 +1151,23 @@ pub async fn execute_query_plan( .await; result_errors = execution_context.errors; // Get the final errors from the execution context result_extensions = execution_context.extensions; // Get the final extensions from the execution context - if !result_data.is_null() || has_introspection { - if result_data.is_null() { - result_data = Value::Object(serde_json::Map::new()); // Initialize as empty object if Null - } - project_data_by_operation( - &mut result_data, - &mut result_errors, - operation, - schema_metadata, - variable_values, - ); + if result_data.is_null() && has_introspection { + result_data = Value::Object(Map::new()); // Ensure data is an empty object if it was null } - - ExecutionResult { - data: if result_data.is_null() { - None - } else { - Some(result_data) - }, - errors: if result_errors.is_empty() { - None - } else { - Some(result_errors) - }, - extensions: if result_extensions.is_empty() { - None - } else { - Some(result_extensions) - }, + if expose_query_plan { + result_extensions.insert( + "queryPlan".to_string(), + serde_json::to_value(query_plan).unwrap(), + ); } + projection::project_by_operation( + &mut result_data, + &mut result_errors, + &result_extensions, + operation, + schema_metadata, + variable_values, + ) } #[cfg(test)] diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs new file mode 100644 index 000000000..57474a6b4 --- /dev/null +++ b/lib/query-plan-executor/src/projection.rs @@ -0,0 +1,235 @@ +use std::collections::HashMap; + +use query_planner::{ + ast::{ + operation::OperationDefinition, selection_item::SelectionItem, selection_set::SelectionSet, + }, + state::supergraph_state::OperationKind, +}; +use serde_json::{Map, Value}; +use tracing::{instrument, warn}; + +use crate::{ + entity_satisfies_type_condition, schema_metadata::SchemaMetadata, GraphQLError, TYPENAME_FIELD, +}; + +#[instrument(level = "trace", skip_all)] +pub fn project_by_operation( + data: &mut Value, + errors: &mut Vec, + extensions: &HashMap, + operation: &OperationDefinition, + schema_metadata: &SchemaMetadata, + variable_values: &Option>, +) -> String { + let root_type_name = match operation.operation_kind { + Some(OperationKind::Query) => "Query", + Some(OperationKind::Mutation) => "Mutation", + Some(OperationKind::Subscription) => "Subscription", + None => "Query", + }; + // Project the data based on the selection set + let data = project_selection_set( + data, + errors, + &operation.selection_set, + root_type_name, + schema_metadata, + variable_values, + ); + let mut items = "{\"data\":".to_string() + &data; + if !errors.is_empty() { + let errors_entry = ",\"errors\":".to_string() + &serde_json::to_string(&errors).unwrap(); + items.push_str(&errors_entry); + } + if !extensions.is_empty() { + let extensions_entry = + ",\"extensions\":".to_string() + &serde_json::to_string(&extensions).unwrap(); + items.push_str(&extensions_entry); + } + + items.push('}'); + + items +} + +#[instrument( + level = "trace", + skip_all, + fields( + type_name = %type_name, + selection_set = ?selection_set.items.iter().map(|s| s.to_string()).collect::>(), + data = ?data + ) +)] +fn project_selection_set( + data: &mut Value, + errors: &mut Vec, + selection_set: &SelectionSet, + type_name: &str, + schema_metadata: &SchemaMetadata, + variable_values: &Option>, +) -> String { + match data { + Value::Null => "null".to_string(), + Value::Bool(true) => "true".to_string(), + Value::Bool(false) => "false".to_string(), + Value::Number(num) => num.to_string(), + Value::String(value) => { + if let Some(enum_values) = schema_metadata.enum_values.get(type_name) { + if !enum_values.contains(value) { + *data = Value::Null; + errors.push(GraphQLError { + message: format!( + "Value is not a valid enum value for type '{}'", + type_name + ), + locations: None, + path: None, + extensions: None, + }); + return "null".to_string(); // Set data to Null if the value is not valid + } + } + "\"".to_string() + value + "\"" // Return the string value wrapped in quotes + } + Value::Array(arr) => { + let items = arr + .iter_mut() + .map(|item| { + project_selection_set( + item, + errors, + selection_set, + type_name, + schema_metadata, + variable_values, + ) + }) + .collect::>(); + "[".to_string() + &items.join(",") + "]" + } + Value::Object(obj) => { + let items = project_selection_set_with_map( + obj, + errors, + selection_set, + type_name, + schema_metadata, + variable_values, + ); + "{".to_string() + &items.join(",") + "}" + } + } +} + +#[instrument( + level = "trace", + skip_all, + fields( + type_name = %type_name, + selection_set = ?selection_set.items.iter().map(|s| s.to_string()).collect::>(), + obj = ?obj + ) +)] +fn project_selection_set_with_map( + obj: &mut Map, + errors: &mut Vec, + selection_set: &SelectionSet, + type_name: &str, + schema_metadata: &SchemaMetadata, + variable_values: &Option>, +) -> Vec { + let type_name = match obj.get(TYPENAME_FIELD) { + Some(Value::String(type_name)) => type_name, + _ => type_name, + } + .to_string(); + let field_map = schema_metadata.type_fields.get(&type_name); + let mut items = vec![]; + for selection in &selection_set.items { + match selection { + SelectionItem::Field(field) => { + // Get the type fields for the current type + // Type is not found in the schema + if field_map.is_none() { + // It won't reach here already, as the selection should be validated before projection + warn!("Type {} not found. Skipping projection.", type_name); + continue; + } + if let Some(ref skip_variable) = field.skip_if { + let variable_value = variable_values + .as_ref() + .and_then(|vars| vars.get(skip_variable)); + if variable_value == Some(&Value::Bool(true)) { + continue; // Skip this field if the variable is true + } + } + if let Some(ref include_variable) = field.include_if { + let variable_value = variable_values + .as_ref() + .and_then(|vars| vars.get(include_variable)); + if variable_value != Some(&Value::Bool(true)) { + continue; // Skip this field if the variable is not true + } + } + let response_key = field.alias.as_ref().unwrap_or(&field.name).to_string(); + if field.name == TYPENAME_FIELD { + items.push("\"".to_string() + &response_key + "\":\"" + &type_name + "\""); + continue; + } + let field_map = field_map.unwrap(); + let field_type = field_map.get(&field.name); + if field.name == "__schema" && type_name == "Query" { + obj.insert( + response_key.to_string(), + schema_metadata.introspection_schema_root_json.clone(), + ); + } + let field_val = obj.get_mut(&response_key); + match (field_type, field_val) { + (Some(field_type), Some(field_val)) => { + let projected = project_selection_set( + field_val, + errors, + &field.selections, + field_type, + schema_metadata, + variable_values, + ); + items.push("\"".to_string() + &response_key + "\":" + &projected); + } + (Some(_field_type), None) => { + // If the field is not found in the object, set it to Null + items.push("\"".to_string() + &response_key + "\":null"); + } + (None, _) => { + // It won't reach here already, as the selection should be validated before projection + warn!( + "Field {} not found in type {}. Skipping projection.", + field.name, type_name + ); + } + } + } + SelectionItem::InlineFragment(inline_fragment) => { + if entity_satisfies_type_condition( + &schema_metadata.possible_types, + &type_name, + &inline_fragment.type_condition, + ) { + let projected = project_selection_set_with_map( + obj, + errors, + &inline_fragment.selections, + &type_name, + schema_metadata, + variable_values, + ); + items.extend(projected); + } + } + } + } + items +} diff --git a/lib/query-plan-executor/src/tests/mod.rs b/lib/query-plan-executor/src/tests/mod.rs index 86ca64bd0..a07feef1f 100644 --- a/lib/query-plan-executor/src/tests/mod.rs +++ b/lib/query-plan-executor/src/tests/mod.rs @@ -46,11 +46,9 @@ fn query_executor_pipeline_locally() { &schema_metadata, normalized_operation, false, + false, ) .await; - insta::assert_snapshot!(format!( - "{}", - serde_json::to_string_pretty(&result).unwrap_or_default() - )); + insta::assert_snapshot!(result); }); } diff --git a/lib/query-plan-executor/src/tests/snapshots/query_plan_executor__tests__query_executor_pipeline_locally.snap b/lib/query-plan-executor/src/tests/snapshots/query_plan_executor__tests__query_executor_pipeline_locally.snap index f36cb38a0..b81cfdd0a 100644 --- a/lib/query-plan-executor/src/tests/snapshots/query_plan_executor__tests__query_executor_pipeline_locally.snap +++ b/lib/query-plan-executor/src/tests/snapshots/query_plan_executor__tests__query_executor_pipeline_locally.snap @@ -1,2336 +1,5 @@ --- source: lib/query-plan-executor/src/tests/mod.rs -expression: "format!(\"{}\", serde_json::to_string_pretty(&result).unwrap_or_default())" +expression: result --- -{ - "data": { - "users": [ - { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - } - ] - }, - { - "id": "2", - "username": "dotansimha", - "name": "Dotan Simha", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - } - ] - }, - { - "id": "3", - "username": "kamilkisiela", - "name": "Kamil Kisiela", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - } - ] - }, - { - "id": "4", - "username": "ardatan", - "name": "Arda Tanrikulu", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - } - ] - }, - { - "id": "5", - "username": "gilgardosh", - "name": "Gil Gardosh", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - } - ] - }, - { - "id": "6", - "username": "laurin", - "name": "Laurin Quast", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - } - } - ] - } - ], - "topProducts": [ - { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100, - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "3", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "4", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - }, - { - "inStock": false, - "name": "Couch", - "price": 1299, - "shippingEstimate": null, - "upc": "2", - "weight": 1000, - "reviews": [ - { - "id": "5", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "6", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "7", - "body": "sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "8", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - }, - { - "inStock": false, - "name": "Glass", - "price": 15, - "shippingEstimate": null, - "upc": "3", - "weight": 20, - "reviews": [ - { - "id": "9", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - }, - { - "inStock": false, - "name": "Chair", - "price": 499, - "shippingEstimate": null, - "upc": "4", - "weight": 100, - "reviews": [ - { - "id": "10", - "body": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - }, - { - "id": "11", - "body": "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.", - "author": { - "id": "1", - "username": "urigo", - "name": "Uri Goldshtein", - "reviews": [ - { - "id": "1", - "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - }, - { - "id": "2", - "body": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi", - "product": { - "inStock": true, - "name": "Table", - "price": 899, - "shippingEstimate": null, - "upc": "1", - "weight": 100 - } - } - ] - } - } - ] - }, - { - "inStock": true, - "name": "TV", - "price": 1299, - "shippingEstimate": null, - "upc": "5", - "weight": 1000, - "reviews": [] - } - ] - } -} +{"data":{"users":[{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}}]},{"id":"2","username":"dotansimha","name":"Dotan Simha","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}}]},{"id":"3","username":"kamilkisiela","name":"Kamil Kisiela","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}}]},{"id":"4","username":"ardatan","name":"Arda Tanrikulu","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}}]},{"id":"5","username":"gilgardosh","name":"Gil Gardosh","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}}]},{"id":"6","username":"laurin","name":"Laurin Quast","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]}}]}],"topProducts":[{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100,"reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"3","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"4","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]},{"inStock":false,"name":"Couch","price":1299,"shippingEstimate":null,"upc":"2","weight":1000,"reviews":[{"id":"5","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"6","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"7","body":"sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"8","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]},{"inStock":false,"name":"Glass","price":15,"shippingEstimate":null,"upc":"3","weight":20,"reviews":[{"id":"9","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]},{"inStock":false,"name":"Chair","price":499,"shippingEstimate":null,"upc":"4","weight":100,"reviews":[{"id":"10","body":"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}},{"id":"11","body":"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.","author":{"id":"1","username":"urigo","name":"Uri Goldshtein","reviews":[{"id":"1","body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}},{"id":"2","body":"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugi","product":{"inStock":true,"name":"Table","price":899,"shippingEstimate":null,"upc":"1","weight":100}}]}}]},{"inStock":true,"name":"TV","price":1299,"shippingEstimate":null,"upc":"5","weight":1000,"reviews":[]}]}} diff --git a/lib/query-planner/Cargo.toml b/lib/query-planner/Cargo.toml index f8a74500b..115567ae9 100644 --- a/lib/query-planner/Cargo.toml +++ b/lib/query-planner/Cargo.toml @@ -11,7 +11,7 @@ graphql-tools = "0.4.0" graphql-parser = "0.4.1" # Serialization serde = "1.0.219" -serde_json = { version = "1.0.140", features = ["preserve_order"] } +serde_json = "1.0.140" # Tracing tracing = { version = "0.1.41" } # Data Structures @@ -22,7 +22,6 @@ lazy-init = "0.5.1" thiserror = "2.0.12" rustc-hash = "2.1.1" - [dev-dependencies] # Testing insta = { version = "1.42.1" } From 3e82aa4140f06d2b86635d79e16513b71903ee5a Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Thu, 26 Jun 2025 17:40:14 +0300 Subject: [PATCH 02/35] Representations --- .../src/pipeline/graphql_request_params.rs | 1 + .../src/executors/async_graphql.rs | 9 + lib/query-plan-executor/src/executors/http.rs | 19 +- lib/query-plan-executor/src/lib.rs | 179 ++++++++---------- 4 files changed, 109 insertions(+), 99 deletions(-) diff --git a/bin/gateway/src/pipeline/graphql_request_params.rs b/bin/gateway/src/pipeline/graphql_request_params.rs index 37009cd24..59bd696c1 100644 --- a/bin/gateway/src/pipeline/graphql_request_params.rs +++ b/bin/gateway/src/pipeline/graphql_request_params.rs @@ -58,6 +58,7 @@ impl TryInto for GETQueryParams { operation_name: self.operation_name, variables, extensions, + representations: None, }; Ok(execution_request) diff --git a/lib/query-plan-executor/src/executors/async_graphql.rs b/lib/query-plan-executor/src/executors/async_graphql.rs index 07f3aab18..c66ce4bbf 100644 --- a/lib/query-plan-executor/src/executors/async_graphql.rs +++ b/lib/query-plan-executor/src/executors/async_graphql.rs @@ -25,6 +25,15 @@ impl From for async_graphql::Request { if let Some(variables) = exec_request.variables { req = req.variables(async_graphql::Variables::from_json(json!(variables))); } + if let Some(representations) = exec_request.representations { + req.variables.insert( + async_graphql::Name::new("representations"), + async_graphql::Value::from_json( + serde_json::from_str(&representations).unwrap_or_default(), + ) + .unwrap(), + ); + } if let Some(operation_name) = exec_request.operation_name { req = req.operation_name(operation_name); } diff --git a/lib/query-plan-executor/src/executors/http.rs b/lib/query-plan-executor/src/executors/http.rs index 2d5923e12..af65e9578 100644 --- a/lib/query-plan-executor/src/executors/http.rs +++ b/lib/query-plan-executor/src/executors/http.rs @@ -22,9 +22,26 @@ impl HTTPSubgraphExecutor { execution_request: ExecutionRequest, ) -> Result { trace!("Executing HTTP request to subgraph at {}", self.endpoint); + let mut body = + "{\"query\":\"".to_string() + &execution_request.query + "\",\"variables\":{"; + if let Some(variables) = &execution_request.variables { + for (key, value) in variables { + body.push_str( + &("\"".to_string() + + key + + "\": " + + &serde_json::to_string(value).unwrap() + + ","), + ); + } + } + if let Some(representations) = &execution_request.representations { + body.push_str(&("\"representations\":".to_string() + representations)); + } + body.push_str("}}"); self.http_client .post(&self.endpoint) - .json(&execution_request) + .body(body) .send() .await? .json::() diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index d370e653d..cc6df9597 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -146,7 +146,7 @@ trait ExecutableFetchNode { async fn execute_for_projected_representations( &self, execution_context: &QueryPlanExecutionContext<'_>, - filtered_representations: Vec, + filtered_representations: String, filtered_repr_indexes: Vec, ) -> ExecuteForRepresentationsResult; fn apply_output_rewrites( @@ -175,7 +175,7 @@ impl ExecutablePlanNode for FetchNode { } struct ProjectRepresentationsResult { - representations: Vec, + representations: String, indexes: Vec, } @@ -202,6 +202,7 @@ impl ExecutableFetchNode for FetchNode { operation_name: self.operation_name.clone(), variables, extensions: None, + representations: None, }; let mut fetch_result = execution_context .subgraph_executor_map @@ -224,30 +225,30 @@ impl ExecutableFetchNode for FetchNode { ) -> ProjectRepresentationsResult { let mut filtered_repr_indexes = Vec::new(); // 1. Filter representations based on requires (if present) - let mut filtered_representations: Vec = Vec::new(); + let mut filtered_representations = vec![]; let requires_nodes = self.requires.as_ref().unwrap(); for (index, entity) in representations.iter().enumerate() { let entity_projected = execution_context.project_requires(&requires_nodes.items, entity); - if !entity_projected.is_null() { + if entity_projected != "null" { filtered_representations.push(entity_projected); filtered_repr_indexes.push(index); } } - if let Some(input_rewrites) = &self.input_rewrites { - for representation in filtered_representations.iter_mut() { - for rewrite in input_rewrites { - rewrite.apply( - &execution_context.schema_metadata.possible_types, - representation, - ); - } - } - } + // if let Some(input_rewrites) = &self.input_rewrites { + // for representation in filtered_representations.iter_mut() { + // for rewrite in input_rewrites { + // rewrite.apply( + // &execution_context.schema_metadata.possible_types, + // representation, + // ); + // } + // } + // } ProjectRepresentationsResult { - representations: filtered_representations, + representations: "[".to_string() + &filtered_representations.join(",") + "]", indexes: filtered_repr_indexes, } } @@ -263,23 +264,16 @@ impl ExecutableFetchNode for FetchNode { async fn execute_for_projected_representations( &self, execution_context: &QueryPlanExecutionContext<'_>, - filtered_representations: Vec, + filtered_representations: String, filtered_repr_indexes: Vec, ) -> ExecuteForRepresentationsResult { // 2. Prepare variables for fetch - let mut variables = self - .prepare_variables_for_fetch_node(execution_context.variable_values) - .unwrap_or_default(); - variables.insert( - "representations".to_string(), - Value::Array(filtered_representations), - ); - let execution_request = ExecutionRequest { query: self.operation.document_str.clone(), operation_name: self.operation_name.clone(), - variables: Some(variables), + variables: self.prepare_variables_for_fetch_node(execution_context.variable_values), extensions: None, + representations: Some(filtered_representations), }; // 3. Execute the fetch operation @@ -891,16 +885,13 @@ pub struct GraphQLErrorLocation { pub column: usize, } -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] +#[derive(Deserialize, Debug, Clone)] pub struct ExecutionRequest { pub query: String, - #[serde(skip_serializing_if = "Option::is_none")] pub operation_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub variables: Option>, - #[serde(skip_serializing_if = "Option::is_none")] pub extensions: Option>, + pub representations: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -929,84 +920,76 @@ pub struct QueryPlanExecutionContext<'a> { } impl QueryPlanExecutionContext<'_> { - #[instrument( - level = "trace", - skip_all, - fields( - requires_selections = ?requires_selections.iter().map(|s| s.to_string()).collect::>(), - entity = ?entity - ) - )] + fn project_requires_map( + &self, + requires_selections: &Vec, + entity_obj: &Map, + ) -> Vec { + let mut items = vec![]; + for requires_selection in requires_selections { + match &requires_selection { + SelectionItem::Field(requires_selection) => { + let field_name = &requires_selection.name; + let response_key = requires_selection.selection_identifier(); + let original = entity_obj + .get(field_name) + .unwrap_or(entity_obj.get(response_key).unwrap_or(&Value::Null)); + let projected_value = + self.project_requires(&requires_selection.selections.items, original); + if projected_value != "null" { + items.push("\"".to_string() + response_key + "\":" + &projected_value) + } + } + SelectionItem::InlineFragment(requires_selection) => { + let type_name = match entity_obj.get(TYPENAME_FIELD) { + Some(Value::String(type_name)) => type_name, + _ => requires_selection.type_condition.as_str(), + }; + if entity_satisfies_type_condition( + &self.schema_metadata.possible_types, + type_name, + &requires_selection.type_condition, + ) { + let projected = self + .project_requires_map(&requires_selection.selections.items, entity_obj); + // Merge the projected value into the result + items.extend(projected); + // If the projected value is not an object, it will be ignored + } + } + } + } + items + } pub fn project_requires( &self, requires_selections: &Vec, entity: &Value, - ) -> Value { + ) -> String { if requires_selections.is_empty() { - return entity.clone(); // No selections to project, return the entity as is + return serde_json::to_string(entity).unwrap(); // No selections to project, return the entity as is } match entity { - Value::Null => Value::Null, - Value::Array(entity_array) => Value::Array( - entity_array - .iter() - .map(|item| self.project_requires(requires_selections, item)) - .collect(), - ), + Value::Null => "null".to_string(), + Value::Array(entity_array) => entity_array + .iter() + .map(|item| self.project_requires(requires_selections, item)) + .collect::>() + .join(",") + .to_string(), Value::Object(entity_obj) => { - let mut result_map = Map::new(); - for requires_selection in requires_selections { - match &requires_selection { - SelectionItem::Field(requires_selection) => { - let field_name = &requires_selection.name; - let response_key = requires_selection.selection_identifier(); - let original = entity_obj - .get(field_name) - .unwrap_or(entity_obj.get(response_key).unwrap_or(&Value::Null)); - let projected_value: Value = self - .project_requires(&requires_selection.selections.items, original); - if !projected_value.is_null() { - result_map.insert(response_key.to_string(), projected_value); - } - } - SelectionItem::InlineFragment(requires_selection) => { - let type_name = match entity_obj.get(TYPENAME_FIELD) { - Some(Value::String(type_name)) => type_name, - _ => requires_selection.type_condition.as_str(), - }; - if entity_satisfies_type_condition( - &self.schema_metadata.possible_types, - type_name, - &requires_selection.type_condition, - ) { - let projected = self - .project_requires(&requires_selection.selections.items, entity); - // Merge the projected value into the result - if let Value::Object(projected_map) = projected { - deep_merge::deep_merge_objects(&mut result_map, projected_map); - } - // If the projected value is not an object, it will be ignored - } - } - SelectionItem::FragmentSpread(_name_ref) => { - // We only minify the queries to subgraphs, so we never have fragment spreads here - unreachable!( - "Fragment spreads should not exist in FetchNode::requires." - ); - } - } - } - if (result_map.is_empty()) - || (result_map.len() == 1 && result_map.contains_key(TYPENAME_FIELD)) - { - Value::Null - } else { - Value::Object(result_map) + let items = self.project_requires_map(requires_selections, entity_obj); + if items.is_empty() { + return "null".to_string(); // No items to project, return null } + // Join the items into a JSON object string + let projected_string = items.join(","); + "{".to_string() + &projected_string + "}" } - Value::Bool(bool) => Value::Bool(*bool), - Value::Number(num) => Value::Number(num.to_owned()), - Value::String(string) => Value::String(string.to_string()), + Value::Bool(false) => "false".to_string(), + Value::Bool(true) => "true".to_string(), + Value::Number(num) => num.to_string(), + Value::String(string) => "\"".to_string() + string + "\"", } } } From 572649b660764fcaec99b887578ace73901c6be9 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Thu, 26 Jun 2025 19:30:24 +0300 Subject: [PATCH 03/35] Escape strings --- .../benches/executor_benches.rs | 11 ++++---- lib/query-plan-executor/src/lib.rs | 28 +++++++++++++------ lib/query-plan-executor/src/projection.rs | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/query-plan-executor/benches/executor_benches.rs b/lib/query-plan-executor/benches/executor_benches.rs index a07e8bb29..26741e093 100644 --- a/lib/query-plan-executor/benches/executor_benches.rs +++ b/lib/query-plan-executor/benches/executor_benches.rs @@ -475,16 +475,17 @@ fn deep_merge_with_simple(c: &mut Criterion) { } fn all_benchmarks(c: &mut Criterion) { - deep_merge_with_simple(c); - deep_merge_with_complex(c); - project_requires(c); - traverse_and_collect(c); - project_data_by_operation(c); query_plan_executor_without_projection_locally(c); query_plan_executor_pipeline_locally(c); query_plan_execution_without_projection_via_http(c); query_plan_executor_pipeline_via_http(c); + + deep_merge_with_simple(c); + deep_merge_with_complex(c); + project_requires(c); + traverse_and_collect(c); + project_data_by_operation(c); } criterion_group!(benches, all_benchmarks); diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index cc6df9597..7ec013ae4 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -936,7 +936,7 @@ impl QueryPlanExecutionContext<'_> { .unwrap_or(entity_obj.get(response_key).unwrap_or(&Value::Null)); let projected_value = self.project_requires(&requires_selection.selections.items, original); - if projected_value != "null" { + if projected_value != "null" && !projected_value.is_empty() { items.push("\"".to_string() + response_key + "\":" + &projected_value) } } @@ -953,7 +953,9 @@ impl QueryPlanExecutionContext<'_> { let projected = self .project_requires_map(&requires_selection.selections.items, entity_obj); // Merge the projected value into the result - items.extend(projected); + if !projected.is_empty() { + items.extend(projected); + } // If the projected value is not an object, it will be ignored } } @@ -971,17 +973,25 @@ impl QueryPlanExecutionContext<'_> { } match entity { Value::Null => "null".to_string(), - Value::Array(entity_array) => entity_array - .iter() - .map(|item| self.project_requires(requires_selections, item)) - .collect::>() - .join(",") - .to_string(), + Value::Array(entity_array) => { + let mut items = Vec::with_capacity(entity_array.len() + 2); + items.push("[".to_string()); + for entity_item in entity_array { + let projected_value = self.project_requires(requires_selections, entity_item); + if projected_value != "null" && !projected_value.is_empty() { + items.push(projected_value); + } + } + items.push("]".to_string()); + items.join(",") + } Value::Object(entity_obj) => { let items = self.project_requires_map(requires_selections, entity_obj); + if items.is_empty() { return "null".to_string(); // No items to project, return null } + // Join the items into a JSON object string let projected_string = items.join(","); "{".to_string() + &projected_string + "}" @@ -989,7 +999,7 @@ impl QueryPlanExecutionContext<'_> { Value::Bool(false) => "false".to_string(), Value::Bool(true) => "true".to_string(), Value::Number(num) => num.to_string(), - Value::String(string) => "\"".to_string() + string + "\"", + Value::String(string) => serde_json::to_string(string).unwrap(), } } } diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index 57474a6b4..6f5f7cd60 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -91,7 +91,7 @@ fn project_selection_set( return "null".to_string(); // Set data to Null if the value is not valid } } - "\"".to_string() + value + "\"" // Return the string value wrapped in quotes + serde_json::to_string(value).unwrap() // Return the string value wrapped in quotes } Value::Array(arr) => { let items = arr From 08e70ad8cf10301cf9d3e5248daaca8d862f5e89 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Thu, 26 Jun 2025 19:45:11 +0300 Subject: [PATCH 04/35] FF --- lib/query-plan-executor/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 7ec013ae4..e7730abb0 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -886,6 +886,7 @@ pub struct GraphQLErrorLocation { } #[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct ExecutionRequest { pub query: String, pub operation_name: Option, From e6a2db993ff828481094c456d0da2499444ed76d Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 27 Jun 2025 04:11:47 +0300 Subject: [PATCH 05/35] Fix --- lib/query-plan-executor/src/executors/http.rs | 6 ++++-- lib/query-plan-executor/src/lib.rs | 6 ++---- lib/query-plan-executor/src/projection.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/query-plan-executor/src/executors/http.rs b/lib/query-plan-executor/src/executors/http.rs index af65e9578..33fe1c140 100644 --- a/lib/query-plan-executor/src/executors/http.rs +++ b/lib/query-plan-executor/src/executors/http.rs @@ -22,8 +22,9 @@ impl HTTPSubgraphExecutor { execution_request: ExecutionRequest, ) -> Result { trace!("Executing HTTP request to subgraph at {}", self.endpoint); - let mut body = - "{\"query\":\"".to_string() + &execution_request.query + "\",\"variables\":{"; + let mut body = "{\"query\":\"".to_string() + + &serde_json::to_string(&execution_request.query).unwrap() + + "\",\"variables\":{"; if let Some(variables) = &execution_request.variables { for (key, value) in variables { body.push_str( @@ -42,6 +43,7 @@ impl HTTPSubgraphExecutor { self.http_client .post(&self.endpoint) .body(body) + .header("Content-Type", "application/json; charset=utf-8") .send() .await? .json::() diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index e7730abb0..f7f78336a 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -975,16 +975,14 @@ impl QueryPlanExecutionContext<'_> { match entity { Value::Null => "null".to_string(), Value::Array(entity_array) => { - let mut items = Vec::with_capacity(entity_array.len() + 2); - items.push("[".to_string()); + let mut items = Vec::with_capacity(entity_array.len()); for entity_item in entity_array { let projected_value = self.project_requires(requires_selections, entity_item); if projected_value != "null" && !projected_value.is_empty() { items.push(projected_value); } } - items.push("]".to_string()); - items.join(",") + "[".to_string() + &items.join(",") + "]" } Value::Object(entity_obj) => { let items = self.project_requires_map(requires_selections, entity_obj); diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index 6f5f7cd60..45e3828d4 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -94,7 +94,7 @@ fn project_selection_set( serde_json::to_string(value).unwrap() // Return the string value wrapped in quotes } Value::Array(arr) => { - let items = arr + let items: Vec = arr .iter_mut() .map(|item| { project_selection_set( @@ -106,7 +106,7 @@ fn project_selection_set( variable_values, ) }) - .collect::>(); + .collect(); "[".to_string() + &items.join(",") + "]" } Value::Object(obj) => { From 01fc14acc086a6b9e5d438559f25911a8811efba Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 27 Jun 2025 05:48:20 +0300 Subject: [PATCH 06/35] Go --- lib/query-plan-executor/src/executors/http.rs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/query-plan-executor/src/executors/http.rs b/lib/query-plan-executor/src/executors/http.rs index 33fe1c140..badffca8a 100644 --- a/lib/query-plan-executor/src/executors/http.rs +++ b/lib/query-plan-executor/src/executors/http.rs @@ -22,24 +22,29 @@ impl HTTPSubgraphExecutor { execution_request: ExecutionRequest, ) -> Result { trace!("Executing HTTP request to subgraph at {}", self.endpoint); - let mut body = "{\"query\":\"".to_string() + + let mut body = "{\"query\":".to_string() + &serde_json::to_string(&execution_request.query).unwrap() - + "\",\"variables\":{"; + + ",\"variables\":{"; + let variables_added = false; if let Some(variables) = &execution_request.variables { - for (key, value) in variables { - body.push_str( - &("\"".to_string() - + key - + "\": " - + &serde_json::to_string(value).unwrap() - + ","), - ); - } + let variables_entry = variables + .iter() + .map(|(key, value)| { + "\"".to_string() + key + "\": " + &serde_json::to_string(value).unwrap() + }) + .collect::>() + .join(","); + body.push_str(&variables_entry); } if let Some(representations) = &execution_request.representations { + if variables_added { + body.push(','); + } body.push_str(&("\"representations\":".to_string() + representations)); } body.push_str("}}"); + self.http_client .post(&self.endpoint) .body(body) From 6bec030f690b3586819b8fcf61b6bb5fc19db67e Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 27 Jun 2025 13:32:44 +0300 Subject: [PATCH 07/35] Rebase --- lib/query-plan-executor/src/projection.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index 45e3828d4..0d147549a 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -118,7 +118,10 @@ fn project_selection_set( schema_metadata, variable_values, ); - "{".to_string() + &items.join(",") + "}" + match items { + Some(items) => "{".to_string() + &items.join(",") + "}", + None => "null".to_string(), + } } } } @@ -139,24 +142,17 @@ fn project_selection_set_with_map( type_name: &str, schema_metadata: &SchemaMetadata, variable_values: &Option>, -) -> Vec { +) -> Option> { let type_name = match obj.get(TYPENAME_FIELD) { Some(Value::String(type_name)) => type_name, _ => type_name, } .to_string(); - let field_map = schema_metadata.type_fields.get(&type_name); + let field_map = schema_metadata.type_fields.get(&type_name)?; let mut items = vec![]; for selection in &selection_set.items { match selection { SelectionItem::Field(field) => { - // Get the type fields for the current type - // Type is not found in the schema - if field_map.is_none() { - // It won't reach here already, as the selection should be validated before projection - warn!("Type {} not found. Skipping projection.", type_name); - continue; - } if let Some(ref skip_variable) = field.skip_if { let variable_value = variable_values .as_ref() @@ -178,7 +174,6 @@ fn project_selection_set_with_map( items.push("\"".to_string() + &response_key + "\":\"" + &type_name + "\""); continue; } - let field_map = field_map.unwrap(); let field_type = field_map.get(&field.name); if field.name == "__schema" && type_name == "Query" { obj.insert( @@ -226,10 +221,12 @@ fn project_selection_set_with_map( schema_metadata, variable_values, ); - items.extend(projected); + if let Some(projected) = projected { + items.extend(projected); + } } } } } - items + Some(items) } From 89691bd21ef249100f55b40b09cbfcedf7e48e57 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Fri, 27 Jun 2025 13:12:31 +0200 Subject: [PATCH 08/35] Refactor projection to use a string builder (#209) This avoids allocating a new string for each intermediate result (same for vector). --- lib/query-plan-executor/src/projection.rs | 142 ++++++++++++++-------- 1 file changed, 89 insertions(+), 53 deletions(-) diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index 0d147549a..989bbf401 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::fmt::Write; use query_planner::{ ast::{ @@ -28,33 +29,44 @@ pub fn project_by_operation( Some(OperationKind::Subscription) => "Subscription", None => "Query", }; - // Project the data based on the selection set - let data = project_selection_set( + + // We may want to remove it, but let's see. + let mut buffer = String::with_capacity(4096); + + write!(buffer, "{{\"data\":").unwrap(); + project_selection_set( data, errors, &operation.selection_set, root_type_name, schema_metadata, variable_values, + &mut buffer, ); - let mut items = "{\"data\":".to_string() + &data; + if !errors.is_empty() { - let errors_entry = ",\"errors\":".to_string() + &serde_json::to_string(&errors).unwrap(); - items.push_str(&errors_entry); + write!( + buffer, + ",\"errors\":{}", + serde_json::to_string(&errors).unwrap() + ) + .unwrap(); } if !extensions.is_empty() { - let extensions_entry = - ",\"extensions\":".to_string() + &serde_json::to_string(&extensions).unwrap(); - items.push_str(&extensions_entry); + write!( + buffer, + ",\"extensions\":{}", + serde_json::to_string(&extensions).unwrap() + ) + .unwrap(); } - items.push('}'); - - items + buffer.push('}'); + buffer } #[instrument( - level = "trace", + level = "trace", skip_all, fields( type_name = %type_name, @@ -69,12 +81,13 @@ fn project_selection_set( type_name: &str, schema_metadata: &SchemaMetadata, variable_values: &Option>, -) -> String { + buffer: &mut String, +) { match data { - Value::Null => "null".to_string(), - Value::Bool(true) => "true".to_string(), - Value::Bool(false) => "false".to_string(), - Value::Number(num) => num.to_string(), + Value::Null => buffer.push_str("null"), + Value::Bool(true) => buffer.push_str("true"), + Value::Bool(false) => buffer.push_str("false"), + Value::Number(num) => write!(buffer, "{}", num).unwrap(), Value::String(value) => { if let Some(enum_values) = schema_metadata.enum_values.get(type_name) { if !enum_values.contains(value) { @@ -88,46 +101,53 @@ fn project_selection_set( path: None, extensions: None, }); - return "null".to_string(); // Set data to Null if the value is not valid + buffer.push_str("null"); + return; } } - serde_json::to_string(value).unwrap() // Return the string value wrapped in quotes + // Use serde_json to handle proper string escaping + write!(buffer, "{}", serde_json::to_string(value).unwrap()).unwrap(); } Value::Array(arr) => { - let items: Vec = arr - .iter_mut() - .map(|item| { - project_selection_set( - item, - errors, - selection_set, - type_name, - schema_metadata, - variable_values, - ) - }) - .collect(); - "[".to_string() + &items.join(",") + "]" + buffer.push('['); + let mut first = true; + for item in arr.iter_mut() { + if !first { + buffer.push(','); + } + project_selection_set( + item, + errors, + selection_set, + type_name, + schema_metadata, + variable_values, + buffer, + ); + first = false; + } + buffer.push(']'); } Value::Object(obj) => { - let items = project_selection_set_with_map( + buffer.push('{'); + let mut first = true; + project_selection_set_with_map( obj, errors, selection_set, type_name, schema_metadata, variable_values, + buffer, + &mut first, ); - match items { - Some(items) => "{".to_string() + &items.join(",") + "}", - None => "null".to_string(), - } + buffer.push('}'); } } } #[instrument( - level = "trace", + level = "trace", skip_all, fields( type_name = %type_name, @@ -135,6 +155,8 @@ fn project_selection_set( obj = ?obj ) )] +// TODO: simplfy args +#[allow(clippy::too_many_arguments)] fn project_selection_set_with_map( obj: &mut Map, errors: &mut Vec, @@ -142,14 +164,16 @@ fn project_selection_set_with_map( type_name: &str, schema_metadata: &SchemaMetadata, variable_values: &Option>, -) -> Option> { + buffer: &mut String, + first: &mut bool, +) { let type_name = match obj.get(TYPENAME_FIELD) { Some(Value::String(type_name)) => type_name, _ => type_name, } .to_string(); - let field_map = schema_metadata.type_fields.get(&type_name)?; - let mut items = vec![]; + let field_map = schema_metadata.type_fields.get(&type_name); + for selection in &selection_set.items { match selection { SelectionItem::Field(field) => { @@ -169,34 +193,48 @@ fn project_selection_set_with_map( continue; // Skip this field if the variable is not true } } - let response_key = field.alias.as_ref().unwrap_or(&field.name).to_string(); + + let response_key = field.alias.as_ref().unwrap_or(&field.name); + + if !*first { + buffer.push(','); + } + *first = false; + if field.name == TYPENAME_FIELD { - items.push("\"".to_string() + &response_key + "\":\"" + &type_name + "\""); + write!(buffer, "\"{}\":\"{}\"", response_key, type_name).unwrap(); continue; } + + write!(buffer, "\"{}\":", response_key).unwrap(); + + let field_map = field_map.unwrap(); let field_type = field_map.get(&field.name); + if field.name == "__schema" && type_name == "Query" { obj.insert( response_key.to_string(), schema_metadata.introspection_schema_root_json.clone(), ); } - let field_val = obj.get_mut(&response_key); + + let field_val = obj.get_mut(response_key); + match (field_type, field_val) { (Some(field_type), Some(field_val)) => { - let projected = project_selection_set( + project_selection_set( field_val, errors, &field.selections, field_type, schema_metadata, variable_values, + buffer, ); - items.push("\"".to_string() + &response_key + "\":" + &projected); } (Some(_field_type), None) => { // If the field is not found in the object, set it to Null - items.push("\"".to_string() + &response_key + "\":null"); + buffer.push_str("null"); } (None, _) => { // It won't reach here already, as the selection should be validated before projection @@ -213,20 +251,18 @@ fn project_selection_set_with_map( &type_name, &inline_fragment.type_condition, ) { - let projected = project_selection_set_with_map( + project_selection_set_with_map( obj, errors, &inline_fragment.selections, &type_name, schema_metadata, variable_values, + buffer, + first, ); - if let Some(projected) = projected { - items.extend(projected); - } } } } } - Some(items) } From b81b2cfee4b72c21fc060c7920dfaa2af1c290ec Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Fri, 27 Jun 2025 13:12:39 +0200 Subject: [PATCH 09/35] Refactor traverse_and_collect to use a recursive helper (#208) Single allocation of the vector. No more `flat_map` and `collect` - this way we remove the allocation and copying of intermediate vectors. --- lib/query-plan-executor/src/lib.rs | 72 ++++++++++-------------------- 1 file changed, 23 insertions(+), 49 deletions(-) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index f7f78336a..23b936426 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -1033,8 +1033,6 @@ fn entity_satisfies_type_condition( } } -// --- Helper Function for Flatten --- - /// Recursively traverses the data according to the path segments, /// handling '@' for array iteration, and collects the final values.current_data.to_vec() #[instrument(level = "trace", skip_all, fields( @@ -1045,58 +1043,34 @@ pub fn traverse_and_collect<'a>( current_data: &'a mut Value, remaining_path: &[FlattenNodePathSegment], ) -> Vec<&'a mut Value> { - // If the path is empty, we're done traversing. - let Some((segment, remaining_path)) = remaining_path.split_first() else { - return match current_data { - // If the final result is an array, return all its items. - Value::Array(arr) => arr.iter_mut().collect(), - // Otherwise, return the value itself in a vector. - _ => vec![current_data], - }; - }; + let mut collected = Vec::new(); + traverse_and_collect_mut(current_data, remaining_path, &mut collected); + collected +} - match segment { - FlattenNodePathSegment::Field(field) => { - // Attempt to access a field on an object - match current_data.get_mut(field) { - Some(next_value) => traverse_and_collect(next_value, remaining_path), - // Either the field doesn't exist, or it's is not an object - None => vec![], - } - } +fn traverse_and_collect_mut<'a>( + current_data: &'a mut Value, + remaining_path: &[&str], + collected: &mut Vec<&'a mut Value>, +) { + if remaining_path.is_empty() { + collected.push(current_data); + return; + } - FlattenNodePathSegment::List => { - match current_data { - Value::Array(arr) => arr - .iter_mut() - .flat_map(|item| traverse_and_collect(item, remaining_path)) - .collect(), - // List is only valid for arrays - _ => vec![], - } - } + let key = remaining_path[0]; + let rest_of_path = &remaining_path[1..]; - FlattenNodePathSegment::Cast(type_name) => { - match current_data { - Value::Object(_) => { - // For a single object, a missing `__typename` is a pass-through - if contains_typename(current_data, type_name, true) { - traverse_and_collect(current_data, remaining_path) - } else { - vec![] - } - } - Value::Array(arr) => { - // Filter an array based on matching `__typename` - arr.iter_mut() - .filter(|item| contains_typename(item, type_name, false)) - .flat_map(|item| traverse_and_collect(item, remaining_path)) - .collect() - } - // Cast is only valid for objects and arrays - _ => vec![], + if key == "@" { + if let Value::Array(list) = current_data { + for item in list.iter_mut() { + traverse_and_collect_mut(item, rest_of_path, collected); } } + } else if let Value::Object(map) = current_data { + if let Some(next_data) = map.get_mut(key) { + traverse_and_collect_mut(next_data, rest_of_path, collected); + } } } From 46af0ab37ac3953dd4d5c64d68675dbdbcd5ec6d Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Fri, 27 Jun 2025 13:32:52 +0200 Subject: [PATCH 10/35] Improve `project_requires` method (#210) Follows the same pattern as the response projection. One string buffer we write to. --- lib/query-plan-executor/src/lib.rs | 129 +++++++++++++++++------------ 1 file changed, 76 insertions(+), 53 deletions(-) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 23b936426..6e7a8f4e6 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -10,7 +10,8 @@ use query_planner::{ }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use std::collections::HashMap; +use std::fmt::Write; +use std::{collections::HashMap, vec}; use tracing::{instrument, trace, warn}; // For reading file in main use crate::{executors::map::SubgraphExecutorMap, schema_metadata::SchemaMetadata}; @@ -921,12 +922,62 @@ pub struct QueryPlanExecutionContext<'a> { } impl QueryPlanExecutionContext<'_> { - fn project_requires_map( + pub fn project_requires( + &self, + requires_selections: &Vec, + entity: &Value, + ) -> String { + // Pre-allocate a buffer, but we can do it without I think + let mut buffer = String::with_capacity(1024); + self.project_requires_mut(requires_selections, entity, &mut buffer); + buffer + } + + fn project_requires_mut( + &self, + requires_selections: &Vec, + entity: &Value, + buffer: &mut String, + ) { + if requires_selections.is_empty() { + // No selections, so serialize the entity directly into the buffer. + write!(buffer, "{}", serde_json::to_string(entity).unwrap()).unwrap(); + return; + } + + match entity { + Value::Null => buffer.push_str("null"), + Value::Bool(b) => write!(buffer, "{}", b).unwrap(), + Value::Number(n) => write!(buffer, "{}", n).unwrap(), + Value::String(s) => write!(buffer, "{}", serde_json::to_string(s).unwrap()).unwrap(), + Value::Array(entity_array) => { + buffer.push('['); + let mut first = true; + for entity_item in entity_array { + if !first { + buffer.push(','); + } + self.project_requires_mut(requires_selections, entity_item, buffer); + first = false; + } + buffer.push(']'); + } + Value::Object(entity_obj) => { + buffer.push('{'); + let mut first = true; + self.project_requires_map_mut(requires_selections, entity_obj, buffer, &mut first); + buffer.push('}'); + } + } + } + + fn project_requires_map_mut( &self, requires_selections: &Vec, entity_obj: &Map, - ) -> Vec { - let mut items = vec![]; + buffer: &mut String, + first: &mut bool, + ) { for requires_selection in requires_selections { match &requires_selection { SelectionItem::Field(requires_selection) => { @@ -935,10 +986,21 @@ impl QueryPlanExecutionContext<'_> { let original = entity_obj .get(field_name) .unwrap_or(entity_obj.get(response_key).unwrap_or(&Value::Null)); - let projected_value = - self.project_requires(&requires_selection.selections.items, original); - if projected_value != "null" && !projected_value.is_empty() { - items.push("\"".to_string() + response_key + "\":" + &projected_value) + + // To avoid writing empty fields, we write to a temporary buffer first + let mut temp_buffer = String::new(); + self.project_requires_mut( + &requires_selection.selections.items, + original, + &mut temp_buffer, + ); + + if temp_buffer != "null" && !temp_buffer.is_empty() { + if !*first { + buffer.push(','); + } + write!(buffer, "\"{}\":{}", response_key, temp_buffer).unwrap(); + *first = false; } } SelectionItem::InlineFragment(requires_selection) => { @@ -951,55 +1013,16 @@ impl QueryPlanExecutionContext<'_> { type_name, &requires_selection.type_condition, ) { - let projected = self - .project_requires_map(&requires_selection.selections.items, entity_obj); - // Merge the projected value into the result - if !projected.is_empty() { - items.extend(projected); - } - // If the projected value is not an object, it will be ignored + self.project_requires_map_mut( + &requires_selection.selections.items, + entity_obj, + buffer, + first, + ); } } } } - items - } - pub fn project_requires( - &self, - requires_selections: &Vec, - entity: &Value, - ) -> String { - if requires_selections.is_empty() { - return serde_json::to_string(entity).unwrap(); // No selections to project, return the entity as is - } - match entity { - Value::Null => "null".to_string(), - Value::Array(entity_array) => { - let mut items = Vec::with_capacity(entity_array.len()); - for entity_item in entity_array { - let projected_value = self.project_requires(requires_selections, entity_item); - if projected_value != "null" && !projected_value.is_empty() { - items.push(projected_value); - } - } - "[".to_string() + &items.join(",") + "]" - } - Value::Object(entity_obj) => { - let items = self.project_requires_map(requires_selections, entity_obj); - - if items.is_empty() { - return "null".to_string(); // No items to project, return null - } - - // Join the items into a JSON object string - let projected_string = items.join(","); - "{".to_string() + &projected_string + "}" - } - Value::Bool(false) => "false".to_string(), - Value::Bool(true) => "true".to_string(), - Value::Number(num) => num.to_string(), - Value::String(string) => serde_json::to_string(string).unwrap(), - } } } From 353aa85fe0b8c2e50763cfd509ed996277ff44b7 Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Fri, 27 Jun 2025 14:45:53 +0200 Subject: [PATCH 11/35] Allocation-free JSON string escaping (#212) In short, instead of creating a new string, we go character by character and escape the string and we write to buffer directly --- lib/query-plan-executor/src/json_writer.rs | 80 ++++++++++++++++++++++ lib/query-plan-executor/src/lib.rs | 8 ++- lib/query-plan-executor/src/projection.rs | 6 +- 3 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 lib/query-plan-executor/src/json_writer.rs diff --git a/lib/query-plan-executor/src/json_writer.rs b/lib/query-plan-executor/src/json_writer.rs new file mode 100644 index 000000000..b2e9dcdeb --- /dev/null +++ b/lib/query-plan-executor/src/json_writer.rs @@ -0,0 +1,80 @@ +//! I took it from https://github.com/zotta/json-writer-rs/blob/f45e2f25cede0e06be76a94f6e45608780a835d4/src/lib.rs#L853 + +const fn get_replacements() -> [u8; 256] { + // NOTE: Only characters smaller than 128 are allowed here. + // Trying to escape values above 128 would generate invalid utf-8 output + // ----- + // see https://www.json.org/json-en.html + let mut result = [0u8; 256]; + // Escape everything from 0 to 0x1F + let mut i = 0; + while i < 0x20 { + result[i] = b'u'; + i += 1; + } + result[b'\"' as usize] = b'"'; + result[b'\\' as usize] = b'\\'; + result[b'/' as usize] = b'/'; + result[8] = b'b'; + result[0xc] = b'f'; + result[b'\n' as usize] = b'n'; + result[b'\r' as usize] = b'r'; + result[b'\t' as usize] = b't'; + result[0] = b'u'; + + result +} + +static REPLACEMENTS: [u8; 256] = get_replacements(); +static HEX: [u8; 16] = *b"0123456789ABCDEF"; + +/// Escapes and append part of string +#[inline(always)] +pub fn write_and_escape_string(output_buffer: &mut String, input: &str) { + output_buffer.push('"'); + + // All of the relevant characters are in the ansi range (<128). + // This means we can safely ignore any utf-8 characters and iterate over the bytes directly + let mut num_bytes_written: usize = 0; + let mut index: usize = 0; + let bytes = input.as_bytes(); + while index < bytes.len() { + let cur_byte = bytes[index]; + let replacement = REPLACEMENTS[cur_byte as usize]; + if replacement != 0 { + if num_bytes_written < index { + // Checks can be omitted here: + // We know that index is smaller than the output_buffer length. + // We also know that num_bytes_written is smaller than index + // We also know that the boundaries are not in the middle of an utf-8 multi byte sequence, because those characters are not escaped + output_buffer.push_str(unsafe { input.get_unchecked(num_bytes_written..index) }); + } + if replacement == b'u' { + let bytes: [u8; 6] = [ + b'\\', + b'u', + b'0', + b'0', + HEX[((cur_byte / 16) & 0xF) as usize], + HEX[(cur_byte & 0xF) as usize], + ]; + // Checks can be omitted here: We know bytes is a valid utf-8 string (see above) + output_buffer.push_str(unsafe { std::str::from_utf8_unchecked(&bytes) }); + } else { + let bytes: [u8; 2] = [b'\\', replacement]; + // Checks can be omitted here: We know bytes is a valid utf-8 string, because the replacement table only contains characters smaller than 128 + output_buffer.push_str(unsafe { std::str::from_utf8_unchecked(&bytes) }); + } + num_bytes_written = index + 1; + } + index += 1; + } + if num_bytes_written < bytes.len() { + // Checks can be omitted here: + // We know that num_bytes_written is smaller than index + // We also know that num_bytes_written not in the middle of an utf-8 multi byte sequence, because those are not escaped + output_buffer.push_str(unsafe { input.get_unchecked(num_bytes_written..bytes.len()) }); + } + + output_buffer.push('"'); +} diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 6e7a8f4e6..1051c8962 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -14,10 +14,14 @@ use std::fmt::Write; use std::{collections::HashMap, vec}; use tracing::{instrument, trace, warn}; // For reading file in main -use crate::{executors::map::SubgraphExecutorMap, schema_metadata::SchemaMetadata}; +use crate::{ + executors::map::SubgraphExecutorMap, json_writer::write_and_escape_string, + schema_metadata::SchemaMetadata, +}; pub mod deep_merge; pub mod executors; pub mod introspection; +mod json_writer; pub mod projection; pub mod schema_metadata; pub mod validation; @@ -949,7 +953,7 @@ impl QueryPlanExecutionContext<'_> { Value::Null => buffer.push_str("null"), Value::Bool(b) => write!(buffer, "{}", b).unwrap(), Value::Number(n) => write!(buffer, "{}", n).unwrap(), - Value::String(s) => write!(buffer, "{}", serde_json::to_string(s).unwrap()).unwrap(), + Value::String(s) => write_and_escape_string(buffer, s), Value::Array(entity_array) => { buffer.push('['); let mut first = true; diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index 989bbf401..82fb93110 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -11,7 +11,8 @@ use serde_json::{Map, Value}; use tracing::{instrument, warn}; use crate::{ - entity_satisfies_type_condition, schema_metadata::SchemaMetadata, GraphQLError, TYPENAME_FIELD, + entity_satisfies_type_condition, json_writer::write_and_escape_string, + schema_metadata::SchemaMetadata, GraphQLError, TYPENAME_FIELD, }; #[instrument(level = "trace", skip_all)] @@ -105,8 +106,7 @@ fn project_selection_set( return; } } - // Use serde_json to handle proper string escaping - write!(buffer, "{}", serde_json::to_string(value).unwrap()).unwrap(); + write_and_escape_string(buffer, value); } Value::Array(arr) => { buffer.push('['); From 0b36ca166af6200b138fa7fe73d5f676e803880f Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Fri, 27 Jun 2025 16:36:28 +0200 Subject: [PATCH 12/35] Use HashSet for possible types and inline entity_satisfies_type_condition in two code paths --- bench/k6.js | 9 +++- lib/query-plan-executor/src/lib.rs | 46 +++++++++++-------- lib/query-plan-executor/src/projection.rs | 15 +++--- .../src/schema_metadata.rs | 12 ++--- 4 files changed, 47 insertions(+), 35 deletions(-) diff --git a/bench/k6.js b/bench/k6.js index 14ea958d8..02ac65f99 100644 --- a/bench/k6.js +++ b/bench/k6.js @@ -7,9 +7,14 @@ const endpoint = __ENV.GATEWAY_ENDPOINT || "http://0.0.0.0:4000/graphql"; const vus = __ENV.BENCH_VUS ? parseInt(__ENV.BENCH_VUS) : 50; const time = __ENV.BENCH_OVER_TIME || "30s"; +// Apollo: 642.824925/s +// Hive: 832.384019/s + export const options = { - vus: vus, - duration: time, + // vus: vus, + // duration: time, + vus: 1, + iterations: 10, }; export function setup() { diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 1051c8962..4518d077f 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -10,6 +10,7 @@ use query_planner::{ }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; +use std::collections::HashSet; use std::fmt::Write; use std::{collections::HashMap, vec}; use tracing::{instrument, trace, warn}; // For reading file in main @@ -156,7 +157,7 @@ trait ExecutableFetchNode { ) -> ExecuteForRepresentationsResult; fn apply_output_rewrites( &self, - possible_types: &HashMap>, + possible_types: &HashMap>, data: &mut Value, ); fn prepare_variables_for_fetch_node( @@ -313,7 +314,7 @@ impl ExecutableFetchNode for FetchNode { fn apply_output_rewrites( &self, - possible_types: &HashMap>, + possible_types: &HashMap>, data: &mut Value, ) { if let Some(output_rewrites) = &self.output_rewrites { @@ -355,17 +356,17 @@ impl ExecutableFetchNode for FetchNode { } trait ApplyFetchRewrite { - fn apply(&self, possible_types: &HashMap>, value: &mut Value); + fn apply(&self, possible_types: &HashMap>, value: &mut Value); fn apply_path( &self, - possible_types: &HashMap>, + possible_types: &HashMap>, value: &mut Value, path: &[FetchNodePathSegment], ); } impl ApplyFetchRewrite for FetchRewrite { - fn apply(&self, possible_types: &HashMap>, value: &mut Value) { + fn apply(&self, possible_types: &HashMap>, value: &mut Value) { match self { FetchRewrite::KeyRenamer(renamer) => renamer.apply(possible_types, value), FetchRewrite::ValueSetter(setter) => setter.apply(possible_types, value), @@ -373,7 +374,7 @@ impl ApplyFetchRewrite for FetchRewrite { } fn apply_path( &self, - possible_types: &HashMap>, + possible_types: &HashMap>, value: &mut Value, path: &[FetchNodePathSegment], ) { @@ -385,13 +386,13 @@ impl ApplyFetchRewrite for FetchRewrite { } impl ApplyFetchRewrite for KeyRenamer { - fn apply(&self, possible_types: &HashMap>, value: &mut Value) { + fn apply(&self, possible_types: &HashMap>, value: &mut Value) { self.apply_path(possible_types, value, &self.path) } // Applies key rename operation on a Value (mutably) fn apply_path( &self, - possible_types: &HashMap>, + possible_types: &HashMap>, value: &mut Value, path: &[FetchNodePathSegment], ) { @@ -438,14 +439,14 @@ impl ApplyFetchRewrite for KeyRenamer { } impl ApplyFetchRewrite for ValueSetter { - fn apply(&self, possible_types: &HashMap>, data: &mut Value) { + fn apply(&self, possible_types: &HashMap>, data: &mut Value) { self.apply_path(possible_types, data, &self.path) } // Applies value setting on a Value (returns a new Value) fn apply_path( &self, - possible_types: &HashMap>, + possible_types: &HashMap>, data: &mut Value, path: &[FetchNodePathSegment], ) { @@ -982,6 +983,10 @@ impl QueryPlanExecutionContext<'_> { buffer: &mut String, first: &mut bool, ) { + let type_name = match entity_obj.get(TYPENAME_FIELD) { + Some(Value::String(tn)) => tn.as_str(), + _ => "", // TODO: improve it + }; for requires_selection in requires_selections { match &requires_selection { SelectionItem::Field(requires_selection) => { @@ -1008,15 +1013,16 @@ impl QueryPlanExecutionContext<'_> { } } SelectionItem::InlineFragment(requires_selection) => { - let type_name = match entity_obj.get(TYPENAME_FIELD) { - Some(Value::String(type_name)) => type_name, - _ => requires_selection.type_condition.as_str(), - }; - if entity_satisfies_type_condition( - &self.schema_metadata.possible_types, - type_name, - &requires_selection.type_condition, - ) { + let type_condition = &requires_selection.type_condition; + + let satisfies_type_condition = type_name == type_condition + || self + .schema_metadata + .possible_types + .get(type_condition) + .map_or(false, |s| s.contains(type_name)); + + if satisfies_type_condition { self.project_requires_map_mut( &requires_selection.selections.items, entity_obj, @@ -1040,7 +1046,7 @@ impl QueryPlanExecutionContext<'_> { ) )] fn entity_satisfies_type_condition( - possible_types: &HashMap>, + possible_types: &HashMap>, type_name: &str, type_condition: &str, ) -> bool { diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index 82fb93110..d8118f01e 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -11,8 +11,8 @@ use serde_json::{Map, Value}; use tracing::{instrument, warn}; use crate::{ - entity_satisfies_type_condition, json_writer::write_and_escape_string, - schema_metadata::SchemaMetadata, GraphQLError, TYPENAME_FIELD, + json_writer::write_and_escape_string, schema_metadata::SchemaMetadata, GraphQLError, + TYPENAME_FIELD, }; #[instrument(level = "trace", skip_all)] @@ -173,6 +173,7 @@ fn project_selection_set_with_map( } .to_string(); let field_map = schema_metadata.type_fields.get(&type_name); + let implemented_interfaces = schema_metadata.possible_types.get(&type_name); for selection in &selection_set.items { match selection { @@ -246,11 +247,11 @@ fn project_selection_set_with_map( } } SelectionItem::InlineFragment(inline_fragment) => { - if entity_satisfies_type_condition( - &schema_metadata.possible_types, - &type_name, - &inline_fragment.type_condition, - ) { + let type_condition = &inline_fragment.type_condition; + let satisfies_type_condition = &type_name == type_condition + || implemented_interfaces.map_or(false, |s| s.contains(type_condition)); + + if satisfies_type_condition { project_selection_set_with_map( obj, errors, diff --git a/lib/query-plan-executor/src/schema_metadata.rs b/lib/query-plan-executor/src/schema_metadata.rs index 73bcab668..a8c9189b3 100644 --- a/lib/query-plan-executor/src/schema_metadata.rs +++ b/lib/query-plan-executor/src/schema_metadata.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use graphql_parser::{ query::Type, @@ -9,7 +9,7 @@ use serde_json::{json, Value}; #[derive(Debug)] pub struct SchemaMetadata { - pub possible_types: HashMap>, + pub possible_types: HashMap>, pub enum_values: HashMap>, pub type_fields: HashMap>, pub introspection_schema_root_json: Value, @@ -77,16 +77,16 @@ impl SchemaWithMetadata for ConsumerSchema { } } - let mut final_possible_types: HashMap> = HashMap::new(); + let mut final_possible_types: HashMap> = HashMap::new(); // Re-iterate over the possible_types for (definition_name_of_x, first_possible_types_of_x) in &first_possible_types { - let mut possible_types_of_x: Vec = Vec::new(); + let mut possible_types_of_x: HashSet = HashSet::new(); for definition_name_of_y in first_possible_types_of_x { - possible_types_of_x.push(definition_name_of_y.to_string()); + possible_types_of_x.insert(definition_name_of_y.to_string()); let possible_types_of_y = first_possible_types.get(definition_name_of_y); if let Some(possible_types_of_y) = possible_types_of_y { for definition_name_of_z in possible_types_of_y { - possible_types_of_x.push(definition_name_of_z.to_string()); + possible_types_of_x.insert(definition_name_of_z.to_string()); } } } From 8b6daf6895423c578330db5ef887b36fa41955b3 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 27 Jun 2025 18:13:02 +0300 Subject: [PATCH 13/35] Fix clippy --- lib/query-plan-executor/src/lib.rs | 52 +++++------------------ lib/query-plan-executor/src/projection.rs | 4 +- 2 files changed, 13 insertions(+), 43 deletions(-) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 4518d077f..bb812409d 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -412,11 +412,11 @@ impl ApplyFetchRewrite for KeyRenamer { Some(Value::String(type_name)) => type_name, _ => type_condition, // Default to type_condition if not found }; - if entity_satisfies_type_condition( - possible_types, - type_name, - type_condition, - ) { + let satisfies_type_condition = type_name == type_condition + || possible_types + .get(type_name) + .is_some_and(|s| s.contains(type_condition)); + if satisfies_type_condition { self.apply_path(possible_types, value, remaining_path) } } @@ -472,11 +472,11 @@ impl ApplyFetchRewrite for ValueSetter { Some(Value::String(type_name)) => type_name, _ => type_condition, // Default to type_condition if not found }; - if entity_satisfies_type_condition( - possible_types, - type_name, - type_condition, - ) { + let satisfies_type_condition = type_name == type_condition + || possible_types + .get(type_name) + .is_some_and(|s| s.contains(type_condition)); + if satisfies_type_condition { self.apply_path(possible_types, data, remaining_path) } } @@ -1020,7 +1020,7 @@ impl QueryPlanExecutionContext<'_> { .schema_metadata .possible_types .get(type_condition) - .map_or(false, |s| s.contains(type_name)); + .is_some_and(|s| s.contains(type_name)); if satisfies_type_condition { self.project_requires_map_mut( @@ -1036,36 +1036,6 @@ impl QueryPlanExecutionContext<'_> { } } -#[instrument( - level = "trace", - skip_all, - name = "entity_satisfies_type_condition", - fields( - type_name = %type_name, - type_condition = %type_condition, - ) -)] -fn entity_satisfies_type_condition( - possible_types: &HashMap>, - type_name: &str, - type_condition: &str, -) -> bool { - if type_name == type_condition { - true - } else { - let possible_types_for_type_condition = possible_types.get(type_condition); - match possible_types_for_type_condition { - Some(possible_types_for_type_condition) => { - possible_types_for_type_condition.contains(&type_name.to_string()) - } - None => { - // If no possible types are found, return false - false - } - } - } -} - /// Recursively traverses the data according to the path segments, /// handling '@' for array iteration, and collects the final values.current_data.to_vec() #[instrument(level = "trace", skip_all, fields( diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index d8118f01e..a53d2206c 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -173,7 +173,7 @@ fn project_selection_set_with_map( } .to_string(); let field_map = schema_metadata.type_fields.get(&type_name); - let implemented_interfaces = schema_metadata.possible_types.get(&type_name); + let possible_types_of_type = schema_metadata.possible_types.get(&type_name); for selection in &selection_set.items { match selection { @@ -249,7 +249,7 @@ fn project_selection_set_with_map( SelectionItem::InlineFragment(inline_fragment) => { let type_condition = &inline_fragment.type_condition; let satisfies_type_condition = &type_name == type_condition - || implemented_interfaces.map_or(false, |s| s.contains(type_condition)); + || possible_types_of_type.is_some_and(|s| s.contains(type_condition)); if satisfies_type_condition { project_selection_set_with_map( From de01474c890b86ea8c57b335e03315cc239d11a2 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 27 Jun 2025 18:13:38 +0300 Subject: [PATCH 14/35] Fix options --- bench/k6.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bench/k6.js b/bench/k6.js index 02ac65f99..54854b0e5 100644 --- a/bench/k6.js +++ b/bench/k6.js @@ -5,16 +5,14 @@ import { githubComment } from "https://raw.githubusercontent.com/dotansimha/k6-g const endpoint = __ENV.GATEWAY_ENDPOINT || "http://0.0.0.0:4000/graphql"; const vus = __ENV.BENCH_VUS ? parseInt(__ENV.BENCH_VUS) : 50; -const time = __ENV.BENCH_OVER_TIME || "30s"; +const duration = __ENV.BENCH_OVER_TIME || "30s"; // Apollo: 642.824925/s // Hive: 832.384019/s export const options = { - // vus: vus, - // duration: time, - vus: 1, - iterations: 10, + vus, + duration, }; export function setup() { From 09c7c55ec001dddbb2e1ec717769754a96da85e7 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 27 Jun 2025 18:13:53 +0300 Subject: [PATCH 15/35] Fix obj empty case (#213) The previous version was never reaching primitive cases because for primitives selection is empty already. This fixes that so it uses our serialization instead of serde_json. --- lib/query-plan-executor/src/lib.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index bb812409d..3ee632a4b 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -944,12 +944,6 @@ impl QueryPlanExecutionContext<'_> { entity: &Value, buffer: &mut String, ) { - if requires_selections.is_empty() { - // No selections, so serialize the entity directly into the buffer. - write!(buffer, "{}", serde_json::to_string(entity).unwrap()).unwrap(); - return; - } - match entity { Value::Null => buffer.push_str("null"), Value::Bool(b) => write!(buffer, "{}", b).unwrap(), @@ -968,6 +962,11 @@ impl QueryPlanExecutionContext<'_> { buffer.push(']'); } Value::Object(entity_obj) => { + if requires_selections.is_empty() { + // It is probably a scalar with an object value, so we write it directly + write!(buffer, "{}", serde_json::to_string(entity_obj).unwrap()).unwrap(); + return; + } buffer.push('{'); let mut first = true; self.project_requires_map_mut(requires_selections, entity_obj, buffer, &mut first); From 3535f30f80969e49fd927f683bad4c57caeccebd Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 30 Jun 2025 12:41:10 +0300 Subject: [PATCH 16/35] Use callbacks to avoid vector allocations and avoid temporary allocations in project_requires (#211) Co-authored-by: Kamil Kisiela --- .../benches/executor_benches.rs | 28 +- lib/query-plan-executor/src/lib.rs | 422 +++++++++--------- lib/query-plan-executor/src/projection.rs | 17 +- 3 files changed, 239 insertions(+), 228 deletions(-) diff --git a/lib/query-plan-executor/benches/executor_benches.rs b/lib/query-plan-executor/benches/executor_benches.rs index 26741e093..edf262ced 100644 --- a/lib/query-plan-executor/benches/executor_benches.rs +++ b/lib/query-plan-executor/benches/executor_benches.rs @@ -266,8 +266,12 @@ fn traverse_and_collect(c: &mut Criterion) { let result = black_box(&mut result); let data = result.get_mut("data").unwrap(); let path = black_box(&path); - let result = query_plan_executor::traverse_and_collect(data, path); - black_box(result); + let mut results = vec![]; + query_plan_executor::traverse_and_callback(data, path, &mut |data| { + results.push(data); + }); + black_box(()); + black_box(results); }); }); } @@ -359,7 +363,10 @@ fn project_requires(c: &mut Criterion) { ]; let mut result: Value = non_projected_result::get_result(); let data = result.get_mut("data").unwrap(); - let representations = query_plan_executor::traverse_and_collect(data, &path); + let mut representations = vec![]; + query_plan_executor::traverse_and_callback(data, &path, &mut |data| { + representations.push(data); + }); let supergraph_sdl = std::fs::read_to_string("../../bench/supergraph.graphql") .expect("Unable to read input file"); let parsed_schema = parse_schema(&supergraph_sdl); @@ -429,11 +436,22 @@ fn project_requires(c: &mut Criterion) { c.bench_function("project_requires", |b| { b.iter(|| { let execution_context = black_box(&execution_context); + let mut buffer = String::with_capacity(1024); + let mut first = true; for representation in black_box(&representations) { - let requires = - execution_context.project_requires(&requires_selections, representation); + let requires = execution_context.project_requires( + &requires_selections, + representation, + &mut buffer, + first, + None, + ); + if requires { + first = false; + } black_box(requires); } + black_box(buffer) }); }); } diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 3ee632a4b..6eb9dbbd0 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -11,7 +11,7 @@ use query_planner::{ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::HashSet; -use std::fmt::Write; +use std::{collections::BTreeSet, fmt::Write}; use std::{collections::HashMap, vec}; use tracing::{instrument, trace, warn}; // For reading file in main @@ -98,42 +98,9 @@ fn process_errors_and_extensions( execution_context.extensions.extend(extensions); } } - -#[instrument( - level = "debug", - skip_all - name = "process_representations_result", - fields( - representations_count = %result.entities.as_ref().map_or(0, |e| e.len()) - ), -)] -fn process_representations_result( - result: ExecuteForRepresentationsResult, - representations: &mut Vec<&mut Value>, - execution_context: &mut QueryPlanExecutionContext<'_>, -) { - if let Some(entities) = result.entities { - trace!( - "Processing representations result: {} entities", - entities.len() - ); - for (entity, index) in entities.into_iter().zip(result.indexes.into_iter()) { - if let Some(representation) = representations.get_mut(index) { - trace!( - "Merging entity into representation at index {}: {:?}", - index, - entity - ); - deep_merge::deep_merge(representation, entity); - } - } - } - process_errors_and_extensions(execution_context, result.errors, result.extensions); -} - struct ExecuteForRepresentationsResult { entities: Option>, - indexes: Vec, + indexes: BTreeSet, errors: Option>, extensions: Option>, } @@ -144,16 +111,11 @@ trait ExecutableFetchNode { &self, execution_context: &QueryPlanExecutionContext<'_>, ) -> ExecutionResult; - fn project_representations( - &self, - execution_context: &QueryPlanExecutionContext<'_>, - representations: &[&mut Value], - ) -> ProjectRepresentationsResult; async fn execute_for_projected_representations( &self, execution_context: &QueryPlanExecutionContext<'_>, filtered_representations: String, - filtered_repr_indexes: Vec, + indexes: BTreeSet, ) -> ExecuteForRepresentationsResult; fn apply_output_rewrites( &self, @@ -180,11 +142,6 @@ impl ExecutablePlanNode for FetchNode { } } -struct ProjectRepresentationsResult { - representations: String, - indexes: Vec, -} - #[async_trait] impl ExecutableFetchNode for FetchNode { #[instrument( @@ -224,41 +181,6 @@ impl ExecutableFetchNode for FetchNode { fetch_result } - fn project_representations( - &self, - execution_context: &QueryPlanExecutionContext<'_>, - representations: &[&mut Value], - ) -> ProjectRepresentationsResult { - let mut filtered_repr_indexes = Vec::new(); - // 1. Filter representations based on requires (if present) - let mut filtered_representations = vec![]; - let requires_nodes = self.requires.as_ref().unwrap(); - for (index, entity) in representations.iter().enumerate() { - let entity_projected = - execution_context.project_requires(&requires_nodes.items, entity); - if entity_projected != "null" { - filtered_representations.push(entity_projected); - filtered_repr_indexes.push(index); - } - } - - // if let Some(input_rewrites) = &self.input_rewrites { - // for representation in filtered_representations.iter_mut() { - // for rewrite in input_rewrites { - // rewrite.apply( - // &execution_context.schema_metadata.possible_types, - // representation, - // ); - // } - // } - // } - - ProjectRepresentationsResult { - representations: "[".to_string() + &filtered_representations.join(",") + "]", - indexes: filtered_repr_indexes, - } - } - #[instrument( level = "debug", skip_all, @@ -271,7 +193,7 @@ impl ExecutableFetchNode for FetchNode { &self, execution_context: &QueryPlanExecutionContext<'_>, filtered_representations: String, - filtered_repr_indexes: Vec, + indexes: BTreeSet, ) -> ExecuteForRepresentationsResult { // 2. Prepare variables for fetch let execution_request = ExecutionRequest { @@ -306,7 +228,7 @@ impl ExecutableFetchNode for FetchNode { }; ExecuteForRepresentationsResult { entities, - indexes: filtered_repr_indexes, + indexes, errors: fetch_result.errors, extensions: fetch_result.extensions, } @@ -640,16 +562,53 @@ impl ExecutablePlanNode for ParallelNode { // Collect Fetch node results and non-fetch nodes for sequential execution let now = std::time::Instant::now(); for node in &self.nodes { - let res = create_execution_step(node, execution_context, data); - - if let Some(fetch_job) = res.fetch_job { - fetch_jobs.push(fetch_job); - } - if let Some(flatten_job) = res.flatten_job { - flatten_jobs.push(flatten_job); - } - if let Some(flatten_path) = res.flatten_path { - flatten_paths.push(flatten_path.as_slice()); + match node { + PlanNode::Fetch(fetch_node) => { + // Execute FetchNode in parallel + let job = fetch_node.execute_for_root(execution_context); + fetch_jobs.push(job); + } + PlanNode::Flatten(flatten_node) => { + let normalized_path: Vec<&str> = + flatten_node.path.iter().map(String::as_str).collect(); + let mut representations = String::with_capacity(1024); + let fetch_node = match flatten_node.node.as_ref() { + PlanNode::Fetch(fetch_node) => fetch_node, + _ => { + warn!( + "FlattenNode can only execute FetchNode as child node, found: {:?}", + flatten_node.node + ); + continue; // Skip if the child node is not a FetchNode + } + }; + let requires_nodes = fetch_node.requires.as_ref().unwrap(); + let mut index = 0; + let mut indexes = BTreeSet::new(); + representations.push('['); + traverse_and_callback(data, &normalized_path, &mut |entity| { + let is_projected = execution_context.project_requires( + &requires_nodes.items, + entity, + &mut representations, + indexes.is_empty(), + None, + ); + if is_projected { + indexes.insert(index); + } + index += 1; + }); + representations.push(']'); + let job = fetch_node.execute_for_projected_representations( + execution_context, + representations, + indexes, + ); + flatten_jobs.push(job); + flatten_paths.push(normalized_path); + } + _ => {} } } trace!( @@ -676,14 +635,18 @@ impl ExecutablePlanNode for ParallelNode { let now = std::time::Instant::now(); for (result, path) in flatten_results.into_iter().zip(flatten_paths) { // Process FlattenNode results - if let Some(entities) = result.entities { - let mut collected_representations = traverse_and_collect(data, path); - for (entity, index) in entities.into_iter().zip(result.indexes.into_iter()) { - if let Some(representation) = collected_representations.get_mut(index) { - // Merge the entity into the representation - deep_merge::deep_merge(representation, entity); + if let Some(mut entities) = result.entities { + let mut index_of_traverse = 0; + let mut index_of_entities = 0; + traverse_and_callback(data, &path, &mut |target| { + if result.indexes.contains(&index_of_traverse) { + let entity = entities.get_mut(index_of_entities).unwrap().take(); + // Merge the entity into the target + deep_merge::deep_merge(target, entity); + index_of_entities += 1; } - } + index_of_traverse += 1; + }); } // Extend errors and extensions from the result if let Some(errors) = result.errors { @@ -746,64 +709,51 @@ impl ExecutablePlanNode for FlattenNode { // Execute the child node. `execution_context` can be borrowed mutably // because `collected_representations` borrows `data_for_flatten`, not `execution_context.data`. let now = std::time::Instant::now(); - let mut representations = traverse_and_collect(data, self.path.as_slice()); + let mut representations = vec![]; + let mut filtered_representations = String::with_capacity(1024); + let fetch_node = match self.node.as_ref() { + PlanNode::Fetch(fetch_node) => fetch_node, + _ => { + warn!( + "FlattenNode can only execute FetchNode as child node, found: {:?}", + self.node + ); + return; // Skip if the child node is not a FetchNode + } + }; + let requires_nodes = fetch_node.requires.as_ref().unwrap(); + filtered_representations.push('['); + let mut first = true; + traverse_and_callback(data, normalized_path.as_slice(), &mut |entity| { + let entity_projected = execution_context.project_requires( + &requires_nodes.items, + entity, + &mut filtered_representations, + first, + None, + ); + if entity_projected { + representations.push(entity); + first = false; + } + }); + filtered_representations.push(']'); trace!( "traversed and collected representations: {:?} in {:#?}", representations.len(), now.elapsed() ); - - if representations.is_empty() { - // If there are no representations, - // return early without executing the child node. - return; - } - - match self.node.as_ref() { - PlanNode::Fetch(fetch_node) => { - let now = std::time::Instant::now(); - let ProjectRepresentationsResult { - representations: filtered_representations, - indexes: filtered_repr_indexes, - } = fetch_node.project_representations(execution_context, &representations); - trace!( - "projected representations: {:?} in {:#?}", - representations.len(), - now.elapsed() - ); - - if filtered_representations.is_empty() { - // If there are no filtered representations, - // return early without executing the child node. - return; - } - - let now = std::time::Instant::now(); - let result = fetch_node - .execute_for_projected_representations( - execution_context, - filtered_representations, - filtered_repr_indexes, - ) - .await; - trace!( - "executed projected representations: {:?} in {:?}", - representations.len(), - now.elapsed() - ); - // Process the result - process_representations_result(result, &mut representations, execution_context); - trace!( - "processed projected representations: {:?} in {:?}", - representations.len(), - now.elapsed() - ); - } - _ => { - unimplemented!( - "FlattenNode can only execute FetchNode as child node, found: {:?}", - self.node - ); + let result = fetch_node + .execute_for_projected_representations( + execution_context, + filtered_representations, + BTreeSet::new(), + ) + .await; + if let Some(entities) = result.entities { + for (entity, target) in entities.into_iter().zip(representations.iter_mut()) { + // Merge the entity into the representation + deep_merge::deep_merge(target, entity); } } } @@ -931,32 +881,65 @@ impl QueryPlanExecutionContext<'_> { &self, requires_selections: &Vec, entity: &Value, - ) -> String { - // Pre-allocate a buffer, but we can do it without I think - let mut buffer = String::with_capacity(1024); - self.project_requires_mut(requires_selections, entity, &mut buffer); - buffer - } - - fn project_requires_mut( - &self, - requires_selections: &Vec, - entity: &Value, buffer: &mut String, - ) { + first: bool, + response_key: Option<&str>, + ) -> bool { match entity { - Value::Null => buffer.push_str("null"), - Value::Bool(b) => write!(buffer, "{}", b).unwrap(), - Value::Number(n) => write!(buffer, "{}", n).unwrap(), - Value::String(s) => write_and_escape_string(buffer, s), + Value::Null => { + return false; + } + Value::Bool(b) => { + if !first { + buffer.push(','); + } + if let Some(response_key) = response_key { + buffer.push('"'); + buffer.push_str(response_key); + buffer.push('"'); + buffer.push(':'); + buffer.push_str(if *b { "true" } else { "false" }); + } else { + buffer.push_str(if *b { "true" } else { "false" }); + } + } + Value::Number(n) => { + if !first { + buffer.push(','); + } + if let Some(response_key) = response_key { + buffer.push('"'); + buffer.push_str(response_key); + buffer.push_str("\":"); + } + + write!(buffer, "{}", n).unwrap() + } + Value::String(s) => { + if !first { + buffer.push(','); + } + if let Some(response_key) = response_key { + buffer.push('"'); + buffer.push_str(response_key); + buffer.push_str("\":"); + } + write_and_escape_string(buffer, s); + } Value::Array(entity_array) => { - buffer.push('['); + if !first { + buffer.push(','); + } + if let Some(response_key) = response_key { + buffer.push('"'); + buffer.push_str(response_key); + buffer.push_str("\":["); + } else { + buffer.push('['); + } let mut first = true; for entity_item in entity_array { - if !first { - buffer.push(','); - } - self.project_requires_mut(requires_selections, entity_item, buffer); + self.project_requires(requires_selections, entity_item, buffer, first, None); first = false; } buffer.push(']'); @@ -964,15 +947,31 @@ impl QueryPlanExecutionContext<'_> { Value::Object(entity_obj) => { if requires_selections.is_empty() { // It is probably a scalar with an object value, so we write it directly - write!(buffer, "{}", serde_json::to_string(entity_obj).unwrap()).unwrap(); - return; + buffer.push_str( + &serde_json::to_string(entity_obj).unwrap() + ); + return true; + } + if entity_obj.is_empty() { + return false; + } + + if !first { + buffer.push(','); + } + if let Some(response_key) = response_key { + buffer.push('"'); + buffer.push_str(response_key); + buffer.push_str("\":{"); + } else { + buffer.push('{'); } - buffer.push('{'); let mut first = true; self.project_requires_map_mut(requires_selections, entity_obj, buffer, &mut first); buffer.push('}'); } - } + }; + true } fn project_requires_map_mut( @@ -982,45 +981,41 @@ impl QueryPlanExecutionContext<'_> { buffer: &mut String, first: &mut bool, ) { - let type_name = match entity_obj.get(TYPENAME_FIELD) { - Some(Value::String(tn)) => tn.as_str(), - _ => "", // TODO: improve it - }; for requires_selection in requires_selections { match &requires_selection { SelectionItem::Field(requires_selection) => { let field_name = &requires_selection.name; let response_key = requires_selection.selection_identifier(); + let original = entity_obj .get(field_name) .unwrap_or(entity_obj.get(response_key).unwrap_or(&Value::Null)); + if original.is_null() { + continue; + } + // To avoid writing empty fields, we write to a temporary buffer first - let mut temp_buffer = String::new(); - self.project_requires_mut( + self.project_requires( &requires_selection.selections.items, original, - &mut temp_buffer, + buffer, + *first, + Some(response_key), ); - - if temp_buffer != "null" && !temp_buffer.is_empty() { - if !*first { - buffer.push(','); - } - write!(buffer, "\"{}\":{}", response_key, temp_buffer).unwrap(); - *first = false; - } + *first = false; } SelectionItem::InlineFragment(requires_selection) => { - let type_condition = &requires_selection.type_condition; - - let satisfies_type_condition = type_name == type_condition + let type_name = match entity_obj.get(TYPENAME_FIELD) { + Some(Value::String(type_name)) => type_name, + _ => requires_selection.type_condition.as_str(), + }; + let satisfies_type_condition = type_name == requires_selection.type_condition || self .schema_metadata .possible_types - .get(type_condition) - .is_some_and(|s| s.contains(type_name)); - + .get(type_name) + .is_some_and(|s| s.contains(&requires_selection.type_condition)); if satisfies_type_condition { self.project_requires_map_mut( &requires_selection.selections.items, @@ -1035,28 +1030,15 @@ impl QueryPlanExecutionContext<'_> { } } -/// Recursively traverses the data according to the path segments, -/// handling '@' for array iteration, and collects the final values.current_data.to_vec() -#[instrument(level = "trace", skip_all, fields( - current_type = ?current_data, - remaining_path = ?remaining_path -))] -pub fn traverse_and_collect<'a>( - current_data: &'a mut Value, - remaining_path: &[FlattenNodePathSegment], -) -> Vec<&'a mut Value> { - let mut collected = Vec::new(); - traverse_and_collect_mut(current_data, remaining_path, &mut collected); - collected -} - -fn traverse_and_collect_mut<'a>( +pub fn traverse_and_callback<'a, Callback>( current_data: &'a mut Value, remaining_path: &[&str], - collected: &mut Vec<&'a mut Value>, -) { + callback: &mut Callback, +) where + Callback: FnMut(&'a mut Value), +{ if remaining_path.is_empty() { - collected.push(current_data); + callback(current_data); return; } @@ -1066,12 +1048,12 @@ fn traverse_and_collect_mut<'a>( if key == "@" { if let Value::Array(list) = current_data { for item in list.iter_mut() { - traverse_and_collect_mut(item, rest_of_path, collected); + traverse_and_callback(item, rest_of_path, callback); } } } else if let Value::Object(map) = current_data { if let Some(next_data) = map.get_mut(key) { - traverse_and_collect_mut(next_data, rest_of_path, collected); + traverse_and_callback(next_data, rest_of_path, callback); } } } diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index a53d2206c..948fd5c1f 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -34,7 +34,12 @@ pub fn project_by_operation( // We may want to remove it, but let's see. let mut buffer = String::with_capacity(4096); - write!(buffer, "{{\"data\":").unwrap(); + buffer.push('{'); + buffer.push('"'); + buffer.push_str("data"); + buffer.push('"'); + buffer.push(':'); + project_selection_set( data, errors, @@ -203,11 +208,17 @@ fn project_selection_set_with_map( *first = false; if field.name == TYPENAME_FIELD { - write!(buffer, "\"{}\":\"{}\"", response_key, type_name).unwrap(); + buffer.push('"'); + buffer.push_str(response_key); + buffer.push_str("\":\""); + buffer.push_str(&type_name); + buffer.push('"'); continue; } - write!(buffer, "\"{}\":", response_key).unwrap(); + buffer.push('"'); + buffer.push_str(response_key); + buffer.push_str("\":"); let field_map = field_map.unwrap(); let field_type = field_map.get(&field.name); From 61acc9822d32d002f37c464d3440956e9ee14ad4 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 30 Jun 2025 12:55:32 +0300 Subject: [PATCH 17/35] Format --- lib/query-plan-executor/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 6eb9dbbd0..99efbed7e 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -947,9 +947,7 @@ impl QueryPlanExecutionContext<'_> { Value::Object(entity_obj) => { if requires_selections.is_empty() { // It is probably a scalar with an object value, so we write it directly - buffer.push_str( - &serde_json::to_string(entity_obj).unwrap() - ); + buffer.push_str(&serde_json::to_string(entity_obj).unwrap()); return true; } if entity_obj.is_empty() { From 09f02de0dd16cade26ca85747acc8ca48f3cdf49 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 30 Jun 2025 14:04:03 +0300 Subject: [PATCH 18/35] Fix null-keys on perf improvements (#217) --- lib/query-plan-executor/src/lib.rs | 48 +++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 99efbed7e..922744c1a 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -954,19 +954,19 @@ impl QueryPlanExecutionContext<'_> { return false; } + let parent_first = first; + let mut first = true; + self.project_requires_map_mut( + requires_selections, + entity_obj, + buffer, + &mut first, + response_key, + parent_first, + ); if !first { - buffer.push(','); - } - if let Some(response_key) = response_key { - buffer.push('"'); - buffer.push_str(response_key); - buffer.push_str("\":{"); - } else { - buffer.push('{'); + buffer.push('}'); } - let mut first = true; - self.project_requires_map_mut(requires_selections, entity_obj, buffer, &mut first); - buffer.push('}'); } }; true @@ -978,12 +978,18 @@ impl QueryPlanExecutionContext<'_> { entity_obj: &Map, buffer: &mut String, first: &mut bool, + parent_response_key: Option<&str>, + parent_first: bool, ) { for requires_selection in requires_selections { match &requires_selection { SelectionItem::Field(requires_selection) => { let field_name = &requires_selection.name; let response_key = requires_selection.selection_identifier(); + if response_key == TYPENAME_FIELD { + // Skip __typename field, it is handled separately + continue; + } let original = entity_obj .get(field_name) @@ -993,6 +999,24 @@ impl QueryPlanExecutionContext<'_> { continue; } + if *first { + if !parent_first { + buffer.push(','); + } + if let Some(parent_response_key) = parent_response_key { + buffer.push('"'); + buffer.push_str(parent_response_key); + buffer.push_str("\":"); + } + buffer.push('{'); + // Write __typename only if the object has other fields + if let Some(Value::String(type_name)) = entity_obj.get(TYPENAME_FIELD) { + buffer.push_str("\"__typename\":"); + write_and_escape_string(buffer, type_name); + buffer.push(','); + } + } + // To avoid writing empty fields, we write to a temporary buffer first self.project_requires( &requires_selection.selections.items, @@ -1020,6 +1044,8 @@ impl QueryPlanExecutionContext<'_> { entity_obj, buffer, first, + parent_response_key, + parent_first, ); } } From 5de27efde571b5f6c174367445c78bb5e2afade0 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 2 Jul 2025 16:16:37 +0300 Subject: [PATCH 19/35] Add missing fragment spread --- lib/query-plan-executor/src/lib.rs | 4 ++++ lib/query-plan-executor/src/projection.rs | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 922744c1a..e382cb6d4 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -1049,6 +1049,10 @@ impl QueryPlanExecutionContext<'_> { ); } } + SelectionItem::FragmentSpread(_name_ref) => { + // We only minify the queries to subgraphs, so we never have fragment spreads here + unreachable!("Fragment spreads should not exist in FetchNode::requires."); + } } } } diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index 948fd5c1f..07c0d62f0 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -275,6 +275,12 @@ fn project_selection_set_with_map( ); } } + SelectionItem::FragmentSpread(_name_ref) => { + // We only minify the queries to subgraphs, so we never have fragment spreads here. + // In this projection, we expect only inline fragments and fields + // as it's the query produced by the ast normalization process. + unreachable!("Fragment spreads should not exist in the final response projection."); + } } } } From 56df831b3f5336deb82f9cb393d983d6262d2bae Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 8 Jul 2025 14:07:41 +0300 Subject: [PATCH 20/35] Fix provides-on-union on the new serialization impl (#218) Co-authored-by: Kamil Kisiela --- bench/k6.js | 3 --- lib/query-plan-executor/src/lib.rs | 42 ++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/bench/k6.js b/bench/k6.js index 54854b0e5..9fcf4efa9 100644 --- a/bench/k6.js +++ b/bench/k6.js @@ -7,9 +7,6 @@ const endpoint = __ENV.GATEWAY_ENDPOINT || "http://0.0.0.0:4000/graphql"; const vus = __ENV.BENCH_VUS ? parseInt(__ENV.BENCH_VUS) : 50; const duration = __ENV.BENCH_OVER_TIME || "30s"; -// Apollo: 642.824925/s -// Hive: 832.384019/s - export const options = { vus, duration, diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index e382cb6d4..1e4a4504b 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -939,8 +939,17 @@ impl QueryPlanExecutionContext<'_> { } let mut first = true; for entity_item in entity_array { - self.project_requires(requires_selections, entity_item, buffer, first, None); - first = false; + let projected = self.project_requires( + requires_selections, + entity_item, + buffer, + first, + None, + ); + if projected { + // Only update `first` if we actually write something + first = false; + } } buffer.push(']'); } @@ -964,7 +973,11 @@ impl QueryPlanExecutionContext<'_> { response_key, parent_first, ); - if !first { + if first { + // If no fields were projected, "first" is still true, + // so we skip writing the closing brace + return false; + } else { buffer.push('}'); } } @@ -1028,16 +1041,20 @@ impl QueryPlanExecutionContext<'_> { *first = false; } SelectionItem::InlineFragment(requires_selection) => { + let type_condition = &requires_selection.type_condition; + let type_name = match entity_obj.get(TYPENAME_FIELD) { Some(Value::String(type_name)) => type_name, - _ => requires_selection.type_condition.as_str(), + _ => type_condition, }; - let satisfies_type_condition = type_name == requires_selection.type_condition + + let satisfies_type_condition = type_name == type_condition || self .schema_metadata .possible_types - .get(type_name) - .is_some_and(|s| s.contains(&requires_selection.type_condition)); + .get(type_condition) + .is_some_and(|s| s.contains(type_name)); + if satisfies_type_condition { self.project_requires_map_mut( &requires_selection.selections.items, @@ -1066,7 +1083,16 @@ pub fn traverse_and_callback<'a, Callback>( Callback: FnMut(&'a mut Value), { if remaining_path.is_empty() { - callback(current_data); + if let Value::Array(arr) = current_data { + // If the path is empty, we call the callback on each item in the array + // We iterate because we want the entity objects directly + for item in arr.iter_mut() { + callback(item); + } + } else { + // If the path is empty and current_data is not an array, just call the callback + callback(current_data); + } return; } From 9e04921ce60b4fbefd35010dc9d82e373aba6fbe Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 8 Jul 2025 14:07:53 +0300 Subject: [PATCH 21/35] Fix requires-with-argument / handle variables correctly on serialization (#220) Also merged this into this PR by accident #221 Sorry :/ --------- Co-authored-by: Kamil Kisiela --- lib/query-plan-executor/src/executors/http.rs | 71 ++++++++++++------- lib/query-plan-executor/src/lib.rs | 6 ++ lib/query-plan-executor/src/projection.rs | 34 +++++++-- 3 files changed, 80 insertions(+), 31 deletions(-) diff --git a/lib/query-plan-executor/src/executors/http.rs b/lib/query-plan-executor/src/executors/http.rs index badffca8a..89a9f0507 100644 --- a/lib/query-plan-executor/src/executors/http.rs +++ b/lib/query-plan-executor/src/executors/http.rs @@ -9,6 +9,8 @@ pub struct HTTPSubgraphExecutor { pub http_client: reqwest::Client, } +const FIRST_VARIABLE_STR: &str = ",\"variables\":{"; + impl HTTPSubgraphExecutor { pub fn new(endpoint: String, http_client: reqwest::Client) -> Self { HTTPSubgraphExecutor { @@ -20,39 +22,65 @@ impl HTTPSubgraphExecutor { async fn _execute( &self, execution_request: ExecutionRequest, - ) -> Result { + ) -> Result { trace!("Executing HTTP request to subgraph at {}", self.endpoint); - let mut body = "{\"query\":".to_string() - + &serde_json::to_string(&execution_request.query).unwrap() - + ",\"variables\":{"; - let variables_added = false; + let mut body = + "{\"query\":".to_string() + &serde_json::to_string(&execution_request.query).unwrap(); + let mut first_variable = true; if let Some(variables) = &execution_request.variables { - let variables_entry = variables - .iter() - .map(|(key, value)| { - "\"".to_string() + key + "\": " + &serde_json::to_string(value).unwrap() - }) - .collect::>() - .join(","); - body.push_str(&variables_entry); + for (variable_name, variable_value) in variables { + if first_variable { + body.push_str(FIRST_VARIABLE_STR); + first_variable = false; + } else { + body.push(','); + } + body.push('"'); + body.push_str(variable_name); + body.push_str("\":"); + let value_str = serde_json::to_string(variable_value).map_err(|err| { + format!("Failed to serialize variable '{}': {}", variable_name, err) + })?; + body.push_str(&value_str); + } } if let Some(representations) = &execution_request.representations { - if variables_added { + if first_variable { + body.push_str(FIRST_VARIABLE_STR); + first_variable = false; + } else { body.push(','); } - body.push_str(&("\"representations\":".to_string() + representations)); + body.push_str("\"representations\":"); + body.push_str(representations); + } + // "first_variable" should be still true if there are no variables + if !first_variable { + body.push('}'); } - body.push_str("}}"); + body.push('}'); self.http_client .post(&self.endpoint) .body(body) .header("Content-Type", "application/json; charset=utf-8") .send() - .await? + .await + .map_err(|e| { + format!( + "Failed to send request to subgraph {}: {}", + self.endpoint, e + ) + })? .json::() .await + .map_err(|e| { + format!( + "Failed to parse response from subgraph {}: {}", + self.endpoint, e + ) + }) } } @@ -61,13 +89,8 @@ impl SubgraphExecutor for HTTPSubgraphExecutor { #[instrument(level = "trace", skip(self), name = "http_subgraph_execute", fields(endpoint = %self.endpoint))] async fn execute(&self, execution_request: ExecutionRequest) -> ExecutionResult { self._execute(execution_request).await.unwrap_or_else(|e| { - error!("Failed to execute request to subgraph: {}", e); - trace!("network error: {:?}", e); - - ExecutionResult::from_error_message(format!( - "Error executing subgraph {}: {}", - self.endpoint, e - )) + error!(e); + ExecutionResult::from_error_message(e) }) } } diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 1e4a4504b..0909fffde 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -743,6 +743,10 @@ impl ExecutablePlanNode for FlattenNode { representations.len(), now.elapsed() ); + if first { + // No representations collected, so we skip the fetch execution + return; + } let result = fetch_node .execute_for_projected_representations( execution_context, @@ -756,6 +760,8 @@ impl ExecutablePlanNode for FlattenNode { deep_merge::deep_merge(target, entity); } } + + process_errors_and_extensions(execution_context, result.errors, result.extensions); } } diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index 07c0d62f0..a8214aa6a 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -134,9 +134,8 @@ fn project_selection_set( buffer.push(']'); } Value::Object(obj) => { - buffer.push('{'); let mut first = true; - project_selection_set_with_map( + let is_projected = project_selection_set_with_map( obj, errors, selection_set, @@ -146,7 +145,16 @@ fn project_selection_set( buffer, &mut first, ); - buffer.push('}'); + if !is_projected { + buffer.push_str("null"); + } else + // If first is mutated, it means we added "{" + if !first { + buffer.push('}'); + } else { + // If first is still true, it means we didn't add anything, so we should just send an empty object + buffer.push_str("{}"); + } } } } @@ -171,13 +179,23 @@ fn project_selection_set_with_map( variable_values: &Option>, buffer: &mut String, first: &mut bool, -) { +) -> bool { let type_name = match obj.get(TYPENAME_FIELD) { Some(Value::String(type_name)) => type_name, _ => type_name, } .to_string(); - let field_map = schema_metadata.type_fields.get(&type_name); + let field_map = match schema_metadata.type_fields.get(&type_name) { + Some(field_map) => field_map, + None => { + // If the type is not found, we can't project anything + warn!( + "Type {} not found in schema metadata. Skipping projection.", + type_name + ); + return false; + } + }; let possible_types_of_type = schema_metadata.possible_types.get(&type_name); for selection in &selection_set.items { @@ -202,7 +220,9 @@ fn project_selection_set_with_map( let response_key = field.alias.as_ref().unwrap_or(&field.name); - if !*first { + if *first { + buffer.push('{'); + } else { buffer.push(','); } *first = false; @@ -220,7 +240,6 @@ fn project_selection_set_with_map( buffer.push_str(response_key); buffer.push_str("\":"); - let field_map = field_map.unwrap(); let field_type = field_map.get(&field.name); if field.name == "__schema" && type_name == "Query" { @@ -283,4 +302,5 @@ fn project_selection_set_with_map( } } } + true } From 9dec09d4f2f9b074be25e13915c27c04b5b2fa5d Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 8 Jul 2025 14:16:03 +0300 Subject: [PATCH 22/35] Fix simple-interface-object audit / fix possible type check (#224) Also includes #226 & #229 & #245 --------- Co-authored-by: Kamil Kisiela --- bench/k6.js | 2 +- bin/gateway/src/main.rs | 2 +- .../src/pipeline/coerce_variables_service.rs | 21 +- bin/gateway/src/pipeline/error.rs | 25 +- bin/gateway/src/pipeline/execution_service.rs | 2 +- .../src/pipeline/graphql_request_params.rs | 17 +- bin/gateway/src/pipeline/normalize_service.rs | 95 ++-- bin/gateway/src/pipeline/parser_service.rs | 56 +- .../src/pipeline/query_plan_service.rs | 2 +- .../src/pipeline/validation_service.rs | 49 +- bin/gateway/src/shared_state.rs | 6 + .../src/executors/async_graphql.rs | 13 +- .../src/executors/common.rs | 5 +- lib/query-plan-executor/src/executors/http.rs | 20 +- lib/query-plan-executor/src/executors/map.rs | 4 +- lib/query-plan-executor/src/lib.rs | 483 +++++++----------- lib/query-plan-executor/src/projection.rs | 81 ++- .../src/schema_metadata.rs | 23 +- 18 files changed, 457 insertions(+), 449 deletions(-) diff --git a/bench/k6.js b/bench/k6.js index 9fcf4efa9..f7f45e6ba 100644 --- a/bench/k6.js +++ b/bench/k6.js @@ -57,7 +57,7 @@ export function handleSummary(data) { }, }); } - return handleBenchmarkSummary(data, { vus, time }); + return handleBenchmarkSummary(data, { vus, duration }); } let printIdentifiersMap = {}; diff --git a/bin/gateway/src/main.rs b/bin/gateway/src/main.rs index 3485df0a3..45fdb4b45 100644 --- a/bin/gateway/src/main.rs +++ b/bin/gateway/src/main.rs @@ -99,10 +99,10 @@ async fn main() -> Result<(), Box> { .layer(GraphiQLResponderService::new_layer()) .layer(GraphQLRequestParamsExtractor::new_layer()) .layer(GraphQLParserService::new_layer()) + .layer(GraphQLValidationService::new_layer()) .layer(ProgressiveOverrideExtractor::new_layer()) .layer(GraphQLOperationNormalizationService::new_layer()) .layer(CoerceVariablesService::new_layer()) - .layer(GraphQLValidationService::new_layer()) .layer(QueryPlanService::new_layer()) .layer(PropagateRequestIdLayer::new(REQUEST_ID_HEADER_NAME.clone())) .service(ExecutionService::new(expose_query_plan)); diff --git a/bin/gateway/src/pipeline/coerce_variables_service.rs b/bin/gateway/src/pipeline/coerce_variables_service.rs index 68c1ba3d0..b024076c3 100644 --- a/bin/gateway/src/pipeline/coerce_variables_service.rs +++ b/bin/gateway/src/pipeline/coerce_variables_service.rs @@ -2,16 +2,17 @@ use std::collections::HashMap; use std::sync::Arc; use axum::body::Body; -use http::Request; +use http::{Method, Request}; use query_plan_executor::variables::collect_variables; -use query_plan_executor::ExecutionRequest; +use query_planner::state::supergraph_state::OperationKind; use serde_json::Value; -use tracing::{trace, warn}; +use tracing::{error, trace, warn}; use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; +use crate::pipeline::graphql_request_params::ExecutionRequest; use crate::pipeline::http_request_params::HttpRequestParams; use crate::pipeline::normalize_service::GraphQLNormalizationPayload; use crate::shared_state::GatewaySharedState; @@ -39,7 +40,7 @@ impl GatewayPipelineLayer for CoerceVariablesService { ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { let normalized_operation = req .extensions() - .get::() + .get::>() .ok_or_else(|| { PipelineErrorVariant::InternalServiceError("GraphQLNormalizationPayload is missing") })?; @@ -59,6 +60,18 @@ impl GatewayPipelineLayer for CoerceVariablesService { PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing") })?; + if http_payload.http_method == Method::GET { + if let Some(OperationKind::Mutation) = normalized_operation + .normalized_document + .operation + .operation_kind + { + error!("Mutation is not allowed over GET, stopping"); + + return Err(PipelineErrorVariant::MutationNotAllowedOverHttpGet.into()); + } + } + match collect_variables( &normalized_operation.operation_for_plan, &execution_params.variables, diff --git a/bin/gateway/src/pipeline/error.rs b/bin/gateway/src/pipeline/error.rs index 1723d60a6..596d3667a 100644 --- a/bin/gateway/src/pipeline/error.rs +++ b/bin/gateway/src/pipeline/error.rs @@ -78,6 +78,18 @@ impl PipelineErrorVariant { Self::UnsupportedHttpMethod(_) => "METHOD_NOT_ALLOWED", Self::PlannerError(_) => "QUERY_PLAN_BUILD_FAILED", Self::InternalServiceError(_) => "INTERNAL_SERVER_ERROR", + Self::FailedToParseOperation(_) => "GRAPHQL_PARSE_FAILED", + Self::ValidationErrors(_) => "GRAPHQL_VALIDATION_FAILED", + Self::VariablesCoercionError(_) => "BAD_USER_INPUT", + Self::NormalizationError(NormalizationError::OperationNotFound) => { + "OPERATION_RESOLUTION_FAILURE" + } + Self::NormalizationError(NormalizationError::SpecifiedOperationNotFound { + operation_name: _, + }) => "OPERATION_RESOLUTION_FAILURE", + Self::NormalizationError(NormalizationError::MultipleMatchingOperationsFound) => { + "OPERATION_RESOLUTION_FAILURE" + } _ => "BAD_REQUEST", } } @@ -111,9 +123,10 @@ impl PipelineErrorVariant { (Self::VariablesCoercionError(_), false) => StatusCode::BAD_REQUEST, (Self::VariablesCoercionError(_), true) => StatusCode::OK, (Self::MutationNotAllowedOverHttpGet, _) => StatusCode::METHOD_NOT_ALLOWED, - (Self::ValidationErrors(_), _) => StatusCode::BAD_REQUEST, - (Self::MissingContentTypeHeader, _) => StatusCode::BAD_REQUEST, - (Self::UnsupportedContentType, _) => StatusCode::BAD_REQUEST, + (Self::ValidationErrors(_), true) => StatusCode::OK, + (Self::ValidationErrors(_), false) => StatusCode::BAD_REQUEST, + (Self::MissingContentTypeHeader, _) => StatusCode::NOT_ACCEPTABLE, + (Self::UnsupportedContentType, _) => StatusCode::UNSUPPORTED_MEDIA_TYPE, } } } @@ -135,10 +148,10 @@ impl From for PipelineError { impl IntoResponse for PipelineError { fn into_response(self) -> Response { - let accept_ok = self + let accept_ok = &self .accept_header .is_some_and(|v| v.contains(APPLICATION_JSON.to_str().unwrap())); - let status = self.error.default_status_code(accept_ok); + let status = self.error.default_status_code(*accept_ok); if let PipelineErrorVariant::ValidationErrors(validation_errors) = self.error { let validation_error_result = ExecutionResult { @@ -148,7 +161,7 @@ impl IntoResponse for PipelineError { }; return ( - StatusCode::OK, + status, serde_json::to_string(&validation_error_result).unwrap(), ) .into_response(); diff --git a/bin/gateway/src/pipeline/execution_service.rs b/bin/gateway/src/pipeline/execution_service.rs index b91480045..5329cb984 100644 --- a/bin/gateway/src/pipeline/execution_service.rs +++ b/bin/gateway/src/pipeline/execution_service.rs @@ -65,7 +65,7 @@ impl Service> for ExecutionService { Box::pin(async move { let normalized_payload = req .extensions() - .get::() + .get::>() .expect("GraphQLNormalizationPayload missing"); let query_plan_payload = req .extensions() diff --git a/bin/gateway/src/pipeline/graphql_request_params.rs b/bin/gateway/src/pipeline/graphql_request_params.rs index 59bd696c1..c5c28a6aa 100644 --- a/bin/gateway/src/pipeline/graphql_request_params.rs +++ b/bin/gateway/src/pipeline/graphql_request_params.rs @@ -1,7 +1,10 @@ +use std::collections::HashMap; + use axum::body::{to_bytes, Body}; use axum::extract::Query; use http::{Method, Request}; -use query_plan_executor::ExecutionRequest; +use serde::Deserialize; +use serde_json::Value; use tracing::{trace, warn}; use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; @@ -24,6 +27,17 @@ struct GETQueryParams { pub extensions: Option, } +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionRequest { + pub query: String, + pub operation_name: Option, + pub variables: Option>, + // TODO: We don't use extensions yet, but we definitely will in the future. + #[allow(dead_code)] + pub extensions: Option>, +} + impl TryInto for GETQueryParams { type Error = PipelineErrorVariant; @@ -58,7 +72,6 @@ impl TryInto for GETQueryParams { operation_name: self.operation_name, variables, extensions, - representations: None, }; Ok(execution_request) diff --git a/bin/gateway/src/pipeline/normalize_service.rs b/bin/gateway/src/pipeline/normalize_service.rs index 0eb5c198e..12d4351d5 100644 --- a/bin/gateway/src/pipeline/normalize_service.rs +++ b/bin/gateway/src/pipeline/normalize_service.rs @@ -1,9 +1,9 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; use std::sync::Arc; use axum::body::Body; use http::Request; use query_plan_executor::introspection::filter_introspection_fields_in_operation; -use query_plan_executor::ExecutionRequest; use query_planner::ast::document::NormalizedDocument; use query_planner::ast::normalization::normalize_operation; use query_planner::ast::operation::OperationDefinition; @@ -12,6 +12,7 @@ use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; +use crate::pipeline::graphql_request_params::ExecutionRequest; use crate::pipeline::http_request_params::HttpRequestParams; use crate::pipeline::parser_service::GraphQLParserPayload; use crate::shared_state::GatewaySharedState; @@ -67,45 +68,71 @@ impl GatewayPipelineLayer for GraphQLOperationNormalizationService { PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing") })?; - match normalize_operation( - &app_state.planner.supergraph, - &parser_payload.parsed_operation, - execution_params.operation_name.as_deref(), - ) { - Ok(doc) => { - trace!( - "Successfully normalized GraphQL operation (operation name={:?}): {}", - doc.operation_name, - doc.operation - ); - - let operation = &doc.operation; - let (has_introspection, filtered_operation_for_plan) = - filter_introspection_fields_in_operation(operation); + let cache_key = match &execution_params.operation_name { + Some(operation_name) => { + let mut hasher = DefaultHasher::new(); + execution_params.query.hash(&mut hasher); + operation_name.hash(&mut hasher); + hasher.finish() + } + None => parser_payload.cache_key, + }; + match app_state.normalize_cache.get(&cache_key).await { + Some(payload) => { trace!( - "Operation after removing introspection fields (introspection found={}): {}", - has_introspection, - filtered_operation_for_plan + "Found normalized GraphQL operation in cache (operation name={:?}): {}", + payload.normalized_document.operation_name, + payload.normalized_document.operation ); - - req.extensions_mut().insert(GraphQLNormalizationPayload { - normalized_document: doc, - operation_for_plan: filtered_operation_for_plan, - has_introspection, - }); - + req.extensions_mut().insert(payload); Ok((req, GatewayPipelineStepDecision::Continue)) } - Err(err) => { - error!("Failed to normalize GraphQL operation: {}", err); - trace!("{:?}", err); + None => match normalize_operation( + &app_state.planner.supergraph, + &parser_payload.parsed_operation, + execution_params.operation_name.as_deref(), + ) { + Ok(doc) => { + trace!( + "Successfully normalized GraphQL operation (operation name={:?}): {}", + doc.operation_name, + doc.operation + ); - return Err(PipelineError::new_with_accept_header( - PipelineErrorVariant::NormalizationError(err), - http_payload.accept_header.clone(), - )); - } + let operation = &doc.operation; + let (has_introspection, filtered_operation_for_plan) = + filter_introspection_fields_in_operation(operation); + + trace!( + "Operation after removing introspection fields (introspection found={}): {}", + has_introspection, + filtered_operation_for_plan + ); + + let payload = GraphQLNormalizationPayload { + normalized_document: doc, + operation_for_plan: filtered_operation_for_plan, + has_introspection, + }; + let payload_arc = Arc::new(payload); + app_state + .normalize_cache + .insert(cache_key, payload_arc.clone()) + .await; + req.extensions_mut().insert(payload_arc); + Ok((req, GatewayPipelineStepDecision::Continue)) + } + Err(err) => { + error!("Failed to normalize GraphQL operation: {}", err); + trace!("{:?}", err); + + Err(PipelineError::new_with_accept_header( + PipelineErrorVariant::NormalizationError(err), + http_payload.accept_header.clone(), + )) + } + }, } } } diff --git a/bin/gateway/src/pipeline/parser_service.rs b/bin/gateway/src/pipeline/parser_service.rs index 3fefa00bd..621d0ebc8 100644 --- a/bin/gateway/src/pipeline/parser_service.rs +++ b/bin/gateway/src/pipeline/parser_service.rs @@ -1,19 +1,24 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::sync::Arc; + use axum::body::Body; use graphql_parser::query::Document; use http::Request; -use query_plan_executor::ExecutionRequest; use query_planner::utils::parsing::safe_parse_operation; use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; +use crate::pipeline::graphql_request_params::ExecutionRequest; use crate::pipeline::http_request_params::HttpRequestParams; +use crate::shared_state::GatewaySharedState; use tracing::{error, trace}; #[derive(Debug, Clone)] pub struct GraphQLParserPayload { - pub parsed_operation: Document<'static, String>, + pub parsed_operation: Arc>, + pub cache_key: u64, } #[derive(Clone, Debug, Default)] @@ -39,23 +44,44 @@ impl GatewayPipelineLayer for GraphQLParserService { PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") })?; - match safe_parse_operation(&execution_params.query) { - Ok(parsed_operation) => { - trace!("sucessfully parsed GraphQL operation"); + let app_state = req + .extensions() + .get::>() + .ok_or_else(|| { + PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing") + })?; - req.extensions_mut() - .insert(GraphQLParserPayload { parsed_operation }); + let cache_key = { + let mut hasher = DefaultHasher::new(); + execution_params.query.hash(&mut hasher); + hasher.finish() + }; - Ok((req, GatewayPipelineStepDecision::Continue)) - } - Err(err) => { + let parsed_operation = if let Some(cached) = app_state.parse_cache.get(&cache_key).await { + trace!("Found cached parsed operation for query"); + cached + } else { + let parsed = safe_parse_operation(&execution_params.query).map_err(|err| { error!("Failed to parse GraphQL operation: {}", err); - - Err(PipelineError::new_with_accept_header( + PipelineError::new_with_accept_header( PipelineErrorVariant::FailedToParseOperation(err), http_params.accept_header.clone(), - )) - } - } + ) + })?; + trace!("sucessfully parsed GraphQL operation"); + let parsed_arc = Arc::new(parsed); + app_state + .parse_cache + .insert(cache_key, parsed_arc.clone()) + .await; + parsed_arc + }; + + req.extensions_mut().insert(GraphQLParserPayload { + parsed_operation, + cache_key, + }); + + Ok((req, GatewayPipelineStepDecision::Continue)) } } diff --git a/bin/gateway/src/pipeline/query_plan_service.rs b/bin/gateway/src/pipeline/query_plan_service.rs index 8108ce746..b22248d0b 100644 --- a/bin/gateway/src/pipeline/query_plan_service.rs +++ b/bin/gateway/src/pipeline/query_plan_service.rs @@ -72,7 +72,7 @@ impl GatewayPipelineLayer for QueryPlanService { ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { let normalized_operation = req .extensions() - .get::() + .get::>() .ok_or_else(|| { PipelineErrorVariant::InternalServiceError("GraphQLNormalizationPayload is missing") })?; diff --git a/bin/gateway/src/pipeline/validation_service.rs b/bin/gateway/src/pipeline/validation_service.rs index 561b1f93c..d3d0684cc 100644 --- a/bin/gateway/src/pipeline/validation_service.rs +++ b/bin/gateway/src/pipeline/validation_service.rs @@ -5,13 +5,11 @@ use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; use crate::pipeline::http_request_params::HttpRequestParams; -use crate::pipeline::normalize_service::GraphQLNormalizationPayload; use crate::pipeline::parser_service::GraphQLParserPayload; use crate::shared_state::GatewaySharedState; use axum::body::Body; use graphql_tools::validation::validate::validate; -use http::{Method, Request}; -use query_planner::state::supergraph_state::OperationKind; +use http::Request; use tracing::{error, trace}; #[derive(Clone, Debug, Default)] @@ -30,17 +28,6 @@ impl GatewayPipelineLayer for GraphQLValidationService { &self, req: Request, ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { - let normalized_operation = req - .extensions() - .get::() - .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GraphQLNormalizationPayload is missing") - })?; - - let http_params = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") - })?; - let parser_payload = req .extensions() .get::() @@ -55,26 +42,17 @@ impl GatewayPipelineLayer for GraphQLValidationService { PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing") })?; - if http_params.http_method == Method::GET { - if let Some(OperationKind::Mutation) = normalized_operation - .normalized_document - .operation - .operation_kind - { - error!("Mutation is not allowed over GET, stopping"); - - return Err(PipelineErrorVariant::MutationNotAllowedOverHttpGet.into()); - } - } - let consumer_schema_ast = &app_state.planner.consumer_schema.document; - let validation_cache_key = normalized_operation.normalized_document.operation.hash(); - let validation_result = match app_state.validate_cache.get(&validation_cache_key).await { + let validation_result = match app_state + .validate_cache + .get(&parser_payload.cache_key) + .await + { Some(cached_validation) => { trace!( "validation result of hash {} has been loaded from cache", - validation_cache_key + parser_payload.cache_key ); cached_validation @@ -82,7 +60,7 @@ impl GatewayPipelineLayer for GraphQLValidationService { None => { trace!( "validation result of hash {} does not exists in cache", - validation_cache_key + parser_payload.cache_key ); let res = validate( @@ -94,7 +72,7 @@ impl GatewayPipelineLayer for GraphQLValidationService { app_state .validate_cache - .insert(validation_cache_key, arc_res.clone()) + .insert(parser_payload.cache_key, arc_res.clone()) .await; arc_res } @@ -107,7 +85,14 @@ impl GatewayPipelineLayer for GraphQLValidationService { ); trace!("Validation errors: {:?}", validation_result); - return Err(PipelineErrorVariant::ValidationErrors(validation_result).into()); + let http_payload = req.extensions().get::().ok_or_else(|| { + PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") + })?; + + return Err(PipelineError::new_with_accept_header( + PipelineErrorVariant::ValidationErrors(validation_result), + http_payload.accept_header.clone(), + )); } Ok((req, GatewayPipelineStepDecision::Continue)) diff --git a/bin/gateway/src/shared_state.rs b/bin/gateway/src/shared_state.rs index b7ff558f4..9227d3db2 100644 --- a/bin/gateway/src/shared_state.rs +++ b/bin/gateway/src/shared_state.rs @@ -10,6 +10,8 @@ use query_planner::{ state::supergraph_state::SupergraphState, }; +use crate::pipeline::normalize_service::GraphQLNormalizationPayload; + pub struct GatewaySharedState { pub schema_metadata: SchemaMetadata, pub planner: Planner, @@ -18,6 +20,8 @@ pub struct GatewaySharedState { pub subgraph_endpoint_map: HashMap, pub plan_cache: Cache>, pub validate_cache: Cache>>, + pub parse_cache: Cache>>, + pub normalize_cache: Cache>, } impl GatewaySharedState { @@ -39,6 +43,8 @@ impl GatewaySharedState { subgraph_endpoint_map, plan_cache: moka::future::Cache::new(1000), validate_cache: moka::future::Cache::new(1000), + parse_cache: moka::future::Cache::new(1000), + normalize_cache: moka::future::Cache::new(1000), }) } } diff --git a/lib/query-plan-executor/src/executors/async_graphql.rs b/lib/query-plan-executor/src/executors/async_graphql.rs index c66ce4bbf..5043150a0 100644 --- a/lib/query-plan-executor/src/executors/async_graphql.rs +++ b/lib/query-plan-executor/src/executors/async_graphql.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use serde_json::json; use crate::{ - executors::common::SubgraphExecutor, ExecutionRequest, ExecutionResult, GraphQLError, - GraphQLErrorLocation, + executors::common::SubgraphExecutor, ExecutionResult, GraphQLError, GraphQLErrorLocation, + SubgraphExecutionRequest, }; #[async_trait] @@ -13,14 +13,17 @@ impl SubgraphExecutor for Executor where Executor: async_graphql::Executor, { - async fn execute(&self, execution_request: ExecutionRequest) -> ExecutionResult { + async fn execute<'a>( + &self, + execution_request: SubgraphExecutionRequest<'a>, + ) -> ExecutionResult { let response: async_graphql::Response = self.execute(execution_request.into()).await; response.into() } } -impl From for async_graphql::Request { - fn from(exec_request: ExecutionRequest) -> Self { +impl<'a> From> for async_graphql::Request { + fn from(exec_request: SubgraphExecutionRequest) -> Self { let mut req = async_graphql::Request::new(exec_request.query); if let Some(variables) = exec_request.variables { req = req.variables(async_graphql::Variables::from_json(json!(variables))); diff --git a/lib/query-plan-executor/src/executors/common.rs b/lib/query-plan-executor/src/executors/common.rs index dbb10e0b7..b6d8a2f55 100644 --- a/lib/query-plan-executor/src/executors/common.rs +++ b/lib/query-plan-executor/src/executors/common.rs @@ -2,11 +2,12 @@ use std::sync::Arc; use async_trait::async_trait; -use crate::{ExecutionRequest, ExecutionResult}; +use crate::{ExecutionResult, SubgraphExecutionRequest}; #[async_trait] pub trait SubgraphExecutor { - async fn execute(&self, execution_request: ExecutionRequest) -> ExecutionResult; + async fn execute<'a>(&self, execution_request: SubgraphExecutionRequest<'a>) + -> ExecutionResult; fn to_boxed_arc<'a>(self) -> Arc> where Self: Sized + Send + Sync + 'a, diff --git a/lib/query-plan-executor/src/executors/http.rs b/lib/query-plan-executor/src/executors/http.rs index 89a9f0507..269f99211 100644 --- a/lib/query-plan-executor/src/executors/http.rs +++ b/lib/query-plan-executor/src/executors/http.rs @@ -1,7 +1,10 @@ use async_trait::async_trait; use tracing::{error, instrument, trace}; -use crate::{executors::common::SubgraphExecutor, ExecutionRequest, ExecutionResult}; +use crate::{ + executors::common::SubgraphExecutor, json_writer::write_and_escape_string, ExecutionResult, + SubgraphExecutionRequest, +}; #[derive(Debug)] pub struct HTTPSubgraphExecutor { @@ -19,14 +22,16 @@ impl HTTPSubgraphExecutor { } } - async fn _execute( + async fn _execute<'a>( &self, - execution_request: ExecutionRequest, + execution_request: SubgraphExecutionRequest<'a>, ) -> Result { trace!("Executing HTTP request to subgraph at {}", self.endpoint); - let mut body = - "{\"query\":".to_string() + &serde_json::to_string(&execution_request.query).unwrap(); + // We may want to remove it, but let's see. + let mut body = String::with_capacity(4096); + body.push_str("{\"query\":"); + write_and_escape_string(&mut body, execution_request.query); let mut first_variable = true; if let Some(variables) = &execution_request.variables { for (variable_name, variable_value) in variables { @@ -87,7 +92,10 @@ impl HTTPSubgraphExecutor { #[async_trait] impl SubgraphExecutor for HTTPSubgraphExecutor { #[instrument(level = "trace", skip(self), name = "http_subgraph_execute", fields(endpoint = %self.endpoint))] - async fn execute(&self, execution_request: ExecutionRequest) -> ExecutionResult { + async fn execute<'a>( + &self, + execution_request: SubgraphExecutionRequest<'a>, + ) -> ExecutionResult { self._execute(execution_request).await.unwrap_or_else(|e| { error!(e); ExecutionResult::from_error_message(e) diff --git a/lib/query-plan-executor/src/executors/map.rs b/lib/query-plan-executor/src/executors/map.rs index a27c8b14a..d80bcb132 100644 --- a/lib/query-plan-executor/src/executors/map.rs +++ b/lib/query-plan-executor/src/executors/map.rs @@ -22,10 +22,10 @@ impl SubgraphExecutorMap { } #[instrument(level = "trace", name = "subgraph_execute", skip_all, fields(subgraph_name = %subgraph_name, execution_request = ?execution_request))] - pub async fn execute( + pub async fn execute<'a>( &self, subgraph_name: &str, - execution_request: crate::ExecutionRequest, + execution_request: crate::SubgraphExecutionRequest<'a>, ) -> crate::ExecutionResult { match self.inner.get(subgraph_name) { Some(executor) => executor.execute(execution_request).await, diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 0909fffde..c1de67a89 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use futures::future::BoxFuture; +use futures::{future::BoxFuture, stream::FuturesUnordered, FutureExt, StreamExt}; use query_planner::{ ast::{operation::OperationDefinition, selection_item::SelectionItem}, planner::plan_nodes::{ @@ -10,14 +10,14 @@ use query_planner::{ }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use std::collections::HashSet; use std::{collections::BTreeSet, fmt::Write}; use std::{collections::HashMap, vec}; use tracing::{instrument, trace, warn}; // For reading file in main use crate::{ - executors::map::SubgraphExecutorMap, json_writer::write_and_escape_string, - schema_metadata::SchemaMetadata, + executors::map::SubgraphExecutorMap, + json_writer::write_and_escape_string, + schema_metadata::{PossibleTypes, SchemaMetadata}, }; pub mod deep_merge; pub mod executors; @@ -117,15 +117,11 @@ trait ExecutableFetchNode { filtered_representations: String, indexes: BTreeSet, ) -> ExecuteForRepresentationsResult; - fn apply_output_rewrites( - &self, - possible_types: &HashMap>, - data: &mut Value, - ); - fn prepare_variables_for_fetch_node( - &self, - variable_values: &Option>, - ) -> Option>; + fn apply_output_rewrites(&self, possible_types: &PossibleTypes, data: &mut Value); + fn prepare_variables_for_fetch_node<'a>( + &'a self, + variable_values: &'a Option>, + ) -> Option>; } #[async_trait] @@ -160,9 +156,9 @@ impl ExecutableFetchNode for FetchNode { ) -> ExecutionResult { let variables = self.prepare_variables_for_fetch_node(execution_context.variable_values); - let execution_request = ExecutionRequest { - query: self.operation.document_str.clone(), - operation_name: self.operation_name.clone(), + let execution_request = SubgraphExecutionRequest { + query: &self.operation.document_str, + operation_name: self.operation_name.as_deref(), variables, extensions: None, representations: None, @@ -196,9 +192,9 @@ impl ExecutableFetchNode for FetchNode { indexes: BTreeSet, ) -> ExecuteForRepresentationsResult { // 2. Prepare variables for fetch - let execution_request = ExecutionRequest { - query: self.operation.document_str.clone(), - operation_name: self.operation_name.clone(), + let execution_request = SubgraphExecutionRequest { + query: &self.operation.document_str, + operation_name: self.operation_name.as_deref(), variables: self.prepare_variables_for_fetch_node(execution_context.variable_values), extensions: None, representations: Some(filtered_representations), @@ -234,11 +230,7 @@ impl ExecutableFetchNode for FetchNode { } } - fn apply_output_rewrites( - &self, - possible_types: &HashMap>, - data: &mut Value, - ) { + fn apply_output_rewrites(&self, possible_types: &PossibleTypes, data: &mut Value) { if let Some(output_rewrites) = &self.output_rewrites { for rewrite in output_rewrites { rewrite.apply(possible_types, data); @@ -251,10 +243,10 @@ impl ExecutableFetchNode for FetchNode { skip(self, variable_values), name = "prepare_variables_for_fetch_node" )] - fn prepare_variables_for_fetch_node( - &self, - variable_values: &Option>, - ) -> Option> { + fn prepare_variables_for_fetch_node<'a>( + &'a self, + variable_values: &'a Option>, + ) -> Option> { match (&self.variable_usages, variable_values) { (Some(ref variable_usages), Some(variable_values)) => { if variable_usages.is_empty() || variable_values.is_empty() { @@ -266,7 +258,7 @@ impl ExecutableFetchNode for FetchNode { .filter_map(|variable_name| { variable_values .get(variable_name) - .map(|v| (variable_name.to_string(), v.clone())) + .map(|v| (variable_name.as_str(), v)) }) .collect(), ) @@ -278,28 +270,18 @@ impl ExecutableFetchNode for FetchNode { } trait ApplyFetchRewrite { - fn apply(&self, possible_types: &HashMap>, value: &mut Value); - fn apply_path( - &self, - possible_types: &HashMap>, - value: &mut Value, - path: &[FetchNodePathSegment], - ); + fn apply(&self, possible_types: &PossibleTypes, value: &mut Value); + fn apply_path(&self, possible_types: &PossibleTypes, value: &mut Value, path: &[FetchNodePathSegment]); } impl ApplyFetchRewrite for FetchRewrite { - fn apply(&self, possible_types: &HashMap>, value: &mut Value) { + fn apply(&self, possible_types: &PossibleTypes, value: &mut Value) { match self { FetchRewrite::KeyRenamer(renamer) => renamer.apply(possible_types, value), FetchRewrite::ValueSetter(setter) => setter.apply(possible_types, value), } } - fn apply_path( - &self, - possible_types: &HashMap>, - value: &mut Value, - path: &[FetchNodePathSegment], - ) { + fn apply_path(&self, possible_types: &PossibleTypes, value: &mut Value, path: &[FetchNodePathSegment]) { match self { FetchRewrite::KeyRenamer(renamer) => renamer.apply_path(possible_types, value, path), FetchRewrite::ValueSetter(setter) => setter.apply_path(possible_types, value, path), @@ -308,16 +290,11 @@ impl ApplyFetchRewrite for FetchRewrite { } impl ApplyFetchRewrite for KeyRenamer { - fn apply(&self, possible_types: &HashMap>, value: &mut Value) { + fn apply(&self, possible_types: &PossibleTypes, value: &mut Value) { self.apply_path(possible_types, value, &self.path) } // Applies key rename operation on a Value (mutably) - fn apply_path( - &self, - possible_types: &HashMap>, - value: &mut Value, - path: &[FetchNodePathSegment], - ) { + fn apply_path(&self, possible_types: &PossibleTypes, value: &mut Value, path: &[FetchNodePathSegment]) { let current_segment = &path[0]; let remaining_path = &path[1..]; @@ -334,11 +311,8 @@ impl ApplyFetchRewrite for KeyRenamer { Some(Value::String(type_name)) => type_name, _ => type_condition, // Default to type_condition if not found }; - let satisfies_type_condition = type_name == type_condition - || possible_types - .get(type_name) - .is_some_and(|s| s.contains(type_condition)); - if satisfies_type_condition { + if possible_types.entity_satisfies_type_condition(type_name, type_condition) + { self.apply_path(possible_types, value, remaining_path) } } @@ -361,17 +335,12 @@ impl ApplyFetchRewrite for KeyRenamer { } impl ApplyFetchRewrite for ValueSetter { - fn apply(&self, possible_types: &HashMap>, data: &mut Value) { + fn apply(&self, possible_types: &PossibleTypes, data: &mut Value) { self.apply_path(possible_types, data, &self.path) } // Applies value setting on a Value (returns a new Value) - fn apply_path( - &self, - possible_types: &HashMap>, - data: &mut Value, - path: &[FetchNodePathSegment], - ) { + fn apply_path(&self, possible_types: &PossibleTypes, data: &mut Value, path: &[FetchNodePathSegment]) { if path.is_empty() { *data = self.set_value_to.to_owned(); return; @@ -394,11 +363,7 @@ impl ApplyFetchRewrite for ValueSetter { Some(Value::String(type_name)) => type_name, _ => type_condition, // Default to type_condition if not found }; - let satisfies_type_condition = type_name == type_condition - || possible_types - .get(type_name) - .is_some_and(|s| s.contains(type_condition)); - if satisfies_type_condition { + if possible_types.entity_satisfies_type_condition(type_name, type_condition) { self.apply_path(possible_types, data, remaining_path) } } @@ -462,86 +427,9 @@ fn process_root_result( ); } -type ExecutionStepJob<'a, T> = BoxFuture<'a, T>; - -#[derive(Default)] -struct ExecutionStep<'a> { - fetch_job: Option>, - flatten_job: Option>, - flatten_path: Option<&'a FlattenNodePath>, -} - -fn create_execution_step<'a>( - node: &'a PlanNode, - execution_context: &'a QueryPlanExecutionContext<'a>, - data: &mut Value, -) -> ExecutionStep<'a> { - match node { - PlanNode::Fetch(fetch_node) => ExecutionStep { - fetch_job: Some(fetch_node.execute_for_root(execution_context)), - ..Default::default() - }, - PlanNode::Flatten(flatten_node) => { - let PlanNode::Fetch(fetch_node) = flatten_node.node.as_ref() else { - warn!( - "FlattenNode can only execute FetchNode as child node, found: {:?}", - flatten_node.node - ); - return ExecutionStep::default(); - }; - - let collected_representations = - traverse_and_collect(data, flatten_node.path.as_slice()); - - if collected_representations.is_empty() { - // No representations collected, skip execution - return ExecutionStep::default(); - } - - let project_result = - fetch_node.project_representations(execution_context, &collected_representations); - - if project_result.representations.is_empty() { - // No representations collected, skip execution - return ExecutionStep::default(); - } - - let job = fetch_node.execute_for_projected_representations( - execution_context, - project_result.representations, - project_result.indexes, - ); - - ExecutionStep { - flatten_job: Some(job), - flatten_path: Some(&flatten_node.path), - ..Default::default() - } - } - PlanNode::Condition(node) => { - let condition_value = execution_context - .variable_values - .as_ref() - .and_then(|vars| vars.get(&node.condition)) - .is_some_and(|val| match val { - Value::Bool(b) => *b, - _ => false, - }); - - let clause = if condition_value { - node.if_clause.as_deref() - } else { - node.else_clause.as_deref() - }; - - if let Some(clause) = clause { - create_execution_step(clause, execution_context, data) - } else { - ExecutionStep::default() - } - } - _ => ExecutionStep::default(), - } +enum ParallelJob<'a> { + Root(ExecutionResult), + Flatten((ExecuteForRepresentationsResult, Vec<&'a str>)), } #[async_trait] @@ -554,137 +442,138 @@ impl ExecutablePlanNode for ParallelNode { execution_context: &mut QueryPlanExecutionContext<'_>, data: &mut Value, ) { - // Here we call fetch nodes in parallel and non-fetch nodes sequentially. - let mut fetch_jobs = vec![]; - let mut flatten_jobs = vec![]; - let mut flatten_paths = vec![]; + let mut all_errors = vec![]; + let mut all_extensions = vec![]; - // Collect Fetch node results and non-fetch nodes for sequential execution - let now = std::time::Instant::now(); - for node in &self.nodes { - match node { - PlanNode::Fetch(fetch_node) => { - // Execute FetchNode in parallel - let job = fetch_node.execute_for_root(execution_context); - fetch_jobs.push(job); - } - PlanNode::Flatten(flatten_node) => { - let normalized_path: Vec<&str> = - flatten_node.path.iter().map(String::as_str).collect(); - let mut representations = String::with_capacity(1024); - let fetch_node = match flatten_node.node.as_ref() { - PlanNode::Fetch(fetch_node) => fetch_node, - _ => { - warn!( + { + let mut jobs: FuturesUnordered> = FuturesUnordered::new(); + + // Collect Fetch node results and flatten nodes for parallel execution + let now = std::time::Instant::now(); + for node in &self.nodes { + match node { + PlanNode::Fetch(fetch_node) => { + let job = fetch_node.execute_for_root(execution_context); + jobs.push(Box::pin(job.map(ParallelJob::Root))); + } + PlanNode::Flatten(flatten_node) => { + let normalized_path: Vec<&str> = + flatten_node.path.iter().map(String::as_str).collect(); + let mut filtered_representations = String::with_capacity(1024); + let fetch_node = match flatten_node.node.as_ref() { + PlanNode::Fetch(fetch_node) => fetch_node, + _ => { + warn!( "FlattenNode can only execute FetchNode as child node, found: {:?}", flatten_node.node ); - continue; // Skip if the child node is not a FetchNode - } - }; - let requires_nodes = fetch_node.requires.as_ref().unwrap(); - let mut index = 0; - let mut indexes = BTreeSet::new(); - representations.push('['); - traverse_and_callback(data, &normalized_path, &mut |entity| { - let is_projected = execution_context.project_requires( - &requires_nodes.items, - entity, - &mut representations, - indexes.is_empty(), - None, + continue; // Skip if the child node is not a FetchNode + } + }; + let requires_nodes = fetch_node.requires.as_ref().unwrap(); + let mut index = 0; + let mut indexes = BTreeSet::new(); + filtered_representations.push('['); + traverse_and_callback(data, &normalized_path, &mut |entity| { + let is_projected = + if let Some(input_rewrites) = &fetch_node.input_rewrites { + // We need to own the value and not modify the original entity + let mut entity_owned = entity.to_owned(); + for input_rewrite in input_rewrites { + input_rewrite.apply( + &execution_context.schema_metadata.possible_types, + &mut entity_owned, + ); + } + execution_context.project_requires( + &requires_nodes.items, + &entity_owned, + &mut filtered_representations, + indexes.is_empty(), + None, + ) + } else { + execution_context.project_requires( + &requires_nodes.items, + entity, + &mut filtered_representations, + indexes.is_empty(), + None, + ) + }; + if is_projected { + indexes.insert(index); + } + index += 1; + }); + filtered_representations.push(']'); + let job = fetch_node.execute_for_projected_representations( + execution_context, + filtered_representations, + indexes, ); - if is_projected { - indexes.insert(index); - } - index += 1; - }); - representations.push(']'); - let job = fetch_node.execute_for_projected_representations( - execution_context, - representations, - indexes, - ); - flatten_jobs.push(job); - flatten_paths.push(normalized_path); + jobs.push(Box::pin( + job.map(|r| ParallelJob::Flatten((r, normalized_path))), + )); + } + _ => {} } - _ => {} } - } - trace!( - "Prepared {} fetch jobs and {} flatten jobs in {:?}", - fetch_jobs.len(), - flatten_jobs.len(), - now.elapsed() - ); - - let mut all_errors = vec![]; - let mut all_extensions = vec![]; - - let now = std::time::Instant::now(); - - let flatten_results = futures::future::join_all(flatten_jobs).await; - let flatten_results_len = flatten_results.len(); - - trace!( - "Executed {} flatten jobs in {:?}", - flatten_results_len, - now.elapsed() - ); - - let now = std::time::Instant::now(); - for (result, path) in flatten_results.into_iter().zip(flatten_paths) { - // Process FlattenNode results - if let Some(mut entities) = result.entities { - let mut index_of_traverse = 0; - let mut index_of_entities = 0; - traverse_and_callback(data, &path, &mut |target| { - if result.indexes.contains(&index_of_traverse) { - let entity = entities.get_mut(index_of_entities).unwrap().take(); - // Merge the entity into the target - deep_merge::deep_merge(target, entity); - index_of_entities += 1; + trace!("Prepared {} jobs in {:?}", jobs.len(), now.elapsed()); + + let now = std::time::Instant::now(); + while let Some(result) = jobs.next().await { + match result { + ParallelJob::Root(fetch_result) => { + // Process root FetchNode results + if let Some(new_data) = fetch_result.data { + if data.is_null() { + *data = new_data; // Initialize with new_data + } else { + deep_merge::deep_merge(data, new_data); + } + } + // Process errors and extensions + if let Some(errors) = fetch_result.errors { + all_errors.extend(errors); + } + if let Some(extensions) = fetch_result.extensions { + all_extensions.push(extensions); + } } - index_of_traverse += 1; - }); - } - // Extend errors and extensions from the result - if let Some(errors) = result.errors { - all_errors.extend(errors); - } - if let Some(extensions) = result.extensions { - all_extensions.push(extensions); + ParallelJob::Flatten((result, path)) => { + if let Some(mut entities) = result.entities { + let mut index_of_traverse = 0; + let mut index_of_entities = 0; + traverse_and_callback(data, &path, &mut |target| { + if result.indexes.contains(&index_of_traverse) { + let entity = + entities.get_mut(index_of_entities).unwrap().take(); + // Merge the entity into the target + deep_merge::deep_merge(target, entity); + index_of_entities += 1; + } + index_of_traverse += 1; + }); + } + // Process errors and extensions + if let Some(errors) = result.errors { + all_errors.extend(errors); + } + if let Some(extensions) = result.extensions { + all_extensions.push(extensions); + } + } + } } - } - - trace!( - "Processed {} flatten results in {:?}", - flatten_results_len, - now.elapsed() - ); - let now = std::time::Instant::now(); - let fetch_results = futures::future::join_all(fetch_jobs).await; - let fetch_results_len = fetch_results.len(); - trace!( - "Executed {} fetch jobs in {:?}", - fetch_results_len, - now.elapsed() - ); - - let now = std::time::Instant::now(); - // Process results from FetchNode executions - for fetch_result in fetch_results { - process_root_result(fetch_result, execution_context, data); + trace!( + "Processed {} parallel jobs in {:?}", + jobs.len(), + now.elapsed() + ); } - - trace!( - "Processed {} fetch results in {:?}", - fetch_results_len, - now.elapsed() - ); - - // Process errors and extensions from FlattenNode results + // 6. Process errors and extensions if !all_errors.is_empty() { execution_context.errors.extend(all_errors); } @@ -724,15 +613,33 @@ impl ExecutablePlanNode for FlattenNode { let requires_nodes = fetch_node.requires.as_ref().unwrap(); filtered_representations.push('['); let mut first = true; - traverse_and_callback(data, normalized_path.as_slice(), &mut |entity| { - let entity_projected = execution_context.project_requires( - &requires_nodes.items, - entity, - &mut filtered_representations, - first, - None, - ); - if entity_projected { + traverse_and_callback(data, &normalized_path, &mut |entity| { + let is_projected = if let Some(input_rewrites) = &fetch_node.input_rewrites { + // We need to own the value and not modify the original entity + let mut entity_owned = entity.to_owned(); + for input_rewrite in input_rewrites { + input_rewrite.apply( + &execution_context.schema_metadata.possible_types, + &mut entity_owned, + ); + } + execution_context.project_requires( + &requires_nodes.items, + &entity_owned, + &mut filtered_representations, + first, + None, + ) + } else { + execution_context.project_requires( + &requires_nodes.items, + entity, + &mut filtered_representations, + first, + None, + ) + }; + if is_projected { representations.push(entity); first = false; } @@ -847,12 +754,11 @@ pub struct GraphQLErrorLocation { pub column: usize, } -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ExecutionRequest { - pub query: String, - pub operation_name: Option, - pub variables: Option>, +#[derive(Debug, Clone)] +pub struct SubgraphExecutionRequest<'a> { + pub query: &'a str, + pub operation_name: Option<&'a str>, + pub variables: Option>, pub extensions: Option>, pub representations: Option, } @@ -1053,15 +959,16 @@ impl QueryPlanExecutionContext<'_> { Some(Value::String(type_name)) => type_name, _ => type_condition, }; - - let satisfies_type_condition = type_name == type_condition + // For projection, both sides of the condition are valid + if self + .schema_metadata + .possible_types + .entity_satisfies_type_condition(type_name, type_condition) || self .schema_metadata .possible_types - .get(type_condition) - .is_some_and(|s| s.contains(type_name)); - - if satisfies_type_condition { + .entity_satisfies_type_condition(type_condition, type_name) + { self.project_requires_map_mut( &requires_selection.selections.items, entity_obj, diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index a8214aa6a..fe9b7a92b 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -81,7 +81,7 @@ pub fn project_by_operation( ) )] fn project_selection_set( - data: &mut Value, + data: &Value, errors: &mut Vec, selection_set: &SelectionSet, type_name: &str, @@ -97,7 +97,6 @@ fn project_selection_set( Value::String(value) => { if let Some(enum_values) = schema_metadata.enum_values.get(type_name) { if !enum_values.contains(value) { - *data = Value::Null; errors.push(GraphQLError { message: format!( "Value is not a valid enum value for type '{}'", @@ -116,7 +115,7 @@ fn project_selection_set( Value::Array(arr) => { buffer.push('['); let mut first = true; - for item in arr.iter_mut() { + for item in arr.iter() { if !first { buffer.push(','); } @@ -171,7 +170,7 @@ fn project_selection_set( // TODO: simplfy args #[allow(clippy::too_many_arguments)] fn project_selection_set_with_map( - obj: &mut Map, + obj: &Map, errors: &mut Vec, selection_set: &SelectionSet, type_name: &str, @@ -183,9 +182,8 @@ fn project_selection_set_with_map( let type_name = match obj.get(TYPENAME_FIELD) { Some(Value::String(type_name)) => type_name, _ => type_name, - } - .to_string(); - let field_map = match schema_metadata.type_fields.get(&type_name) { + }; + let field_map = match schema_metadata.type_fields.get(type_name) { Some(field_map) => field_map, None => { // If the type is not found, we can't project anything @@ -196,7 +194,6 @@ fn project_selection_set_with_map( return false; } }; - let possible_types_of_type = schema_metadata.possible_types.get(&type_name); for selection in &selection_set.items { match selection { @@ -231,7 +228,7 @@ fn project_selection_set_with_map( buffer.push('"'); buffer.push_str(response_key); buffer.push_str("\":\""); - buffer.push_str(&type_name); + buffer.push_str(type_name); buffer.push('"'); continue; } @@ -240,53 +237,43 @@ fn project_selection_set_with_map( buffer.push_str(response_key); buffer.push_str("\":"); - let field_type = field_map.get(&field.name); - - if field.name == "__schema" && type_name == "Query" { - obj.insert( - response_key.to_string(), - schema_metadata.introspection_schema_root_json.clone(), - ); - } + let field_type: &str = field_map + .get(&field.name) + .map(|s| s.as_str()) + .unwrap_or("Any"); - let field_val = obj.get_mut(response_key); + let field_val = if field.name == "__schema" && type_name == "Query" { + Some(&schema_metadata.introspection_schema_root_json) + } else { + obj.get(response_key) + }; - match (field_type, field_val) { - (Some(field_type), Some(field_val)) => { - project_selection_set( - field_val, - errors, - &field.selections, - field_type, - schema_metadata, - variable_values, - buffer, - ); - } - (Some(_field_type), None) => { - // If the field is not found in the object, set it to Null - buffer.push_str("null"); - } - (None, _) => { - // It won't reach here already, as the selection should be validated before projection - warn!( - "Field {} not found in type {}. Skipping projection.", - field.name, type_name - ); - } + if let Some(field_val) = field_val { + project_selection_set( + field_val, + errors, + &field.selections, + field_type, + schema_metadata, + variable_values, + buffer, + ); + } else { + // If the field is not found in the object, set it to Null + buffer.push_str("null"); + continue; } } SelectionItem::InlineFragment(inline_fragment) => { - let type_condition = &inline_fragment.type_condition; - let satisfies_type_condition = &type_name == type_condition - || possible_types_of_type.is_some_and(|s| s.contains(type_condition)); - - if satisfies_type_condition { + if schema_metadata + .possible_types + .entity_satisfies_type_condition(type_name, &inline_fragment.type_condition) + { project_selection_set_with_map( obj, errors, &inline_fragment.selections, - &type_name, + type_name, schema_metadata, variable_values, buffer, diff --git a/lib/query-plan-executor/src/schema_metadata.rs b/lib/query-plan-executor/src/schema_metadata.rs index a8c9189b3..7dd227f60 100644 --- a/lib/query-plan-executor/src/schema_metadata.rs +++ b/lib/query-plan-executor/src/schema_metadata.rs @@ -9,12 +9,29 @@ use serde_json::{json, Value}; #[derive(Debug)] pub struct SchemaMetadata { - pub possible_types: HashMap>, + pub possible_types: PossibleTypes, pub enum_values: HashMap>, pub type_fields: HashMap>, pub introspection_schema_root_json: Value, } +#[derive(Debug)] +pub struct PossibleTypes { + map: HashMap>, +} + +impl PossibleTypes { + pub fn entity_satisfies_type_condition(&self, type_name: &str, type_condition: &str) -> bool { + if type_name == type_condition { + true + } else if let Some(possible_types_of_type) = self.map.get(type_condition) { + possible_types_of_type.contains(type_name) + } else { + false + } + } +} + pub trait SchemaWithMetadata { fn schema_metadata(&self) -> SchemaMetadata; } @@ -98,7 +115,9 @@ impl SchemaWithMetadata for ConsumerSchema { let introspection_schema_root_json = json!(introspection_query.__schema); SchemaMetadata { - possible_types: final_possible_types, + possible_types: PossibleTypes { + map: final_possible_types, + }, enum_values, type_fields, introspection_schema_root_json, From b782fbabd6cf1d0dd6b6a77a354d47d148994fff Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Tue, 8 Jul 2025 14:23:51 +0300 Subject: [PATCH 23/35] Fixes based on main --- bin/gateway/src/pipeline/execution_service.rs | 11 +--- .../benches/executor_benches.rs | 6 +-- lib/query-plan-executor/src/lib.rs | 52 +++++++++++++++---- lib/query-plan-executor/src/tests/mod.rs | 2 +- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/bin/gateway/src/pipeline/execution_service.rs b/bin/gateway/src/pipeline/execution_service.rs index 5329cb984..2196e5069 100644 --- a/bin/gateway/src/pipeline/execution_service.rs +++ b/bin/gateway/src/pipeline/execution_service.rs @@ -11,8 +11,8 @@ use crate::pipeline::query_plan_service::QueryPlanPayload; use crate::shared_state::GatewaySharedState; use axum::body::Body; use http::header::CONTENT_TYPE; -use http::{Request, Response}; -use query_plan_executor::execute_query_plan; +use http::{HeaderName, Request, Response}; +use query_plan_executor::{execute_query_plan, ExposeQueryPlanMode}; use tower::Service; #[derive(Clone, Debug, Default)] @@ -22,13 +22,6 @@ pub struct ExecutionService { static EXPOSE_QUERY_PLAN_HEADER: HeaderName = HeaderName::from_static("hive-expose-query-plan"); -#[derive(Clone, Debug, PartialEq, Eq)] -enum ExposeQueryPlanMode { - Yes, - No, - DryRun, -} - impl ExecutionService { pub fn new(expose_query_plan: bool) -> Self { Self { expose_query_plan } diff --git a/lib/query-plan-executor/benches/executor_benches.rs b/lib/query-plan-executor/benches/executor_benches.rs index edf262ced..10e3faf35 100644 --- a/lib/query-plan-executor/benches/executor_benches.rs +++ b/lib/query-plan-executor/benches/executor_benches.rs @@ -11,9 +11,9 @@ use query_planner::graph::PlannerOverrideContext; use query_planner::planner::plan_nodes::FlattenNodePathSegment; use std::hint::black_box; -use query_plan_executor::execute_query_plan; use query_plan_executor::schema_metadata::SchemaWithMetadata; use query_plan_executor::ExecutableQueryPlan; +use query_plan_executor::{execute_query_plan, ExposeQueryPlanMode}; use query_planner::ast::normalization::normalize_operation; use query_planner::utils::parsing::parse_operation; use query_planner::utils::parsing::parse_schema; @@ -57,7 +57,7 @@ fn query_plan_executor_pipeline_via_http(c: &mut Criterion) { schema_metadata, operation, has_introspection, - false, + ExposeQueryPlanMode::No, ) .await; black_box(result) @@ -149,7 +149,7 @@ fn query_plan_executor_pipeline_locally(c: &mut Criterion) { schema_metadata, operation, has_introspection, - false, + ExposeQueryPlanMode::No, ) .await; black_box(result) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index c1de67a89..21b2d8233 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -271,7 +271,12 @@ impl ExecutableFetchNode for FetchNode { trait ApplyFetchRewrite { fn apply(&self, possible_types: &PossibleTypes, value: &mut Value); - fn apply_path(&self, possible_types: &PossibleTypes, value: &mut Value, path: &[FetchNodePathSegment]); + fn apply_path( + &self, + possible_types: &PossibleTypes, + value: &mut Value, + path: &[FetchNodePathSegment], + ); } impl ApplyFetchRewrite for FetchRewrite { @@ -281,7 +286,12 @@ impl ApplyFetchRewrite for FetchRewrite { FetchRewrite::ValueSetter(setter) => setter.apply(possible_types, value), } } - fn apply_path(&self, possible_types: &PossibleTypes, value: &mut Value, path: &[FetchNodePathSegment]) { + fn apply_path( + &self, + possible_types: &PossibleTypes, + value: &mut Value, + path: &[FetchNodePathSegment], + ) { match self { FetchRewrite::KeyRenamer(renamer) => renamer.apply_path(possible_types, value, path), FetchRewrite::ValueSetter(setter) => setter.apply_path(possible_types, value, path), @@ -294,7 +304,12 @@ impl ApplyFetchRewrite for KeyRenamer { self.apply_path(possible_types, value, &self.path) } // Applies key rename operation on a Value (mutably) - fn apply_path(&self, possible_types: &PossibleTypes, value: &mut Value, path: &[FetchNodePathSegment]) { + fn apply_path( + &self, + possible_types: &PossibleTypes, + value: &mut Value, + path: &[FetchNodePathSegment], + ) { let current_segment = &path[0]; let remaining_path = &path[1..]; @@ -340,7 +355,12 @@ impl ApplyFetchRewrite for ValueSetter { } // Applies value setting on a Value (returns a new Value) - fn apply_path(&self, possible_types: &PossibleTypes, data: &mut Value, path: &[FetchNodePathSegment]) { + fn apply_path( + &self, + possible_types: &PossibleTypes, + data: &mut Value, + path: &[FetchNodePathSegment], + ) { if path.is_empty() { *data = self.set_value_to.to_owned(); return; @@ -363,7 +383,8 @@ impl ApplyFetchRewrite for ValueSetter { Some(Value::String(type_name)) => type_name, _ => type_condition, // Default to type_condition if not found }; - if possible_types.entity_satisfies_type_condition(type_name, type_condition) { + if possible_types.entity_satisfies_type_condition(type_name, type_condition) + { self.apply_path(possible_types, data, remaining_path) } } @@ -1034,6 +1055,13 @@ fn contains_typename(value: &Value, type_name: &str, default_for_missing: bool) .map_or(default_for_missing, |s| s == type_name) } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ExposeQueryPlanMode { + Yes, + No, + DryRun, +} + #[instrument( level = "trace", skip_all, @@ -1050,7 +1078,7 @@ pub async fn execute_query_plan( schema_metadata: &SchemaMetadata, operation: &OperationDefinition, has_introspection: bool, - expose_query_plan: bool, + expose_query_plan: ExposeQueryPlanMode, ) -> String { let mut result_data = Value::Null; // Initialize data as Null let mut result_errors = vec![]; // Initial errors are empty @@ -1063,15 +1091,19 @@ pub async fn execute_query_plan( errors: result_errors, extensions: result_extensions, }; - query_plan - .execute(&mut execution_context, &mut result_data) - .await; + if expose_query_plan != ExposeQueryPlanMode::DryRun { + query_plan + .execute(&mut execution_context, &mut result_data) + .await; + } result_errors = execution_context.errors; // Get the final errors from the execution context result_extensions = execution_context.extensions; // Get the final extensions from the execution context if result_data.is_null() && has_introspection { result_data = Value::Object(Map::new()); // Ensure data is an empty object if it was null } - if expose_query_plan { + if expose_query_plan == ExposeQueryPlanMode::Yes + || expose_query_plan == ExposeQueryPlanMode::DryRun + { result_extensions.insert( "queryPlan".to_string(), serde_json::to_value(query_plan).unwrap(), diff --git a/lib/query-plan-executor/src/tests/mod.rs b/lib/query-plan-executor/src/tests/mod.rs index a07feef1f..7da7726d6 100644 --- a/lib/query-plan-executor/src/tests/mod.rs +++ b/lib/query-plan-executor/src/tests/mod.rs @@ -46,7 +46,7 @@ fn query_executor_pipeline_locally() { &schema_metadata, normalized_operation, false, - false, + crate::ExposeQueryPlanMode::No, ) .await; insta::assert_snapshot!(result); From dd786e5938b0df3d41b80dd6a056dd32ef3fad1c Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 9 Jul 2025 12:34:47 +0300 Subject: [PATCH 24/35] Fixes based on main --- lib/query-plan-executor/src/lib.rs | 55 ++++++++++++++---- lib/query-plan-executor/src/projection.rs | 71 +++++++++++++++++------ 2 files changed, 98 insertions(+), 28 deletions(-) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 21b2d8233..5b666a15c 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -472,6 +472,18 @@ impl ExecutablePlanNode for ParallelNode { // Collect Fetch node results and flatten nodes for parallel execution let now = std::time::Instant::now(); for node in &self.nodes { + let node = if let PlanNode::Condition(condition_node) = node { + // If the node is a ConditionNode, we need to check the condition + if let Some(inner_node) = + condition_node.get_inner_node(execution_context.variable_values) + { + inner_node + } else { + continue; // Skip this node if the condition is not met + } + } else { + node + }; match node { PlanNode::Fetch(fetch_node) => { let job = fetch_node.execute_for_root(execution_context); @@ -693,16 +705,17 @@ impl ExecutablePlanNode for FlattenNode { } } -#[async_trait] -impl ExecutablePlanNode for ConditionNode { - #[instrument(level = "trace", skip_all, name = "ConditionNode::execute")] - async fn execute( +trait GetInnerNodeOfConditionNode { + fn get_inner_node(&self, variable_values: &Option>) + -> Option<&PlanNode>; +} + +impl GetInnerNodeOfConditionNode for ConditionNode { + fn get_inner_node( &self, - execution_context: &mut QueryPlanExecutionContext<'_>, - data: &mut Value, - ) { - let condition_value = execution_context - .variable_values + variable_values: &Option>, + ) -> Option<&PlanNode> { + let condition_value = variable_values .as_ref() .and_then(|vars| vars.get(&self.condition)) .is_some_and(|val| match val { @@ -711,10 +724,30 @@ impl ExecutablePlanNode for ConditionNode { }); if condition_value { if let Some(if_clause) = &self.if_clause { - return if_clause.execute(execution_context, data).await; + return Some(if_clause); } } else if let Some(else_clause) = &self.else_clause { - return else_clause.execute(execution_context, data).await; + return Some(else_clause); + } + None + } +} + +#[async_trait] +impl ExecutablePlanNode for ConditionNode { + #[instrument(level = "trace", skip_all, name = "ConditionNode::execute")] + async fn execute( + &self, + execution_context: &mut QueryPlanExecutionContext<'_>, + data: &mut Value, + ) { + let inner_node = self.get_inner_node(execution_context.variable_values); + if let Some(node) = inner_node { + // Execute the inner node if it exists + node.execute(execution_context, data).await; + } else { + // If no inner node, we do nothing + trace!("ConditionNode condition not met, skipping execution."); } } } diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index fe9b7a92b..e74d85aa5 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -158,6 +158,56 @@ fn project_selection_set( } } +trait IncludeOrSkipByVariable { + fn include_or_skip_by_variable(&self, variable_values: &Option>) + -> bool; +} + +impl IncludeOrSkipByVariable for SelectionItem { + fn include_or_skip_by_variable( + &self, + variable_values: &Option>, + ) -> bool { + if let Some(skip_variable) = match self { + SelectionItem::Field(field) => field.skip_if.as_ref(), + SelectionItem::InlineFragment(inline_fragment) => inline_fragment.skip_if.as_ref(), + SelectionItem::FragmentSpread(_) => { + // Fragment spreads should not exist in the final response projection. + unreachable!("Fragment spreads should not exist in the final response projection.") + } + } { + if let Some(variable_value) = variable_values + .as_ref() + .and_then(|vars| vars.get(skip_variable)) + { + if variable_value == &Value::Bool(true) { + return false; // Skip this field if the variable is true + } + } + return true; + } + if let Some(include_variable) = match self { + SelectionItem::Field(field) => field.include_if.as_ref(), + SelectionItem::InlineFragment(inline_fragment) => inline_fragment.include_if.as_ref(), + SelectionItem::FragmentSpread(_) => { + // Fragment spreads should not exist in the final response projection. + unreachable!("Fragment spreads should not exist in the final response projection.") + } + } { + if let Some(variable_value) = variable_values + .as_ref() + .and_then(|vars| vars.get(include_variable)) + { + if variable_value == &Value::Bool(true) { + return true; // Skip this field if the variable is not true + } + } + return false; + } + true + } +} + #[instrument( level = "trace", skip_all, @@ -196,25 +246,12 @@ fn project_selection_set_with_map( }; for selection in &selection_set.items { + if !selection.include_or_skip_by_variable(variable_values) { + // If the selection is not included by variable, skip it + continue; + } match selection { SelectionItem::Field(field) => { - if let Some(ref skip_variable) = field.skip_if { - let variable_value = variable_values - .as_ref() - .and_then(|vars| vars.get(skip_variable)); - if variable_value == Some(&Value::Bool(true)) { - continue; // Skip this field if the variable is true - } - } - if let Some(ref include_variable) = field.include_if { - let variable_value = variable_values - .as_ref() - .and_then(|vars| vars.get(include_variable)); - if variable_value != Some(&Value::Bool(true)) { - continue; // Skip this field if the variable is not true - } - } - let response_key = field.alias.as_ref().unwrap_or(&field.name); if *first { From 45eb8e103c7b6a2d1841e417ed4f3fe70d903faf Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 9 Jul 2025 13:04:02 +0300 Subject: [PATCH 25/35] Improvements --- lib/query-plan-executor/src/lib.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 5b666a15c..30d651d32 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -475,7 +475,7 @@ impl ExecutablePlanNode for ParallelNode { let node = if let PlanNode::Condition(condition_node) = node { // If the node is a ConditionNode, we need to check the condition if let Some(inner_node) = - condition_node.get_inner_node(execution_context.variable_values) + condition_node.inner_node_by_variables(execution_context.variable_values) { inner_node } else { @@ -705,17 +705,19 @@ impl ExecutablePlanNode for FlattenNode { } } -trait GetInnerNodeOfConditionNode { - fn get_inner_node(&self, variable_values: &Option>) - -> Option<&PlanNode>; +trait GetInnerNodeByVariables { + fn inner_node_by_variables( + &self, + variables: &Option>, + ) -> Option<&PlanNode>; } -impl GetInnerNodeOfConditionNode for ConditionNode { - fn get_inner_node( +impl GetInnerNodeByVariables for ConditionNode { + fn inner_node_by_variables( &self, - variable_values: &Option>, + variables: &Option>, ) -> Option<&PlanNode> { - let condition_value = variable_values + let condition_value = variables .as_ref() .and_then(|vars| vars.get(&self.condition)) .is_some_and(|val| match val { @@ -724,12 +726,15 @@ impl GetInnerNodeOfConditionNode for ConditionNode { }); if condition_value { if let Some(if_clause) = &self.if_clause { - return Some(if_clause); + Some(if_clause) + } else { + None } } else if let Some(else_clause) = &self.else_clause { - return Some(else_clause); + Some(else_clause) + } else { + None } - None } } @@ -741,7 +746,7 @@ impl ExecutablePlanNode for ConditionNode { execution_context: &mut QueryPlanExecutionContext<'_>, data: &mut Value, ) { - let inner_node = self.get_inner_node(execution_context.variable_values); + let inner_node = self.inner_node_by_variables(execution_context.variable_values); if let Some(node) = inner_node { // Execute the inner node if it exists node.execute(execution_context, data).await; From d79baea808e86fa038a9dcc0c09f81bb762b3551 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 9 Jul 2025 15:42:57 +0300 Subject: [PATCH 26/35] Avoid .remove --- lib/query-plan-executor/src/lib.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 30d651d32..b000ff03b 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -213,10 +213,13 @@ impl ExecutableFetchNode for FetchNode { &mut data, ); match data { - Value::Object(mut obj) => match obj.remove("_entities") { - Some(Value::Array(arr)) => Some(arr), - _ => None, // If _entities is not found or not an array - }, + Value::Object(mut obj) => { + let entities = obj.get_mut("_entities").map(|v| v.take()); + match entities { + Some(Value::Array(arr)) => Some(arr), + _ => None, + } + } _ => None, // If data is not an object } } else { From 0d71c01708ebd7becb59a6b5e46a5e81b2d74b57 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 16 Jul 2025 17:09:06 +0300 Subject: [PATCH 27/35] No reqwest (#254) Co-authored-by: Kamil Kisiela --- Cargo.lock | 741 ++++-------------- bin/gateway/Cargo.toml | 4 + bin/gateway/src/main.rs | 4 + .../src/pipeline/coerce_variables_service.rs | 6 +- bin/gateway/src/pipeline/error.rs | 6 +- bin/gateway/src/pipeline/gateway_layer.rs | 34 +- bin/gateway/src/pipeline/graphiql_service.rs | 11 +- .../src/pipeline/graphql_request_params.rs | 55 +- .../src/pipeline/http_request_params.rs | 6 +- bin/gateway/src/pipeline/normalize_service.rs | 8 +- bin/gateway/src/pipeline/parser_service.rs | 6 +- .../src/pipeline/query_plan_service.rs | 6 +- .../src/pipeline/validation_service.rs | 6 +- flamegraph.svg | 491 ++++++++++++ lib/query-plan-executor/Cargo.toml | 6 +- lib/query-plan-executor/src/executors/http.rs | 63 +- lib/query-plan-executor/src/executors/map.rs | 26 +- 17 files changed, 820 insertions(+), 659 deletions(-) create mode 100644 flamegraph.svg diff --git a/Cargo.lock b/Cargo.lock index 4da8ab491..d1a9fe0c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,16 +490,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -660,17 +650,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "dyn-clone" version = "1.0.19" @@ -750,6 +729,18 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "faststr" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6503af7917fea18ffef8f7e8553fb8dff89e2e6837e94e09dd7fb069c82d62c" +dependencies = [ + "bytes", + "rkyv", + "serde", + "simdutf8", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -768,21 +759,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -898,14 +874,17 @@ dependencies = [ "graphql-parser", "graphql-tools", "http", + "http-body-util", "hyper", "lazy_static", + "mimalloc", "moka", "query-plan-executor", "query-planner", "rand", "serde", "serde_json", + "sonic-rs", "thiserror 2.0.12", "tokio", "tower", @@ -941,17 +920,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.3.3" @@ -1138,45 +1106,12 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" dependencies = [ - "base64", "bytes", "futures-channel", "futures-core", @@ -1184,16 +1119,12 @@ dependencies = [ "http", "http-body", "hyper", - "ipnet", "libc", - "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -1220,119 +1151,12 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -1377,22 +1201,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "itertools" version = "0.10.5" @@ -1446,16 +1254,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] -name = "linux-raw-sys" -version = "0.9.4" +name = "libmimalloc-sys" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d" +dependencies = [ + "cc", + "libc", +] [[package]] -name = "litemap" -version = "0.8.0" +name = "linux-raw-sys" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "lock_api" @@ -1507,6 +1319,15 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "mimalloc" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -1593,20 +1414,23 @@ dependencies = [ ] [[package]] -name = "native-tls" -version = "0.2.14" +name = "munge" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "9cce144fab80fbb74ec5b89d1ca9d41ddf6b644ab7e986f7d3ed0aab31625cb1" dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "574af9cd5b9971cbfdf535d6a8d533778481b241c447826d976101e0149392a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1664,50 +1488,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "openssl" -version = "0.10.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "overload" version = "0.1.1" @@ -1817,12 +1597,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "plotters" version = "0.3.7" @@ -1857,15 +1631,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -1899,6 +1664,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9e76f66d3f9606f44e45598d155cb13ecf09f4a28199e48daf8c8fc937ea90" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "qp-dev-cli" version = "0.0.1" @@ -1920,11 +1705,15 @@ dependencies = [ "futures", "graphql-parser", "graphql-tools", + "http", + "http-body-util", + "hyper", + "hyper-util", "insta", "query-planner", - "reqwest", "serde", "serde_json", + "sonic-rs", "subgraphs", "tokio", "tokio-test", @@ -1978,6 +1767,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rancor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf5f7161924b9d1cea0e4cabc97c372cea92b5f927fc13c6bca67157a0ad947" +dependencies = [ + "ptr_meta", +] + [[package]] name = "rand" version = "0.9.2" @@ -2004,7 +1802,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom", ] [[package]] @@ -2101,57 +1899,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "reqwest" -version = "0.12.22" +name = "rend" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35e8a6bf28cd121053a66aa2e6a2e3eaffad4a60012179f0e864aa5ffeff215" + +[[package]] +name = "rkyv" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "1e147371c75553e1e2fcdb483944a8540b8438c31426279553b9a8182a9b7b65" dependencies = [ - "base64", "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", + "hashbrown 0.15.4", + "indexmap 2.10.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", ] [[package]] -name = "ring" -version = "0.17.14" +name = "rkyv_derive" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "246b40ac189af6c675d124b802e8ef6d5246c53e17367ce9501f8f66a81abb7a" dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2242,15 +2021,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "schemars" version = "0.9.0" @@ -2287,29 +2057,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.26" @@ -2448,6 +2195,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -2477,16 +2230,48 @@ dependencies = [ ] [[package]] -name = "spin" -version = "0.9.8" +name = "sonic-number" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "a8a74044c092f4f43ca7a6cfd62854cf9fb5ac8502b131347c990bf22bef1dfe" +dependencies = [ + "cfg-if", +] [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "sonic-rs" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0275f9f2f07d47556fe60c2759da8bc4be6083b047b491b2d476aa0bfa558eb1" +dependencies = [ + "bumpalo", + "bytes", + "cfg-if", + "faststr", + "itoa", + "ref-cast", + "ryu", + "serde", + "simdutf8", + "sonic-number", + "sonic-simd", + "thiserror 2.0.12", +] + +[[package]] +name = "sonic-simd" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b421f7b6aa4a5de8f685aaf398dfaa828346ee639d2b1c1061ab43d40baa6223" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "static_assertions_next" @@ -2534,12 +2319,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.104" @@ -2556,41 +2335,6 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] [[package]] name = "tagptr" @@ -2605,7 +2349,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2691,16 +2435,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -2711,6 +2445,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.46.1" @@ -2742,26 +2491,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.17" @@ -2860,14 +2589,12 @@ dependencies = [ "http-body-util", "http-range-header", "httpdate", - "iri-string", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "tokio", "tokio-util", - "tower", "tower-layer", "tower-service", "tracing", @@ -3042,42 +2769,19 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "uuid" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.3.3", + "getrandom", "js-sys", "wasm-bindgen", ] @@ -3088,12 +2792,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -3364,17 +3062,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -3575,36 +3262,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.8.26" @@ -3624,63 +3281,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/bin/gateway/Cargo.toml b/bin/gateway/Cargo.toml index b5a8356e0..80e6f465d 100644 --- a/bin/gateway/Cargo.toml +++ b/bin/gateway/Cargo.toml @@ -11,6 +11,8 @@ path = "src/main.rs" query-planner = { path = "../../lib/query-planner" } query-plan-executor = { path = "../../lib/query-plan-executor" } +mimalloc = { version = "0.1.47", features = ["override"] } + # Tokio runtime tokio = { version = "1.38.0", features = ["full"] } @@ -21,6 +23,7 @@ graphql-tools = "0.4.0" # Using version from original file # Serialization serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.140" +sonic-rs = "0.3" # HTTP client and caching moka = { version = "0.12.8", features = ["future"] } @@ -48,6 +51,7 @@ tower-http = { version = "0.6.6", features = [ "cors", "request-id", ] } +http-body-util = "0.1.3" # Utils thiserror = "2.0.12" diff --git a/bin/gateway/src/main.rs b/bin/gateway/src/main.rs index 45fdb4b45..f077bd3ad 100644 --- a/bin/gateway/src/main.rs +++ b/bin/gateway/src/main.rs @@ -18,6 +18,7 @@ use axum::{ Router, }; use http::Request; +use mimalloc::MiMalloc; use tokio::signal; use axum::Extension; @@ -43,6 +44,9 @@ use tower_http::{ }; use tracing::info; +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + #[tokio::main] async fn main() -> Result<(), Box> { let perfetto_file = env::var("PERFETTO_OUT").ok().is_some_and(|v| v == "1"); diff --git a/bin/gateway/src/pipeline/coerce_variables_service.rs b/bin/gateway/src/pipeline/coerce_variables_service.rs index b024076c3..7bd8a6832 100644 --- a/bin/gateway/src/pipeline/coerce_variables_service.rs +++ b/bin/gateway/src/pipeline/coerce_variables_service.rs @@ -36,8 +36,8 @@ impl GatewayPipelineLayer for CoerceVariablesService { #[tracing::instrument(level = "trace", name = "CoerceVariablesService", skip_all)] async fn process( &self, - mut req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { + req: &mut Request, + ) -> Result { let normalized_operation = req .extensions() .get::>() @@ -87,7 +87,7 @@ impl GatewayPipelineLayer for CoerceVariablesService { variables_map: values, }); - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } Err(err_msg) => { warn!( diff --git a/bin/gateway/src/pipeline/error.rs b/bin/gateway/src/pipeline/error.rs index 596d3667a..c8e0b0824 100644 --- a/bin/gateway/src/pipeline/error.rs +++ b/bin/gateway/src/pipeline/error.rs @@ -55,11 +55,11 @@ pub enum PipelineErrorVariant { // GraphQL-specific errors #[error("Failed to parse GraphQL request payload")] - FailedToParseBody(serde_json::Error), + FailedToParseBody(sonic_rs::Error), #[error("Failed to parse GraphQL variables JSON")] - FailedToParseVariables(serde_json::Error), + FailedToParseVariables(sonic_rs::Error), #[error("Failed to parse GraphQL extensions JSON")] - FailedToParseExtensions(serde_json::Error), + FailedToParseExtensions(sonic_rs::Error), #[error("Failed to parse GraphQL operation")] FailedToParseOperation(graphql_parser::query::ParseError), #[error("Failed to normalize GraphQL operation")] diff --git a/bin/gateway/src/pipeline/gateway_layer.rs b/bin/gateway/src/pipeline/gateway_layer.rs index 1e8c1c400..47293f476 100644 --- a/bin/gateway/src/pipeline/gateway_layer.rs +++ b/bin/gateway/src/pipeline/gateway_layer.rs @@ -2,6 +2,7 @@ use axum::body::Body; use axum::response::IntoResponse; use http::{Request, Response}; use std::convert::Infallible; +use std::sync::Arc; use std::{ future::Future, pin::Pin, @@ -16,8 +17,8 @@ use crate::pipeline::error::PipelineError; pub trait GatewayPipelineLayer: Clone + Send + Sync + 'static { async fn process( &self, - req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError>; + req: &mut Request, + ) -> Result; } pub enum GatewayPipelineStepDecision { @@ -28,12 +29,15 @@ pub enum GatewayPipelineStepDecision { #[derive(Debug, Clone)] pub struct ProcessorService { inner: S, - processor: P, + processor_arc: Arc

, } impl ProcessorService { - pub fn new_layer(inner: S, processor: P) -> Self { - Self { inner, processor } + pub fn new_layer(inner: S, processor_arc: Arc

) -> Self { + Self { + inner, + processor_arc, + } } } @@ -42,6 +46,7 @@ where S: Service, Response = Response, Error = Infallible> + Clone + Send + + Sync + 'static, S::Future: Send + 'static, P: GatewayPipelineLayer, @@ -54,12 +59,12 @@ where self.inner.poll_ready(cx) } - fn call(&mut self, req: Request) -> Self::Future { + fn call(&mut self, mut req: Request) -> Self::Future { let mut inner = self.inner.clone(); - let processor = self.processor.clone(); + let processor_arc = self.processor_arc.clone(); Box::pin(async move { - let result = processor.process(req).await; + let result = processor_arc.process(&mut req).await; match result { Err(err) => { @@ -68,12 +73,12 @@ where Ok(err.into_response()) } - Ok((req, GatewayPipelineStepDecision::Continue)) => { + Ok(GatewayPipelineStepDecision::Continue) => { trace!("Pipeline step decision is to continue"); inner.call(req).await } - Ok((_req, GatewayPipelineStepDecision::RespondWith(response))) => { + Ok(GatewayPipelineStepDecision::RespondWith(response)) => { trace!("Pipeline step decision is to short circuit"); Ok(response) @@ -85,12 +90,14 @@ where #[derive(Debug, Clone)] pub struct ProcessorLayer

{ - processor: P, + processor_arc: Arc

, } impl

ProcessorLayer

{ pub fn new(processor: P) -> Self { - Self { processor } + Self { + processor_arc: Arc::new(processor), + } } } @@ -101,6 +108,7 @@ where type Service = ProcessorService; fn layer(&self, inner: S) -> Self::Service { - ProcessorService::new_layer(inner, self.processor.clone()) + let processor_arc = self.processor_arc.clone(); + ProcessorService::new_layer(inner, processor_arc) } } diff --git a/bin/gateway/src/pipeline/graphiql_service.rs b/bin/gateway/src/pipeline/graphiql_service.rs index ea350b5bf..c465bddef 100644 --- a/bin/gateway/src/pipeline/graphiql_service.rs +++ b/bin/gateway/src/pipeline/graphiql_service.rs @@ -19,21 +19,20 @@ pub struct GraphiQLResponderService; impl GatewayPipelineLayer for GraphiQLResponderService { async fn process( &self, - req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { + req: &mut Request, + ) -> Result { let http_params = req.extensions().get::().ok_or_else(|| { PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") })?; if http_params.http_method == Method::GET && http_params.accept_header.contains("text/html") { - return Ok(( - req, - GatewayPipelineStepDecision::RespondWith(Html(GRAPHIQL_HTML).into_response()), + return Ok(GatewayPipelineStepDecision::RespondWith( + Html(GRAPHIQL_HTML).into_response(), )); } - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } } diff --git a/bin/gateway/src/pipeline/graphql_request_params.rs b/bin/gateway/src/pipeline/graphql_request_params.rs index c5c28a6aa..13d6fb35a 100644 --- a/bin/gateway/src/pipeline/graphql_request_params.rs +++ b/bin/gateway/src/pipeline/graphql_request_params.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; -use axum::body::{to_bytes, Body}; +use axum::body::Body; use axum::extract::Query; use http::{Method, Request}; +use http_body_util::BodyExt; use serde::Deserialize; use serde_json::Value; use tracing::{trace, warn}; @@ -13,8 +14,6 @@ use crate::pipeline::gateway_layer::{ }; use crate::pipeline::http_request_params::{HttpRequestParams, APPLICATION_JSON}; -static MAX_BODY_SIZE: usize = 2 * 1024 * 1024; // 2 MB in bytes, like Axum's default - #[derive(Clone, Debug, Default)] pub struct GraphQLRequestParamsExtractor; @@ -48,7 +47,7 @@ impl TryInto for GETQueryParams { }; let variables = match self.variables.as_deref() { - Some(v_str) if !v_str.is_empty() => match serde_json::from_str(v_str) { + Some(v_str) if !v_str.is_empty() => match sonic_rs::from_str(v_str) { Ok(vars) => Some(vars), Err(e) => { return Err(PipelineErrorVariant::FailedToParseVariables(e)); @@ -58,7 +57,7 @@ impl TryInto for GETQueryParams { }; let extensions = match self.extensions.as_deref() { - Some(e_str) if !e_str.is_empty() => match serde_json::from_str(e_str) { + Some(e_str) if !e_str.is_empty() => match sonic_rs::from_str(e_str) { Ok(exts) => Some(exts), Err(e) => { return Err(PipelineErrorVariant::FailedToParseExtensions(e)); @@ -83,8 +82,8 @@ impl GatewayPipelineLayer for GraphQLRequestParamsExtractor { #[tracing::instrument(level = "trace", name = "GraphQLRequestParamsExtractor", skip_all)] async fn process( &self, - mut req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { + req: &mut Request, + ) -> Result { let http_params = req.extensions().get::().ok_or_else(|| { PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") })?; @@ -131,28 +130,32 @@ impl GatewayPipelineLayer for GraphQLRequestParamsExtractor { } } - let (parts, body) = req.into_parts(); - let body_bytes = to_bytes(body, MAX_BODY_SIZE).await.map_err(|err| { - warn!("Failed to read body bytes: {}", err); - - PipelineError::new_with_accept_header( - PipelineErrorVariant::FailedToReadBodyBytes(err), - accept_header.clone(), - ) - })?; - - let execution_request = serde_json::from_slice::(&body_bytes) - .map_err(|e| { - warn!("Failed to parse body: {}", e); + let body_bytes = req + .body_mut() + .collect() + .await + .map_err(|err| { + warn!("Failed to read body bytes: {}", err); PipelineError::new_with_accept_header( - PipelineErrorVariant::FailedToParseBody(e), - accept_header, + PipelineErrorVariant::FailedToReadBodyBytes(err), + accept_header.clone(), ) - })?; + })? + .to_bytes(); + + let execution_request = unsafe { + sonic_rs::from_slice_unchecked::(&body_bytes).map_err( + |e| { + warn!("Failed to parse body: {}", e); - trace!("Body is parsed, will proceed"); - req = Request::from_parts(parts, Body::from(body_bytes)); + PipelineError::new_with_accept_header( + PipelineErrorVariant::FailedToParseBody(e), + accept_header, + ) + }, + )? + }; execution_request } @@ -168,7 +171,7 @@ impl GatewayPipelineLayer for GraphQLRequestParamsExtractor { req.extensions_mut().insert(execution_request); - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } } diff --git a/bin/gateway/src/pipeline/http_request_params.rs b/bin/gateway/src/pipeline/http_request_params.rs index e6f57ada9..7f47fbdb4 100644 --- a/bin/gateway/src/pipeline/http_request_params.rs +++ b/bin/gateway/src/pipeline/http_request_params.rs @@ -37,8 +37,8 @@ impl GatewayPipelineLayer for HttpRequestParamsExtractor { #[tracing::instrument(level = "trace", name = "HttpRequestParamsExtractor", skip_all)] async fn process( &self, - mut req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { + req: &mut Request, + ) -> Result { let http_method = req.method().to_owned(); let accept_header = req .headers() @@ -93,6 +93,6 @@ impl GatewayPipelineLayer for HttpRequestParamsExtractor { req.extensions_mut().insert(extracted_params); - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } } diff --git a/bin/gateway/src/pipeline/normalize_service.rs b/bin/gateway/src/pipeline/normalize_service.rs index 12d4351d5..e7057d842 100644 --- a/bin/gateway/src/pipeline/normalize_service.rs +++ b/bin/gateway/src/pipeline/normalize_service.rs @@ -45,8 +45,8 @@ impl GatewayPipelineLayer for GraphQLOperationNormalizationService { )] async fn process( &self, - mut req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { + req: &mut Request, + ) -> Result { let parser_payload = req .extensions() .get::() @@ -86,7 +86,7 @@ impl GatewayPipelineLayer for GraphQLOperationNormalizationService { payload.normalized_document.operation ); req.extensions_mut().insert(payload); - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } None => match normalize_operation( &app_state.planner.supergraph, @@ -121,7 +121,7 @@ impl GatewayPipelineLayer for GraphQLOperationNormalizationService { .insert(cache_key, payload_arc.clone()) .await; req.extensions_mut().insert(payload_arc); - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } Err(err) => { error!("Failed to normalize GraphQL operation: {}", err); diff --git a/bin/gateway/src/pipeline/parser_service.rs b/bin/gateway/src/pipeline/parser_service.rs index 621d0ebc8..daaa3a92c 100644 --- a/bin/gateway/src/pipeline/parser_service.rs +++ b/bin/gateway/src/pipeline/parser_service.rs @@ -35,8 +35,8 @@ impl GatewayPipelineLayer for GraphQLParserService { #[tracing::instrument(level = "trace", name = "GraphQLParserService", skip_all)] async fn process( &self, - mut req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { + req: &mut Request, + ) -> Result { let execution_params = req.extensions().get::().ok_or_else(|| { PipelineErrorVariant::InternalServiceError("ExecutionRequest is missing") })?; @@ -82,6 +82,6 @@ impl GatewayPipelineLayer for GraphQLParserService { cache_key, }); - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } } diff --git a/bin/gateway/src/pipeline/query_plan_service.rs b/bin/gateway/src/pipeline/query_plan_service.rs index b22248d0b..c37d926a6 100644 --- a/bin/gateway/src/pipeline/query_plan_service.rs +++ b/bin/gateway/src/pipeline/query_plan_service.rs @@ -68,8 +68,8 @@ impl GatewayPipelineLayer for QueryPlanService { #[tracing::instrument(level = "trace", name = "QueryPlanService", skip_all)] async fn process( &self, - mut req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { + req: &mut Request, + ) -> Result { let normalized_operation = req .extensions() .get::>() @@ -150,7 +150,7 @@ impl GatewayPipelineLayer for QueryPlanService { query_plan: query_plan_arc, }); - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } } diff --git a/bin/gateway/src/pipeline/validation_service.rs b/bin/gateway/src/pipeline/validation_service.rs index d3d0684cc..590e7990a 100644 --- a/bin/gateway/src/pipeline/validation_service.rs +++ b/bin/gateway/src/pipeline/validation_service.rs @@ -26,8 +26,8 @@ impl GatewayPipelineLayer for GraphQLValidationService { #[tracing::instrument(level = "trace", name = "GraphQLValidationService", skip_all)] async fn process( &self, - req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { + req: &mut Request, + ) -> Result { let parser_payload = req .extensions() .get::() @@ -95,6 +95,6 @@ impl GatewayPipelineLayer for GraphQLValidationService { )); } - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } } diff --git a/flamegraph.svg b/flamegraph.svg new file mode 100644 index 000000000..6eb1be709 --- /dev/null +++ b/flamegraph.svg @@ -0,0 +1,491 @@ +Flame Graph Reset ZoomSearch __libkernel_init (1 samples, 0.57%)mach_init_doit (1 samples, 0.57%)dyld4::APIs::runAllInitializersForMain() (2 samples, 1.14%)dyld4::PrebuiltLoader::runInitializers(dyld4::RuntimeState&) const (2 samples, 1.14%)dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const (2 samples, 1.14%)dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const (2 samples, 1.14%)dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const (2 samples, 1.14%)dyld3::MachOFile::forEachLoadCommand(Diagnostics&, void (load_command const*, bool&) block_pointer) const (2 samples, 1.14%)invocation function for block in dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const (2 samples, 1.14%)invocation function for block in dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const (2 samples, 1.14%)invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const (2 samples, 1.14%)libSystem_initializer (2 samples, 1.14%)libdispatch_init (1 samples, 0.57%)_os_object_init (1 samples, 0.57%)_objc_init (1 samples, 0.57%)dyld4::APIs::_dyld_objc_register_callbacks(_dyld_objc_callbacks const*) (1 samples, 0.57%)dyld4::RuntimeState::setObjCNotifiers(void (*)(char const*, mach_header const*), void (*)(mach_header const*, void*, mach_header const*, void const*), void (*)(unsigned int, _dyld_objc_notify_mapped_info const*), void (*)(_dyld_objc_notify_mapped_info const*), void (*)(unsigned int, _dyld_objc_notify_mapped_info const*, void (unsigned int) block_pointer)) (1 samples, 0.57%)dyld4::RuntimeLocks::withLoadersReadLock(void () block_pointer) (1 samples, 0.57%)invocation function for block in dyld4::RuntimeState::setObjCNotifiers(void (*)(char const*, mach_header const*), void (*)(mach_header const*, void*, mach_header const*, void const*), void (*)(unsigned int, _dyld_objc_notify_mapped_info const*), void (*)(_dyld_objc_notify_mapped_info const*), void (*)(unsigned int, _dyld_objc_notify_mapped_info const*, void (unsigned int) block_pointer))::$_0::operator()() const (1 samples, 0.57%)map_images (1 samples, 0.57%)map_images_nolock (1 samples, 0.57%)dyld4::JustInTimeLoader::applyFixups(Diagnostics&, dyld4::RuntimeState&, dyld4::DyldCacheDataConstLazyScopedWriter&, bool, lsl::Vector<std::__1::pair<dyld4::Loader const*, char const*>>*) const (1 samples, 0.57%)dyld4::Loader::applyFixupsGeneric(Diagnostics&, dyld4::RuntimeState&, unsigned long long, dyld3::Array<void const*> const&, dyld3::Array<void const*> const&, bool, dyld3::Array<dyld4::Loader::MissingFlatLazySymbol> const&) const (1 samples, 0.57%)dyld3::MachOAnalyzer::forEachRebaseLocation_Opcodes(Diagnostics&, void (unsigned long long, bool&) block_pointer) const (1 samples, 0.57%)dyld3::MachOAnalyzer::forEachRebase_Opcodes(Diagnostics&, dyld3::MachOLoaded::LinkEditInfo const&, dyld3::MachOFile::SegmentInfo const*, void (char const*, dyld3::MachOLoaded::LinkEditInfo const&, dyld3::MachOFile::SegmentInfo const*, bool, unsigned int, unsigned char, unsigned long long, dyld3::MachOAnalyzer::Rebase, bool&) block_pointer) const (1 samples, 0.57%)dyld3::MachOFile::read_uleb128(Diagnostics&, unsigned char const*&, unsigned char const*) (1 samples, 0.57%)dyld4::PrebuiltLoader::invalidateInIsolation(dyld4::RuntimeState const&) const (3 samples, 1.70%)dyld4::ProcessConfig::PathOverrides::forEachPathVariant(char const*, dyld3::Platform, bool, bool, bool&, void (char const*, dyld4::ProcessConfig::PathOverrides::Type, bool&) block_pointer) const (3 samples, 1.70%)invocation function for block in dyld4::PrebuiltLoader::invalidateInIsolation(dyld4::RuntimeState const&) const (3 samples, 1.70%)dyld4::SyscallDelegate::fileExists(char const*, dyld4::FileID*, int*) const (3 samples, 1.70%)dyld3::stat(char const*, stat*) (3 samples, 1.70%)stat$INODE64 (3 samples, 1.70%)dyld4::start(dyld4::KernelArgs*, void*, void*)::$_0::operator()() const (8 samples, 4.55%)dyld4..dyld4::prepare(dyld4::APIs&, dyld3::MachOAnalyzer const*) (7 samples, 3.98%)dyld..dyld4::JustInTimeLoader::loadDependents(Diagnostics&, dyld4::RuntimeState&, dyld4::Loader::LoadOptions const&) (4 samples, 2.27%)d..mach_o::Header::forEachLinkedDylib(void (char const*, mach_o::LinkedDylibAttributes, mach_o::Version32, mach_o::Version32, bool&) block_pointer) const (4 samples, 2.27%)m..mach_o::Header::forEachLoadCommand(void (load_command const*, bool&) block_pointer) const (4 samples, 2.27%)m..invocation function for block in mach_o::Header::forEachLinkedDylib(void (char const*, mach_o::LinkedDylibAttributes, mach_o::Version32, mach_o::Version32, bool&) block_pointer) const (4 samples, 2.27%)i..invocation function for block in dyld4::JustInTimeLoader::loadDependents(Diagnostics&, dyld4::RuntimeState&, dyld4::Loader::LoadOptions const&) (4 samples, 2.27%)i..dyld4::Loader::getLoader(Diagnostics&, dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&) (4 samples, 2.27%)d..dyld4::Loader::forEachPath(Diagnostics&, dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&, void (char const*, dyld4::ProcessConfig::PathOverrides::Type, bool&) block_pointer) (4 samples, 2.27%)d..dyld4::ProcessConfig::PathOverrides::forEachPathVariant(char const*, dyld3::Platform, bool, bool, bool&, void (char const*, dyld4::ProcessConfig::PathOverrides::Type, bool&) block_pointer) const (4 samples, 2.27%)d..dyld4::Loader::forEachResolvedAtPathVar(dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&, dyld4::ProcessConfig::PathOverrides::Type, bool&, void (char const*, dyld4::ProcessConfig::PathOverrides::Type, bool&) block_pointer) (4 samples, 2.27%)d..invocation function for block in dyld4::Loader::getLoader(Diagnostics&, dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&) (4 samples, 2.27%)i..dyld4::Loader::makeDyldCacheLoader(Diagnostics&, dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&, unsigned int, mach_o::Layout const*) (4 samples, 2.27%)d..dyld4::RuntimeState::findPrebuiltLoader(char const*) const (4 samples, 2.27%)d..dyld4::PrebuiltLoader::isValid(dyld4::RuntimeState const&) const (4 samples, 2.27%)d..dyld4::PrebuiltLoader::invalidateShallow(dyld4::RuntimeState const&) const (1 samples, 0.57%)dyld4::PrebuiltLoader::dependent(dyld4::RuntimeState const&, unsigned int, mach_o::LinkedDylibAttributes*) const (1 samples, 0.57%)<axum::serve::private::ServeFuture as core::future::future::Future>::poll (1 samples, 0.57%)<axum::serve::WithGracefulShutdown<L,M,S,F> as core::future::into_future::IntoFuture>::into_future::_{{closure}} (1 samples, 0.57%)tokio::net::tcp::listener::TcpListener::accept::_{{closure}} (1 samples, 0.57%)tokio::net::tcp::stream::TcpStream::new (1 samples, 0.57%)tokio::io::poll_evented::PollEvented<E>::new_with_interest (1 samples, 0.57%)tokio::runtime::io::registration::Registration::new_with_interest_and_handle (1 samples, 0.57%)tokio::runtime::io::driver::Handle::add_source (1 samples, 0.57%)kevent (1 samples, 0.57%)gateway::logger::configure_logging (1 samples, 0.57%)tracing_subscriber::registry (1 samples, 0.57%)<query_planner::consumer_schema::ConsumerSchema as query_plan_executor::schema_metadata::SchemaWithMetadata>::schema_metadata (1 samples, 0.57%)graphql_tools::introspection::introspection::_::_<impl serde::ser::Serialize for graphql_tools::introspection::introspection::IntrospectionSchema>::serialize (1 samples, 0.57%)query_plan_executor::executors::map::SubgraphExecutorMap::from_http_endpoint_map (1 samples, 0.57%)<std::collections::hash::map::HashMap<K,V,S> as core::iter::traits::collect::FromIterator<(K,V)>>::from_iter (1 samples, 0.57%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (1 samples, 0.57%)query_plan_executor::executors::http::HTTPSubgraphExecutor::new (1 samples, 0.57%)http::header::name::HdrName::from_static (1 samples, 0.57%)http::header::map::HeaderMap<T>::try_reserve_one (1 samples, 0.57%)gateway::shared_state::GatewaySharedState::new (3 samples, 1.70%)query_planner::planner::Planner::new_from_supergraph (1 samples, 0.57%)query_planner::planner::Planner::new_from_supergraph_state (1 samples, 0.57%)query_planner::graph::Graph::graph_from_supergraph_state (1 samples, 0.57%)query_planner::graph::Graph::build_graph (1 samples, 0.57%)query_planner::graph::Graph::build_field_edges (1 samples, 0.57%)query_planner::federation_spec::normalize_fields_argument_value_mut (1 samples, 0.57%)core::ptr::drop_in_place<graphql_parser::query::ast::Definition<alloc::string::String>> (1 samples, 0.57%)core::ptr::drop_in_place<graphql_parser::query::ast::Selection<alloc::string::String>> (1 samples, 0.57%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<T,I>>::spec_extend (1 samples, 0.57%)<combine::parser::repeat::Iter<Input,P,S,M> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)graphql_parser::schema::grammar::definition (1 samples, 0.57%)graphql_parser::schema::grammar::described_definition (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)graphql_parser::schema::grammar::directive_definition (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)graphql_parser::schema::grammar::directive_locations (1 samples, 0.57%)start (16 samples, 9.09%)startmain (8 samples, 4.55%)mainstd::rt::lang_start_internal (8 samples, 4.55%)std::..std::rt::lang_start::_{{closure}} (7 samples, 3.98%)std:..core::ops::function::FnOnce::call_once (7 samples, 3.98%)core..tokio::runtime::runtime::Runtime::block_on (7 samples, 3.98%)toki..gateway::main::_{{closure}} (6 samples, 3.41%)gat..query_planner::utils::parsing::parse_schema (2 samples, 1.14%)graphql_parser::schema::grammar::parse_schema (2 samples, 1.14%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (2 samples, 1.14%)graphql_parser::schema::grammar::definition (1 samples, 0.57%)graphql_parser::schema::grammar::schema (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)graphql_parser::common::directives (1 samples, 0.57%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<T,I>>::spec_extend (1 samples, 0.57%)<combine::parser::repeat::Iter<Input,P,S,M> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)graphql_parser::common::arguments (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (1 samples, 0.57%)tlv_get_addr (1 samples, 0.57%)parking_lot::condvar::Condvar::wait_until_internal (2 samples, 1.14%)__psynch_cvwait (2 samples, 1.14%)kevent (1 samples, 0.57%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (4 samples, 2.27%)t..tokio::runtime::scheduler::multi_thread::park::Parker::park (4 samples, 2.27%)t..tokio::runtime::time::Driver::park_internal (2 samples, 1.14%)tokio::runtime::io::driver::Driver::turn (2 samples, 1.14%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (1 samples, 0.57%)tokio::runtime::task::waker::wake_by_val (1 samples, 0.57%)parking_lot::condvar::Condvar::notify_one_slow (2 samples, 1.14%)__psynch_cvsignal (2 samples, 1.14%)_platform_memmove$VARIANT$Haswell (1 samples, 0.57%)<hyper::proto::h1::dispatch::Client<B> as hyper::proto::h1::dispatch::Dispatch>::recv_msg (2 samples, 1.14%)hyper::client::dispatch::Callback<T,U>::send (1 samples, 0.57%)tokio::sync::oneshot::Sender<T>::send (1 samples, 0.57%)tokio::sync::oneshot::State::set_complete (1 samples, 0.57%)hyper::body::incoming::Sender::try_send_data (1 samples, 0.57%)futures_channel::mpsc::BoundedSenderInner<T>::try_send (1 samples, 0.57%)std::sys::sync::once_box::OnceBox<T>::initialize (1 samples, 0.57%)pthread_mutex_init (1 samples, 0.57%)hyper::proto::h1::conn::Conn<I,B,T>::poll_flush (2 samples, 1.14%)hyper::proto::h1::io::Buffered<T,B>::poll_flush (2 samples, 1.14%)<tokio::net::tcp::stream::TcpStream as tokio::io::async_write::AsyncWrite>::poll_write_vectored (2 samples, 1.14%)tokio::runtime::io::registration::Registration::poll_io (2 samples, 1.14%)<&mio::net::tcp::stream::TcpStream as std::io::Write>::write_vectored (2 samples, 1.14%)writev (2 samples, 1.14%)hyper::proto::h1::conn::Conn<I,B,T>::poll_read_head (3 samples, 1.70%)hyper::proto::h1::io::Buffered<T,B>::parse (3 samples, 1.70%)hyper::proto::h1::io::Buffered<T,B>::poll_read_from_io (3 samples, 1.70%)tokio::io::poll_evented::PollEvented<E>::poll_read (3 samples, 1.70%)<&mio::net::tcp::stream::TcpStream as std::io::Read>::read (3 samples, 1.70%)__recvfrom (3 samples, 1.70%)hyper::proto::h1::conn::Conn<I,B,T>::write_head (1 samples, 0.57%)<hyper::proto::h1::role::Client as hyper::proto::h1::Http1Transaction>::encode (1 samples, 0.57%)hyper::proto::h1::role::set_content_length (1 samples, 0.57%)<http::header::value::HeaderValue as core::convert::From<u64>>::from (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll (12 samples, 6.82%)<futures_..<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll (10 samples, 5.68%)<future..<hyper::client::conn::http1::upgrades::UpgradeableConnection<I,B> as core::future::future::Future>::poll (10 samples, 5.68%)<hyper:..tokio::sync::mpsc::chan::Rx<T,S>::recv (1 samples, 0.57%)tokio::sync::mpsc::list::Rx<T>::pop (1 samples, 0.57%)tokio::runtime::task::harness::Harness<T,S>::poll (13 samples, 7.39%)tokio::run..tokio::runtime::scheduler::multi_thread::handle::_<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::yield_now (1 samples, 0.57%)tokio::runtime::context::with_scheduler (1 samples, 0.57%)tokio::runtime::context::scoped::Scoped<T>::with (1 samples, 0.57%)parking_lot::condvar::Condvar::notify_one_slow (1 samples, 0.57%)query_planner::planner::best::find_best_combination (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::tree::query_tree_node::QueryTreeNode::merge_nodes (1 samples, 0.57%)query_planner::planner::tree::query_tree_node::QueryTreeNode::merge_nodes (1 samples, 0.57%)query_planner::planner::tree::query_tree_node::QueryTreeNode::merge_nodes (1 samples, 0.57%)alloc::sync::Arc<T,A>::make_mut (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)_tiny_check_and_zero_inline_meta_from_freelist (1 samples, 0.57%)<gateway::pipeline::query_plan_service::QueryPlanService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}}::_{{closure}} (3 samples, 1.70%)query_planner::planner::Planner::plan_from_normalized_operation (3 samples, 1.70%)query_planner::planner::walker::walk_operation (2 samples, 1.14%)query_planner::planner::walker::process_selection (1 samples, 0.57%)query_planner::planner::walker::pathfinder::find_indirect_paths (1 samples, 0.57%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (1 samples, 0.57%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (1 samples, 0.57%)query_planner::planner::tree::query_tree::QueryTree::from_path (1 samples, 0.57%)<core::pin::Pin<P> as core::future::future::Future>::poll (4 samples, 2.27%)<..<gateway::pipeline::query_plan_service::QueryPlanService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}} (4 samples, 2.27%)<..moka::future::base_cache::BaseCache<K,V,S>::get_with_hash::_{{closure}} (1 samples, 0.57%)moka::future::base_cache::BaseCache<K,V,S>::apply_reads_if_needed::_{{closure}} (1 samples, 0.57%)moka::future::housekeeper::Housekeeper::do_run_pending_tasks::_{{closure}} (1 samples, 0.57%)<core::pin::Pin<P> as core::future::future::Future>::poll (1 samples, 0.57%)moka::future::housekeeper::Housekeeper::do_run_pending_tasks::_{{closure}}::_{{closure}} (1 samples, 0.57%)std::sys::sync::mutex::pthread::Mutex::lock (1 samples, 0.57%)std::sys::pal::unix::sync::mutex::Mutex::lock (1 samples, 0.57%)pthread_mutex_lock (1 samples, 0.57%)_platform_memmove$VARIANT$Haswell (1 samples, 0.57%)_szone_free (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)core::ptr::drop_in_place<hyper_util::client::legacy::client::Client<hyper_util::client::legacy::connect::http::HttpConnector,http_body_util::full::Full<bytes::bytes::Bytes>>::send_request::{{closure}}> (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)hyper_util::client::legacy::client::Client<C,B>::send_request::_{{closure}} (1 samples, 0.57%)<alloc::vec::Vec<T,A> as core::clone::Clone>::clone (1 samples, 0.57%)<hyper::body::incoming::Incoming as http_body::Body>::poll_frame (1 samples, 0.57%)<futures_channel::mpsc::Receiver<T> as futures_core::stream::Stream>::poll_next (1 samples, 0.57%)futures_channel::mpsc::Receiver<T>::next_message (1 samples, 0.57%)alloc::sync::Arc<T,A>::drop_slow (1 samples, 0.57%)free_tiny (1 samples, 0.57%)tiny_free_no_lock (1 samples, 0.57%)tiny_free_list_remove_ptr (1 samples, 0.57%)<http_body_util::combinators::collect::Collect<T> as core::future::future::Future>::poll (2 samples, 1.14%)http_body_util::collected::Collected<B>::push_frame (1 samples, 0.57%)alloc::collections::vec_deque::VecDeque<T,A>::grow (1 samples, 0.57%)alloc::raw_vec::RawVec<T,A>::grow_one (1 samples, 0.57%)alloc::raw_vec::finish_grow (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)<hyper_util::common::lazy::Lazy<F,R> as core::future::future::Future>::poll (1 samples, 0.57%)<futures_util::future::try_future::try_flatten::TryFlatten<Fut,<Fut as futures_core::future::TryFuture>::Ok> as core::future::future::Future>::poll (1 samples, 0.57%)<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll (1 samples, 0.57%)<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll (1 samples, 0.57%)<hyper_util::service::oneshot::Oneshot<S,Req> as core::future::future::Future>::poll (1 samples, 0.57%)<hyper_util::client::legacy::connect::http::HttpConnector<R> as tower_service::Service<http::uri::Uri>>::call::_{{closure}} (1 samples, 0.57%)hyper_util::client::legacy::connect::http::connect (1 samples, 0.57%)socket2::socket::Socket::new (1 samples, 0.57%)__fcntl (1 samples, 0.57%)bytes::bytes::shared_clone (1 samples, 0.57%)hyper::client::dispatch::Sender<T,U>::try_send (1 samples, 0.57%)tokio::sync::mpsc::unbounded::UnboundedSender<T>::send (1 samples, 0.57%)tokio::runtime::task::waker::wake_by_val (1 samples, 0.57%)tokio::runtime::scheduler::multi_thread::handle::_<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::schedule (1 samples, 0.57%)tokio::runtime::context::with_scheduler (1 samples, 0.57%)tokio::runtime::context::scoped::Scoped<T>::with (1 samples, 0.57%)tokio::runtime::driver::Handle::unpark (1 samples, 0.57%)kevent (1 samples, 0.57%)<hyper_util::client::legacy::client::ResponseFuture as core::future::future::Future>::poll (4 samples, 2.27%)<..hyper_util::client::legacy::client::Client<C,B>::send_request::_{{closure}} (4 samples, 2.27%)h..szone_try_free_default (1 samples, 0.57%)free_small (2 samples, 1.14%)hyper_util::client::legacy::client::Client<C,B>::request (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)small_malloc_should_clear (1 samples, 0.57%)small_malloc_from_free_list (1 samples, 0.57%)small_free_list_add_ptr (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (1 samples, 0.57%)core::str::converts::from_utf8 (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)DYLD-STUB$$_platform_memmove (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (5 samples, 2.84%)<s..serde_json::read::next_or_eof (4 samples, 2.27%)s..<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (4 samples, 2.27%)<.._platform_memmove$VARIANT$Haswell (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (9 samples, 5.11%)<bytes.._platform_memmove$VARIANT$Haswell (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (16 samples, 9.09%)<serde_json::..serde_json::read::next_or_eof (16 samples, 9.09%)serde_json::r..<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (12 samples, 6.82%)<std::io:.._platform_memmove$VARIANT$Haswell (2 samples, 1.14%)0xfffffffffffffffe (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (23 samples, 13.07%)<serde_json::read::I..serde_json::read::next_or_eof (22 samples, 12.50%)serde_json::read::n..<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (18 samples, 10.23%)<std::io::Bytes..<bytes::buf::reader::Reader<B> as std::io::Read>::read (16 samples, 9.09%)<bytes::buf::.._platform_memmove$VARIANT$Haswell (2 samples, 1.14%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (2 samples, 1.14%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (2 samples, 1.14%)serde_json::read::next_or_eof (1 samples, 0.57%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (3 samples, 1.70%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (2 samples, 1.14%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (2 samples, 1.14%)_platform_memmove$VARIANT$Haswell (2 samples, 1.14%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (31 samples, 17.61%)<core::marker::PhantomData<..tiny_malloc_should_clear (1 samples, 0.57%)<serde_json::de::MapAccess<R> as serde::de::MapAccess>::next_key_seed::has_next_key (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (34 samples, 19.32%)<core::marker::PhantomData<T> ..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (34 samples, 19.32%)<serde_json::value::de::<impl ..serde::de::impls::_<impl serde::de::Deserialize for alloc::string::String>::deserialize (2 samples, 1.14%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (36 samples, 20.45%)<core::marker::PhantomData<T> as..szone_malloc_should_clear (2 samples, 1.14%)tiny_malloc_should_clear (2 samples, 1.14%)tiny_malloc_from_free_list (2 samples, 1.14%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)_platform_memmove$VARIANT$Haswell (1 samples, 0.57%)serde::de::impls::_<impl serde::de::Deserialize for alloc::string::String>::deserialize (3 samples, 1.70%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (3 samples, 1.70%)serde_json::read::next_or_eof (2 samples, 1.14%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (57 samples, 32.39%)<core::marker::PhantomData<T> as serde::de::Deserial..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (41 samples, 23.30%)<serde_json::value::de::<impl serde::..serde_json::de::Deserializer<R>::parse_object_colon (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (58 samples, 32.95%)<core::marker::PhantomData<T> as serde::de::Deseriali..<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (58 samples, 32.95%)<core::marker::PhantomData<T> as serde::de::Deseriali..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (58 samples, 32.95%)<serde_json::value::de::<impl serde::de::Deserialize ..serde::de::impls::_<impl serde::de::Deserialize for alloc::string::String>::deserialize (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (64 samples, 36.36%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (64 samples, 36.36%)<serde_json::value::de::<impl serde::de::Deserialize for se..<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (64 samples, 36.36%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (59 samples, 33.52%)<serde_json::value::de::<impl serde::de::Deserialize f..<serde_json::de::MapAccess<R> as serde::de::MapAccess>::next_key_seed::has_next_key (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)serde_json::de::Deserializer<R>::parse_ident (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)serde_json::de::Deserializer<R>::parse_integer (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (70 samples, 39.77%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::des..szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)tiny_free_list_add_ptr (1 samples, 0.57%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (4 samples, 2.27%)<..<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (4 samples, 2.27%)<..serde_json::read::next_or_eof (3 samples, 1.70%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (3 samples, 1.70%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (3 samples, 1.70%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (79 samples, 44.89%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (78 samples, 44.32%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::valu..alloc::collections::btree::map::BTreeMap<K,V,A>::insert (4 samples, 2.27%)a..alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert_entry (4 samples, 2.27%)a..alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert_recursing (2 samples, 1.14%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (80 samples, 45.45%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserializealloc::raw_vec::RawVec<T,A>::grow_one (1 samples, 0.57%)alloc::raw_vec::finish_grow (1 samples, 0.57%)_realloc (1 samples, 0.57%)_malloc_zone_realloc (1 samples, 0.57%)szone_realloc (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)small_malloc_should_clear (1 samples, 0.57%)small_malloc_from_free_list (1 samples, 0.57%)small_free_list_remove_ptr_no_clear (1 samples, 0.57%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (1 samples, 0.57%)<query_planner::planner::plan_nodes::FetchNode as query_plan_executor::ExecutableFetchNode>::execute_for_projected_representations::_{{closure}} (96 samples, 54.55%)<query_planner::planner::plan_nodes::FetchNode as query_plan_executor::ExecutableFetchNode..query_plan_executor::executors::map::SubgraphExecutorMap::execute::_{{closure}} (96 samples, 54.55%)query_plan_executor::executors::map::SubgraphExecutorMap::execute::_{{closure}}<query_plan_executor::executors::http::HTTPSubgraphExecutor as query_plan_executor::executors::common::SubgraphExecutor>::execute::_{{closure}} (94 samples, 53.41%)<query_plan_executor::executors::http::HTTPSubgraphExecutor as query_plan_executor::exec..serde_json::de::from_reader (82 samples, 46.59%)serde_json::de::from_reader<&mut serde_json::de::Deserializer<R> as serde::de::Deserializer>::deserialize_struct (82 samples, 46.59%)<&mut serde_json::de::Deserializer<R> as serde::de::Deserializer>::deseriali..serde::de::impls::_<impl serde::de::Deserialize for core::option::Option<T>>::deserialize (82 samples, 46.59%)serde::de::impls::_<impl serde::de::Deserialize for core::option::Option<T>>..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (82 samples, 46.59%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::..alloc::collections::btree::map::BTreeMap<K,V,A>::insert (1 samples, 0.57%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert_entry (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)<serde_json::de::MapAccess<R> as serde::de::MapAccess>::next_key_seed::has_next_key (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)futures_util::stream::stream::StreamExt::poll_next_unpin (102 samples, 57.95%)futures_util::stream::stream::StreamExt::poll_next_unpin<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll (101 samples, 57.39%)<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll (100 samples, 56.82%)<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll<query_planner::planner::plan_nodes::FetchNode as query_plan_executor::ExecutableFetchNode>::execute_for_root::_{{closure}} (3 samples, 1.70%)query_plan_executor::executors::map::SubgraphExecutorMap::execute::_{{closure}} (3 samples, 1.70%)<query_plan_executor::executors::http::HTTPSubgraphExecutor as query_plan_executor::executors::common::SubgraphExecutor>::execute::_{{closure}} (3 samples, 1.70%)serde_json::de::from_reader (3 samples, 1.70%)<&mut serde_json::de::Deserializer<R> as serde::de::Deserializer>::deserialize_struct (3 samples, 1.70%)serde::de::impls::_<impl serde::de::Deserialize for core::option::Option<T>>::deserialize (3 samples, 1.70%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (3 samples, 1.70%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (3 samples, 1.70%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (3 samples, 1.70%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (3 samples, 1.70%)serde::de::impls::_<impl serde::de::Deserialize for alloc::string::String>::deserialize (2 samples, 1.14%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (2 samples, 1.14%)serde_json::read::next_or_eof (2 samples, 1.14%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<query_planner::planner::plan_nodes::ParallelNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}}::_{{closure}}::_{{closure}} (1 samples, 0.57%)query_plan_executor::QueryPlanExecutionContext::project_requires (1 samples, 0.57%)query_plan_executor::QueryPlanExecutionContext::project_requires_map_mut (1 samples, 0.57%)query_plan_executor::QueryPlanExecutionContext::project_requires_map_mut (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge_objects (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)free_tiny (1 samples, 0.57%)_platform_memcmp$VARIANT$Base (1 samples, 0.57%)<query_planner::planner::plan_nodes::ParallelNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}}::_{{closure}}::_{{closure}} (2 samples, 1.14%)query_plan_executor::QueryPlanExecutionContext::project_requires (2 samples, 1.14%)query_plan_executor::QueryPlanExecutionContext::project_requires_map_mut (2 samples, 1.14%)query_plan_executor::QueryPlanExecutionContext::project_requires_map_mut (2 samples, 1.14%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (1 samples, 0.57%)alloc::raw_vec::finish_grow (1 samples, 0.57%)_realloc (1 samples, 0.57%)_malloc_zone_realloc (1 samples, 0.57%)szone_realloc (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)small_malloc_should_clear (1 samples, 0.57%)small_malloc_from_free_list (1 samples, 0.57%)small_free_list_remove_ptr_no_clear (1 samples, 0.57%)_platform_memcmp$VARIANT$Base (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge_objects (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)free_tiny (1 samples, 0.57%)get_tiny_previous_free_msize (1 samples, 0.57%)_platform_memcmp$VARIANT$Base (1 samples, 0.57%)query_plan_executor::traverse_and_callback (12 samples, 6.82%)query_pla..query_plan_executor::traverse_and_callback (12 samples, 6.82%)query_pla..query_plan_executor::traverse_and_callback (11 samples, 6.25%)query_pl..query_plan_executor::traverse_and_callback (9 samples, 5.11%)query_..query_plan_executor::traverse_and_callback (2 samples, 1.14%)query_plan_executor::deep_merge::deep_merge (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge_objects (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)<query_planner::planner::plan_nodes::QueryPlan as query_plan_executor::ExecutableQueryPlan>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::QueryPlan as query_plan_executor::ExecutableQueryPlan>::execute::_{{closu..<query_planner::planner::plan_nodes::PlanNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::PlanNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure..<query_planner::planner::plan_nodes::SequenceNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::SequenceNode as query_plan_executor::ExecutablePlanNode>::execute::_{{clo..<query_planner::planner::plan_nodes::PlanNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::PlanNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure..<query_planner::planner::plan_nodes::ParallelNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::ParallelNode as query_plan_executor::ExecutablePlanNode>::execute::_{{clo..std::sys::pal::unix::time::Timespec::now (1 samples, 0.57%)clock_gettime (1 samples, 0.57%)mach_absolute_time (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)free_tiny (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)free_tiny (1 samples, 0.57%)core::ptr::drop_in_place<serde_json::value::Value> (2 samples, 1.14%)free_tiny (1 samples, 0.57%)tiny_free_no_lock (1 samples, 0.57%)tiny_free_list_remove_ptr (1 samples, 0.57%)core::ptr::drop_in_place<serde_json::value::Value> (5 samples, 2.84%)co..core::ptr::drop_in_place<serde_json::value::Value> (5 samples, 2.84%)co..free_tiny (2 samples, 1.14%)tiny_free_no_lock (1 samples, 0.57%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<alloc::string::String,serde_json::value::Value>> (11 samples, 6.25%)core::pt..core::mem::maybe_uninit::MaybeUninit<T>::assume_init_drop (11 samples, 6.25%)core::me..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (10 samples, 5.68%)core::p..core::ptr::drop_in_place<serde_json::value::Value> (10 samples, 5.68%)core::p..core::ptr::drop_in_place<serde_json::value::Value> (10 samples, 5.68%)core::p..free_tiny (4 samples, 2.27%)f..tiny_free_no_lock (3 samples, 1.70%)tiny_free_list_add_ptr (1 samples, 0.57%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (1 samples, 0.57%)alloc::raw_vec::finish_grow (1 samples, 0.57%)_realloc (1 samples, 0.57%)_malloc_zone_realloc (1 samples, 0.57%)szone_realloc (1 samples, 0.57%)_szone_free (1 samples, 0.57%)madvise (1 samples, 0.57%)<core::pin::Pin<P> as core::future::future::Future>::poll (136 samples, 77.27%)<core::pin::Pin<P> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (136 samples, 77.27%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::gateway_layer::ProcessorService<S,P> as tower_service::Service<http::request::Request<axum_core::body::Body>>>::call::_{{closure}} (132 samples, 75.00%)<gateway::pipeline::gateway_layer::ProcessorService<S,P> as tower_service::Service<http::request::Request<axum_core::body::Bo..<core::pin::Pin<P> as core::future::future::Future>::poll (132 samples, 75.00%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::execution_service::ExecutionService as tower_service::Service<http::request::Request<axum_core::body::Body>>>::call::_{{closure}} (132 samples, 75.00%)<gateway::pipeline::execution_service::ExecutionService as tower_service::Service<http::request::Request<axum_core::body::Bod..query_plan_executor::projection::project_by_operation (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (1 samples, 0.57%)query_plan_executor::projection::project_selection_set_with_map (1 samples, 0.57%)core::hash::BuildHasher::hash_one (1 samples, 0.57%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (1 samples, 0.57%)<core::pin::Pin<P> as core::future::future::Future>::poll (137 samples, 77.84%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::validation_service::GraphQLValidationService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}} (1 samples, 0.57%)<gateway::pipeline::validation_service::GraphQLValidationService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}}::_{{closure}} (1 samples, 0.57%)graphql_tools::validation::validate::validate (1 samples, 0.57%)<graphql_tools::validation::rules::fields_on_correct_type::FieldsOnCorrectType as graphql_tools::validation::rules::rule::ValidationRule>::validate (1 samples, 0.57%)graphql_tools::ast::operation_visitor::visit_document (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)core::ptr::drop_in_place<graphql_parser::common::Type<alloc::string::String>> (1 samples, 0.57%)<hyper_util::service::glue::TowerToHyperServiceFuture<S,R> as core::future::future::Future>::poll (138 samples, 78.41%)<hyper_util::service::glue::TowerToHyperServiceFuture<S,R> as core::future::future::Future>::poll<hyper_util::service::oneshot::Oneshot<S,Req> as core::future::future::Future>::poll (138 samples, 78.41%)<hyper_util::service::oneshot::Oneshot<S,Req> as core::future::future::Future>::poll<axum::routing::route::RouteFuture<E> as core::future::future::Future>::poll (138 samples, 78.41%)<axum::routing::route::RouteFuture<E> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll (138 samples, 78.41%)<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll<F as futures_core::future::TryFuture>::try_poll (138 samples, 78.41%)<F as futures_core::future::TryFuture>::try_poll<tower_http::cors::ResponseFuture<F> as core::future::future::Future>::poll (138 samples, 78.41%)<tower_http::cors::ResponseFuture<F> as core::future::future::Future>::poll<axum::routing::route::RouteFuture<E> as core::future::future::Future>::poll (138 samples, 78.41%)<axum::routing::route::RouteFuture<E> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<axum::util::MapIntoResponseFuture<F> as core::future::future::Future>::poll (138 samples, 78.41%)<axum::util::MapIntoResponseFuture<F> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::gateway_layer::ProcessorService<S,P> as tower_service::Service<http::request::Request<axum_core::body::Body>>>::call::_{{closure}} (138 samples, 78.41%)<gateway::pipeline::gateway_layer::ProcessorService<S,P> as tower_service::Service<http::request::Request<axum_core::body::Body>>>..<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::parser_service::GraphQLParserService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}} (1 samples, 0.57%)moka::cht::segment::HashMap<K,V,S>::get_key_value_and_then (1 samples, 0.57%)crossbeam_epoch::default::pin (1 samples, 0.57%)crossbeam_epoch::default::with_handle (1 samples, 0.57%)crossbeam_epoch::internal::Global::collect (1 samples, 0.57%)hyper::proto::h1::conn::Conn<I,B,T>::force_io_read (1 samples, 0.57%)bytes::bytes_mut::BytesMut::reserve (1 samples, 0.57%)bytes::bytes_mut::BytesMut::reserve_inner (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)small_malloc_should_clear (1 samples, 0.57%)all (176 samples, 100%)thread_start (160 samples, 90.91%)thread_start_pthread_start (160 samples, 90.91%)_pthread_startstd::sys::pal::unix::thread::Thread::new::thread_start (160 samples, 90.91%)std::sys::pal::unix::thread::Thread::new::thread_startcore::ops::function::FnOnce::call_once{{vtable.shim}} (160 samples, 90.91%)core::ops::function::FnOnce::call_once{{vtable.shim}}std::sys::backtrace::__rust_begin_short_backtrace (160 samples, 90.91%)std::sys::backtrace::__rust_begin_short_backtracetokio::runtime::blocking::pool::Inner::run (160 samples, 90.91%)tokio::runtime::blocking::pool::Inner::runtokio::runtime::task::harness::Harness<T,S>::poll (160 samples, 90.91%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::core::Core<T,S>::poll (160 samples, 90.91%)tokio::runtime::task::core::Core<T,S>::polltokio::runtime::scheduler::multi_thread::worker::run (160 samples, 90.91%)tokio::runtime::scheduler::multi_thread::worker::runtokio::runtime::context::runtime::enter_runtime (160 samples, 90.91%)tokio::runtime::context::runtime::enter_runtimetokio::runtime::context::scoped::Scoped<T>::set (160 samples, 90.91%)tokio::runtime::context::scoped::Scoped<T>::settokio::runtime::scheduler::multi_thread::worker::Context::run (160 samples, 90.91%)tokio::runtime::scheduler::multi_thread::worker::Context::runtokio::runtime::scheduler::multi_thread::worker::Context::run_task (155 samples, 88.07%)tokio::runtime::scheduler::multi_thread::worker::Context::run_tasktokio::runtime::task::harness::poll_future::_{{closure}} (140 samples, 79.55%)tokio::runtime::task::harness::poll_future::_{{closure}}tokio::runtime::task::core::Core<T,S>::poll::_{{closure}} (140 samples, 79.55%)tokio::runtime::task::core::Core<T,S>::poll::_{{closure}}axum::serve::handle_connection::_{{closure}}::_{{closure}} (140 samples, 79.55%)axum::serve::handle_connection::_{{closure}}::_{{closure}}<core::pin::Pin<P> as core::future::future::Future>::poll (140 samples, 79.55%)<core::pin::Pin<P> as core::future::future::Future>::pollhyper::proto::h1::dispatch::Dispatcher<D,Bs,I,T>::poll_read (1 samples, 0.57%)hyper::body::incoming::Sender::try_send_data (1 samples, 0.57%)futures_channel::mpsc::BoundedSenderInner<T>::try_send (1 samples, 0.57%)std::sys::sync::once_box::OnceBox<T>::initialize (1 samples, 0.57%)pthread_mutex_init (1 samples, 0.57%) \ No newline at end of file diff --git a/lib/query-plan-executor/Cargo.toml b/lib/query-plan-executor/Cargo.toml index bdacd1ad3..010e78449 100644 --- a/lib/query-plan-executor/Cargo.toml +++ b/lib/query-plan-executor/Cargo.toml @@ -10,13 +10,17 @@ async-trait = "0.1" serde = "1.0.219" serde_json = "1.0.140" futures = "0.3" -reqwest = { version = "0.12", features = ["json"] } query-planner = { path = "../query-planner" } graphql-parser = "0.4.1" graphql-tools = "0.4.0" tracing = "0.1.41" # TODO: Move this to dev-dependencies but currently benchmarks need it async-graphql = { version = "7.0.17" } +hyper = { version= "1.6.0", features = ["client"] } +hyper-util = { version= "0.1.15", features = ["client", "client-legacy", "http1", "http2", "tokio"] } +http-body-util = "0.1.3" +http = "1.3.1" +sonic-rs = "0.3" [dev-dependencies] criterion = { version = "0.6", features = ["html_reports", "async_tokio"] } diff --git a/lib/query-plan-executor/src/executors/http.rs b/lib/query-plan-executor/src/executors/http.rs index 269f99211..8690f52e3 100644 --- a/lib/query-plan-executor/src/executors/http.rs +++ b/lib/query-plan-executor/src/executors/http.rs @@ -1,4 +1,12 @@ +use std::sync::Arc; + use async_trait::async_trait; +use http::HeaderMap; +use http::HeaderValue; +use http_body_util::BodyExt; +use http_body_util::Full; +use hyper::{body::Bytes, Version}; +use hyper_util::client::legacy::{connect::HttpConnector, Client}; use tracing::{error, instrument, trace}; use crate::{ @@ -9,16 +17,28 @@ use crate::{ #[derive(Debug)] pub struct HTTPSubgraphExecutor { pub endpoint: String, - pub http_client: reqwest::Client, + pub http_client: Arc>>, + pub uri: http::Uri, + pub header_map: HeaderMap, } const FIRST_VARIABLE_STR: &str = ",\"variables\":{"; impl HTTPSubgraphExecutor { - pub fn new(endpoint: String, http_client: reqwest::Client) -> Self { + pub fn new(endpoint: String, http_client: Arc>>) -> Self { + let uri = endpoint + .parse::() + .expect("Failed to parse endpoint as URI"); + let mut header_map = HeaderMap::new(); + header_map.insert( + "Content-Type", + HeaderValue::from_static("application/json; charset=utf-8"), + ); HTTPSubgraphExecutor { endpoint, + uri, http_client, + header_map, } } @@ -66,26 +86,47 @@ impl HTTPSubgraphExecutor { } body.push('}'); - self.http_client - .post(&self.endpoint) - .body(body) - .header("Content-Type", "application/json; charset=utf-8") - .send() - .await + let mut req = hyper::Request::builder() + .method(http::Method::POST) + .uri(&self.uri) + .version(Version::HTTP_11) + .body(body.into()) .map_err(|e| { format!( - "Failed to send request to subgraph {}: {}", + "Failed to build request to subgraph {}: {}", self.endpoint, e ) - })? - .json::() + })?; + + *req.headers_mut() = self.header_map.clone(); + + let res = self.http_client.request(req).await.map_err(|e| { + format!( + "Failed to send request to subgraph {}: {}", + self.endpoint, e + ) + })?; + + let bytes = res + .into_body() + .collect() .await .map_err(|e| { format!( "Failed to parse response from subgraph {}: {}", self.endpoint, e ) + })? + .to_bytes(); + + unsafe { + sonic_rs::from_slice_unchecked(&bytes).map_err(|e| { + format!( + "Failed to parse response from subgraph {}: {}", + self.endpoint, e + ) }) + } } } diff --git a/lib/query-plan-executor/src/executors/map.rs b/lib/query-plan-executor/src/executors/map.rs index d80bcb132..e4531ef81 100644 --- a/lib/query-plan-executor/src/executors/map.rs +++ b/lib/query-plan-executor/src/executors/map.rs @@ -1,8 +1,15 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use hyper_util::{ + client::legacy::Client, + rt::{TokioExecutor, TokioTimer}, +}; use tracing::{instrument, warn}; -use crate::executors::common::{SubgraphExecutor, SubgraphExecutorBoxedArc}; +use crate::executors::{ + common::{SubgraphExecutor, SubgraphExecutorBoxedArc}, + http::HTTPSubgraphExecutor, +}; pub struct SubgraphExecutorMap { inner: HashMap, @@ -47,15 +54,18 @@ impl SubgraphExecutorMap { } pub fn from_http_endpoint_map(subgraph_endpoint_map: HashMap) -> Self { - let http_client = reqwest::Client::new(); + let mut builder = Client::builder(TokioExecutor::new()); + let builder_mut = builder + .pool_timer(TokioTimer::new()) + .pool_idle_timeout(Duration::from_secs(60 * 60)) + .pool_max_idle_per_host(usize::MAX); + let http_client = builder_mut.build_http(); + let http_client_arc = Arc::new(http_client); let executor_map = subgraph_endpoint_map .into_iter() .map(|(subgraph_name, endpoint)| { - let executor = crate::executors::http::HTTPSubgraphExecutor::new( - endpoint, - http_client.clone(), - ) - .to_boxed_arc(); + let executor = + HTTPSubgraphExecutor::new(endpoint, http_client_arc.clone()).to_boxed_arc(); (subgraph_name, executor) }) .collect::>(); From f8e45f1a2151e42a4e1be055fa00418fe3f45649 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 16 Jul 2025 17:13:51 +0300 Subject: [PATCH 28/35] Fix progressive override --- Cargo.lock | 33 ------------------- .../pipeline/progressive_override_service.rs | 6 ++-- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1a9fe0c1..3cac37880 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1967,39 +1967,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "rustls" -version = "0.23.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" -dependencies = [ - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.21" diff --git a/bin/gateway/src/pipeline/progressive_override_service.rs b/bin/gateway/src/pipeline/progressive_override_service.rs index 6d1a9e813..76386a5d2 100644 --- a/bin/gateway/src/pipeline/progressive_override_service.rs +++ b/bin/gateway/src/pipeline/progressive_override_service.rs @@ -32,8 +32,8 @@ impl GatewayPipelineLayer for ProgressiveOverrideExtractor { #[tracing::instrument(level = "trace", name = "ProgressiveOverrideExtractor", skip_all)] async fn process( &self, - mut req: Request, - ) -> Result<(Request, GatewayPipelineStepDecision), PipelineError> { + req: &mut Request, + ) -> Result { // No active flags by default - until we implement it let active_flags = HashSet::new(); @@ -52,7 +52,7 @@ impl GatewayPipelineLayer for ProgressiveOverrideExtractor { req.extensions_mut().insert(override_context); - Ok((req, GatewayPipelineStepDecision::Continue)) + Ok(GatewayPipelineStepDecision::Continue) } } From 3d1d0051ce94a8e67d461e78ee3fab0f10bd1637 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 16 Jul 2025 17:50:53 +0300 Subject: [PATCH 29/35] Remove extra string endpoint property in HTTPSubgraphExecutor --- lib/query-plan-executor/src/executors/http.rs | 10 ++++------ lib/query-plan-executor/src/executors/map.rs | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/query-plan-executor/src/executors/http.rs b/lib/query-plan-executor/src/executors/http.rs index 8690f52e3..464180217 100644 --- a/lib/query-plan-executor/src/executors/http.rs +++ b/lib/query-plan-executor/src/executors/http.rs @@ -16,17 +16,16 @@ use crate::{ #[derive(Debug)] pub struct HTTPSubgraphExecutor { - pub endpoint: String, + pub endpoint: http::Uri, pub http_client: Arc>>, - pub uri: http::Uri, pub header_map: HeaderMap, } const FIRST_VARIABLE_STR: &str = ",\"variables\":{"; impl HTTPSubgraphExecutor { - pub fn new(endpoint: String, http_client: Arc>>) -> Self { - let uri = endpoint + pub fn new(endpoint: &str, http_client: Arc>>) -> Self { + let endpoint = endpoint .parse::() .expect("Failed to parse endpoint as URI"); let mut header_map = HeaderMap::new(); @@ -36,7 +35,6 @@ impl HTTPSubgraphExecutor { ); HTTPSubgraphExecutor { endpoint, - uri, http_client, header_map, } @@ -88,7 +86,7 @@ impl HTTPSubgraphExecutor { let mut req = hyper::Request::builder() .method(http::Method::POST) - .uri(&self.uri) + .uri(&self.endpoint) .version(Version::HTTP_11) .body(body.into()) .map_err(|e| { diff --git a/lib/query-plan-executor/src/executors/map.rs b/lib/query-plan-executor/src/executors/map.rs index e4531ef81..52c8eda8d 100644 --- a/lib/query-plan-executor/src/executors/map.rs +++ b/lib/query-plan-executor/src/executors/map.rs @@ -65,7 +65,7 @@ impl SubgraphExecutorMap { .into_iter() .map(|(subgraph_name, endpoint)| { let executor = - HTTPSubgraphExecutor::new(endpoint, http_client_arc.clone()).to_boxed_arc(); + HTTPSubgraphExecutor::new(&endpoint, http_client_arc.clone()).to_boxed_arc(); (subgraph_name, executor) }) .collect::>(); From c6afa58d3fcee42da0f306a968d00746479f27b9 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 18 Jul 2025 13:41:49 +0300 Subject: [PATCH 30/35] Rebase from main --- lib/query-plan-executor/src/lib.rs | 79 +++++++++++------- .../src/schema_metadata.rs | 4 +- lib/query-plan-executor/src/tests/mod.rs | 2 +- ...nd_collect.rs => traverse_and_callback.rs} | 81 +++++++++++++++---- 4 files changed, 117 insertions(+), 49 deletions(-) rename lib/query-plan-executor/src/tests/{traverse_and_collect.rs => traverse_and_callback.rs} (73%) diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index b000ff03b..db3d23155 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -3,9 +3,7 @@ use futures::{future::BoxFuture, stream::FuturesUnordered, FutureExt, StreamExt} use query_planner::{ ast::{operation::OperationDefinition, selection_item::SelectionItem}, planner::plan_nodes::{ - ConditionNode, FetchNode, FetchNodePathSegment, FetchRewrite, FlattenNode, FlattenNodePath, - FlattenNodePathSegment, KeyRenamer, ParallelNode, PlanNode, QueryPlan, SequenceNode, - ValueSetter, + ConditionNode, FetchNode, FetchNodePathSegment, FetchRewrite, FlattenNode, FlattenNodePathSegment, KeyRenamer, ParallelNode, PlanNode, QueryPlan, SequenceNode, ValueSetter }, }; use serde::{Deserialize, Serialize}; @@ -453,7 +451,7 @@ fn process_root_result( enum ParallelJob<'a> { Root(ExecutionResult), - Flatten((ExecuteForRepresentationsResult, Vec<&'a str>)), + Flatten(ExecuteForRepresentationsResult, &'a[FlattenNodePathSegment]), } #[async_trait] @@ -493,8 +491,6 @@ impl ExecutablePlanNode for ParallelNode { jobs.push(Box::pin(job.map(ParallelJob::Root))); } PlanNode::Flatten(flatten_node) => { - let normalized_path: Vec<&str> = - flatten_node.path.iter().map(String::as_str).collect(); let mut filtered_representations = String::with_capacity(1024); let fetch_node = match flatten_node.node.as_ref() { PlanNode::Fetch(fetch_node) => fetch_node, @@ -509,8 +505,9 @@ impl ExecutablePlanNode for ParallelNode { let requires_nodes = fetch_node.requires.as_ref().unwrap(); let mut index = 0; let mut indexes = BTreeSet::new(); + let normalized_path = flatten_node.path.as_slice(); filtered_representations.push('['); - traverse_and_callback(data, &normalized_path, &mut |entity| { + traverse_and_callback(data, normalized_path, execution_context.schema_metadata, &mut |entity| { let is_projected = if let Some(input_rewrites) = &fetch_node.input_rewrites { // We need to own the value and not modify the original entity @@ -549,7 +546,7 @@ impl ExecutablePlanNode for ParallelNode { indexes, ); jobs.push(Box::pin( - job.map(|r| ParallelJob::Flatten((r, normalized_path))), + job.map(|r| ParallelJob::Flatten(r, normalized_path)), )); } _ => {} @@ -577,11 +574,11 @@ impl ExecutablePlanNode for ParallelNode { all_extensions.push(extensions); } } - ParallelJob::Flatten((result, path)) => { + ParallelJob::Flatten(result, path) => { if let Some(mut entities) = result.entities { let mut index_of_traverse = 0; let mut index_of_entities = 0; - traverse_and_callback(data, &path, &mut |target| { + traverse_and_callback(data, path, execution_context.schema_metadata, &mut |target| { if result.indexes.contains(&index_of_traverse) { let entity = entities.get_mut(index_of_entities).unwrap().take(); @@ -649,7 +646,7 @@ impl ExecutablePlanNode for FlattenNode { let requires_nodes = fetch_node.requires.as_ref().unwrap(); filtered_representations.push('['); let mut first = true; - traverse_and_callback(data, &normalized_path, &mut |entity| { + traverse_and_callback(data, self.path.as_slice(), execution_context.schema_metadata, &mut |entity| { let is_projected = if let Some(input_rewrites) = &fetch_node.input_rewrites { // We need to own the value and not modify the original entity let mut entity_owned = entity.to_owned(); @@ -1052,7 +1049,8 @@ impl QueryPlanExecutionContext<'_> { pub fn traverse_and_callback<'a, Callback>( current_data: &'a mut Value, - remaining_path: &[&str], + remaining_path: &[FlattenNodePathSegment], + schema_metadata: &SchemaMetadata, callback: &mut Callback, ) where Callback: FnMut(&'a mut Value), @@ -1071,30 +1069,49 @@ pub fn traverse_and_callback<'a, Callback>( return; } - let key = remaining_path[0]; - let rest_of_path = &remaining_path[1..]; - - if key == "@" { - if let Value::Array(list) = current_data { - for item in list.iter_mut() { - traverse_and_callback(item, rest_of_path, callback); + match &remaining_path[0] { + FlattenNodePathSegment::List => { + // If the key is List, we expect current_data to be an array + if let Value::Array(arr) = current_data { + let rest_of_path = &remaining_path[1..]; + for item in arr.iter_mut() { + traverse_and_callback(item, rest_of_path, schema_metadata, callback); + } + } + }, + FlattenNodePathSegment::Field(field_name) => { + // If the key is Field, we expect current_data to be an object + if let Value::Object(map) = current_data { + if let Some(next_data) = map.get_mut(field_name) { + let rest_of_path = &remaining_path[1..]; + traverse_and_callback(next_data, rest_of_path, schema_metadata, callback); + } } } - } else if let Value::Object(map) = current_data { - if let Some(next_data) = map.get_mut(key) { - traverse_and_callback(next_data, rest_of_path, callback); + FlattenNodePathSegment::Cast(type_condition) => { + // If the key is Cast, we expect current_data to be an object or an array + if let Value::Object(obj) = current_data { + let type_name = match obj.get(TYPENAME_FIELD) { + Some(Value::String(type_name)) => type_name, + _ => type_condition, // Default to type_condition if not found + }; + if schema_metadata.possible_types.entity_satisfies_type_condition( + type_name, + type_condition, + ) { + let rest_of_path = &remaining_path[1..]; + traverse_and_callback(current_data, rest_of_path, schema_metadata, callback); + } + } else if let Value::Array(arr) = current_data { + // If the current data is an array, we need to check each item + for item in arr.iter_mut() { + traverse_and_callback(item, remaining_path, schema_metadata, callback); + } + } } } -} -/// Checks if a serde_json::Value has a `__typename` that matches the given `type_name`. -fn contains_typename(value: &Value, type_name: &str, default_for_missing: bool) -> bool { - value - .as_object() - .and_then(|obj| obj.get("__typename")) - .and_then(Value::as_str) - .map_or(default_for_missing, |s| s == type_name) -} + } #[derive(Clone, Debug, PartialEq, Eq)] pub enum ExposeQueryPlanMode { diff --git a/lib/query-plan-executor/src/schema_metadata.rs b/lib/query-plan-executor/src/schema_metadata.rs index 7dd227f60..fc182cafc 100644 --- a/lib/query-plan-executor/src/schema_metadata.rs +++ b/lib/query-plan-executor/src/schema_metadata.rs @@ -7,7 +7,7 @@ use graphql_parser::{ use query_planner::consumer_schema::ConsumerSchema; use serde_json::{json, Value}; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct SchemaMetadata { pub possible_types: PossibleTypes, pub enum_values: HashMap>, @@ -15,7 +15,7 @@ pub struct SchemaMetadata { pub introspection_schema_root_json: Value, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct PossibleTypes { map: HashMap>, } diff --git a/lib/query-plan-executor/src/tests/mod.rs b/lib/query-plan-executor/src/tests/mod.rs index 7da7726d6..cb82db05e 100644 --- a/lib/query-plan-executor/src/tests/mod.rs +++ b/lib/query-plan-executor/src/tests/mod.rs @@ -3,7 +3,7 @@ use subgraphs::accounts; use crate::executors::{common::SubgraphExecutor, map::SubgraphExecutorMap}; -mod traverse_and_collect; +mod traverse_and_callback; #[test] fn query_executor_pipeline_locally() { diff --git a/lib/query-plan-executor/src/tests/traverse_and_collect.rs b/lib/query-plan-executor/src/tests/traverse_and_callback.rs similarity index 73% rename from lib/query-plan-executor/src/tests/traverse_and_collect.rs rename to lib/query-plan-executor/src/tests/traverse_and_callback.rs index f42854775..f3b128fac 100644 --- a/lib/query-plan-executor/src/tests/traverse_and_collect.rs +++ b/lib/query-plan-executor/src/tests/traverse_and_callback.rs @@ -1,7 +1,7 @@ use query_planner::planner::plan_nodes::FlattenNodePathSegment; -use serde_json::json; +use serde_json::{json}; -use crate::traverse_and_collect; +use crate::{schema_metadata::SchemaMetadata, traverse_and_callback}; #[test] fn array_cast_test() -> () { @@ -35,7 +35,10 @@ fn array_cast_test() -> () { ] }); - let result = traverse_and_collect(&mut data, &path); + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); let expected = json!([{ "id": "p4", "__typename": "Magazine", @@ -56,7 +59,11 @@ fn array_cast_test() -> () { fn simple_field_access() { let path = [FlattenNodePathSegment::Field("a".into())]; let mut data = json!({"a": 1, "b": 2}); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), serde_json::to_string_pretty(&json!([1])).unwrap_or_default() @@ -70,7 +77,11 @@ fn nested_field_access() { FlattenNodePathSegment::Field("b".into()), ]; let mut data = json!({"a": {"b": 3}}); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), serde_json::to_string_pretty(&json!([3])).unwrap_or_default() @@ -84,7 +95,11 @@ fn simple_list_access() { FlattenNodePathSegment::List, ]; let mut data = json!({"a": [1, 2, 3]}); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), serde_json::to_string_pretty(&json!([1, 2, 3])).unwrap_or_default() @@ -99,7 +114,11 @@ fn field_access_in_list() { FlattenNodePathSegment::Field("b".into()), ]; let mut data = json!({"a": [{"b": 1}, {"b": 2}]}); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), serde_json::to_string_pretty(&json!([1, 2])).unwrap_or_default() @@ -119,7 +138,11 @@ fn cast_in_list_with_field_access() { {"__typename": "TypeB", "b": 2}, {"__typename": "TypeA", "b": 3} ]}); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), serde_json::to_string_pretty(&json!([1, 3])).unwrap_or_default() @@ -146,7 +169,11 @@ fn filter_list_by_cast() { } ] }); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); let expected = json!([ { "__typename": "Movie", @@ -163,7 +190,11 @@ fn filter_list_by_cast() { fn invalid_field() { let path = [FlattenNodePathSegment::Field("c".into())]; let mut data = json!({"a": 1}); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), "[]" @@ -177,7 +208,11 @@ fn invalid_nested_field() { FlattenNodePathSegment::Field("c".into()), ]; let mut data = json!({"a": {"b": 1}}); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), "[]" @@ -188,7 +223,11 @@ fn invalid_nested_field() { fn initial_data_is_array() { let path = [FlattenNodePathSegment::List]; let mut data = json!([1, 2, 3]); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), serde_json::to_string_pretty(&json!([1, 2, 3])).unwrap_or_default() @@ -202,7 +241,11 @@ fn initial_data_is_array_with_field_access() { FlattenNodePathSegment::Field("a".into()), ]; let mut data = json!([{"a": 1}, {"a": 2}]); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), serde_json::to_string_pretty(&json!([1, 2])).unwrap_or_default() @@ -216,7 +259,11 @@ fn cast_on_object_without_typename() { FlattenNodePathSegment::Field("a".into()), ]; let mut data = json!({"a": 1}); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), serde_json::to_string_pretty(&json!([1])).unwrap_or_default() @@ -234,7 +281,11 @@ fn no_match_on_cast() { {"__typename": "TypeA", "b": 1}, {"__typename": "TypeB", "b": 2} ]}); - let result = traverse_and_collect(&mut data, &path); + + let mut result = vec![]; + traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { + result.push(value); + }); assert_eq!( serde_json::to_string_pretty(&result).unwrap_or_default(), "[]" From 5d525e45fe6f538e41486df238c5d09ea572e78f Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 18 Jul 2025 13:45:02 +0300 Subject: [PATCH 31/35] Fix format and clippy errors --- .../benches/executor_benches.rs | 14 +- lib/query-plan-executor/src/lib.rs | 181 ++++++++++-------- .../src/tests/traverse_and_callback.rs | 28 +-- 3 files changed, 122 insertions(+), 101 deletions(-) diff --git a/lib/query-plan-executor/benches/executor_benches.rs b/lib/query-plan-executor/benches/executor_benches.rs index 10e3faf35..51ebdb83e 100644 --- a/lib/query-plan-executor/benches/executor_benches.rs +++ b/lib/query-plan-executor/benches/executor_benches.rs @@ -11,7 +11,7 @@ use query_planner::graph::PlannerOverrideContext; use query_planner::planner::plan_nodes::FlattenNodePathSegment; use std::hint::black_box; -use query_plan_executor::schema_metadata::SchemaWithMetadata; +use query_plan_executor::schema_metadata::{SchemaMetadata, SchemaWithMetadata}; use query_plan_executor::ExecutableQueryPlan; use query_plan_executor::{execute_query_plan, ExposeQueryPlanMode}; use query_planner::ast::normalization::normalize_operation; @@ -261,13 +261,15 @@ fn traverse_and_collect(c: &mut Criterion) { FlattenNodePathSegment::Field("product".into()), ]; let mut result: Value = non_projected_result::get_result(); + let schema_metadata = SchemaMetadata::default(); c.bench_function("traverse_and_collect", |b| { b.iter(|| { let result = black_box(&mut result); + let schema_metadata = black_box(&schema_metadata); let data = result.get_mut("data").unwrap(); let path = black_box(&path); let mut results = vec![]; - query_plan_executor::traverse_and_callback(data, path, &mut |data| { + query_plan_executor::traverse_and_callback(data, path, schema_metadata, &mut |data| { results.push(data); }); black_box(()); @@ -363,16 +365,16 @@ fn project_requires(c: &mut Criterion) { ]; let mut result: Value = non_projected_result::get_result(); let data = result.get_mut("data").unwrap(); - let mut representations = vec![]; - query_plan_executor::traverse_and_callback(data, &path, &mut |data| { - representations.push(data); - }); let supergraph_sdl = std::fs::read_to_string("../../bench/supergraph.graphql") .expect("Unable to read input file"); let parsed_schema = parse_schema(&supergraph_sdl); let planner = query_planner::planner::Planner::new_from_supergraph(&parsed_schema) .expect("Failed to create planner from supergraph"); let schema_metadata = &planner.consumer_schema.schema_metadata(); + let mut representations = vec![]; + query_plan_executor::traverse_and_callback(data, &path, schema_metadata, &mut |data| { + representations.push(data); + }); let subgraph_executor_map = SubgraphExecutorMap::from_http_endpoint_map(planner.supergraph.subgraph_endpoint_map); let execution_context = query_plan_executor::QueryPlanExecutionContext { diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index db3d23155..61ae3e41c 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -3,7 +3,9 @@ use futures::{future::BoxFuture, stream::FuturesUnordered, FutureExt, StreamExt} use query_planner::{ ast::{operation::OperationDefinition, selection_item::SelectionItem}, planner::plan_nodes::{ - ConditionNode, FetchNode, FetchNodePathSegment, FetchRewrite, FlattenNode, FlattenNodePathSegment, KeyRenamer, ParallelNode, PlanNode, QueryPlan, SequenceNode, ValueSetter + ConditionNode, FetchNode, FetchNodePathSegment, FetchRewrite, FlattenNode, + FlattenNodePathSegment, KeyRenamer, ParallelNode, PlanNode, QueryPlan, SequenceNode, + ValueSetter, }, }; use serde::{Deserialize, Serialize}; @@ -451,7 +453,10 @@ fn process_root_result( enum ParallelJob<'a> { Root(ExecutionResult), - Flatten(ExecuteForRepresentationsResult, &'a[FlattenNodePathSegment]), + Flatten( + ExecuteForRepresentationsResult, + &'a [FlattenNodePathSegment], + ), } #[async_trait] @@ -507,38 +512,43 @@ impl ExecutablePlanNode for ParallelNode { let mut indexes = BTreeSet::new(); let normalized_path = flatten_node.path.as_slice(); filtered_representations.push('['); - traverse_and_callback(data, normalized_path, execution_context.schema_metadata, &mut |entity| { - let is_projected = - if let Some(input_rewrites) = &fetch_node.input_rewrites { - // We need to own the value and not modify the original entity - let mut entity_owned = entity.to_owned(); - for input_rewrite in input_rewrites { - input_rewrite.apply( - &execution_context.schema_metadata.possible_types, - &mut entity_owned, - ); - } - execution_context.project_requires( - &requires_nodes.items, - &entity_owned, - &mut filtered_representations, - indexes.is_empty(), - None, - ) - } else { - execution_context.project_requires( - &requires_nodes.items, - entity, - &mut filtered_representations, - indexes.is_empty(), - None, - ) - }; - if is_projected { - indexes.insert(index); - } - index += 1; - }); + traverse_and_callback( + data, + normalized_path, + execution_context.schema_metadata, + &mut |entity| { + let is_projected = + if let Some(input_rewrites) = &fetch_node.input_rewrites { + // We need to own the value and not modify the original entity + let mut entity_owned = entity.to_owned(); + for input_rewrite in input_rewrites { + input_rewrite.apply( + &execution_context.schema_metadata.possible_types, + &mut entity_owned, + ); + } + execution_context.project_requires( + &requires_nodes.items, + &entity_owned, + &mut filtered_representations, + indexes.is_empty(), + None, + ) + } else { + execution_context.project_requires( + &requires_nodes.items, + entity, + &mut filtered_representations, + indexes.is_empty(), + None, + ) + }; + if is_projected { + indexes.insert(index); + } + index += 1; + }, + ); filtered_representations.push(']'); let job = fetch_node.execute_for_projected_representations( execution_context, @@ -578,16 +588,21 @@ impl ExecutablePlanNode for ParallelNode { if let Some(mut entities) = result.entities { let mut index_of_traverse = 0; let mut index_of_entities = 0; - traverse_and_callback(data, path, execution_context.schema_metadata, &mut |target| { - if result.indexes.contains(&index_of_traverse) { - let entity = - entities.get_mut(index_of_entities).unwrap().take(); - // Merge the entity into the target - deep_merge::deep_merge(target, entity); - index_of_entities += 1; - } - index_of_traverse += 1; - }); + traverse_and_callback( + data, + path, + execution_context.schema_metadata, + &mut |target| { + if result.indexes.contains(&index_of_traverse) { + let entity = + entities.get_mut(index_of_entities).unwrap().take(); + // Merge the entity into the target + deep_merge::deep_merge(target, entity); + index_of_entities += 1; + } + index_of_traverse += 1; + }, + ); } // Process errors and extensions if let Some(errors) = result.errors { @@ -646,37 +661,42 @@ impl ExecutablePlanNode for FlattenNode { let requires_nodes = fetch_node.requires.as_ref().unwrap(); filtered_representations.push('['); let mut first = true; - traverse_and_callback(data, self.path.as_slice(), execution_context.schema_metadata, &mut |entity| { - let is_projected = if let Some(input_rewrites) = &fetch_node.input_rewrites { - // We need to own the value and not modify the original entity - let mut entity_owned = entity.to_owned(); - for input_rewrite in input_rewrites { - input_rewrite.apply( - &execution_context.schema_metadata.possible_types, - &mut entity_owned, - ); + traverse_and_callback( + data, + self.path.as_slice(), + execution_context.schema_metadata, + &mut |entity| { + let is_projected = if let Some(input_rewrites) = &fetch_node.input_rewrites { + // We need to own the value and not modify the original entity + let mut entity_owned = entity.to_owned(); + for input_rewrite in input_rewrites { + input_rewrite.apply( + &execution_context.schema_metadata.possible_types, + &mut entity_owned, + ); + } + execution_context.project_requires( + &requires_nodes.items, + &entity_owned, + &mut filtered_representations, + first, + None, + ) + } else { + execution_context.project_requires( + &requires_nodes.items, + entity, + &mut filtered_representations, + first, + None, + ) + }; + if is_projected { + representations.push(entity); + first = false; } - execution_context.project_requires( - &requires_nodes.items, - &entity_owned, - &mut filtered_representations, - first, - None, - ) - } else { - execution_context.project_requires( - &requires_nodes.items, - entity, - &mut filtered_representations, - first, - None, - ) - }; - if is_projected { - representations.push(entity); - first = false; - } - }); + }, + ); filtered_representations.push(']'); trace!( "traversed and collected representations: {:?} in {:#?}", @@ -1078,7 +1098,7 @@ pub fn traverse_and_callback<'a, Callback>( traverse_and_callback(item, rest_of_path, schema_metadata, callback); } } - }, + } FlattenNodePathSegment::Field(field_name) => { // If the key is Field, we expect current_data to be an object if let Value::Object(map) = current_data { @@ -1095,10 +1115,10 @@ pub fn traverse_and_callback<'a, Callback>( Some(Value::String(type_name)) => type_name, _ => type_condition, // Default to type_condition if not found }; - if schema_metadata.possible_types.entity_satisfies_type_condition( - type_name, - type_condition, - ) { + if schema_metadata + .possible_types + .entity_satisfies_type_condition(type_name, type_condition) + { let rest_of_path = &remaining_path[1..]; traverse_and_callback(current_data, rest_of_path, schema_metadata, callback); } @@ -1110,8 +1130,7 @@ pub fn traverse_and_callback<'a, Callback>( } } } - - } +} #[derive(Clone, Debug, PartialEq, Eq)] pub enum ExposeQueryPlanMode { diff --git a/lib/query-plan-executor/src/tests/traverse_and_callback.rs b/lib/query-plan-executor/src/tests/traverse_and_callback.rs index f3b128fac..a41e46c17 100644 --- a/lib/query-plan-executor/src/tests/traverse_and_callback.rs +++ b/lib/query-plan-executor/src/tests/traverse_and_callback.rs @@ -1,10 +1,10 @@ use query_planner::planner::plan_nodes::FlattenNodePathSegment; -use serde_json::{json}; +use serde_json::json; use crate::{schema_metadata::SchemaMetadata, traverse_and_callback}; #[test] -fn array_cast_test() -> () { +fn array_cast_test() { let path = [ FlattenNodePathSegment::Field("magazine".into()), FlattenNodePathSegment::List, @@ -59,7 +59,7 @@ fn array_cast_test() -> () { fn simple_field_access() { let path = [FlattenNodePathSegment::Field("a".into())]; let mut data = json!({"a": 1, "b": 2}); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -77,7 +77,7 @@ fn nested_field_access() { FlattenNodePathSegment::Field("b".into()), ]; let mut data = json!({"a": {"b": 3}}); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -95,7 +95,7 @@ fn simple_list_access() { FlattenNodePathSegment::List, ]; let mut data = json!({"a": [1, 2, 3]}); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -114,7 +114,7 @@ fn field_access_in_list() { FlattenNodePathSegment::Field("b".into()), ]; let mut data = json!({"a": [{"b": 1}, {"b": 2}]}); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -138,7 +138,7 @@ fn cast_in_list_with_field_access() { {"__typename": "TypeB", "b": 2}, {"__typename": "TypeA", "b": 3} ]}); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -169,7 +169,7 @@ fn filter_list_by_cast() { } ] }); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -190,7 +190,7 @@ fn filter_list_by_cast() { fn invalid_field() { let path = [FlattenNodePathSegment::Field("c".into())]; let mut data = json!({"a": 1}); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -208,7 +208,7 @@ fn invalid_nested_field() { FlattenNodePathSegment::Field("c".into()), ]; let mut data = json!({"a": {"b": 1}}); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -223,7 +223,7 @@ fn invalid_nested_field() { fn initial_data_is_array() { let path = [FlattenNodePathSegment::List]; let mut data = json!([1, 2, 3]); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -241,7 +241,7 @@ fn initial_data_is_array_with_field_access() { FlattenNodePathSegment::Field("a".into()), ]; let mut data = json!([{"a": 1}, {"a": 2}]); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -259,7 +259,7 @@ fn cast_on_object_without_typename() { FlattenNodePathSegment::Field("a".into()), ]; let mut data = json!({"a": 1}); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); @@ -281,7 +281,7 @@ fn no_match_on_cast() { {"__typename": "TypeA", "b": 1}, {"__typename": "TypeB", "b": 2} ]}); - + let mut result = vec![]; traverse_and_callback(&mut data, &path, &SchemaMetadata::default(), &mut |value| { result.push(value); From 757a21c4751b47a60f4779a9f597c52361fb993f Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 21 Jul 2025 13:08:41 +0300 Subject: [PATCH 32/35] Projection preparation to handle conflicting fields (#285) Closes https://github.com/graphql-hive/gateway-rs/pull/249 --- Cargo.lock | 1 + .../src/pipeline/coerce_variables_service.rs | 6 +- bin/gateway/src/pipeline/execution_service.rs | 3 +- bin/gateway/src/pipeline/normalize_service.rs | 17 +- lib/query-plan-executor/Cargo.toml | 1 + .../benches/executor_benches.rs | 37 +- lib/query-plan-executor/src/lib.rs | 41 +- lib/query-plan-executor/src/projection.rs | 658 ++++++++++++------ .../src/schema_metadata.rs | 19 +- lib/query-plan-executor/src/tests/mod.rs | 10 +- 10 files changed, 545 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cac37880..eb36947e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1709,6 +1709,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", + "indexmap 2.10.0", "insta", "query-planner", "serde", diff --git a/bin/gateway/src/pipeline/coerce_variables_service.rs b/bin/gateway/src/pipeline/coerce_variables_service.rs index 7bd8a6832..16b5c1a57 100644 --- a/bin/gateway/src/pipeline/coerce_variables_service.rs +++ b/bin/gateway/src/pipeline/coerce_variables_service.rs @@ -61,10 +61,8 @@ impl GatewayPipelineLayer for CoerceVariablesService { })?; if http_payload.http_method == Method::GET { - if let Some(OperationKind::Mutation) = normalized_operation - .normalized_document - .operation - .operation_kind + if let Some(OperationKind::Mutation) = + normalized_operation.operation_for_plan.operation_kind { error!("Mutation is not allowed over GET, stopping"); diff --git a/bin/gateway/src/pipeline/execution_service.rs b/bin/gateway/src/pipeline/execution_service.rs index 2196e5069..9cc605258 100644 --- a/bin/gateway/src/pipeline/execution_service.rs +++ b/bin/gateway/src/pipeline/execution_service.rs @@ -84,7 +84,8 @@ impl Service> for ExecutionService { &app_state.subgraph_executor_map, &variable_payload.variables_map, &app_state.schema_metadata, - &normalized_payload.normalized_document.operation, + normalized_payload.root_type_name, + &normalized_payload.projection_plan, normalized_payload.has_introspection, expose_query_plan, ) diff --git a/bin/gateway/src/pipeline/normalize_service.rs b/bin/gateway/src/pipeline/normalize_service.rs index e7057d842..fe6356822 100644 --- a/bin/gateway/src/pipeline/normalize_service.rs +++ b/bin/gateway/src/pipeline/normalize_service.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use axum::body::Body; use http::Request; use query_plan_executor::introspection::filter_introspection_fields_in_operation; -use query_planner::ast::document::NormalizedDocument; +use query_plan_executor::projection::FieldProjectionPlan; use query_planner::ast::normalization::normalize_operation; use query_planner::ast::operation::OperationDefinition; @@ -18,12 +18,12 @@ use crate::pipeline::parser_service::GraphQLParserPayload; use crate::shared_state::GatewaySharedState; use tracing::{error, trace}; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct GraphQLNormalizationPayload { - /// The raw, normalized GraphQL document. - pub normalized_document: NormalizedDocument, /// The operation to execute, without introspection fields. pub operation_for_plan: OperationDefinition, + pub root_type_name: &'static str, + pub projection_plan: Vec, pub has_introspection: bool, } @@ -82,8 +82,8 @@ impl GatewayPipelineLayer for GraphQLOperationNormalizationService { Some(payload) => { trace!( "Found normalized GraphQL operation in cache (operation name={:?}): {}", - payload.normalized_document.operation_name, - payload.normalized_document.operation + payload.operation_for_plan.name, + payload.operation_for_plan ); req.extensions_mut().insert(payload); Ok(GatewayPipelineStepDecision::Continue) @@ -110,8 +110,11 @@ impl GatewayPipelineLayer for GraphQLOperationNormalizationService { filtered_operation_for_plan ); + let (root_type_name, projection_plan) = + FieldProjectionPlan::from_operation(operation, &app_state.schema_metadata); let payload = GraphQLNormalizationPayload { - normalized_document: doc, + root_type_name, + projection_plan, operation_for_plan: filtered_operation_for_plan, has_introspection, }; diff --git a/lib/query-plan-executor/Cargo.toml b/lib/query-plan-executor/Cargo.toml index 010e78449..87c92f655 100644 --- a/lib/query-plan-executor/Cargo.toml +++ b/lib/query-plan-executor/Cargo.toml @@ -21,6 +21,7 @@ hyper-util = { version= "0.1.15", features = ["client", "client-legacy", "http1" http-body-util = "0.1.3" http = "1.3.1" sonic-rs = "0.3" +indexmap = "2.10.0" [dev-dependencies] criterion = { version = "0.6", features = ["html_reports", "async_tokio"] } diff --git a/lib/query-plan-executor/benches/executor_benches.rs b/lib/query-plan-executor/benches/executor_benches.rs index 51ebdb83e..d8a349aa2 100644 --- a/lib/query-plan-executor/benches/executor_benches.rs +++ b/lib/query-plan-executor/benches/executor_benches.rs @@ -43,19 +43,26 @@ fn query_plan_executor_pipeline_via_http(c: &mut Criterion) { let subgraph_endpoint_map = planner.supergraph.subgraph_endpoint_map; let schema_metadata = planner.consumer_schema.schema_metadata(); let subgraph_executor_map = SubgraphExecutorMap::from_http_endpoint_map(subgraph_endpoint_map); + let (root_type_name, projection_selections) = + query_plan_executor::projection::FieldProjectionPlan::from_operation( + normalized_operation, + &schema_metadata, + ); c.bench_function("query_plan_executor_pipeline_via_http", |b| { b.to_async(&rt).iter(|| async { let query_plan = black_box(&query_plan); let schema_metadata = black_box(&schema_metadata); - let operation = black_box(&normalized_operation); let subgraph_executor_map = black_box(&subgraph_executor_map); + let projection_selections = black_box(&projection_selections); + let root_type_name = black_box(root_type_name); let has_introspection = false; let result = execute_query_plan( query_plan, subgraph_executor_map, &None, schema_metadata, - operation, + root_type_name, + projection_selections, has_introspection, ExposeQueryPlanMode::No, ) @@ -135,19 +142,27 @@ fn query_plan_executor_pipeline_locally(c: &mut Criterion) { subgraph_executor_map.insert_boxed_arc("products".to_string(), products.to_boxed_arc()); subgraph_executor_map.insert_boxed_arc("reviews".to_string(), reviews.to_boxed_arc()); + let (root_type_name, projection_selections) = + query_plan_executor::projection::FieldProjectionPlan::from_operation( + normalized_operation, + &schema_metadata, + ); + c.bench_function("query_plan_executor_pipeline_locally", |b| { b.to_async(&rt).iter(|| async { let query_plan = black_box(&query_plan); let schema_metadata = black_box(&schema_metadata); - let operation = black_box(&normalized_operation); let subgraph_executor_map = black_box(&subgraph_executor_map); + let projection_selections = black_box(&projection_selections); + let root_type_name = black_box(root_type_name); let has_introspection = false; let result = execute_query_plan( query_plan, subgraph_executor_map, &None, schema_metadata, - operation, + root_type_name, + projection_selections, has_introspection, ExposeQueryPlanMode::No, ) @@ -222,7 +237,11 @@ fn project_data_by_operation(c: &mut Criterion) { .expect("Failed to normalize operation"); let normalized_operation = normalized_document.executable_operation(); let schema_metadata = planner.consumer_schema.schema_metadata(); - let operation = black_box(&normalized_operation); + let (root_type_name, projection_selections) = + query_plan_executor::projection::FieldProjectionPlan::from_operation( + normalized_operation, + &schema_metadata, + ); c.bench_function("project_data_by_operation", |b| { b.iter(|| { let mut data = non_projected_result::get_result(); @@ -231,14 +250,14 @@ fn project_data_by_operation(c: &mut Criterion) { let errors = black_box(&mut errors); let extensions = HashMap::new(); let extensions = black_box(&extensions); - let operation = black_box(&operation); - let schema_metadata = black_box(&schema_metadata); + let projection_selections = black_box(&projection_selections); + let root_type_name = black_box(root_type_name); let result = query_plan_executor::projection::project_by_operation( data, errors, extensions, - operation, - schema_metadata, + root_type_name, + projection_selections, &None, ); black_box(result); diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index 61ae3e41c..e7bc3f004 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use futures::{future::BoxFuture, stream::FuturesUnordered, FutureExt, StreamExt}; use query_planner::{ - ast::{operation::OperationDefinition, selection_item::SelectionItem}, + ast::selection_item::SelectionItem, planner::plan_nodes::{ ConditionNode, FetchNode, FetchNodePathSegment, FetchRewrite, FlattenNode, FlattenNodePathSegment, KeyRenamer, ParallelNode, PlanNode, QueryPlan, SequenceNode, @@ -17,6 +17,7 @@ use tracing::{instrument, trace, warn}; // For reading file in main use crate::{ executors::map::SubgraphExecutorMap, json_writer::write_and_escape_string, + projection::FieldProjectionPlan, schema_metadata::{PossibleTypes, SchemaMetadata}, }; pub mod deep_merge; @@ -1145,22 +1146,35 @@ pub enum ExposeQueryPlanMode { fields( query_plan = ?query_plan, variable_values = ?variable_values, - operation = ?operation.to_string(), ) )] +#[allow(clippy::too_many_arguments)] pub async fn execute_query_plan( query_plan: &QueryPlan, subgraph_executor_map: &SubgraphExecutorMap, variable_values: &Option>, schema_metadata: &SchemaMetadata, - operation: &OperationDefinition, + operation_type_name: &str, + selections: &Vec, has_introspection: bool, expose_query_plan: ExposeQueryPlanMode, ) -> String { - let mut result_data = Value::Null; // Initialize data as Null + let mut result_data = if has_introspection { + schema_metadata.introspection_query_json.clone() + } else { + Value::Null + }; let mut result_errors = vec![]; // Initial errors are empty - #[allow(unused_mut)] - let mut result_extensions = HashMap::new(); // Initial extensions are empty + let mut result_extensions = if expose_query_plan == ExposeQueryPlanMode::Yes + || expose_query_plan == ExposeQueryPlanMode::DryRun + { + HashMap::from_iter([( + "queryPlan".to_string(), + serde_json::to_value(query_plan).unwrap(), + )]) + } else { + HashMap::new() + }; let mut execution_context = QueryPlanExecutionContext { variable_values, subgraph_executor_map, @@ -1175,23 +1189,12 @@ pub async fn execute_query_plan( } result_errors = execution_context.errors; // Get the final errors from the execution context result_extensions = execution_context.extensions; // Get the final extensions from the execution context - if result_data.is_null() && has_introspection { - result_data = Value::Object(Map::new()); // Ensure data is an empty object if it was null - } - if expose_query_plan == ExposeQueryPlanMode::Yes - || expose_query_plan == ExposeQueryPlanMode::DryRun - { - result_extensions.insert( - "queryPlan".to_string(), - serde_json::to_value(query_plan).unwrap(), - ); - } projection::project_by_operation( &mut result_data, &mut result_errors, &result_extensions, - operation, - schema_metadata, + operation_type_name, + selections, variable_values, ) } diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index e74d85aa5..c0875188b 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -1,6 +1,7 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Write; +use indexmap::IndexMap; use query_planner::{ ast::{ operation::OperationDefinition, selection_item::SelectionItem, selection_set::SelectionSet, @@ -15,22 +16,368 @@ use crate::{ TYPENAME_FIELD, }; +#[derive(Debug)] +pub struct FieldProjectionPlan { + field_name: String, + field_type: String, + response_key: String, + conditions: FieldProjectionCondition, + selections: Option>, +} + +#[derive(Debug, Clone)] +pub enum FieldProjectionCondition { + IncludeIfVariable(String), + SkipIfVariable(String), + ParentTypeCondition(HashSet), + FieldTypeCondition(HashSet), + EnumValuesCondition(HashSet), + Or(Box, Box), + And(Box, Box), +} + +pub enum FieldProjectionConditionError { + InvalidParentType, + InvalidFieldType, + Skip, + InvalidEnumValue, +} + +impl FieldProjectionCondition { + pub fn check( + &self, + parent_type_name: &str, + field_type_name: &str, + field_value: &Option<&Value>, + variable_values: &Option>, + ) -> Result<(), FieldProjectionConditionError> { + match self { + FieldProjectionCondition::And(condition_a, condition_b) => condition_a + .check( + parent_type_name, + field_type_name, + field_value, + variable_values, + ) + .and(condition_b.check( + parent_type_name, + field_type_name, + field_value, + variable_values, + )), + FieldProjectionCondition::Or(condition_a, condition_b) => condition_a + .check( + parent_type_name, + field_type_name, + field_value, + variable_values, + ) + .or(condition_b.check( + parent_type_name, + field_type_name, + field_value, + variable_values, + )), + FieldProjectionCondition::IncludeIfVariable(variable_name) => { + if let Some(values) = variable_values { + if values + .get(variable_name) + .is_some_and(|v| v.as_bool().unwrap_or(false)) + { + Ok(()) + } else { + Err(FieldProjectionConditionError::Skip) + } + } else { + Err(FieldProjectionConditionError::Skip) + } + } + FieldProjectionCondition::SkipIfVariable(variable_name) => { + if let Some(values) = variable_values { + if values + .get(variable_name) + .is_some_and(|v| v.as_bool().unwrap_or(false)) + { + return Err(FieldProjectionConditionError::Skip); + } + } + Ok(()) + } + FieldProjectionCondition::ParentTypeCondition(possible_types) => { + if possible_types.contains(parent_type_name) { + Ok(()) + } else { + Err(FieldProjectionConditionError::InvalidParentType) + } + } + FieldProjectionCondition::FieldTypeCondition(possible_types) => { + let field_type_name = field_value + .and_then(|value| value.get(TYPENAME_FIELD)) + .and_then(|v| v.as_str()) + .unwrap_or(field_type_name); + if possible_types.contains(field_type_name) { + Ok(()) + } else { + Err(FieldProjectionConditionError::InvalidFieldType) + } + } + FieldProjectionCondition::EnumValuesCondition(enum_values) => { + if let Some(Value::String(string_value)) = field_value { + if enum_values.contains(string_value) { + Ok(()) + } else { + Err(FieldProjectionConditionError::InvalidEnumValue) + } + } else { + Ok(()) + } + } + } + } +} + +impl FieldProjectionPlan { + pub fn from_selection_set( + selection_set: &SelectionSet, + schema_metadata: &SchemaMetadata, + type_name: &str, + condition: &FieldProjectionCondition, + ) -> Option> { + let mut field_selections: IndexMap = IndexMap::new(); + for selection_item in &selection_set.items { + match selection_item { + SelectionItem::Field(field) => { + let field_name = &field.name; + let response_key = field.alias.as_ref().unwrap_or(field_name); + let field_type = if field_name == TYPENAME_FIELD { + "String" + } else { + let field_map = match schema_metadata.type_fields.get(type_name) { + Some(fields) => fields, + None => { + warn!("No fields found for type {} in schema metadata.", type_name); + return None; + } + }; + match field_map.get(field_name) { + Some(field_type) => field_type, + None => { + warn!( + "Field {} not found in type {} in schema metadata.", + field_name, type_name + ); + continue; + } + } + }; + + let possible_types_for_field = schema_metadata + .possible_types + .get_possible_types(field_type); + let mut conditions_for_selections = + FieldProjectionCondition::ParentTypeCondition( + possible_types_for_field.clone(), + ); + if let Some(include_if) = &field.include_if { + conditions_for_selections = FieldProjectionCondition::And( + Box::new(conditions_for_selections), + Box::new(FieldProjectionCondition::IncludeIfVariable( + include_if.clone(), + )), + ); + } + if let Some(skip_if) = &field.skip_if { + conditions_for_selections = FieldProjectionCondition::And( + Box::new(conditions_for_selections), + Box::new(FieldProjectionCondition::SkipIfVariable(skip_if.clone())), + ); + } + + let mut condition_for_field: FieldProjectionCondition = condition.clone(); + condition_for_field = FieldProjectionCondition::And( + Box::new(condition_for_field), + Box::new(FieldProjectionCondition::FieldTypeCondition( + possible_types_for_field, + )), + ); + if let Some(include_if) = &field.include_if { + condition_for_field = FieldProjectionCondition::And( + Box::new(condition_for_field), + Box::new(FieldProjectionCondition::IncludeIfVariable( + include_if.clone(), + )), + ); + } + if let Some(skip_if) = &field.skip_if { + condition_for_field = FieldProjectionCondition::And( + Box::new(condition_for_field), + Box::new(FieldProjectionCondition::SkipIfVariable(skip_if.clone())), + ); + } + if let Some(enum_values) = schema_metadata.enum_values.get(field_type) { + condition_for_field = FieldProjectionCondition::And( + Box::new(condition_for_field), + Box::new(FieldProjectionCondition::EnumValuesCondition( + enum_values.clone(), + )), + ); + } + + if let Some(existing_field) = field_selections.get_mut(response_key) { + existing_field.conditions = FieldProjectionCondition::Or( + Box::new(existing_field.conditions.clone()), + Box::new(condition_for_field), + ); + + if let Some(new_selections) = { + FieldProjectionPlan::from_selection_set( + &field.selections, + schema_metadata, + field_type, + &conditions_for_selections, + ) + } { + match existing_field.selections { + Some(ref mut selections) => { + selections.extend(new_selections); + } + None => { + existing_field.selections = Some(new_selections); + } + } + } + } else { + let new_plan = FieldProjectionPlan { + field_name: field_name.to_string(), + field_type: field_type.to_string(), + response_key: response_key.to_string(), + conditions: condition_for_field, + selections: FieldProjectionPlan::from_selection_set( + &field.selections, + schema_metadata, + field_type, + &conditions_for_selections, + ), + }; + field_selections.insert(response_key.to_string(), new_plan); + } + } + SelectionItem::InlineFragment(inline_fragment) => { + let inline_fragment_type = &inline_fragment.type_condition; + let mut condition_for_inline_fragment = condition.clone(); + let possible_types_for_inline_fragment = schema_metadata + .possible_types + .get_possible_types(inline_fragment_type); + condition_for_inline_fragment = FieldProjectionCondition::And( + Box::new(condition_for_inline_fragment), + Box::new(FieldProjectionCondition::ParentTypeCondition( + possible_types_for_inline_fragment, + )), + ); + if let Some(skip_if) = &inline_fragment.skip_if { + condition_for_inline_fragment = FieldProjectionCondition::And( + Box::new(condition_for_inline_fragment), + Box::new(FieldProjectionCondition::SkipIfVariable(skip_if.clone())), + ); + } + if let Some(include_if) = &inline_fragment.include_if { + condition_for_inline_fragment = FieldProjectionCondition::And( + Box::new(condition_for_inline_fragment), + Box::new(FieldProjectionCondition::IncludeIfVariable( + include_if.clone(), + )), + ); + } + if let Some(inline_fragment_selections) = + FieldProjectionPlan::from_selection_set( + &inline_fragment.selections, + schema_metadata, + inline_fragment_type, + &condition_for_inline_fragment, + ) + { + for selection in inline_fragment_selections { + if let Some(existing_field) = + field_selections.get_mut(&selection.response_key) + { + existing_field.conditions = FieldProjectionCondition::Or( + Box::new(existing_field.conditions.clone()), + Box::new(selection.conditions), + ); + if let Some(new_selections) = selection.selections { + match existing_field.selections { + Some(ref mut selections) => { + selections.extend(new_selections); + } + None => { + existing_field.selections = Some(new_selections); + } + } + } + } else { + field_selections + .insert(selection.response_key.to_string(), selection); + } + } + } + } + SelectionItem::FragmentSpread(_name_ref) => { + // Fragment spreads should not exist in the final response projection. + unreachable!( + "Fragment spreads should not exist in the final response projection." + ); + } + } + } + + if field_selections.is_empty() { + None + } else { + Some( + field_selections + .into_iter() + .map(|(_, selection)| selection) + .collect::>(), + ) + } + } + pub fn from_operation( + operation: &OperationDefinition, + schema_metadata: &SchemaMetadata, + ) -> (&'static str, Vec) { + let root_type_name = match operation.operation_kind { + Some(OperationKind::Query) => "Query", + Some(OperationKind::Mutation) => "Mutation", + Some(OperationKind::Subscription) => "Subscription", + None => "Query", + }; + + let field_type_conditions = schema_metadata + .possible_types + .get_possible_types(root_type_name); + let conditions = FieldProjectionCondition::ParentTypeCondition(field_type_conditions); + ( + root_type_name, + FieldProjectionPlan::from_selection_set( + &operation.selection_set, + schema_metadata, + root_type_name, + &conditions, + ) + .unwrap_or_default(), + ) + } +} + #[instrument(level = "trace", skip_all)] pub fn project_by_operation( data: &mut Value, errors: &mut Vec, extensions: &HashMap, - operation: &OperationDefinition, - schema_metadata: &SchemaMetadata, + operation_type_name: &str, + selections: &Vec, variable_values: &Option>, ) -> String { - let root_type_name = match operation.operation_kind { - Some(OperationKind::Query) => "Query", - Some(OperationKind::Mutation) => "Mutation", - Some(OperationKind::Subscription) => "Subscription", - None => "Query", - }; - // We may want to remove it, but let's see. let mut buffer = String::with_capacity(4096); @@ -40,15 +387,24 @@ pub fn project_by_operation( buffer.push('"'); buffer.push(':'); - project_selection_set( - data, - errors, - &operation.selection_set, - root_type_name, - schema_metadata, - variable_values, - &mut buffer, - ); + if let Some(data_map) = data.as_object_mut() { + let mut first = true; + project_selection_set_with_map( + data_map, + errors, + selections, + variable_values, + operation_type_name, + &mut buffer, + &mut first, // Start with first as true to add the opening brace + ); + if !first { + buffer.push('}'); + } else { + // If no selections were made, we should return an empty object + buffer.push_str("{}"); + } + } if !errors.is_empty() { write!( @@ -75,17 +431,13 @@ pub fn project_by_operation( level = "trace", skip_all, fields( - type_name = %type_name, - selection_set = ?selection_set.items.iter().map(|s| s.to_string()).collect::>(), data = ?data ) )] fn project_selection_set( data: &Value, errors: &mut Vec, - selection_set: &SelectionSet, - type_name: &str, - schema_metadata: &SchemaMetadata, + selection: &FieldProjectionPlan, variable_values: &Option>, buffer: &mut String, ) { @@ -95,21 +447,6 @@ fn project_selection_set( Value::Bool(false) => buffer.push_str("false"), Value::Number(num) => write!(buffer, "{}", num).unwrap(), Value::String(value) => { - if let Some(enum_values) = schema_metadata.enum_values.get(type_name) { - if !enum_values.contains(value) { - errors.push(GraphQLError { - message: format!( - "Value is not a valid enum value for type '{}'", - type_name - ), - locations: None, - path: None, - extensions: None, - }); - buffer.push_str("null"); - return; - } - } write_and_escape_string(buffer, value); } Value::Array(arr) => { @@ -119,92 +456,41 @@ fn project_selection_set( if !first { buffer.push(','); } - project_selection_set( - item, - errors, - selection_set, - type_name, - schema_metadata, - variable_values, - buffer, - ); + project_selection_set(item, errors, selection, variable_values, buffer); first = false; } buffer.push(']'); } Value::Object(obj) => { - let mut first = true; - let is_projected = project_selection_set_with_map( - obj, - errors, - selection_set, - type_name, - schema_metadata, - variable_values, - buffer, - &mut first, - ); - if !is_projected { - buffer.push_str("null"); - } else - // If first is mutated, it means we added "{" - if !first { - buffer.push('}'); - } else { - // If first is still true, it means we didn't add anything, so we should just send an empty object - buffer.push_str("{}"); - } - } - } -} - -trait IncludeOrSkipByVariable { - fn include_or_skip_by_variable(&self, variable_values: &Option>) - -> bool; -} - -impl IncludeOrSkipByVariable for SelectionItem { - fn include_or_skip_by_variable( - &self, - variable_values: &Option>, - ) -> bool { - if let Some(skip_variable) = match self { - SelectionItem::Field(field) => field.skip_if.as_ref(), - SelectionItem::InlineFragment(inline_fragment) => inline_fragment.skip_if.as_ref(), - SelectionItem::FragmentSpread(_) => { - // Fragment spreads should not exist in the final response projection. - unreachable!("Fragment spreads should not exist in the final response projection.") - } - } { - if let Some(variable_value) = variable_values - .as_ref() - .and_then(|vars| vars.get(skip_variable)) - { - if variable_value == &Value::Bool(true) { - return false; // Skip this field if the variable is true + match selection.selections.as_ref() { + Some(selections) => { + let mut first = true; + let type_name = obj + .get(TYPENAME_FIELD) + .and_then(Value::as_str) + .unwrap_or(&selection.field_type); + project_selection_set_with_map( + obj, + errors, + selections, + variable_values, + type_name, + buffer, + &mut first, + ); + if !first { + buffer.push('}'); + } else { + // If no selections were made, we should return an empty object + buffer.push_str("{}"); + } } - } - return true; - } - if let Some(include_variable) = match self { - SelectionItem::Field(field) => field.include_if.as_ref(), - SelectionItem::InlineFragment(inline_fragment) => inline_fragment.include_if.as_ref(), - SelectionItem::FragmentSpread(_) => { - // Fragment spreads should not exist in the final response projection. - unreachable!("Fragment spreads should not exist in the final response projection.") - } - } { - if let Some(variable_value) = variable_values - .as_ref() - .and_then(|vars| vars.get(include_variable)) - { - if variable_value == &Value::Bool(true) { - return true; // Skip this field if the variable is not true + None => { + // If the selection set is not projected, we should return null + buffer.push_str("null"); } } - return false; } - true } } @@ -212,8 +498,6 @@ impl IncludeOrSkipByVariable for SelectionItem { level = "trace", skip_all, fields( - type_name = %type_name, - selection_set = ?selection_set.items.iter().map(|s| s.to_string()).collect::>(), obj = ?obj ) )] @@ -222,38 +506,23 @@ impl IncludeOrSkipByVariable for SelectionItem { fn project_selection_set_with_map( obj: &Map, errors: &mut Vec, - selection_set: &SelectionSet, - type_name: &str, - schema_metadata: &SchemaMetadata, + selections: &Vec, variable_values: &Option>, + parent_type_name: &str, buffer: &mut String, first: &mut bool, -) -> bool { - let type_name = match obj.get(TYPENAME_FIELD) { - Some(Value::String(type_name)) => type_name, - _ => type_name, - }; - let field_map = match schema_metadata.type_fields.get(type_name) { - Some(field_map) => field_map, - None => { - // If the type is not found, we can't project anything - warn!( - "Type {} not found in schema metadata. Skipping projection.", - type_name - ); - return false; - } - }; - - for selection in &selection_set.items { - if !selection.include_or_skip_by_variable(variable_values) { - // If the selection is not included by variable, skip it - continue; - } - match selection { - SelectionItem::Field(field) => { - let response_key = field.alias.as_ref().unwrap_or(&field.name); - +) { + for selection in selections { + let field_val = obj + .get(&selection.field_name) + .or_else(|| obj.get(&selection.response_key)); + match selection.conditions.check( + parent_type_name, + &selection.field_type, + &field_val, + variable_values, + ) { + Ok(_) => { if *first { buffer.push('{'); } else { @@ -261,70 +530,61 @@ fn project_selection_set_with_map( } *first = false; - if field.name == TYPENAME_FIELD { - buffer.push('"'); - buffer.push_str(response_key); - buffer.push_str("\":\""); - buffer.push_str(type_name); - buffer.push('"'); - continue; - } - buffer.push('"'); - buffer.push_str(response_key); + buffer.push_str(&selection.response_key); buffer.push_str("\":"); - let field_type: &str = field_map - .get(&field.name) - .map(|s| s.as_str()) - .unwrap_or("Any"); - - let field_val = if field.name == "__schema" && type_name == "Query" { - Some(&schema_metadata.introspection_schema_root_json) - } else { - obj.get(response_key) - }; - if let Some(field_val) = field_val { - project_selection_set( - field_val, - errors, - &field.selections, - field_type, - schema_metadata, - variable_values, - buffer, - ); + project_selection_set(field_val, errors, selection, variable_values, buffer); + } else if selection.field_name == TYPENAME_FIELD { + // If the field is TYPENAME_FIELD, we should set it to the parent type name + buffer.push('"'); + buffer.push_str(parent_type_name); + buffer.push('"'); } else { // If the field is not found in the object, set it to Null buffer.push_str("null"); - continue; } } - SelectionItem::InlineFragment(inline_fragment) => { - if schema_metadata - .possible_types - .entity_satisfies_type_condition(type_name, &inline_fragment.type_condition) - { - project_selection_set_with_map( - obj, - errors, - &inline_fragment.selections, - type_name, - schema_metadata, - variable_values, - buffer, - first, - ); + Err(FieldProjectionConditionError::Skip) => { + // Skip this field + continue; + } + Err(FieldProjectionConditionError::InvalidParentType) => { + // Skip this field as the parent type does not match + continue; + } + Err(FieldProjectionConditionError::InvalidEnumValue) => { + if *first { + buffer.push('{'); + } else { + buffer.push(','); } + *first = false; + + buffer.push('"'); + buffer.push_str(&selection.response_key); + buffer.push_str("\":null"); + errors.push(GraphQLError { + message: "Value is not a valid enum value".to_string(), + locations: None, + path: None, + extensions: None, + }); } - SelectionItem::FragmentSpread(_name_ref) => { - // We only minify the queries to subgraphs, so we never have fragment spreads here. - // In this projection, we expect only inline fragments and fields - // as it's the query produced by the ast normalization process. - unreachable!("Fragment spreads should not exist in the final response projection."); + Err(FieldProjectionConditionError::InvalidFieldType) => { + if *first { + buffer.push('{'); + } else { + buffer.push(','); + } + *first = false; + + // Skip this field as the field type does not match + buffer.push('"'); + buffer.push_str(&selection.response_key); + buffer.push_str("\":null"); } } } - true } diff --git a/lib/query-plan-executor/src/schema_metadata.rs b/lib/query-plan-executor/src/schema_metadata.rs index fc182cafc..40691db3f 100644 --- a/lib/query-plan-executor/src/schema_metadata.rs +++ b/lib/query-plan-executor/src/schema_metadata.rs @@ -10,9 +10,9 @@ use serde_json::{json, Value}; #[derive(Debug, Default)] pub struct SchemaMetadata { pub possible_types: PossibleTypes, - pub enum_values: HashMap>, + pub enum_values: HashMap>, pub type_fields: HashMap>, - pub introspection_schema_root_json: Value, + pub introspection_query_json: Value, } #[derive(Debug, Default)] @@ -30,6 +30,11 @@ impl PossibleTypes { false } } + pub fn get_possible_types(&self, type_name: &str) -> HashSet { + let mut possible_types = self.map.get(type_name).cloned().unwrap_or_default(); + possible_types.insert(type_name.to_string()); + possible_types + } } pub trait SchemaWithMetadata { @@ -40,15 +45,15 @@ impl SchemaWithMetadata for ConsumerSchema { fn schema_metadata(&self) -> SchemaMetadata { let mut first_possible_types: HashMap> = HashMap::new(); let mut type_fields: HashMap> = HashMap::new(); - let mut enum_values: HashMap> = HashMap::new(); + let mut enum_values: HashMap> = HashMap::new(); for definition in &self.document.definitions { match definition { Definition::TypeDefinition(TypeDefinition::Enum(enum_type)) => { let name = enum_type.name.to_string(); - let mut values = vec![]; + let mut values = HashSet::new(); for enum_value in &enum_type.values { - values.push(enum_value.name.to_string()); + values.insert(enum_value.name.to_string()); } enum_values.insert(name, values); } @@ -112,7 +117,7 @@ impl SchemaWithMetadata for ConsumerSchema { let introspection_query = crate::introspection::introspection_query_from_ast(&self.document); - let introspection_schema_root_json = json!(introspection_query.__schema); + let introspection_query_json = json!(introspection_query); SchemaMetadata { possible_types: PossibleTypes { @@ -120,7 +125,7 @@ impl SchemaWithMetadata for ConsumerSchema { }, enum_values, type_fields, - introspection_schema_root_json, + introspection_query_json, } } } diff --git a/lib/query-plan-executor/src/tests/mod.rs b/lib/query-plan-executor/src/tests/mod.rs index cb82db05e..383210bf5 100644 --- a/lib/query-plan-executor/src/tests/mod.rs +++ b/lib/query-plan-executor/src/tests/mod.rs @@ -1,7 +1,10 @@ use query_planner::graph::PlannerOverrideContext; use subgraphs::accounts; -use crate::executors::{common::SubgraphExecutor, map::SubgraphExecutorMap}; +use crate::{ + executors::{common::SubgraphExecutor, map::SubgraphExecutorMap}, + projection, +}; mod traverse_and_callback; @@ -39,12 +42,15 @@ fn query_executor_pipeline_locally() { subgraph_executor_map.insert_boxed_arc("inventory".to_string(), inventory.to_boxed_arc()); subgraph_executor_map.insert_boxed_arc("products".to_string(), products.to_boxed_arc()); subgraph_executor_map.insert_boxed_arc("reviews".to_string(), reviews.to_boxed_arc()); + let (root_type_name, projection_selections) = + projection::FieldProjectionPlan::from_operation(normalized_operation, &schema_metadata); let result = crate::execute_query_plan( &query_plan, &subgraph_executor_map, &None, &schema_metadata, - normalized_operation, + root_type_name, + &projection_selections, false, crate::ExposeQueryPlanMode::No, ) From fd835e7d8cf04786af1380b360cb6b50d41e6f6a Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 21 Jul 2025 14:51:20 +0300 Subject: [PATCH 33/35] Remove flamegraph --- flamegraph.svg | 491 ------------------------------------------------- 1 file changed, 491 deletions(-) delete mode 100644 flamegraph.svg diff --git a/flamegraph.svg b/flamegraph.svg deleted file mode 100644 index 6eb1be709..000000000 --- a/flamegraph.svg +++ /dev/null @@ -1,491 +0,0 @@ -Flame Graph Reset ZoomSearch __libkernel_init (1 samples, 0.57%)mach_init_doit (1 samples, 0.57%)dyld4::APIs::runAllInitializersForMain() (2 samples, 1.14%)dyld4::PrebuiltLoader::runInitializers(dyld4::RuntimeState&) const (2 samples, 1.14%)dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const (2 samples, 1.14%)dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const (2 samples, 1.14%)dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const (2 samples, 1.14%)dyld3::MachOFile::forEachLoadCommand(Diagnostics&, void (load_command const*, bool&) block_pointer) const (2 samples, 1.14%)invocation function for block in dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const (2 samples, 1.14%)invocation function for block in dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const (2 samples, 1.14%)invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const (2 samples, 1.14%)libSystem_initializer (2 samples, 1.14%)libdispatch_init (1 samples, 0.57%)_os_object_init (1 samples, 0.57%)_objc_init (1 samples, 0.57%)dyld4::APIs::_dyld_objc_register_callbacks(_dyld_objc_callbacks const*) (1 samples, 0.57%)dyld4::RuntimeState::setObjCNotifiers(void (*)(char const*, mach_header const*), void (*)(mach_header const*, void*, mach_header const*, void const*), void (*)(unsigned int, _dyld_objc_notify_mapped_info const*), void (*)(_dyld_objc_notify_mapped_info const*), void (*)(unsigned int, _dyld_objc_notify_mapped_info const*, void (unsigned int) block_pointer)) (1 samples, 0.57%)dyld4::RuntimeLocks::withLoadersReadLock(void () block_pointer) (1 samples, 0.57%)invocation function for block in dyld4::RuntimeState::setObjCNotifiers(void (*)(char const*, mach_header const*), void (*)(mach_header const*, void*, mach_header const*, void const*), void (*)(unsigned int, _dyld_objc_notify_mapped_info const*), void (*)(_dyld_objc_notify_mapped_info const*), void (*)(unsigned int, _dyld_objc_notify_mapped_info const*, void (unsigned int) block_pointer))::$_0::operator()() const (1 samples, 0.57%)map_images (1 samples, 0.57%)map_images_nolock (1 samples, 0.57%)dyld4::JustInTimeLoader::applyFixups(Diagnostics&, dyld4::RuntimeState&, dyld4::DyldCacheDataConstLazyScopedWriter&, bool, lsl::Vector<std::__1::pair<dyld4::Loader const*, char const*>>*) const (1 samples, 0.57%)dyld4::Loader::applyFixupsGeneric(Diagnostics&, dyld4::RuntimeState&, unsigned long long, dyld3::Array<void const*> const&, dyld3::Array<void const*> const&, bool, dyld3::Array<dyld4::Loader::MissingFlatLazySymbol> const&) const (1 samples, 0.57%)dyld3::MachOAnalyzer::forEachRebaseLocation_Opcodes(Diagnostics&, void (unsigned long long, bool&) block_pointer) const (1 samples, 0.57%)dyld3::MachOAnalyzer::forEachRebase_Opcodes(Diagnostics&, dyld3::MachOLoaded::LinkEditInfo const&, dyld3::MachOFile::SegmentInfo const*, void (char const*, dyld3::MachOLoaded::LinkEditInfo const&, dyld3::MachOFile::SegmentInfo const*, bool, unsigned int, unsigned char, unsigned long long, dyld3::MachOAnalyzer::Rebase, bool&) block_pointer) const (1 samples, 0.57%)dyld3::MachOFile::read_uleb128(Diagnostics&, unsigned char const*&, unsigned char const*) (1 samples, 0.57%)dyld4::PrebuiltLoader::invalidateInIsolation(dyld4::RuntimeState const&) const (3 samples, 1.70%)dyld4::ProcessConfig::PathOverrides::forEachPathVariant(char const*, dyld3::Platform, bool, bool, bool&, void (char const*, dyld4::ProcessConfig::PathOverrides::Type, bool&) block_pointer) const (3 samples, 1.70%)invocation function for block in dyld4::PrebuiltLoader::invalidateInIsolation(dyld4::RuntimeState const&) const (3 samples, 1.70%)dyld4::SyscallDelegate::fileExists(char const*, dyld4::FileID*, int*) const (3 samples, 1.70%)dyld3::stat(char const*, stat*) (3 samples, 1.70%)stat$INODE64 (3 samples, 1.70%)dyld4::start(dyld4::KernelArgs*, void*, void*)::$_0::operator()() const (8 samples, 4.55%)dyld4..dyld4::prepare(dyld4::APIs&, dyld3::MachOAnalyzer const*) (7 samples, 3.98%)dyld..dyld4::JustInTimeLoader::loadDependents(Diagnostics&, dyld4::RuntimeState&, dyld4::Loader::LoadOptions const&) (4 samples, 2.27%)d..mach_o::Header::forEachLinkedDylib(void (char const*, mach_o::LinkedDylibAttributes, mach_o::Version32, mach_o::Version32, bool&) block_pointer) const (4 samples, 2.27%)m..mach_o::Header::forEachLoadCommand(void (load_command const*, bool&) block_pointer) const (4 samples, 2.27%)m..invocation function for block in mach_o::Header::forEachLinkedDylib(void (char const*, mach_o::LinkedDylibAttributes, mach_o::Version32, mach_o::Version32, bool&) block_pointer) const (4 samples, 2.27%)i..invocation function for block in dyld4::JustInTimeLoader::loadDependents(Diagnostics&, dyld4::RuntimeState&, dyld4::Loader::LoadOptions const&) (4 samples, 2.27%)i..dyld4::Loader::getLoader(Diagnostics&, dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&) (4 samples, 2.27%)d..dyld4::Loader::forEachPath(Diagnostics&, dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&, void (char const*, dyld4::ProcessConfig::PathOverrides::Type, bool&) block_pointer) (4 samples, 2.27%)d..dyld4::ProcessConfig::PathOverrides::forEachPathVariant(char const*, dyld3::Platform, bool, bool, bool&, void (char const*, dyld4::ProcessConfig::PathOverrides::Type, bool&) block_pointer) const (4 samples, 2.27%)d..dyld4::Loader::forEachResolvedAtPathVar(dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&, dyld4::ProcessConfig::PathOverrides::Type, bool&, void (char const*, dyld4::ProcessConfig::PathOverrides::Type, bool&) block_pointer) (4 samples, 2.27%)d..invocation function for block in dyld4::Loader::getLoader(Diagnostics&, dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&) (4 samples, 2.27%)i..dyld4::Loader::makeDyldCacheLoader(Diagnostics&, dyld4::RuntimeState&, char const*, dyld4::Loader::LoadOptions const&, unsigned int, mach_o::Layout const*) (4 samples, 2.27%)d..dyld4::RuntimeState::findPrebuiltLoader(char const*) const (4 samples, 2.27%)d..dyld4::PrebuiltLoader::isValid(dyld4::RuntimeState const&) const (4 samples, 2.27%)d..dyld4::PrebuiltLoader::invalidateShallow(dyld4::RuntimeState const&) const (1 samples, 0.57%)dyld4::PrebuiltLoader::dependent(dyld4::RuntimeState const&, unsigned int, mach_o::LinkedDylibAttributes*) const (1 samples, 0.57%)<axum::serve::private::ServeFuture as core::future::future::Future>::poll (1 samples, 0.57%)<axum::serve::WithGracefulShutdown<L,M,S,F> as core::future::into_future::IntoFuture>::into_future::_{{closure}} (1 samples, 0.57%)tokio::net::tcp::listener::TcpListener::accept::_{{closure}} (1 samples, 0.57%)tokio::net::tcp::stream::TcpStream::new (1 samples, 0.57%)tokio::io::poll_evented::PollEvented<E>::new_with_interest (1 samples, 0.57%)tokio::runtime::io::registration::Registration::new_with_interest_and_handle (1 samples, 0.57%)tokio::runtime::io::driver::Handle::add_source (1 samples, 0.57%)kevent (1 samples, 0.57%)gateway::logger::configure_logging (1 samples, 0.57%)tracing_subscriber::registry (1 samples, 0.57%)<query_planner::consumer_schema::ConsumerSchema as query_plan_executor::schema_metadata::SchemaWithMetadata>::schema_metadata (1 samples, 0.57%)graphql_tools::introspection::introspection::_::_<impl serde::ser::Serialize for graphql_tools::introspection::introspection::IntrospectionSchema>::serialize (1 samples, 0.57%)query_plan_executor::executors::map::SubgraphExecutorMap::from_http_endpoint_map (1 samples, 0.57%)<std::collections::hash::map::HashMap<K,V,S> as core::iter::traits::collect::FromIterator<(K,V)>>::from_iter (1 samples, 0.57%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (1 samples, 0.57%)query_plan_executor::executors::http::HTTPSubgraphExecutor::new (1 samples, 0.57%)http::header::name::HdrName::from_static (1 samples, 0.57%)http::header::map::HeaderMap<T>::try_reserve_one (1 samples, 0.57%)gateway::shared_state::GatewaySharedState::new (3 samples, 1.70%)query_planner::planner::Planner::new_from_supergraph (1 samples, 0.57%)query_planner::planner::Planner::new_from_supergraph_state (1 samples, 0.57%)query_planner::graph::Graph::graph_from_supergraph_state (1 samples, 0.57%)query_planner::graph::Graph::build_graph (1 samples, 0.57%)query_planner::graph::Graph::build_field_edges (1 samples, 0.57%)query_planner::federation_spec::normalize_fields_argument_value_mut (1 samples, 0.57%)core::ptr::drop_in_place<graphql_parser::query::ast::Definition<alloc::string::String>> (1 samples, 0.57%)core::ptr::drop_in_place<graphql_parser::query::ast::Selection<alloc::string::String>> (1 samples, 0.57%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<T,I>>::spec_extend (1 samples, 0.57%)<combine::parser::repeat::Iter<Input,P,S,M> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)graphql_parser::schema::grammar::definition (1 samples, 0.57%)graphql_parser::schema::grammar::described_definition (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)graphql_parser::schema::grammar::directive_definition (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)graphql_parser::schema::grammar::directive_locations (1 samples, 0.57%)start (16 samples, 9.09%)startmain (8 samples, 4.55%)mainstd::rt::lang_start_internal (8 samples, 4.55%)std::..std::rt::lang_start::_{{closure}} (7 samples, 3.98%)std:..core::ops::function::FnOnce::call_once (7 samples, 3.98%)core..tokio::runtime::runtime::Runtime::block_on (7 samples, 3.98%)toki..gateway::main::_{{closure}} (6 samples, 3.41%)gat..query_planner::utils::parsing::parse_schema (2 samples, 1.14%)graphql_parser::schema::grammar::parse_schema (2 samples, 1.14%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (2 samples, 1.14%)graphql_parser::schema::grammar::definition (1 samples, 0.57%)graphql_parser::schema::grammar::schema (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)graphql_parser::common::directives (1 samples, 0.57%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<T,I>>::spec_extend (1 samples, 0.57%)<combine::parser::repeat::Iter<Input,P,S,M> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)graphql_parser::common::arguments (1 samples, 0.57%)<combine::parser::FirstMode as combine::parser::ParseMode>::parse (1 samples, 0.57%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (1 samples, 0.57%)tlv_get_addr (1 samples, 0.57%)parking_lot::condvar::Condvar::wait_until_internal (2 samples, 1.14%)__psynch_cvwait (2 samples, 1.14%)kevent (1 samples, 0.57%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (4 samples, 2.27%)t..tokio::runtime::scheduler::multi_thread::park::Parker::park (4 samples, 2.27%)t..tokio::runtime::time::Driver::park_internal (2 samples, 1.14%)tokio::runtime::io::driver::Driver::turn (2 samples, 1.14%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (1 samples, 0.57%)tokio::runtime::task::waker::wake_by_val (1 samples, 0.57%)parking_lot::condvar::Condvar::notify_one_slow (2 samples, 1.14%)__psynch_cvsignal (2 samples, 1.14%)_platform_memmove$VARIANT$Haswell (1 samples, 0.57%)<hyper::proto::h1::dispatch::Client<B> as hyper::proto::h1::dispatch::Dispatch>::recv_msg (2 samples, 1.14%)hyper::client::dispatch::Callback<T,U>::send (1 samples, 0.57%)tokio::sync::oneshot::Sender<T>::send (1 samples, 0.57%)tokio::sync::oneshot::State::set_complete (1 samples, 0.57%)hyper::body::incoming::Sender::try_send_data (1 samples, 0.57%)futures_channel::mpsc::BoundedSenderInner<T>::try_send (1 samples, 0.57%)std::sys::sync::once_box::OnceBox<T>::initialize (1 samples, 0.57%)pthread_mutex_init (1 samples, 0.57%)hyper::proto::h1::conn::Conn<I,B,T>::poll_flush (2 samples, 1.14%)hyper::proto::h1::io::Buffered<T,B>::poll_flush (2 samples, 1.14%)<tokio::net::tcp::stream::TcpStream as tokio::io::async_write::AsyncWrite>::poll_write_vectored (2 samples, 1.14%)tokio::runtime::io::registration::Registration::poll_io (2 samples, 1.14%)<&mio::net::tcp::stream::TcpStream as std::io::Write>::write_vectored (2 samples, 1.14%)writev (2 samples, 1.14%)hyper::proto::h1::conn::Conn<I,B,T>::poll_read_head (3 samples, 1.70%)hyper::proto::h1::io::Buffered<T,B>::parse (3 samples, 1.70%)hyper::proto::h1::io::Buffered<T,B>::poll_read_from_io (3 samples, 1.70%)tokio::io::poll_evented::PollEvented<E>::poll_read (3 samples, 1.70%)<&mio::net::tcp::stream::TcpStream as std::io::Read>::read (3 samples, 1.70%)__recvfrom (3 samples, 1.70%)hyper::proto::h1::conn::Conn<I,B,T>::write_head (1 samples, 0.57%)<hyper::proto::h1::role::Client as hyper::proto::h1::Http1Transaction>::encode (1 samples, 0.57%)hyper::proto::h1::role::set_content_length (1 samples, 0.57%)<http::header::value::HeaderValue as core::convert::From<u64>>::from (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll (12 samples, 6.82%)<futures_..<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll (10 samples, 5.68%)<future..<hyper::client::conn::http1::upgrades::UpgradeableConnection<I,B> as core::future::future::Future>::poll (10 samples, 5.68%)<hyper:..tokio::sync::mpsc::chan::Rx<T,S>::recv (1 samples, 0.57%)tokio::sync::mpsc::list::Rx<T>::pop (1 samples, 0.57%)tokio::runtime::task::harness::Harness<T,S>::poll (13 samples, 7.39%)tokio::run..tokio::runtime::scheduler::multi_thread::handle::_<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::yield_now (1 samples, 0.57%)tokio::runtime::context::with_scheduler (1 samples, 0.57%)tokio::runtime::context::scoped::Scoped<T>::with (1 samples, 0.57%)parking_lot::condvar::Condvar::notify_one_slow (1 samples, 0.57%)query_planner::planner::best::find_best_combination (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::best::explore_tree_combinations (1 samples, 0.57%)query_planner::planner::tree::query_tree_node::QueryTreeNode::merge_nodes (1 samples, 0.57%)query_planner::planner::tree::query_tree_node::QueryTreeNode::merge_nodes (1 samples, 0.57%)query_planner::planner::tree::query_tree_node::QueryTreeNode::merge_nodes (1 samples, 0.57%)alloc::sync::Arc<T,A>::make_mut (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)_tiny_check_and_zero_inline_meta_from_freelist (1 samples, 0.57%)<gateway::pipeline::query_plan_service::QueryPlanService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}}::_{{closure}} (3 samples, 1.70%)query_planner::planner::Planner::plan_from_normalized_operation (3 samples, 1.70%)query_planner::planner::walker::walk_operation (2 samples, 1.14%)query_planner::planner::walker::process_selection (1 samples, 0.57%)query_planner::planner::walker::pathfinder::find_indirect_paths (1 samples, 0.57%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (1 samples, 0.57%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (1 samples, 0.57%)query_planner::planner::tree::query_tree::QueryTree::from_path (1 samples, 0.57%)<core::pin::Pin<P> as core::future::future::Future>::poll (4 samples, 2.27%)<..<gateway::pipeline::query_plan_service::QueryPlanService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}} (4 samples, 2.27%)<..moka::future::base_cache::BaseCache<K,V,S>::get_with_hash::_{{closure}} (1 samples, 0.57%)moka::future::base_cache::BaseCache<K,V,S>::apply_reads_if_needed::_{{closure}} (1 samples, 0.57%)moka::future::housekeeper::Housekeeper::do_run_pending_tasks::_{{closure}} (1 samples, 0.57%)<core::pin::Pin<P> as core::future::future::Future>::poll (1 samples, 0.57%)moka::future::housekeeper::Housekeeper::do_run_pending_tasks::_{{closure}}::_{{closure}} (1 samples, 0.57%)std::sys::sync::mutex::pthread::Mutex::lock (1 samples, 0.57%)std::sys::pal::unix::sync::mutex::Mutex::lock (1 samples, 0.57%)pthread_mutex_lock (1 samples, 0.57%)_platform_memmove$VARIANT$Haswell (1 samples, 0.57%)_szone_free (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)core::ptr::drop_in_place<hyper_util::client::legacy::client::Client<hyper_util::client::legacy::connect::http::HttpConnector,http_body_util::full::Full<bytes::bytes::Bytes>>::send_request::{{closure}}> (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)hyper_util::client::legacy::client::Client<C,B>::send_request::_{{closure}} (1 samples, 0.57%)<alloc::vec::Vec<T,A> as core::clone::Clone>::clone (1 samples, 0.57%)<hyper::body::incoming::Incoming as http_body::Body>::poll_frame (1 samples, 0.57%)<futures_channel::mpsc::Receiver<T> as futures_core::stream::Stream>::poll_next (1 samples, 0.57%)futures_channel::mpsc::Receiver<T>::next_message (1 samples, 0.57%)alloc::sync::Arc<T,A>::drop_slow (1 samples, 0.57%)free_tiny (1 samples, 0.57%)tiny_free_no_lock (1 samples, 0.57%)tiny_free_list_remove_ptr (1 samples, 0.57%)<http_body_util::combinators::collect::Collect<T> as core::future::future::Future>::poll (2 samples, 1.14%)http_body_util::collected::Collected<B>::push_frame (1 samples, 0.57%)alloc::collections::vec_deque::VecDeque<T,A>::grow (1 samples, 0.57%)alloc::raw_vec::RawVec<T,A>::grow_one (1 samples, 0.57%)alloc::raw_vec::finish_grow (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)<hyper_util::common::lazy::Lazy<F,R> as core::future::future::Future>::poll (1 samples, 0.57%)<futures_util::future::try_future::try_flatten::TryFlatten<Fut,<Fut as futures_core::future::TryFuture>::Ok> as core::future::future::Future>::poll (1 samples, 0.57%)<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll (1 samples, 0.57%)<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll (1 samples, 0.57%)<hyper_util::service::oneshot::Oneshot<S,Req> as core::future::future::Future>::poll (1 samples, 0.57%)<hyper_util::client::legacy::connect::http::HttpConnector<R> as tower_service::Service<http::uri::Uri>>::call::_{{closure}} (1 samples, 0.57%)hyper_util::client::legacy::connect::http::connect (1 samples, 0.57%)socket2::socket::Socket::new (1 samples, 0.57%)__fcntl (1 samples, 0.57%)bytes::bytes::shared_clone (1 samples, 0.57%)hyper::client::dispatch::Sender<T,U>::try_send (1 samples, 0.57%)tokio::sync::mpsc::unbounded::UnboundedSender<T>::send (1 samples, 0.57%)tokio::runtime::task::waker::wake_by_val (1 samples, 0.57%)tokio::runtime::scheduler::multi_thread::handle::_<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::schedule (1 samples, 0.57%)tokio::runtime::context::with_scheduler (1 samples, 0.57%)tokio::runtime::context::scoped::Scoped<T>::with (1 samples, 0.57%)tokio::runtime::driver::Handle::unpark (1 samples, 0.57%)kevent (1 samples, 0.57%)<hyper_util::client::legacy::client::ResponseFuture as core::future::future::Future>::poll (4 samples, 2.27%)<..hyper_util::client::legacy::client::Client<C,B>::send_request::_{{closure}} (4 samples, 2.27%)h..szone_try_free_default (1 samples, 0.57%)free_small (2 samples, 1.14%)hyper_util::client::legacy::client::Client<C,B>::request (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)small_malloc_should_clear (1 samples, 0.57%)small_malloc_from_free_list (1 samples, 0.57%)small_free_list_add_ptr (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (1 samples, 0.57%)core::str::converts::from_utf8 (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)DYLD-STUB$$_platform_memmove (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (5 samples, 2.84%)<s..serde_json::read::next_or_eof (4 samples, 2.27%)s..<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (4 samples, 2.27%)<.._platform_memmove$VARIANT$Haswell (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (9 samples, 5.11%)<bytes.._platform_memmove$VARIANT$Haswell (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (16 samples, 9.09%)<serde_json::..serde_json::read::next_or_eof (16 samples, 9.09%)serde_json::r..<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (12 samples, 6.82%)<std::io:.._platform_memmove$VARIANT$Haswell (2 samples, 1.14%)0xfffffffffffffffe (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (23 samples, 13.07%)<serde_json::read::I..serde_json::read::next_or_eof (22 samples, 12.50%)serde_json::read::n..<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (18 samples, 10.23%)<std::io::Bytes..<bytes::buf::reader::Reader<B> as std::io::Read>::read (16 samples, 9.09%)<bytes::buf::.._platform_memmove$VARIANT$Haswell (2 samples, 1.14%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (2 samples, 1.14%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (2 samples, 1.14%)serde_json::read::next_or_eof (1 samples, 0.57%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (3 samples, 1.70%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)0xfffffffffffffffe (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (2 samples, 1.14%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (2 samples, 1.14%)_platform_memmove$VARIANT$Haswell (2 samples, 1.14%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (31 samples, 17.61%)<core::marker::PhantomData<..tiny_malloc_should_clear (1 samples, 0.57%)<serde_json::de::MapAccess<R> as serde::de::MapAccess>::next_key_seed::has_next_key (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (34 samples, 19.32%)<core::marker::PhantomData<T> ..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (34 samples, 19.32%)<serde_json::value::de::<impl ..serde::de::impls::_<impl serde::de::Deserialize for alloc::string::String>::deserialize (2 samples, 1.14%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (36 samples, 20.45%)<core::marker::PhantomData<T> as..szone_malloc_should_clear (2 samples, 1.14%)tiny_malloc_should_clear (2 samples, 1.14%)tiny_malloc_from_free_list (2 samples, 1.14%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)_platform_memmove$VARIANT$Haswell (1 samples, 0.57%)serde::de::impls::_<impl serde::de::Deserialize for alloc::string::String>::deserialize (3 samples, 1.70%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (3 samples, 1.70%)serde_json::read::next_or_eof (2 samples, 1.14%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (57 samples, 32.39%)<core::marker::PhantomData<T> as serde::de::Deserial..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (41 samples, 23.30%)<serde_json::value::de::<impl serde::..serde_json::de::Deserializer<R>::parse_object_colon (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (58 samples, 32.95%)<core::marker::PhantomData<T> as serde::de::Deseriali..<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (58 samples, 32.95%)<core::marker::PhantomData<T> as serde::de::Deseriali..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (58 samples, 32.95%)<serde_json::value::de::<impl serde::de::Deserialize ..serde::de::impls::_<impl serde::de::Deserialize for alloc::string::String>::deserialize (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (64 samples, 36.36%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (64 samples, 36.36%)<serde_json::value::de::<impl serde::de::Deserialize for se..<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (64 samples, 36.36%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (59 samples, 33.52%)<serde_json::value::de::<impl serde::de::Deserialize f..<serde_json::de::MapAccess<R> as serde::de::MapAccess>::next_key_seed::has_next_key (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (1 samples, 0.57%)serde_json::read::next_or_eof (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)serde_json::de::Deserializer<R>::parse_ident (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)serde_json::de::Deserializer<R>::parse_integer (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (70 samples, 39.77%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::des..szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)tiny_free_list_add_ptr (1 samples, 0.57%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (4 samples, 2.27%)<..<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (4 samples, 2.27%)<..serde_json::read::next_or_eof (3 samples, 1.70%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (3 samples, 1.70%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (3 samples, 1.70%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (79 samples, 44.89%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (78 samples, 44.32%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::valu..alloc::collections::btree::map::BTreeMap<K,V,A>::insert (4 samples, 2.27%)a..alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert_entry (4 samples, 2.27%)a..alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert_recursing (2 samples, 1.14%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (80 samples, 45.45%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserializealloc::raw_vec::RawVec<T,A>::grow_one (1 samples, 0.57%)alloc::raw_vec::finish_grow (1 samples, 0.57%)_realloc (1 samples, 0.57%)_malloc_zone_realloc (1 samples, 0.57%)szone_realloc (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)small_malloc_should_clear (1 samples, 0.57%)small_malloc_from_free_list (1 samples, 0.57%)small_free_list_remove_ptr_no_clear (1 samples, 0.57%)<serde_json::value::de::KeyClassifier as serde::de::DeserializeSeed>::deserialize (1 samples, 0.57%)<query_planner::planner::plan_nodes::FetchNode as query_plan_executor::ExecutableFetchNode>::execute_for_projected_representations::_{{closure}} (96 samples, 54.55%)<query_planner::planner::plan_nodes::FetchNode as query_plan_executor::ExecutableFetchNode..query_plan_executor::executors::map::SubgraphExecutorMap::execute::_{{closure}} (96 samples, 54.55%)query_plan_executor::executors::map::SubgraphExecutorMap::execute::_{{closure}}<query_plan_executor::executors::http::HTTPSubgraphExecutor as query_plan_executor::executors::common::SubgraphExecutor>::execute::_{{closure}} (94 samples, 53.41%)<query_plan_executor::executors::http::HTTPSubgraphExecutor as query_plan_executor::exec..serde_json::de::from_reader (82 samples, 46.59%)serde_json::de::from_reader<&mut serde_json::de::Deserializer<R> as serde::de::Deserializer>::deserialize_struct (82 samples, 46.59%)<&mut serde_json::de::Deserializer<R> as serde::de::Deserializer>::deseriali..serde::de::impls::_<impl serde::de::Deserialize for core::option::Option<T>>::deserialize (82 samples, 46.59%)serde::de::impls::_<impl serde::de::Deserialize for core::option::Option<T>>..<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (82 samples, 46.59%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::..alloc::collections::btree::map::BTreeMap<K,V,A>::insert (1 samples, 0.57%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert_entry (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)tiny_malloc_should_clear (1 samples, 0.57%)tiny_malloc_from_free_list (1 samples, 0.57%)<serde_json::de::MapAccess<R> as serde::de::MapAccess>::next_key_seed::has_next_key (1 samples, 0.57%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)futures_util::stream::stream::StreamExt::poll_next_unpin (102 samples, 57.95%)futures_util::stream::stream::StreamExt::poll_next_unpin<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll (101 samples, 57.39%)<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll (100 samples, 56.82%)<futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll<query_planner::planner::plan_nodes::FetchNode as query_plan_executor::ExecutableFetchNode>::execute_for_root::_{{closure}} (3 samples, 1.70%)query_plan_executor::executors::map::SubgraphExecutorMap::execute::_{{closure}} (3 samples, 1.70%)<query_plan_executor::executors::http::HTTPSubgraphExecutor as query_plan_executor::executors::common::SubgraphExecutor>::execute::_{{closure}} (3 samples, 1.70%)serde_json::de::from_reader (3 samples, 1.70%)<&mut serde_json::de::Deserializer<R> as serde::de::Deserializer>::deserialize_struct (3 samples, 1.70%)serde::de::impls::_<impl serde::de::Deserialize for core::option::Option<T>>::deserialize (3 samples, 1.70%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (3 samples, 1.70%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (3 samples, 1.70%)<core::marker::PhantomData<T> as serde::de::DeserializeSeed>::deserialize (3 samples, 1.70%)<serde_json::value::de::<impl serde::de::Deserialize for serde_json::value::Value>::deserialize::ValueVisitor as serde::de::Visitor>::visit_map (3 samples, 1.70%)serde::de::impls::_<impl serde::de::Deserialize for alloc::string::String>::deserialize (2 samples, 1.14%)<serde_json::read::IoRead<R> as serde_json::read::Read>::parse_str (2 samples, 1.14%)serde_json::read::next_or_eof (2 samples, 1.14%)<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next (1 samples, 0.57%)<bytes::buf::reader::Reader<B> as std::io::Read>::read (1 samples, 0.57%)<query_planner::planner::plan_nodes::ParallelNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}}::_{{closure}}::_{{closure}} (1 samples, 0.57%)query_plan_executor::QueryPlanExecutionContext::project_requires (1 samples, 0.57%)query_plan_executor::QueryPlanExecutionContext::project_requires_map_mut (1 samples, 0.57%)query_plan_executor::QueryPlanExecutionContext::project_requires_map_mut (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge_objects (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)free_tiny (1 samples, 0.57%)_platform_memcmp$VARIANT$Base (1 samples, 0.57%)<query_planner::planner::plan_nodes::ParallelNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}}::_{{closure}}::_{{closure}} (2 samples, 1.14%)query_plan_executor::QueryPlanExecutionContext::project_requires (2 samples, 1.14%)query_plan_executor::QueryPlanExecutionContext::project_requires_map_mut (2 samples, 1.14%)query_plan_executor::QueryPlanExecutionContext::project_requires_map_mut (2 samples, 1.14%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (1 samples, 0.57%)alloc::raw_vec::finish_grow (1 samples, 0.57%)_realloc (1 samples, 0.57%)_malloc_zone_realloc (1 samples, 0.57%)szone_realloc (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)small_malloc_should_clear (1 samples, 0.57%)small_malloc_from_free_list (1 samples, 0.57%)small_free_list_remove_ptr_no_clear (1 samples, 0.57%)_platform_memcmp$VARIANT$Base (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge_objects (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)free_tiny (1 samples, 0.57%)get_tiny_previous_free_msize (1 samples, 0.57%)_platform_memcmp$VARIANT$Base (1 samples, 0.57%)query_plan_executor::traverse_and_callback (12 samples, 6.82%)query_pla..query_plan_executor::traverse_and_callback (12 samples, 6.82%)query_pla..query_plan_executor::traverse_and_callback (11 samples, 6.25%)query_pl..query_plan_executor::traverse_and_callback (9 samples, 5.11%)query_..query_plan_executor::traverse_and_callback (2 samples, 1.14%)query_plan_executor::deep_merge::deep_merge (1 samples, 0.57%)query_plan_executor::deep_merge::deep_merge_objects (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)<query_planner::planner::plan_nodes::QueryPlan as query_plan_executor::ExecutableQueryPlan>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::QueryPlan as query_plan_executor::ExecutableQueryPlan>::execute::_{{closu..<query_planner::planner::plan_nodes::PlanNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::PlanNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure..<query_planner::planner::plan_nodes::SequenceNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::SequenceNode as query_plan_executor::ExecutablePlanNode>::execute::_{{clo..<query_planner::planner::plan_nodes::PlanNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::PlanNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure..<query_planner::planner::plan_nodes::ParallelNode as query_plan_executor::ExecutablePlanNode>::execute::_{{closure}} (117 samples, 66.48%)<query_planner::planner::plan_nodes::ParallelNode as query_plan_executor::ExecutablePlanNode>::execute::_{{clo..std::sys::pal::unix::time::Timespec::now (1 samples, 0.57%)clock_gettime (1 samples, 0.57%)mach_absolute_time (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)free_tiny (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (1 samples, 0.57%)free_tiny (1 samples, 0.57%)core::ptr::drop_in_place<serde_json::value::Value> (2 samples, 1.14%)free_tiny (1 samples, 0.57%)tiny_free_no_lock (1 samples, 0.57%)tiny_free_list_remove_ptr (1 samples, 0.57%)core::ptr::drop_in_place<serde_json::value::Value> (5 samples, 2.84%)co..core::ptr::drop_in_place<serde_json::value::Value> (5 samples, 2.84%)co..free_tiny (2 samples, 1.14%)tiny_free_no_lock (1 samples, 0.57%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<alloc::string::String,serde_json::value::Value>> (11 samples, 6.25%)core::pt..core::mem::maybe_uninit::MaybeUninit<T>::assume_init_drop (11 samples, 6.25%)core::me..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (11 samples, 6.25%)core::pt..core::ptr::drop_in_place<serde_json::value::Value> (10 samples, 5.68%)core::p..core::ptr::drop_in_place<serde_json::value::Value> (10 samples, 5.68%)core::p..core::ptr::drop_in_place<serde_json::value::Value> (10 samples, 5.68%)core::p..free_tiny (4 samples, 2.27%)f..tiny_free_no_lock (3 samples, 1.70%)tiny_free_list_add_ptr (1 samples, 0.57%)alloc::raw_vec::RawVecInner<A>::reserve::do_reserve_and_handle (1 samples, 0.57%)alloc::raw_vec::finish_grow (1 samples, 0.57%)_realloc (1 samples, 0.57%)_malloc_zone_realloc (1 samples, 0.57%)szone_realloc (1 samples, 0.57%)_szone_free (1 samples, 0.57%)madvise (1 samples, 0.57%)<core::pin::Pin<P> as core::future::future::Future>::poll (136 samples, 77.27%)<core::pin::Pin<P> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (136 samples, 77.27%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::gateway_layer::ProcessorService<S,P> as tower_service::Service<http::request::Request<axum_core::body::Body>>>::call::_{{closure}} (132 samples, 75.00%)<gateway::pipeline::gateway_layer::ProcessorService<S,P> as tower_service::Service<http::request::Request<axum_core::body::Bo..<core::pin::Pin<P> as core::future::future::Future>::poll (132 samples, 75.00%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::execution_service::ExecutionService as tower_service::Service<http::request::Request<axum_core::body::Body>>>::call::_{{closure}} (132 samples, 75.00%)<gateway::pipeline::execution_service::ExecutionService as tower_service::Service<http::request::Request<axum_core::body::Bod..query_plan_executor::projection::project_by_operation (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (2 samples, 1.14%)query_plan_executor::projection::project_selection_set_with_map (2 samples, 1.14%)query_plan_executor::projection::project_selection_set (1 samples, 0.57%)query_plan_executor::projection::project_selection_set_with_map (1 samples, 0.57%)core::hash::BuildHasher::hash_one (1 samples, 0.57%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (1 samples, 0.57%)<core::pin::Pin<P> as core::future::future::Future>::poll (137 samples, 77.84%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::validation_service::GraphQLValidationService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}} (1 samples, 0.57%)<gateway::pipeline::validation_service::GraphQLValidationService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}}::_{{closure}} (1 samples, 0.57%)graphql_tools::validation::validate::validate (1 samples, 0.57%)<graphql_tools::validation::rules::fields_on_correct_type::FieldsOnCorrectType as graphql_tools::validation::rules::rule::ValidationRule>::validate (1 samples, 0.57%)graphql_tools::ast::operation_visitor::visit_document (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)graphql_tools::ast::operation_visitor::OperationVisitorContext::with_parent_type (1 samples, 0.57%)core::ptr::drop_in_place<graphql_parser::common::Type<alloc::string::String>> (1 samples, 0.57%)<hyper_util::service::glue::TowerToHyperServiceFuture<S,R> as core::future::future::Future>::poll (138 samples, 78.41%)<hyper_util::service::glue::TowerToHyperServiceFuture<S,R> as core::future::future::Future>::poll<hyper_util::service::oneshot::Oneshot<S,Req> as core::future::future::Future>::poll (138 samples, 78.41%)<hyper_util::service::oneshot::Oneshot<S,Req> as core::future::future::Future>::poll<axum::routing::route::RouteFuture<E> as core::future::future::Future>::poll (138 samples, 78.41%)<axum::routing::route::RouteFuture<E> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll (138 samples, 78.41%)<futures_util::future::future::Map<Fut,F> as core::future::future::Future>::poll<F as futures_core::future::TryFuture>::try_poll (138 samples, 78.41%)<F as futures_core::future::TryFuture>::try_poll<tower_http::cors::ResponseFuture<F> as core::future::future::Future>::poll (138 samples, 78.41%)<tower_http::cors::ResponseFuture<F> as core::future::future::Future>::poll<axum::routing::route::RouteFuture<E> as core::future::future::Future>::poll (138 samples, 78.41%)<axum::routing::route::RouteFuture<E> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<axum::util::MapIntoResponseFuture<F> as core::future::future::Future>::poll (138 samples, 78.41%)<axum::util::MapIntoResponseFuture<F> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::gateway_layer::ProcessorService<S,P> as tower_service::Service<http::request::Request<axum_core::body::Body>>>::call::_{{closure}} (138 samples, 78.41%)<gateway::pipeline::gateway_layer::ProcessorService<S,P> as tower_service::Service<http::request::Request<axum_core::body::Body>>>..<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<core::pin::Pin<P> as core::future::future::Future>::poll (138 samples, 78.41%)<core::pin::Pin<P> as core::future::future::Future>::poll<gateway::pipeline::parser_service::GraphQLParserService as gateway::pipeline::gateway_layer::GatewayPipelineLayer>::process::_{{closure}} (1 samples, 0.57%)moka::cht::segment::HashMap<K,V,S>::get_key_value_and_then (1 samples, 0.57%)crossbeam_epoch::default::pin (1 samples, 0.57%)crossbeam_epoch::default::with_handle (1 samples, 0.57%)crossbeam_epoch::internal::Global::collect (1 samples, 0.57%)hyper::proto::h1::conn::Conn<I,B,T>::force_io_read (1 samples, 0.57%)bytes::bytes_mut::BytesMut::reserve (1 samples, 0.57%)bytes::bytes_mut::BytesMut::reserve_inner (1 samples, 0.57%)szone_malloc_should_clear (1 samples, 0.57%)small_malloc_should_clear (1 samples, 0.57%)all (176 samples, 100%)thread_start (160 samples, 90.91%)thread_start_pthread_start (160 samples, 90.91%)_pthread_startstd::sys::pal::unix::thread::Thread::new::thread_start (160 samples, 90.91%)std::sys::pal::unix::thread::Thread::new::thread_startcore::ops::function::FnOnce::call_once{{vtable.shim}} (160 samples, 90.91%)core::ops::function::FnOnce::call_once{{vtable.shim}}std::sys::backtrace::__rust_begin_short_backtrace (160 samples, 90.91%)std::sys::backtrace::__rust_begin_short_backtracetokio::runtime::blocking::pool::Inner::run (160 samples, 90.91%)tokio::runtime::blocking::pool::Inner::runtokio::runtime::task::harness::Harness<T,S>::poll (160 samples, 90.91%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::core::Core<T,S>::poll (160 samples, 90.91%)tokio::runtime::task::core::Core<T,S>::polltokio::runtime::scheduler::multi_thread::worker::run (160 samples, 90.91%)tokio::runtime::scheduler::multi_thread::worker::runtokio::runtime::context::runtime::enter_runtime (160 samples, 90.91%)tokio::runtime::context::runtime::enter_runtimetokio::runtime::context::scoped::Scoped<T>::set (160 samples, 90.91%)tokio::runtime::context::scoped::Scoped<T>::settokio::runtime::scheduler::multi_thread::worker::Context::run (160 samples, 90.91%)tokio::runtime::scheduler::multi_thread::worker::Context::runtokio::runtime::scheduler::multi_thread::worker::Context::run_task (155 samples, 88.07%)tokio::runtime::scheduler::multi_thread::worker::Context::run_tasktokio::runtime::task::harness::poll_future::_{{closure}} (140 samples, 79.55%)tokio::runtime::task::harness::poll_future::_{{closure}}tokio::runtime::task::core::Core<T,S>::poll::_{{closure}} (140 samples, 79.55%)tokio::runtime::task::core::Core<T,S>::poll::_{{closure}}axum::serve::handle_connection::_{{closure}}::_{{closure}} (140 samples, 79.55%)axum::serve::handle_connection::_{{closure}}::_{{closure}}<core::pin::Pin<P> as core::future::future::Future>::poll (140 samples, 79.55%)<core::pin::Pin<P> as core::future::future::Future>::pollhyper::proto::h1::dispatch::Dispatcher<D,Bs,I,T>::poll_read (1 samples, 0.57%)hyper::body::incoming::Sender::try_send_data (1 samples, 0.57%)futures_channel::mpsc::BoundedSenderInner<T>::try_send (1 samples, 0.57%)std::sys::sync::once_box::OnceBox<T>::initialize (1 samples, 0.57%)pthread_mutex_init (1 samples, 0.57%) \ No newline at end of file From 7189b3ded6c2b88e42f25b3e418737f7a19f2c57 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Mon, 21 Jul 2025 20:40:44 +0300 Subject: [PATCH 34/35] Remove HttpRequestParams layer to prevent extra copy/clone/allocations (#287) `HttpRequestParams` service clones stuff from `req` object which already has all the details. Instead we can extract those only when we need it from `req` using `trait`s as in this PR - Now PipelineError is created by a method of Request named `new_pipeline_error` that extract accept header inside. - PipelineErrorVariant -> PipelineError conversion is removed because it is error-prone to create a PipelineError without accept header. `req.new_pipeline_error` method is encouraged - Avoid copying accept header value, and check the accept header value in place --- bin/gateway/src/main.rs | 2 - .../src/pipeline/coerce_variables_service.rs | 33 ++--- bin/gateway/src/pipeline/error.rs | 55 +++---- bin/gateway/src/pipeline/execution_service.rs | 32 +++-- bin/gateway/src/pipeline/graphiql_service.rs | 11 +- .../src/pipeline/graphql_request_params.rs | 136 +++++++----------- bin/gateway/src/pipeline/header.rs | 65 +++++++++ .../src/pipeline/http_request_params.rs | 98 ------------- bin/gateway/src/pipeline/mod.rs | 2 +- bin/gateway/src/pipeline/normalize_service.rs | 23 ++- bin/gateway/src/pipeline/parser_service.rs | 19 ++- .../src/pipeline/query_plan_service.rs | 20 ++- .../src/pipeline/validation_service.rs | 22 ++- 13 files changed, 219 insertions(+), 299 deletions(-) create mode 100644 bin/gateway/src/pipeline/header.rs delete mode 100644 bin/gateway/src/pipeline/http_request_params.rs diff --git a/bin/gateway/src/main.rs b/bin/gateway/src/main.rs index f077bd3ad..cd6ce2648 100644 --- a/bin/gateway/src/main.rs +++ b/bin/gateway/src/main.rs @@ -29,7 +29,6 @@ use crate::pipeline::{ coerce_variables_service::CoerceVariablesService, execution_service::ExecutionService, graphiql_service::GraphiQLResponderService, graphql_request_params::GraphQLRequestParamsExtractor, - http_request_params::HttpRequestParamsExtractor, normalize_service::GraphQLOperationNormalizationService, parser_service::GraphQLParserService, progressive_override_service::ProgressiveOverrideExtractor, query_plan_service::QueryPlanService, validation_service::GraphQLValidationService, @@ -99,7 +98,6 @@ async fn main() -> Result<(), Box> { ) }), ) - .layer(HttpRequestParamsExtractor::new_layer()) .layer(GraphiQLResponderService::new_layer()) .layer(GraphQLRequestParamsExtractor::new_layer()) .layer(GraphQLParserService::new_layer()) diff --git a/bin/gateway/src/pipeline/coerce_variables_service.rs b/bin/gateway/src/pipeline/coerce_variables_service.rs index 16b5c1a57..850a16b96 100644 --- a/bin/gateway/src/pipeline/coerce_variables_service.rs +++ b/bin/gateway/src/pipeline/coerce_variables_service.rs @@ -8,12 +8,11 @@ use query_planner::state::supergraph_state::OperationKind; use serde_json::Value; use tracing::{error, trace, warn}; -use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; +use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; use crate::pipeline::graphql_request_params::ExecutionRequest; -use crate::pipeline::http_request_params::HttpRequestParams; use crate::pipeline::normalize_service::GraphQLNormalizationPayload; use crate::shared_state::GatewaySharedState; @@ -42,31 +41,35 @@ impl GatewayPipelineLayer for CoerceVariablesService { .extensions() .get::>() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GraphQLNormalizationPayload is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "GraphQLNormalizationPayload is missing", + )) })?; - let http_payload = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") - })?; - let execution_params = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("ExecutionRequest is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "ExecutionRequest is missing", + )) })?; let app_state = req .extensions() .get::>() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "GatewaySharedState is missing", + )) })?; - if http_payload.http_method == Method::GET { + if req.method() == Method::GET { if let Some(OperationKind::Mutation) = normalized_operation.operation_for_plan.operation_kind { error!("Mutation is not allowed over GET, stopping"); - return Err(PipelineErrorVariant::MutationNotAllowedOverHttpGet.into()); + return Err( + req.new_pipeline_error(PipelineErrorVariant::MutationNotAllowedOverHttpGet) + ); } } @@ -92,11 +95,9 @@ impl GatewayPipelineLayer for CoerceVariablesService { "failed to collect variables from incoming request: {}", err_msg ); - - return Err(PipelineError::new_with_accept_header( - PipelineErrorVariant::VariablesCoercionError(err_msg), - http_payload.accept_header.clone(), - )); + return Err( + req.new_pipeline_error(PipelineErrorVariant::VariablesCoercionError(err_msg)) + ); } } } diff --git a/bin/gateway/src/pipeline/error.rs b/bin/gateway/src/pipeline/error.rs index c8e0b0824..73ad19641 100644 --- a/bin/gateway/src/pipeline/error.rs +++ b/bin/gateway/src/pipeline/error.rs @@ -2,28 +2,27 @@ use std::{collections::HashMap, sync::Arc}; use axum::{body::Body, extract::rejection::QueryRejection, response::IntoResponse}; use graphql_tools::validation::utils::ValidationError; -use http::{Response, StatusCode}; +use http::{HeaderName, Method, Request, Response, StatusCode}; use query_plan_executor::{ExecutionResult, GraphQLError}; use query_planner::{ast::normalization::error::NormalizationError, planner::PlannerError}; use serde_json::Value; -use crate::pipeline::http_request_params::APPLICATION_JSON; +use crate::pipeline::header::{RequestAccepts, APPLICATION_GRAPHQL_RESPONSE_JSON_STR}; #[derive(Debug)] pub struct PipelineError { - pub accept_header: Option, + pub accept_ok: bool, pub error: PipelineErrorVariant, } -impl PipelineError { - pub fn new_with_accept_header( - error: PipelineErrorVariant, - accept_header_value: String, - ) -> Self { - Self { - accept_header: Some(accept_header_value), - error, - } +pub trait PipelineErrorFromAcceptHeader { + fn new_pipeline_error(&self, error: PipelineErrorVariant) -> PipelineError; +} + +impl PipelineErrorFromAcceptHeader for Request { + fn new_pipeline_error(&self, error: PipelineErrorVariant) -> PipelineError { + let accept_ok = !self.accepts_content_type(&APPLICATION_GRAPHQL_RESPONSE_JSON_STR); + PipelineError { accept_ok, error } } } @@ -35,9 +34,9 @@ pub enum PipelineErrorVariant { // HTTP-related errors #[error("Unsupported HTTP method: {0}")] - UnsupportedHttpMethod(String), + UnsupportedHttpMethod(Method), #[error("Header '{0}' has invalid value")] - InvalidHeaderValue(String), + InvalidHeaderValue(HeaderName), #[error("Failed to read body: {0}")] FailedToReadBodyBytes(axum::Error), #[error("Content-Type header is missing")] @@ -96,13 +95,9 @@ impl PipelineErrorVariant { pub fn graphql_error_message(&self) -> String { match self { - Self::PlannerError(_) | Self::InternalServiceError(_) => { - return "Unexpected error".to_string() - } - _ => {} + Self::PlannerError(_) | Self::InternalServiceError(_) => "Unexpected error".to_string(), + _ => self.to_string(), } - - self.to_string() } pub fn default_status_code(&self, prefer_ok: bool) -> StatusCode { @@ -131,27 +126,9 @@ impl PipelineErrorVariant { } } -impl From for PipelineErrorVariant { - fn from(error: PipelineError) -> Self { - error.error - } -} - -impl From for PipelineError { - fn from(error: PipelineErrorVariant) -> Self { - Self { - error, - accept_header: None, - } - } -} - impl IntoResponse for PipelineError { fn into_response(self) -> Response { - let accept_ok = &self - .accept_header - .is_some_and(|v| v.contains(APPLICATION_JSON.to_str().unwrap())); - let status = self.error.default_status_code(*accept_ok); + let status = self.error.default_status_code(self.accept_ok); if let PipelineErrorVariant::ValidationErrors(validation_errors) = self.error { let validation_error_result = ExecutionResult { diff --git a/bin/gateway/src/pipeline/execution_service.rs b/bin/gateway/src/pipeline/execution_service.rs index 9cc605258..85245a83d 100644 --- a/bin/gateway/src/pipeline/execution_service.rs +++ b/bin/gateway/src/pipeline/execution_service.rs @@ -5,15 +5,18 @@ use std::sync::Arc; use std::task::{Context, Poll}; use crate::pipeline::coerce_variables_service::CoerceVariablesPayload; -use crate::pipeline::http_request_params::HttpRequestParams; +use crate::pipeline::header::{ + RequestAccepts, APPLICATION_GRAPHQL_RESPONSE_JSON, APPLICATION_GRAPHQL_RESPONSE_JSON_STR, + APPLICATION_JSON, +}; use crate::pipeline::normalize_service::GraphQLNormalizationPayload; use crate::pipeline::query_plan_service::QueryPlanPayload; use crate::shared_state::GatewaySharedState; use axum::body::Body; -use http::header::CONTENT_TYPE; -use http::{HeaderName, Request, Response}; +use http::{HeaderName, HeaderValue, Request, Response}; use query_plan_executor::{execute_query_plan, ExposeQueryPlanMode}; use tower::Service; +use tracing::trace; #[derive(Clone, Debug, Default)] pub struct ExecutionService { @@ -74,11 +77,6 @@ impl Service> for ExecutionService { .get::() .expect("CoerceVariablesPayload missing"); - let http_request_params = req - .extensions() - .get::() - .expect("HttpRequestParams missing"); - let execution_result = execute_query_plan( &query_plan_payload.query_plan, &app_state.subgraph_executor_map, @@ -92,11 +90,23 @@ impl Service> for ExecutionService { .await; let mut response = Response::new(Body::from(execution_result)); - response.headers_mut().insert( - CONTENT_TYPE, - http_request_params.response_content_type.clone(), + + let response_content_type: &'static HeaderValue = + if req.accepts_content_type(*APPLICATION_GRAPHQL_RESPONSE_JSON_STR) { + &APPLICATION_GRAPHQL_RESPONSE_JSON + } else { + &APPLICATION_JSON + }; + + trace!( + "Will use the following Content-Type header for response: {:?}", + response_content_type ); + response + .headers_mut() + .insert(http::header::CONTENT_TYPE, response_content_type.clone()); + Ok(response) }) } diff --git a/bin/gateway/src/pipeline/graphiql_service.rs b/bin/gateway/src/pipeline/graphiql_service.rs index c465bddef..29f9957a8 100644 --- a/bin/gateway/src/pipeline/graphiql_service.rs +++ b/bin/gateway/src/pipeline/graphiql_service.rs @@ -2,11 +2,11 @@ use axum::body::Body; use axum::response::IntoResponse; use http::{Method, Request}; -use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; +use crate::pipeline::error::PipelineError; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; -use crate::pipeline::http_request_params::HttpRequestParams; +use crate::pipeline::header::RequestAccepts; use axum::response::Html; @@ -21,12 +21,7 @@ impl GatewayPipelineLayer for GraphiQLResponderService { &self, req: &mut Request, ) -> Result { - let http_params = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") - })?; - - if http_params.http_method == Method::GET && http_params.accept_header.contains("text/html") - { + if req.method() == Method::GET && req.accepts_content_type("text/html") { return Ok(GatewayPipelineStepDecision::RespondWith( Html(GRAPHIQL_HTML).into_response(), )); diff --git a/bin/gateway/src/pipeline/graphql_request_params.rs b/bin/gateway/src/pipeline/graphql_request_params.rs index 13d6fb35a..cf93cdd64 100644 --- a/bin/gateway/src/pipeline/graphql_request_params.rs +++ b/bin/gateway/src/pipeline/graphql_request_params.rs @@ -8,11 +8,11 @@ use serde::Deserialize; use serde_json::Value; use tracing::{trace, warn}; -use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; +use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; -use crate::pipeline::http_request_params::{HttpRequestParams, APPLICATION_JSON}; +use crate::pipeline::header::AssertRequestJson; #[derive(Clone, Debug, Default)] pub struct GraphQLRequestParamsExtractor; @@ -84,90 +84,58 @@ impl GatewayPipelineLayer for GraphQLRequestParamsExtractor { &self, req: &mut Request, ) -> Result { - let http_params = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") - })?; - - let accept_header = http_params.accept_header.clone(); - let execution_request: ExecutionRequest = match http_params.http_method { - Method::GET => { - trace!("processing GET GraphQL operation"); - - let query_params = Query::::try_from_uri(req.uri()) - .map_err(|qe| { - PipelineError::new_with_accept_header( - PipelineErrorVariant::GetInvalidQueryParams(qe), - accept_header.clone(), - ) - })? - .0; - - trace!("parsed GET query params: {:?}", query_params); - - query_params.try_into()? - } - Method::POST => { - trace!("Processing POST GraphQL request"); - - match &http_params.request_content_type { - None => { - trace!("POST without content type detected"); - - return Err(PipelineError::new_with_accept_header( - PipelineErrorVariant::MissingContentTypeHeader, - accept_header.clone(), - )); - } - Some(content_type) => { - if !content_type.contains(APPLICATION_JSON.to_str().unwrap()) { - warn!("Invalid content type on a POST request: {}", content_type); - - return Err(PipelineError::new_with_accept_header( - PipelineErrorVariant::UnsupportedContentType, - accept_header.clone(), - )); - } - } + let http_method = req.method(); + let execution_request: ExecutionRequest = + match *http_method { + Method::GET => { + trace!("processing GET GraphQL operation"); + + let query_params = Query::::try_from_uri(req.uri()) + .map_err(|qe| { + req.new_pipeline_error(PipelineErrorVariant::GetInvalidQueryParams(qe)) + })? + .0; + + trace!("parsed GET query params: {:?}", query_params); + + query_params + .try_into() + .map_err(|err| req.new_pipeline_error(err))? } + Method::POST => { + trace!("Processing POST GraphQL request"); + + req.assert_json_content_type()?; + + let body_bytes = req + .body_mut() + .collect() + .await + .map_err(|err| { + warn!("Failed to read body bytes: {}", err); + req.new_pipeline_error(PipelineErrorVariant::FailedToReadBodyBytes(err)) + })? + .to_bytes(); + + let execution_request = unsafe { + sonic_rs::from_slice_unchecked::(&body_bytes).map_err( + |e| { + warn!("Failed to parse body: {}", e); + req.new_pipeline_error(PipelineErrorVariant::FailedToParseBody(e)) + }, + )? + }; + + execution_request + } + _ => { + warn!("unsupported HTTP method: {}", http_method); - let body_bytes = req - .body_mut() - .collect() - .await - .map_err(|err| { - warn!("Failed to read body bytes: {}", err); - - PipelineError::new_with_accept_header( - PipelineErrorVariant::FailedToReadBodyBytes(err), - accept_header.clone(), - ) - })? - .to_bytes(); - - let execution_request = unsafe { - sonic_rs::from_slice_unchecked::(&body_bytes).map_err( - |e| { - warn!("Failed to parse body: {}", e); - - PipelineError::new_with_accept_header( - PipelineErrorVariant::FailedToParseBody(e), - accept_header, - ) - }, - )? - }; - - execution_request - } - _ => { - warn!("unsupported HTTP method: {}", http_params.http_method); - - return Err(PipelineErrorVariant::UnsupportedHttpMethod( - http_params.http_method.to_string(), - ) - .into()); - } - }; + return Err(req.new_pipeline_error( + PipelineErrorVariant::UnsupportedHttpMethod(http_method.to_owned()), + )); + } + }; req.extensions_mut().insert(execution_request); diff --git a/bin/gateway/src/pipeline/header.rs b/bin/gateway/src/pipeline/header.rs new file mode 100644 index 000000000..1dc620ce1 --- /dev/null +++ b/bin/gateway/src/pipeline/header.rs @@ -0,0 +1,65 @@ +use http::{ + header::{ACCEPT, CONTENT_TYPE}, + HeaderValue, +}; +use lazy_static::lazy_static; +use tracing::{trace, warn}; + +use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; + +lazy_static! { + pub static ref APPLICATION_JSON_STR: &'static str = "application/json"; + pub static ref APPLICATION_JSON: HeaderValue = HeaderValue::from_static(&APPLICATION_JSON_STR); + pub static ref APPLICATION_GRAPHQL_RESPONSE_JSON_STR: &'static str = + "application/graphql-response+json"; + pub static ref APPLICATION_GRAPHQL_RESPONSE_JSON: HeaderValue = + HeaderValue::from_static(&APPLICATION_GRAPHQL_RESPONSE_JSON_STR); +} + +pub trait RequestAccepts { + fn accepts_content_type(&self, content_type: &str) -> bool; +} + +impl RequestAccepts for http::Request { + fn accepts_content_type(&self, content_type: &str) -> bool { + let accept_header = self.headers().get(ACCEPT); + if let Some(value) = accept_header { + value + .to_str() + .map(|s| s.contains(content_type)) + .unwrap_or(false) + } else { + false + } + } +} + +pub trait AssertRequestJson { + fn assert_json_content_type(&self) -> Result<(), PipelineError>; +} + +impl AssertRequestJson for http::Request { + fn assert_json_content_type(&self) -> Result<(), PipelineError> { + match self.headers().get(CONTENT_TYPE) { + Some(value) => { + let content_type_str = value.to_str().map_err(|_| { + self.new_pipeline_error(PipelineErrorVariant::InvalidHeaderValue(CONTENT_TYPE)) + })?; + if !content_type_str.contains(*APPLICATION_JSON_STR) { + warn!( + "Invalid content type on a POST request: {}", + content_type_str + ); + return Err( + self.new_pipeline_error(PipelineErrorVariant::UnsupportedContentType) + ); + } + Ok(()) + } + None => { + trace!("POST without content type detected"); + Err(self.new_pipeline_error(PipelineErrorVariant::MissingContentTypeHeader)) + } + } + } +} diff --git a/bin/gateway/src/pipeline/http_request_params.rs b/bin/gateway/src/pipeline/http_request_params.rs deleted file mode 100644 index 7f47fbdb4..000000000 --- a/bin/gateway/src/pipeline/http_request_params.rs +++ /dev/null @@ -1,98 +0,0 @@ -use axum::body::Body; -use http::header::{ACCEPT, CONTENT_TYPE}; -use http::{HeaderValue, Method, Request}; -use lazy_static::lazy_static; -use tracing::trace; - -use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; -use crate::pipeline::gateway_layer::{ - GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, -}; - -lazy_static! { - pub static ref APPLICATION_JSON: HeaderValue = HeaderValue::from_static("application/json"); - pub static ref APPLICATION_GRAPHQL_RESPONSE_JSON: HeaderValue = - HeaderValue::from_static("application/graphql-response+json"); -} - -#[derive(Debug, Clone)] -pub struct HttpRequestParams { - pub accept_header: String, - pub http_method: Method, - pub request_content_type: Option, - pub response_content_type: HeaderValue, -} - -#[derive(Clone, Debug, Default)] -pub struct HttpRequestParamsExtractor; - -impl HttpRequestParamsExtractor { - pub fn new_layer() -> ProcessorLayer { - ProcessorLayer::new(Self) - } -} - -#[async_trait::async_trait] -impl GatewayPipelineLayer for HttpRequestParamsExtractor { - #[tracing::instrument(level = "trace", name = "HttpRequestParamsExtractor", skip_all)] - async fn process( - &self, - req: &mut Request, - ) -> Result { - let http_method = req.method().to_owned(); - let accept_header = req - .headers() - .get(ACCEPT) - .unwrap_or(&APPLICATION_JSON) - .to_str() - .map_err(|_| PipelineErrorVariant::InvalidHeaderValue(ACCEPT.to_string()))? - .to_owned(); - - trace!( - "Using the following Accept header for request: {}", - &accept_header - ); - - let request_content_type: Option = match req.headers().get(CONTENT_TYPE) { - None => None, - Some(content_type) => { - let value = content_type - .to_str() - .map_err(|_| PipelineErrorVariant::InvalidHeaderValue(ACCEPT.to_string()))? - .to_owned(); - - Some(value) - } - }; - - trace!( - "Using the following Content-Type header for request: {:?}", - &request_content_type - ); - - let response_content_type = - if accept_header.contains(APPLICATION_GRAPHQL_RESPONSE_JSON.to_str().unwrap()) { - APPLICATION_GRAPHQL_RESPONSE_JSON.clone() - } else { - APPLICATION_JSON.clone() - }; - - trace!( - "Will use the following Content-Type header for response: {:?}", - &response_content_type - ); - - let extracted_params = HttpRequestParams { - http_method, - accept_header, - response_content_type, - request_content_type, - }; - - trace!("Extracted HTTP params: {:?}", extracted_params); - - req.extensions_mut().insert(extracted_params); - - Ok(GatewayPipelineStepDecision::Continue) - } -} diff --git a/bin/gateway/src/pipeline/mod.rs b/bin/gateway/src/pipeline/mod.rs index 2b61cd5f2..d6c145017 100644 --- a/bin/gateway/src/pipeline/mod.rs +++ b/bin/gateway/src/pipeline/mod.rs @@ -4,7 +4,7 @@ pub mod execution_service; pub mod gateway_layer; pub mod graphiql_service; pub mod graphql_request_params; -pub mod http_request_params; +pub mod header; pub mod normalize_service; pub mod parser_service; pub mod progressive_override_service; diff --git a/bin/gateway/src/pipeline/normalize_service.rs b/bin/gateway/src/pipeline/normalize_service.rs index fe6356822..08bdee1b5 100644 --- a/bin/gateway/src/pipeline/normalize_service.rs +++ b/bin/gateway/src/pipeline/normalize_service.rs @@ -8,12 +8,11 @@ use query_plan_executor::projection::FieldProjectionPlan; use query_planner::ast::normalization::normalize_operation; use query_planner::ast::operation::OperationDefinition; -use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; +use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; use crate::pipeline::graphql_request_params::ExecutionRequest; -use crate::pipeline::http_request_params::HttpRequestParams; use crate::pipeline::parser_service::GraphQLParserPayload; use crate::shared_state::GatewaySharedState; use tracing::{error, trace}; @@ -51,21 +50,24 @@ impl GatewayPipelineLayer for GraphQLOperationNormalizationService { .extensions() .get::() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GraphQLParserPayload is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "GraphQLParserPayload is missing", + )) })?; - let http_payload = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") - })?; let execution_params = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("ExecutionRequest is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "ExecutionRequest is missing", + )) })?; let app_state = req .extensions() .get::>() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "GatewaySharedState is missing", + )) })?; let cache_key = match &execution_params.operation_name { @@ -130,10 +132,7 @@ impl GatewayPipelineLayer for GraphQLOperationNormalizationService { error!("Failed to normalize GraphQL operation: {}", err); trace!("{:?}", err); - Err(PipelineError::new_with_accept_header( - PipelineErrorVariant::NormalizationError(err), - http_payload.accept_header.clone(), - )) + Err(req.new_pipeline_error(PipelineErrorVariant::NormalizationError(err))) } }, } diff --git a/bin/gateway/src/pipeline/parser_service.rs b/bin/gateway/src/pipeline/parser_service.rs index daaa3a92c..33cc9dc69 100644 --- a/bin/gateway/src/pipeline/parser_service.rs +++ b/bin/gateway/src/pipeline/parser_service.rs @@ -6,12 +6,11 @@ use graphql_parser::query::Document; use http::Request; use query_planner::utils::parsing::safe_parse_operation; -use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; +use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; use crate::pipeline::graphql_request_params::ExecutionRequest; -use crate::pipeline::http_request_params::HttpRequestParams; use crate::shared_state::GatewaySharedState; use tracing::{error, trace}; @@ -38,17 +37,18 @@ impl GatewayPipelineLayer for GraphQLParserService { req: &mut Request, ) -> Result { let execution_params = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("ExecutionRequest is missing") - })?; - let http_params = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "ExecutionRequest is missing", + )) })?; let app_state = req .extensions() .get::>() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "GatewaySharedState is missing", + )) })?; let cache_key = { @@ -63,10 +63,7 @@ impl GatewayPipelineLayer for GraphQLParserService { } else { let parsed = safe_parse_operation(&execution_params.query).map_err(|err| { error!("Failed to parse GraphQL operation: {}", err); - PipelineError::new_with_accept_header( - PipelineErrorVariant::FailedToParseOperation(err), - http_params.accept_header.clone(), - ) + req.new_pipeline_error(PipelineErrorVariant::FailedToParseOperation(err)) })?; trace!("sucessfully parsed GraphQL operation"); let parsed_arc = Arc::new(parsed); diff --git a/bin/gateway/src/pipeline/query_plan_service.rs b/bin/gateway/src/pipeline/query_plan_service.rs index c37d926a6..049d3c15a 100644 --- a/bin/gateway/src/pipeline/query_plan_service.rs +++ b/bin/gateway/src/pipeline/query_plan_service.rs @@ -3,7 +3,7 @@ use std::hash::DefaultHasher; use std::hash::{Hash, Hasher}; use std::sync::Arc; -use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; +use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; @@ -74,20 +74,26 @@ impl GatewayPipelineLayer for QueryPlanService { .extensions() .get::>() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GraphQLNormalizationPayload is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "GraphQLNormalizationPayload is missing", + )) })?; let app_state = req .extensions() .get::>() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "GatewaySharedState is missing", + )) })?; let request_override_context = req .extensions() .get::() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("ProgressiveOverride is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "ProgressiveOverride is missing", + )) })?; let stable_override_context = @@ -124,7 +130,11 @@ impl GatewayPipelineLayer for QueryPlanService { request_override_context.into(), ) { Ok(p) => p, - Err(err) => return Err(PipelineErrorVariant::PlannerError(err).into()), + Err(err) => { + return Err( + req.new_pipeline_error(PipelineErrorVariant::PlannerError(err)) + ) + } } }; diff --git a/bin/gateway/src/pipeline/validation_service.rs b/bin/gateway/src/pipeline/validation_service.rs index 590e7990a..cc2dd67f3 100644 --- a/bin/gateway/src/pipeline/validation_service.rs +++ b/bin/gateway/src/pipeline/validation_service.rs @@ -1,10 +1,9 @@ use std::sync::Arc; -use crate::pipeline::error::{PipelineError, PipelineErrorVariant}; +use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant}; use crate::pipeline::gateway_layer::{ GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer, }; -use crate::pipeline::http_request_params::HttpRequestParams; use crate::pipeline::parser_service::GraphQLParserPayload; use crate::shared_state::GatewaySharedState; use axum::body::Body; @@ -32,14 +31,18 @@ impl GatewayPipelineLayer for GraphQLValidationService { .extensions() .get::() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GraphQLParserPayload is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "GraphQLParserPayload is missing", + )) })?; let app_state = req .extensions() .get::>() .ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing") + req.new_pipeline_error(PipelineErrorVariant::InternalServiceError( + "GatewaySharedState is missing", + )) })?; let consumer_schema_ast = &app_state.planner.consumer_schema.document; @@ -85,14 +88,9 @@ impl GatewayPipelineLayer for GraphQLValidationService { ); trace!("Validation errors: {:?}", validation_result); - let http_payload = req.extensions().get::().ok_or_else(|| { - PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing") - })?; - - return Err(PipelineError::new_with_accept_header( - PipelineErrorVariant::ValidationErrors(validation_result), - http_payload.accept_header.clone(), - )); + return Err( + req.new_pipeline_error(PipelineErrorVariant::ValidationErrors(validation_result)) + ); } Ok(GatewayPipelineStepDecision::Continue) From 6702d9b3ce40ddf4bb56ba6a5a210878fe2efa39 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Wed, 23 Jul 2025 15:13:43 +0300 Subject: [PATCH 35/35] Use io::Write (#288) --- bin/gateway/src/pipeline/execution_service.rs | 14 +- .../benches/executor_benches.rs | 23 +- .../src/executors/async_graphql.rs | 2 +- lib/query-plan-executor/src/executors/http.rs | 58 ++--- lib/query-plan-executor/src/json_writer.rs | 54 ++--- lib/query-plan-executor/src/lib.rs | 201 ++++++++++-------- lib/query-plan-executor/src/projection.rs | 137 ++++++------ lib/query-plan-executor/src/tests/mod.rs | 2 +- 8 files changed, 262 insertions(+), 229 deletions(-) diff --git a/bin/gateway/src/pipeline/execution_service.rs b/bin/gateway/src/pipeline/execution_service.rs index 85245a83d..26c1f6064 100644 --- a/bin/gateway/src/pipeline/execution_service.rs +++ b/bin/gateway/src/pipeline/execution_service.rs @@ -87,7 +87,19 @@ impl Service> for ExecutionService { normalized_payload.has_introspection, expose_query_plan, ) - .await; + .await + .unwrap_or_else(|err| { + tracing::error!("Failed to execute query plan: {}", err); + serde_json::to_vec(&serde_json::json!({ + "errors": [{ + "message": "Internal server error", + "extensions": { + "code": "INTERNAL_SERVER_ERROR" + } + }] + })) + .unwrap_or_default() + }); let mut response = Response::new(Body::from(execution_result)); diff --git a/lib/query-plan-executor/benches/executor_benches.rs b/lib/query-plan-executor/benches/executor_benches.rs index d8a349aa2..6de38a6f5 100644 --- a/lib/query-plan-executor/benches/executor_benches.rs +++ b/lib/query-plan-executor/benches/executor_benches.rs @@ -252,7 +252,9 @@ fn project_data_by_operation(c: &mut Criterion) { let extensions = black_box(&extensions); let projection_selections = black_box(&projection_selections); let root_type_name = black_box(root_type_name); + let mut writer = black_box(Vec::with_capacity(4096)); let result = query_plan_executor::projection::project_by_operation( + &mut writer, data, errors, extensions, @@ -260,7 +262,8 @@ fn project_data_by_operation(c: &mut Criterion) { projection_selections, &None, ); - black_box(result); + result.unwrap_or_default(); + black_box(()); }); }); } @@ -457,16 +460,18 @@ fn project_requires(c: &mut Criterion) { c.bench_function("project_requires", |b| { b.iter(|| { let execution_context = black_box(&execution_context); - let mut buffer = String::with_capacity(1024); + let mut buffer = Vec::with_capacity(1024); let mut first = true; for representation in black_box(&representations) { - let requires = execution_context.project_requires( - &requires_selections, - representation, - &mut buffer, - first, - None, - ); + let requires = execution_context + .project_requires( + &requires_selections, + representation, + &mut buffer, + first, + None, + ) + .unwrap_or(false); if requires { first = false; } diff --git a/lib/query-plan-executor/src/executors/async_graphql.rs b/lib/query-plan-executor/src/executors/async_graphql.rs index 5043150a0..68af81fd9 100644 --- a/lib/query-plan-executor/src/executors/async_graphql.rs +++ b/lib/query-plan-executor/src/executors/async_graphql.rs @@ -32,7 +32,7 @@ impl<'a> From> for async_graphql::Request { req.variables.insert( async_graphql::Name::new("representations"), async_graphql::Value::from_json( - serde_json::from_str(&representations).unwrap_or_default(), + serde_json::from_slice(&representations).unwrap_or_default(), ) .unwrap(), ); diff --git a/lib/query-plan-executor/src/executors/http.rs b/lib/query-plan-executor/src/executors/http.rs index 464180217..9a6906eeb 100644 --- a/lib/query-plan-executor/src/executors/http.rs +++ b/lib/query-plan-executor/src/executors/http.rs @@ -1,3 +1,4 @@ +use std::io::Write; use std::sync::Arc; use async_trait::async_trait; @@ -21,7 +22,7 @@ pub struct HTTPSubgraphExecutor { pub header_map: HeaderMap, } -const FIRST_VARIABLE_STR: &str = ",\"variables\":{"; +const FIRST_VARIABLE_STR: &[u8; 14] = b",\"variables\":{"; impl HTTPSubgraphExecutor { pub fn new(endpoint: &str, http_client: Arc>>) -> Self { @@ -40,49 +41,56 @@ impl HTTPSubgraphExecutor { } } - async fn _execute<'a>( - &self, - execution_request: SubgraphExecutionRequest<'a>, - ) -> Result { - trace!("Executing HTTP request to subgraph at {}", self.endpoint); + fn write_body( + execution_request: &SubgraphExecutionRequest, + writer: &mut impl Write, + ) -> std::io::Result<()> { + writer.write_all(b"{\"query\":")?; + write_and_escape_string(writer, execution_request.query)?; - // We may want to remove it, but let's see. - let mut body = String::with_capacity(4096); - body.push_str("{\"query\":"); - write_and_escape_string(&mut body, execution_request.query); let mut first_variable = true; if let Some(variables) = &execution_request.variables { for (variable_name, variable_value) in variables { if first_variable { - body.push_str(FIRST_VARIABLE_STR); + writer.write_all(FIRST_VARIABLE_STR)?; first_variable = false; } else { - body.push(','); + writer.write_all(b",")?; } - body.push('"'); - body.push_str(variable_name); - body.push_str("\":"); - let value_str = serde_json::to_string(variable_value).map_err(|err| { - format!("Failed to serialize variable '{}': {}", variable_name, err) - })?; - body.push_str(&value_str); + writer.write_all(b"\"")?; + writer.write_all(variable_name.as_bytes())?; + writer.write_all(b"\":")?; + serde_json::to_writer(&mut *writer, variable_value)?; } } if let Some(representations) = &execution_request.representations { if first_variable { - body.push_str(FIRST_VARIABLE_STR); + writer.write_all(FIRST_VARIABLE_STR)?; first_variable = false; } else { - body.push(','); + writer.write_all(b",")?; } - body.push_str("\"representations\":"); - body.push_str(representations); + writer.write_all(b"\"representations\":")?; + writer.write_all(representations)?; } // "first_variable" should be still true if there are no variables if !first_variable { - body.push('}'); + writer.write_all(b"}")?; } - body.push('}'); + writer.write_all(b"}")?; + Ok(()) + } + + async fn _execute<'a>( + &self, + execution_request: SubgraphExecutionRequest<'a>, + ) -> Result { + trace!("Executing HTTP request to subgraph at {}", self.endpoint); + + // We may want to remove it, but let's see. + let mut body = Vec::with_capacity(4096); + Self::write_body(&execution_request, &mut body) + .map_err(|e| format!("Failed to write request body: {}", e))?; let mut req = hyper::Request::builder() .method(http::Method::POST) diff --git a/lib/query-plan-executor/src/json_writer.rs b/lib/query-plan-executor/src/json_writer.rs index b2e9dcdeb..fe1d42a0d 100644 --- a/lib/query-plan-executor/src/json_writer.rs +++ b/lib/query-plan-executor/src/json_writer.rs @@ -30,51 +30,43 @@ static HEX: [u8; 16] = *b"0123456789ABCDEF"; /// Escapes and append part of string #[inline(always)] -pub fn write_and_escape_string(output_buffer: &mut String, input: &str) { - output_buffer.push('"'); +pub fn write_and_escape_string( + writer: &mut impl std::io::Write, + input: &str, +) -> std::io::Result<()> { + writer.write_all(b"\"")?; - // All of the relevant characters are in the ansi range (<128). - // This means we can safely ignore any utf-8 characters and iterate over the bytes directly - let mut num_bytes_written: usize = 0; - let mut index: usize = 0; let bytes = input.as_bytes(); - while index < bytes.len() { - let cur_byte = bytes[index]; - let replacement = REPLACEMENTS[cur_byte as usize]; + let mut last_write = 0; + + for (i, &byte) in bytes.iter().enumerate() { + let replacement = REPLACEMENTS[byte as usize]; if replacement != 0 { - if num_bytes_written < index { - // Checks can be omitted here: - // We know that index is smaller than the output_buffer length. - // We also know that num_bytes_written is smaller than index - // We also know that the boundaries are not in the middle of an utf-8 multi byte sequence, because those characters are not escaped - output_buffer.push_str(unsafe { input.get_unchecked(num_bytes_written..index) }); + if last_write < i { + writer.write_all(&bytes[last_write..i])?; } + if replacement == b'u' { - let bytes: [u8; 6] = [ + let hex_bytes: [u8; 6] = [ b'\\', b'u', b'0', b'0', - HEX[((cur_byte / 16) & 0xF) as usize], - HEX[(cur_byte & 0xF) as usize], + HEX[((byte / 16) & 0xF) as usize], + HEX[(byte & 0xF) as usize], ]; - // Checks can be omitted here: We know bytes is a valid utf-8 string (see above) - output_buffer.push_str(unsafe { std::str::from_utf8_unchecked(&bytes) }); + writer.write_all(&hex_bytes)?; } else { - let bytes: [u8; 2] = [b'\\', replacement]; - // Checks can be omitted here: We know bytes is a valid utf-8 string, because the replacement table only contains characters smaller than 128 - output_buffer.push_str(unsafe { std::str::from_utf8_unchecked(&bytes) }); + let escaped_bytes: [u8; 2] = [b'\\', replacement]; + writer.write_all(&escaped_bytes)?; } - num_bytes_written = index + 1; + last_write = i + 1; } - index += 1; } - if num_bytes_written < bytes.len() { - // Checks can be omitted here: - // We know that num_bytes_written is smaller than index - // We also know that num_bytes_written not in the middle of an utf-8 multi byte sequence, because those are not escaped - output_buffer.push_str(unsafe { input.get_unchecked(num_bytes_written..bytes.len()) }); + + if last_write < bytes.len() { + writer.write_all(&bytes[last_write..])?; } - output_buffer.push('"'); + writer.write_all(b"\"") } diff --git a/lib/query-plan-executor/src/lib.rs b/lib/query-plan-executor/src/lib.rs index e7bc3f004..708571494 100644 --- a/lib/query-plan-executor/src/lib.rs +++ b/lib/query-plan-executor/src/lib.rs @@ -10,7 +10,7 @@ use query_planner::{ }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use std::{collections::BTreeSet, fmt::Write}; +use std::collections::BTreeSet; use std::{collections::HashMap, vec}; use tracing::{instrument, trace, warn}; // For reading file in main @@ -115,7 +115,7 @@ trait ExecutableFetchNode { async fn execute_for_projected_representations( &self, execution_context: &QueryPlanExecutionContext<'_>, - filtered_representations: String, + filtered_representations: Vec, indexes: BTreeSet, ) -> ExecuteForRepresentationsResult; fn apply_output_rewrites(&self, possible_types: &PossibleTypes, data: &mut Value); @@ -189,7 +189,7 @@ impl ExecutableFetchNode for FetchNode { async fn execute_for_projected_representations( &self, execution_context: &QueryPlanExecutionContext<'_>, - filtered_representations: String, + filtered_representations: Vec, indexes: BTreeSet, ) -> ExecuteForRepresentationsResult { // 2. Prepare variables for fetch @@ -497,7 +497,7 @@ impl ExecutablePlanNode for ParallelNode { jobs.push(Box::pin(job.map(ParallelJob::Root))); } PlanNode::Flatten(flatten_node) => { - let mut filtered_representations = String::with_capacity(1024); + let mut filtered_representations = Vec::with_capacity(1024); let fetch_node = match flatten_node.node.as_ref() { PlanNode::Fetch(fetch_node) => fetch_node, _ => { @@ -512,7 +512,7 @@ impl ExecutablePlanNode for ParallelNode { let mut index = 0; let mut indexes = BTreeSet::new(); let normalized_path = flatten_node.path.as_slice(); - filtered_representations.push('['); + filtered_representations.push(b'['); traverse_and_callback( data, normalized_path, @@ -528,21 +528,25 @@ impl ExecutablePlanNode for ParallelNode { &mut entity_owned, ); } - execution_context.project_requires( - &requires_nodes.items, - &entity_owned, - &mut filtered_representations, - indexes.is_empty(), - None, - ) + execution_context + .project_requires( + &requires_nodes.items, + &entity_owned, + &mut filtered_representations, + indexes.is_empty(), + None, + ) + .unwrap_or(false) } else { - execution_context.project_requires( - &requires_nodes.items, - entity, - &mut filtered_representations, - indexes.is_empty(), - None, - ) + execution_context + .project_requires( + &requires_nodes.items, + entity, + &mut filtered_representations, + indexes.is_empty(), + None, + ) + .unwrap_or(false) }; if is_projected { indexes.insert(index); @@ -550,7 +554,7 @@ impl ExecutablePlanNode for ParallelNode { index += 1; }, ); - filtered_representations.push(']'); + filtered_representations.push(b']'); let job = fetch_node.execute_for_projected_representations( execution_context, filtered_representations, @@ -648,7 +652,7 @@ impl ExecutablePlanNode for FlattenNode { // because `collected_representations` borrows `data_for_flatten`, not `execution_context.data`. let now = std::time::Instant::now(); let mut representations = vec![]; - let mut filtered_representations = String::with_capacity(1024); + let mut filtered_representations = Vec::with_capacity(1024); let fetch_node = match self.node.as_ref() { PlanNode::Fetch(fetch_node) => fetch_node, _ => { @@ -660,7 +664,7 @@ impl ExecutablePlanNode for FlattenNode { } }; let requires_nodes = fetch_node.requires.as_ref().unwrap(); - filtered_representations.push('['); + filtered_representations.push(b'['); let mut first = true; traverse_and_callback( data, @@ -676,21 +680,25 @@ impl ExecutablePlanNode for FlattenNode { &mut entity_owned, ); } - execution_context.project_requires( - &requires_nodes.items, - &entity_owned, - &mut filtered_representations, - first, - None, - ) + execution_context + .project_requires( + &requires_nodes.items, + &entity_owned, + &mut filtered_representations, + first, + None, + ) + .unwrap_or(false) } else { - execution_context.project_requires( - &requires_nodes.items, - entity, - &mut filtered_representations, - first, - None, - ) + execution_context + .project_requires( + &requires_nodes.items, + entity, + &mut filtered_representations, + first, + None, + ) + .unwrap_or(false) }; if is_projected { representations.push(entity); @@ -698,7 +706,7 @@ impl ExecutablePlanNode for FlattenNode { } }, ); - filtered_representations.push(']'); + filtered_representations.push(b']'); trace!( "traversed and collected representations: {:?} in {:#?}", representations.len(), @@ -840,7 +848,7 @@ pub struct SubgraphExecutionRequest<'a> { pub operation_name: Option<&'a str>, pub variables: Option>, pub extensions: Option>, - pub representations: Option, + pub representations: Option>, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -873,86 +881,92 @@ impl QueryPlanExecutionContext<'_> { &self, requires_selections: &Vec, entity: &Value, - buffer: &mut String, + writer: &mut impl std::io::Write, first: bool, response_key: Option<&str>, - ) -> bool { + ) -> std::io::Result { match entity { Value::Null => { - return false; + return Ok(false); } Value::Bool(b) => { if !first { - buffer.push(','); + writer.write_all(b",")?; } if let Some(response_key) = response_key { - buffer.push('"'); - buffer.push_str(response_key); - buffer.push('"'); - buffer.push(':'); - buffer.push_str(if *b { "true" } else { "false" }); + writer.write_all(b"\"")?; + writer.write_all(response_key.as_bytes())?; + writer.write_all(b"\"")?; + writer.write_all(b":")?; + if *b { + writer.write_all(b"true")?; + } else { + writer.write_all(b"false")?; + } + } else if *b { + writer.write_all(b"true")?; } else { - buffer.push_str(if *b { "true" } else { "false" }); + writer.write_all(b"false")?; } } Value::Number(n) => { if !first { - buffer.push(','); + writer.write_all(b",")?; } if let Some(response_key) = response_key { - buffer.push('"'); - buffer.push_str(response_key); - buffer.push_str("\":"); + writer.write_all(b"\"")?; + writer.write_all(response_key.as_bytes())?; + writer.write_all(b"\":")?; } - write!(buffer, "{}", n).unwrap() + std::io::Write::write_fmt(writer, format_args!("{}", n))?; } Value::String(s) => { if !first { - buffer.push(','); + writer.write_all(b",")?; } if let Some(response_key) = response_key { - buffer.push('"'); - buffer.push_str(response_key); - buffer.push_str("\":"); + writer.write_all(b"\"")?; + writer.write_all(response_key.as_bytes())?; + writer.write_all(b"\":")?; } - write_and_escape_string(buffer, s); + write_and_escape_string(writer, s)?; } Value::Array(entity_array) => { if !first { - buffer.push(','); + writer.write_all(b",")?; } if let Some(response_key) = response_key { - buffer.push('"'); - buffer.push_str(response_key); - buffer.push_str("\":["); + writer.write_all(b"\"")?; + writer.write_all(response_key.as_bytes())?; + writer.write_all(b"\":[")?; } else { - buffer.push('['); + writer.write_all(b"[")?; } let mut first = true; for entity_item in entity_array { let projected = self.project_requires( requires_selections, entity_item, - buffer, + writer, first, None, - ); + )?; if projected { // Only update `first` if we actually write something first = false; } } - buffer.push(']'); + writer.write_all(b"]")?; } Value::Object(entity_obj) => { if requires_selections.is_empty() { // It is probably a scalar with an object value, so we write it directly - buffer.push_str(&serde_json::to_string(entity_obj).unwrap()); - return true; + serde_json::to_writer(writer, entity_obj)?; + return Ok(true); } if entity_obj.is_empty() { - return false; + return Ok(false); } let parent_first = first; @@ -960,32 +974,32 @@ impl QueryPlanExecutionContext<'_> { self.project_requires_map_mut( requires_selections, entity_obj, - buffer, + writer, &mut first, response_key, parent_first, - ); + )?; if first { // If no fields were projected, "first" is still true, // so we skip writing the closing brace - return false; + return Ok(false); } else { - buffer.push('}'); + writer.write_all(b"}")?; } } }; - true + Ok(true) } fn project_requires_map_mut( &self, requires_selections: &Vec, entity_obj: &Map, - buffer: &mut String, + writer: &mut impl std::io::Write, first: &mut bool, parent_response_key: Option<&str>, parent_first: bool, - ) { + ) -> std::io::Result<()> { for requires_selection in requires_selections { match &requires_selection { SelectionItem::Field(requires_selection) => { @@ -1006,19 +1020,19 @@ impl QueryPlanExecutionContext<'_> { if *first { if !parent_first { - buffer.push(','); + writer.write_all(b",")?; } if let Some(parent_response_key) = parent_response_key { - buffer.push('"'); - buffer.push_str(parent_response_key); - buffer.push_str("\":"); + writer.write_all(b"\"")?; + writer.write_all(parent_response_key.as_bytes())?; + writer.write_all(b"\":")?; } - buffer.push('{'); + writer.write_all(b"{")?; // Write __typename only if the object has other fields if let Some(Value::String(type_name)) = entity_obj.get(TYPENAME_FIELD) { - buffer.push_str("\"__typename\":"); - write_and_escape_string(buffer, type_name); - buffer.push(','); + writer.write_all(b"\"__typename\":")?; + write_and_escape_string(writer, type_name)?; + writer.write_all(b",")?; } } @@ -1026,10 +1040,10 @@ impl QueryPlanExecutionContext<'_> { self.project_requires( &requires_selection.selections.items, original, - buffer, + writer, *first, Some(response_key), - ); + )?; *first = false; } SelectionItem::InlineFragment(requires_selection) => { @@ -1052,11 +1066,11 @@ impl QueryPlanExecutionContext<'_> { self.project_requires_map_mut( &requires_selection.selections.items, entity_obj, - buffer, + writer, first, parent_response_key, parent_first, - ); + )?; } } SelectionItem::FragmentSpread(_name_ref) => { @@ -1065,6 +1079,7 @@ impl QueryPlanExecutionContext<'_> { } } } + Ok(()) } } @@ -1158,7 +1173,7 @@ pub async fn execute_query_plan( selections: &Vec, has_introspection: bool, expose_query_plan: ExposeQueryPlanMode, -) -> String { +) -> std::io::Result> { let mut result_data = if has_introspection { schema_metadata.introspection_query_json.clone() } else { @@ -1189,14 +1204,18 @@ pub async fn execute_query_plan( } result_errors = execution_context.errors; // Get the final errors from the execution context result_extensions = execution_context.extensions; // Get the final extensions from the execution context + let mut writer = Vec::with_capacity(4096); projection::project_by_operation( - &mut result_data, + &mut writer, + &result_data, &mut result_errors, &result_extensions, operation_type_name, selections, variable_values, - ) + )?; + + Ok(writer) } #[cfg(test)] diff --git a/lib/query-plan-executor/src/projection.rs b/lib/query-plan-executor/src/projection.rs index c0875188b..748738498 100644 --- a/lib/query-plan-executor/src/projection.rs +++ b/lib/query-plan-executor/src/projection.rs @@ -1,5 +1,7 @@ -use std::collections::{HashMap, HashSet}; -use std::fmt::Write; +use std::{ + collections::{HashMap, HashSet}, + io::Write, +}; use indexmap::IndexMap; use query_planner::{ @@ -371,23 +373,21 @@ impl FieldProjectionPlan { #[instrument(level = "trace", skip_all)] pub fn project_by_operation( - data: &mut Value, + writer: &mut impl Write, + data: &Value, errors: &mut Vec, extensions: &HashMap, operation_type_name: &str, selections: &Vec, variable_values: &Option>, -) -> String { - // We may want to remove it, but let's see. - let mut buffer = String::with_capacity(4096); +) -> std::io::Result<()> { + writer.write_all(b"{")?; + writer.write_all(b"\"")?; + writer.write_all(b"data")?; + writer.write_all(b"\"")?; + writer.write_all(b":")?; - buffer.push('{'); - buffer.push('"'); - buffer.push_str("data"); - buffer.push('"'); - buffer.push(':'); - - if let Some(data_map) = data.as_object_mut() { + if let Some(data_map) = data.as_object() { let mut first = true; project_selection_set_with_map( data_map, @@ -395,36 +395,29 @@ pub fn project_by_operation( selections, variable_values, operation_type_name, - &mut buffer, + writer, &mut first, // Start with first as true to add the opening brace - ); + )?; if !first { - buffer.push('}'); + writer.write_all(b"}")?; } else { // If no selections were made, we should return an empty object - buffer.push_str("{}"); + writer.write_all(b"{}")?; } } if !errors.is_empty() { - write!( - buffer, - ",\"errors\":{}", - serde_json::to_string(&errors).unwrap() - ) - .unwrap(); + writer.write_all(b",\"errors\":")?; + serde_json::to_writer(&mut *writer, errors)?; } if !extensions.is_empty() { - write!( - buffer, - ",\"extensions\":{}", - serde_json::to_string(&extensions).unwrap() - ) - .unwrap(); + writer.write_all(b",\"extensions\":")?; + serde_json::to_writer(&mut *writer, extensions)?; } - buffer.push('}'); - buffer + writer.write_all(b"}")?; + + Ok(()) } #[instrument( @@ -439,27 +432,28 @@ fn project_selection_set( errors: &mut Vec, selection: &FieldProjectionPlan, variable_values: &Option>, - buffer: &mut String, -) { + writer: &mut impl std::io::Write, +) -> std::io::Result<()> { match data { - Value::Null => buffer.push_str("null"), - Value::Bool(true) => buffer.push_str("true"), - Value::Bool(false) => buffer.push_str("false"), - Value::Number(num) => write!(buffer, "{}", num).unwrap(), + Value::Null => writer.write_all(b"null"), + Value::Bool(true) => writer.write_all(b"true"), + Value::Bool(false) => writer.write_all(b"false"), + Value::Number(num) => writer.write_all(num.to_string().as_bytes()), Value::String(value) => { - write_and_escape_string(buffer, value); + // Assuming write_and_escape_string is modified to take Vec + write_and_escape_string(writer, value) } Value::Array(arr) => { - buffer.push('['); + writer.write_all(b"[")?; let mut first = true; for item in arr.iter() { if !first { - buffer.push(','); + writer.write_all(b",")?; } - project_selection_set(item, errors, selection, variable_values, buffer); + project_selection_set(item, errors, selection, variable_values, writer)?; first = false; } - buffer.push(']'); + writer.write_all(b"]") } Value::Object(obj) => { match selection.selections.as_ref() { @@ -475,23 +469,24 @@ fn project_selection_set( selections, variable_values, type_name, - buffer, + writer, &mut first, - ); + )?; if !first { - buffer.push('}'); + writer.write_all(b"}") } else { // If no selections were made, we should return an empty object - buffer.push_str("{}"); + writer.write_all(b"{}") } } None => { // If the selection set is not projected, we should return null - buffer.push_str("null"); + writer.write_all(b"null") } } } - } + }?; + Ok(()) } #[instrument( @@ -509,9 +504,9 @@ fn project_selection_set_with_map( selections: &Vec, variable_values: &Option>, parent_type_name: &str, - buffer: &mut String, + writer: &mut impl std::io::Write, first: &mut bool, -) { +) -> std::io::Result<()> { for selection in selections { let field_val = obj .get(&selection.field_name) @@ -524,26 +519,26 @@ fn project_selection_set_with_map( ) { Ok(_) => { if *first { - buffer.push('{'); + writer.write_all(b"{")?; } else { - buffer.push(','); + writer.write_all(b",")?; } *first = false; - buffer.push('"'); - buffer.push_str(&selection.response_key); - buffer.push_str("\":"); + writer.write_all(b"\"")?; + writer.write_all(selection.response_key.as_bytes())?; + writer.write_all(b"\":")?; if let Some(field_val) = field_val { - project_selection_set(field_val, errors, selection, variable_values, buffer); + project_selection_set(field_val, errors, selection, variable_values, writer)?; } else if selection.field_name == TYPENAME_FIELD { // If the field is TYPENAME_FIELD, we should set it to the parent type name - buffer.push('"'); - buffer.push_str(parent_type_name); - buffer.push('"'); + writer.write_all(b"\"")?; + writer.write_all(parent_type_name.as_bytes())?; + writer.write_all(b"\"")?; } else { // If the field is not found in the object, set it to Null - buffer.push_str("null"); + writer.write_all(b"null")?; } } Err(FieldProjectionConditionError::Skip) => { @@ -555,16 +550,17 @@ fn project_selection_set_with_map( continue; } Err(FieldProjectionConditionError::InvalidEnumValue) => { + println!("Invalid enum value for field: {}", selection.field_name); if *first { - buffer.push('{'); + writer.write_all(b"{")?; } else { - buffer.push(','); + writer.write_all(b",")?; } *first = false; - buffer.push('"'); - buffer.push_str(&selection.response_key); - buffer.push_str("\":null"); + writer.write_all(b"\"")?; + writer.write_all(selection.response_key.as_bytes())?; + writer.write_all(b"\":null")?; errors.push(GraphQLError { message: "Value is not a valid enum value".to_string(), locations: None, @@ -574,17 +570,18 @@ fn project_selection_set_with_map( } Err(FieldProjectionConditionError::InvalidFieldType) => { if *first { - buffer.push('{'); + writer.write_all(b"{")?; } else { - buffer.push(','); + writer.write_all(b",")?; } *first = false; // Skip this field as the field type does not match - buffer.push('"'); - buffer.push_str(&selection.response_key); - buffer.push_str("\":null"); + writer.write_all(b"\"")?; + writer.write_all(selection.response_key.as_bytes())?; + writer.write_all(b"\":null")?; } } } + Ok(()) } diff --git a/lib/query-plan-executor/src/tests/mod.rs b/lib/query-plan-executor/src/tests/mod.rs index 383210bf5..5cde7d2c8 100644 --- a/lib/query-plan-executor/src/tests/mod.rs +++ b/lib/query-plan-executor/src/tests/mod.rs @@ -55,6 +55,6 @@ fn query_executor_pipeline_locally() { crate::ExposeQueryPlanMode::No, ) .await; - insta::assert_snapshot!(result); + insta::assert_snapshot!(String::from_utf8(result.unwrap()).unwrap(),); }); }