diff --git a/federation-2/router-bridge/src/introspect.rs b/federation-2/router-bridge/src/introspect.rs index 49c031e54..b04958779 100644 --- a/federation-2/router-bridge/src/introspect.rs +++ b/federation-2/router-bridge/src/introspect.rs @@ -309,6 +309,7 @@ fragment TypeRef on __Type { }), graphql_validation: true, reuse_query_fragments: None, + generate_query_fragments: None, debug: Default::default(), }, ) diff --git a/federation-2/router-bridge/src/planner.rs b/federation-2/router-bridge/src/planner.rs index 38732427f..dab25f68c 100644 --- a/federation-2/router-bridge/src/planner.rs +++ b/federation-2/router-bridge/src/planner.rs @@ -677,6 +677,12 @@ pub struct QueryPlannerConfig { /// planner's default. pub reuse_query_fragments: Option, + /// If enabled, the query planner will extract inline fragments into + /// fragment definitions before sending queries to subgraphs. This can + /// significantly reduce the size of the query sent to subgraphs, but may + /// increase the time it takes to plan the query. + pub generate_query_fragments: Option, + /// A sub-set of configurations that are meant for debugging or testing. All the configurations in this /// sub-set are provided without guarantees of stability (they may be dangerous) or continued support (they /// may be removed without warning). @@ -691,6 +697,7 @@ impl Default for QueryPlannerConfig { }), graphql_validation: true, reuse_query_fragments: None, + generate_query_fragments: None, debug: Default::default(), } } @@ -764,6 +771,8 @@ mod tests { const NO_OPERATION: &str = include_str!("testdata/no_operation.graphql"); const QUERY_REUSE_QUERY_FRAGMENTS: &str = include_str!("testdata/query_reuse_query_fragments.graphql"); + const QUERY_GENERATE_QUERY_FRAGMENTS: &str = + include_str!("testdata/query_generate_query_fragments.graphql"); const MULTIPLE_ANONYMOUS_QUERIES: &str = include_str!("testdata/query_with_multiple_anonymous_operations.graphql"); @@ -773,6 +782,8 @@ mod tests { include_str!("testdata/schema_without_review_body.graphql"); const SCHEMA_REUSE_QUERY_FRAGMENTS: &str = include_str!("testdata/schema_reuse_query_fragments.graphql"); + const SCHEMA_GENERATE_QUERY_FRAGMENTS: &str = + include_str!("testdata/schema_generate_query_fragments.graphql"); const CORE_IN_V0_1: &str = include_str!("testdata/core_in_v0.1.graphql"); const UNSUPPORTED_FEATURE: &str = include_str!("testdata/unsupported_feature.graphql"); const UNSUPPORTED_FEATURE_FOR_EXECUTION: &str = @@ -970,6 +981,78 @@ mod tests { insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap()); } + #[tokio::test] + async fn generate_query_fragments_defaults_to_false() { + let planner = Planner::::new( + SCHEMA_GENERATE_QUERY_FRAGMENTS.to_string(), + QueryPlannerConfig::default(), + ) + .await + .unwrap(); + + let payload = planner + .plan( + QUERY_GENERATE_QUERY_FRAGMENTS.to_string(), + None, + PlanOptions::default(), + ) + .await + .unwrap() + .into_result() + .unwrap(); + insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap()); + } + + #[tokio::test] + async fn generate_query_fragments_explicit_false() { + let planner = Planner::::new( + SCHEMA_GENERATE_QUERY_FRAGMENTS.to_string(), + QueryPlannerConfig { + generate_query_fragments: Some(false), + ..Default::default() + }, + ) + .await + .unwrap(); + + let payload = planner + .plan( + QUERY_GENERATE_QUERY_FRAGMENTS.to_string(), + None, + PlanOptions::default(), + ) + .await + .unwrap() + .into_result() + .unwrap(); + insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap()); + } + + #[tokio::test] + async fn generate_query_fragments_true() { + let planner = Planner::::new( + SCHEMA_GENERATE_QUERY_FRAGMENTS.to_string(), + QueryPlannerConfig { + generate_query_fragments: Some(true), + ..Default::default() + }, + ) + .await + .unwrap(); + + let payload = planner + .plan( + QUERY_GENERATE_QUERY_FRAGMENTS.to_string(), + None, + PlanOptions::default(), + ) + .await + .unwrap() + .into_result() + .unwrap(); + insta::assert_snapshot!(serde_json::to_string_pretty(&payload.data).unwrap()); + } + #[tokio::test] async fn parse_errors_return_the_right_usage_reporting_data() { let planner = @@ -2034,6 +2117,7 @@ feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but i }), graphql_validation: true, reuse_query_fragments: None, + generate_query_fragments: None, debug: Default::default(), }, ) @@ -2113,6 +2197,7 @@ feature https://specs.apollo.dev/unsupported-feature/v0.1 is for: SECURITY but i }), graphql_validation: true, reuse_query_fragments: None, + generate_query_fragments: None, debug: Default::default(), }, ) diff --git a/federation-2/router-bridge/src/snapshots/router_bridge__planner__tests__generate_query_fragments_defaults_to_false.snap b/federation-2/router-bridge/src/snapshots/router_bridge__planner__tests__generate_query_fragments_defaults_to_false.snap new file mode 100644 index 000000000..f11e0a842 --- /dev/null +++ b/federation-2/router-bridge/src/snapshots/router_bridge__planner__tests__generate_query_fragments_defaults_to_false.snap @@ -0,0 +1,18 @@ +--- +source: router-bridge/src/planner.rs +assertion_line: 1003 +expression: "serde_json::to_string_pretty(&payload.data).unwrap()" +--- +{ + "queryPlan": { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "Subgraph1", + "variableUsages": [], + "operation": "{t{__typename ...on A{x y t{__typename ...on A{x y}...on B{z}}}}}", + "operationKind": "query" + } + }, + "formattedQueryPlan": "QueryPlan {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n ... on A {\n x\n y\n t {\n __typename\n ... on A {\n x\n y\n }\n ... on B {\n z\n }\n }\n }\n }\n }\n },\n}" +} diff --git a/federation-2/router-bridge/src/snapshots/router_bridge__planner__tests__generate_query_fragments_explicit_false.snap b/federation-2/router-bridge/src/snapshots/router_bridge__planner__tests__generate_query_fragments_explicit_false.snap new file mode 100644 index 000000000..631f012dd --- /dev/null +++ b/federation-2/router-bridge/src/snapshots/router_bridge__planner__tests__generate_query_fragments_explicit_false.snap @@ -0,0 +1,18 @@ +--- +source: router-bridge/src/planner.rs +assertion_line: 1028 +expression: "serde_json::to_string_pretty(&payload.data).unwrap()" +--- +{ + "queryPlan": { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "Subgraph1", + "variableUsages": [], + "operation": "{t{__typename ...on A{x y t{__typename ...on A{x y}...on B{z}}}}}", + "operationKind": "query" + } + }, + "formattedQueryPlan": "QueryPlan {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n ... on A {\n x\n y\n t {\n __typename\n ... on A {\n x\n y\n }\n ... on B {\n z\n }\n }\n }\n }\n }\n },\n}" +} diff --git a/federation-2/router-bridge/src/snapshots/router_bridge__planner__tests__generate_query_fragments_true.snap b/federation-2/router-bridge/src/snapshots/router_bridge__planner__tests__generate_query_fragments_true.snap new file mode 100644 index 000000000..b618575d5 --- /dev/null +++ b/federation-2/router-bridge/src/snapshots/router_bridge__planner__tests__generate_query_fragments_true.snap @@ -0,0 +1,18 @@ +--- +source: router-bridge/src/planner.rs +assertion_line: 1054 +expression: "serde_json::to_string_pretty(&payload.data).unwrap()" +--- +{ + "queryPlan": { + "kind": "QueryPlan", + "node": { + "kind": "Fetch", + "serviceName": "Subgraph1", + "variableUsages": [], + "operation": "{t{__typename ..._generated_onA3_0}}fragment _generated_onA2_0 on A{x y}fragment _generated_onA3_0 on A{x y t{__typename ..._generated_onA2_0 ...on B{z}}}", + "operationKind": "query" + } + }, + "formattedQueryPlan": "QueryPlan {\n Fetch(service: \"Subgraph1\") {\n {\n t {\n __typename\n ..._generated_onA3_0\n }\n }\n \n fragment _generated_onA2_0 on A {\n x\n y\n }\n \n fragment _generated_onA3_0 on A {\n x\n y\n t {\n __typename\n ..._generated_onA2_0\n ... on B {\n z\n }\n }\n }\n },\n}" +} diff --git a/federation-2/router-bridge/src/testdata/query_generate_query_fragments.graphql b/federation-2/router-bridge/src/testdata/query_generate_query_fragments.graphql new file mode 100644 index 000000000..2812eea60 --- /dev/null +++ b/federation-2/router-bridge/src/testdata/query_generate_query_fragments.graphql @@ -0,0 +1,17 @@ +query { + t { + ... on A { + x + y + t { + ... on A { + x + y + } + ... on B { + z + } + } + } + } +} diff --git a/federation-2/router-bridge/src/testdata/schema_generate_query_fragments.graphql b/federation-2/router-bridge/src/testdata/schema_generate_query_fragments.graphql new file mode 100644 index 000000000..adff902ab --- /dev/null +++ b/federation-2/router-bridge/src/testdata/schema_generate_query_fragments.graphql @@ -0,0 +1,94 @@ +schema + @link(url: "https://specs.apollo.dev/link/v1.0") + @link(url: "https://specs.apollo.dev/join/v0.4", for: EXECUTION) { + query: Query +} + +directive @join__directive( + graphs: [join__Graph!] + name: String! + args: join__DirectiveArguments +) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION + +directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE + +directive @join__field( + graph: join__Graph + requires: join__FieldSet + provides: join__FieldSet + type: String + external: Boolean + override: String + usedOverridden: Boolean + overrideLabel: String +) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +directive @join__implements( + graph: join__Graph! + interface: String! +) repeatable on OBJECT | INTERFACE + +directive @join__type( + graph: join__Graph! + key: join__FieldSet + extension: Boolean! = false + resolvable: Boolean! = true + isInterfaceObject: Boolean! = false +) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR + +directive @join__unionMember( + graph: join__Graph! + member: String! +) repeatable on UNION + +directive @link( + url: String + as: String + for: link__Purpose + import: [link__Import] +) repeatable on SCHEMA + +type A @join__type(graph: SUBGRAPH1) { + x: Int + y: Int + t: T +} + +type B @join__type(graph: SUBGRAPH1) { + z: Int +} + +scalar join__DirectiveArguments + +scalar join__FieldSet + +enum join__Graph { + SUBGRAPH1 @join__graph(name: "Subgraph1", url: "") +} + +scalar link__Import + +enum link__Purpose { + """ + \`SECURITY\` features provide metadata necessary to securely resolve fields. + """ + SECURITY + """ + \`EXECUTION\` features provide metadata necessary for operation execution. + """ + EXECUTION +} + +type Query @join__type(graph: SUBGRAPH1) { + t: T + t2: T +} + +union T + @join__type(graph: SUBGRAPH1) + @join__unionMember(graph: SUBGRAPH1, member: "A") + @join__unionMember(graph: SUBGRAPH1, member: "B") = + A + | B