diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Nodes/Resolve.cs b/src/HotChocolate/Fusion/src/Core/Execution/Nodes/Resolve.cs index 908786f37ca..88437886519 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/Nodes/Resolve.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/Nodes/Resolve.cs @@ -42,7 +42,7 @@ protected override async Task OnExecuteAsync( { if (state.TryGetState(SelectionSet, out var executionState)) { - var requests = new SubgraphGraphQLRequest[executionState.Count]; + var requests = new SubgraphGraphQLRequest[executionState.Count]; // first we will create request for all of our selection sets. InitializeRequests(context, executionState, requests); @@ -57,7 +57,12 @@ protected override async Task OnExecuteAsync( // but need to wait until the transport layer is finished and disposes the result. context.Result.RegisterForCleanup(responses, ReturnResults); - ProcessResponses(context, executionState, requests, responses, SubgraphName); + // we need to lock the state before mutating it since there could be multiple + // query plan nodes be interested in it. + lock (executionState) + { + ProcessResponses(context, executionState, requests, responses, SubgraphName); + } } } diff --git a/src/HotChocolate/Fusion/src/Core/Execution/Nodes/ResolveByKeyBatch.cs b/src/HotChocolate/Fusion/src/Core/Execution/Nodes/ResolveByKeyBatch.cs index b5b4f43026b..5d8a01dd6e7 100644 --- a/src/HotChocolate/Fusion/src/Core/Execution/Nodes/ResolveByKeyBatch.cs +++ b/src/HotChocolate/Fusion/src/Core/Execution/Nodes/ResolveByKeyBatch.cs @@ -75,7 +75,12 @@ protected override async Task OnExecuteAsync( // for cleanup so that the memory can be released at the end of the execution. context.Result.RegisterForCleanup(response, ReturnResult); - ProcessResult(context, response, batchExecutionState); + // we need to lock the state before mutating it since there could be multiple + // query plan nodes be interested in it. + lock (executionState) + { + ProcessResult(context, response, batchExecutionState); + } } } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionNodeBuilderMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionNodeBuilderMiddleware.cs index cec7afd90e4..108801d8ada 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionNodeBuilderMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionNodeBuilderMiddleware.cs @@ -295,10 +295,19 @@ private Subscribe CreateSubscribeNode( private ISelectionSet ResolveSelectionSet( QueryPlanContext context, - ExecutionStep executionStep) - => executionStep.ParentSelection is null + SelectionExecutionStep executionStep) + { + if (executionStep.Resolver is null && + executionStep.SelectionResolvers.Count == 0 && + executionStep.ParentSelectionPath is not null) + { + return context.Operation.RootSelectionSet; + } + + return executionStep.ParentSelection is null ? context.Operation.RootSelectionSet : context.Operation.GetSelectionSet( executionStep.ParentSelection, _schema.GetType(executionStep.SelectionSetTypeMetadata.Name)); + } } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs index 70f1fb7e506..0c7c93c7292 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs @@ -60,6 +60,7 @@ public void Invoke(QueryPlanContext context, QueryPlanDelegate next) selectionSetType, selections, null, + null, false); while (backlog.TryDequeue(out var item)) @@ -70,6 +71,7 @@ public void Invoke(QueryPlanContext context, QueryPlanDelegate next) item.DeclaringTypeMetadata, item.Selections, item.ParentSelection, + item.SelectionPath, item.PreferBatching); } @@ -82,12 +84,14 @@ private void CreateExecutionSteps( ObjectTypeMetadata selectionSetTypeMetadata, IReadOnlyList selections, ISelection? parentSelection, + SelectionPath? parentSelectionPath, bool preferBatching) { var variablesInContext = new HashSet(); var operation = context.Operation; List? leftovers = null; - + var path = new List(); + // if this is the root selection set of a query we will // look for some special selections. if (!context.HasHandledSpecialQueryFields && parentSelection is null) @@ -117,6 +121,7 @@ private void CreateExecutionSteps( context.NextStepId(), subgraph, parentSelection, + parentSelectionPath, _schema.GetType(selectionSetTypeMetadata.Name), selectionSetTypeMetadata); leftovers = null; @@ -134,6 +139,9 @@ private void CreateExecutionSteps( foreach (var selection in current) { + var pathIndex = path.Count; + path.Add(selection); + var field = selection.Field; var fieldInfo = selectionSetTypeMetadata.Fields[field.Name]; @@ -191,10 +199,29 @@ private void CreateExecutionSteps( backlog, operation, selection, + parentSelectionPath, + path, executionStep, preferBatching, context.ParentSelections); } + + path.RemoveAt(pathIndex); + } + + // if the current execution step has now way to resolve the data + // we will try to resolve it from the root. + if(executionStep.ParentSelection is not null && + executionStep.ParentSelectionPath is not null && + executionStep.Resolver is null && + executionStep.SelectionResolvers.Count == 0) + { + if (!EnsureStepCanBeResolvedFromRoot( + executionStep.SubgraphName, + executionStep.ParentSelectionPath)) + { + throw ThrowHelper.NoResolverInContext(); + } } context.Steps.Add(executionStep); @@ -258,6 +285,8 @@ private void CollectNestedSelections( Queue backlog, IOperation operation, ISelection parentSelection, + SelectionPath? rootSelectionPath, + List path, SelectionExecutionStep executionStep, bool preferBatching, Dictionary parentSelectionLookup) @@ -273,6 +302,8 @@ private void CollectNestedSelections( backlog, operation, parentSelection, + rootSelectionPath, + path, executionStep, possibleType, preferBatching, @@ -284,6 +315,8 @@ private void CollectNestedSelections( Queue backlog, IOperation operation, ISelection parentSelection, + SelectionPath? rootSelectionPath, + List path, SelectionExecutionStep executionStep, IObjectType possibleType, bool preferBatching, @@ -297,6 +330,9 @@ private void CollectNestedSelections( foreach (var selection in selectionSet.Selections) { + var pathIndex = path.Count; + path.Add(selection); + parentSelectionLookup.TryAdd(selection, parentSelection); var field = declaringType.Fields[selection.Field.Name]; @@ -346,6 +382,8 @@ private void CollectNestedSelections( backlog, operation, selection, + rootSelectionPath, + path, executionStep, preferBatching, parentSelectionLookup); @@ -355,6 +393,8 @@ private void CollectNestedSelections( { (leftovers ??= new()).Add(selection); } + + path.RemoveAt(pathIndex); } if (leftovers is not null) @@ -362,12 +402,27 @@ private void CollectNestedSelections( backlog.Enqueue( new BacklogItem( parentSelection, + CreateSelectionPath(rootSelectionPath, path), declaringType, leftovers, preferBatching)); } } + private static SelectionPath? CreateSelectionPath(SelectionPath? rootPath, List pathSegments) + { + var parent = rootPath; + + for (var i = 0; i < pathSegments.Count; i++) + { + parent = parent is null + ? new SelectionPath(pathSegments[i]) + : parent.Append(pathSegments[i]); + } + + return parent; + } + private static void AddIntrospectionStepIfNotExists( QueryPlanContext context, IObjectField field, @@ -497,6 +552,8 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps( backlog, operation, nodeSelection, + null, + new List(), executionStep, entityType, preferBatching, @@ -733,14 +790,38 @@ private static bool IsNodeField(IObjectField field, IOperation operation) field.DeclaringType.Equals(operation.RootType) && (field.Name.EqualsOrdinal("node") || field.Name.EqualsOrdinal("nodes")); + private bool EnsureStepCanBeResolvedFromRoot( + string subgraphName, + SelectionPath path) + { + var current = path; + + while (current is not null) + { + var typeMetadata = _config.GetType(path.Selection.DeclaringType.Name); + + if (!typeMetadata.Fields[path.Selection.Field.Name].Bindings.ContainsSubgraph(subgraphName)) + { + return false; + } + + current = current.Parent; + } + + return true; + } + private readonly struct BacklogItem( ISelection parentSelection, + SelectionPath? selectionPath, ObjectTypeMetadata declaringTypeMetadata, IReadOnlyList selections, bool preferBatching) { public ISelection ParentSelection { get; } = parentSelection; + public SelectionPath? SelectionPath { get; } = selectionPath; + public ObjectTypeMetadata DeclaringTypeMetadata { get; } = declaringTypeMetadata; public IReadOnlyList Selections { get; } = selections; diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionTreeBuilderMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionTreeBuilderMiddleware.cs index ea7d4a65e8e..226218fb7fe 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionTreeBuilderMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionTreeBuilderMiddleware.cs @@ -251,11 +251,21 @@ private static void TryEnqueueBatch( private ISelectionSet ResolveSelectionSet( QueryPlanContext context, ExecutionStep executionStep) - => executionStep.ParentSelection is null + { + if (executionStep is SelectionExecutionStep selectionExecStep && + selectionExecStep.Resolver is null && + selectionExecStep.SelectionResolvers.Count == 0 && + selectionExecStep.ParentSelectionPath is not null) + { + return context.Operation.RootSelectionSet; + } + + return executionStep.ParentSelection is null ? context.Operation.RootSelectionSet : context.Operation.GetSelectionSet( executionStep.ParentSelection, _schema.GetType(executionStep.SelectionSetTypeMetadata.Name)); + } private readonly record struct BacklogItem(NodeAndStep[] Batch, Sequence Parent); } diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/FieldRequirementsPlannerMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/FieldRequirementsPlannerMiddleware.cs index 30ad56aa42e..636436cf818 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/FieldRequirementsPlannerMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/FieldRequirementsPlannerMiddleware.cs @@ -196,6 +196,7 @@ private static void RegisterRequirementStep( context.NextStepId(), subgraph, parentSelection, + null, selection.DeclaringType, typeMetadata); diff --git a/src/HotChocolate/Fusion/src/Core/Planning/RequestFormatters/RequestDocumentFormatter.cs b/src/HotChocolate/Fusion/src/Core/Planning/RequestFormatters/RequestDocumentFormatter.cs index 2c5dc64cf40..c3bd46b1596 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/RequestFormatters/RequestDocumentFormatter.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/RequestFormatters/RequestDocumentFormatter.cs @@ -59,6 +59,15 @@ internal RequestDocument CreateRequestDocument( path = p; } + if (executionStep.Resolver is null && + executionStep.SelectionResolvers.Count == 0 && + executionStep.ParentSelectionPath is not null) + { + rootSelectionSetNode = CreateRootLevelQuery( + executionStep.ParentSelectionPath, + rootSelectionSetNode); + } + var operationDefinitionNode = new OperationDefinitionNode( null, context.CreateRemoteOperationName(), @@ -75,6 +84,26 @@ internal RequestDocument CreateRequestDocument( path); } + private SelectionSetNode CreateRootLevelQuery( + SelectionPath path, + SelectionSetNode selectionSet) + { + var current = path; + + while (current is not null) + { + selectionSet = new SelectionSetNode( + new[] + { + current.Selection.SyntaxNode.WithSelectionSet(selectionSet) + }); + + current = current.Parent; + } + + return selectionSet; + } + protected virtual SelectionSetNode CreateRootSelectionSetNode( QueryPlanContext context, SelectionExecutionStep executionStep) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionExecutionStep.cs b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionExecutionStep.cs index 36d534fcf9b..858c8e8ad22 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionExecutionStep.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionExecutionStep.cs @@ -33,7 +33,7 @@ public SelectionExecutionStep( string subgraphName, IObjectType selectionSet, ObjectTypeMetadata selectionSetTypeMetadata) - : this(id, subgraphName, null, selectionSet, selectionSetTypeMetadata) + : this(id, subgraphName, null, null, selectionSet, selectionSetTypeMetadata) { } @@ -49,6 +49,9 @@ public SelectionExecutionStep( /// /// The parent selection of this execution step. /// + /// + /// The selection path from which this execution step was spawned. + /// /// /// The selection set that is part of this execution step. /// @@ -59,17 +62,24 @@ public SelectionExecutionStep( int id, string subgraphName, ISelection? parentSelection, + SelectionPath? parentSelectionPath, IObjectType selectionSet, ObjectTypeMetadata selectionSetTypeMetadata) : base(id, parentSelection, selectionSet, selectionSetTypeMetadata) { SubgraphName = subgraphName; + ParentSelectionPath = parentSelectionPath; } /// /// Gets the subgraph from which this execution step will fetch data. /// public string SubgraphName { get; } + + /// + /// Gets the selection path from which this execution step was spawned. + /// + public SelectionPath? ParentSelectionPath { get; } /// /// Gets the resolver for this execution step. diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs new file mode 100644 index 00000000000..dca7e4c9bd4 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Core/Planning/Steps/SelectionPath.cs @@ -0,0 +1,45 @@ +using System.Text; +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Fusion.Planning; + +internal sealed class SelectionPath +{ + public SelectionPath(ISelection selection) + : this(selection, null) + { + } + + private SelectionPath(ISelection selection, SelectionPath? parent) + { + Selection = selection; + Parent = parent; + } + + public SelectionPath? Parent { get; } + + public ISelection Selection { get; } + + public SelectionPath Append(ISelection selection) + => new(selection, this); + + public override string ToString() + { + if (Parent is null) + { + return $"/{Selection.ResponseName}"; + } + + var sb = new StringBuilder(); + var current = this; + + while (current is not null) + { + sb.Append('/'); + sb.Append(Selection.ResponseName); + current = current.Parent; + } + + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs index 22554e06758..40709bdef31 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/RequestPlannerTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using static HotChocolate.Fusion.Shared.DemoProjectSchemaExtensions; using static HotChocolate.Language.Utf8GraphQLParser; +using HttpClientConfiguration = HotChocolate.Fusion.Composition.HttpClientConfiguration; namespace HotChocolate.Fusion; @@ -1096,6 +1097,185 @@ query Query { snapshot.Add(result.QueryPlan, nameof(result.QueryPlan)); await snapshot.MatchAsync(); } + + [Fact] + public async Task Query_Plan_28_Simple_Root_Data() + { + // arrange + var schemaA = + """ + type Query { + data: Data + } + + type Data { + a: String + } + + schema { + query: Query + } + """; + + var schemaB = + """ + type Query { + data: Data + } + + type Data { + b: String + } + + schema { + query: Query + } + """; + + var fusionGraph = await FusionGraphComposer.ComposeAsync( + new[] + { + new SubgraphConfiguration("A", schemaA, Array.Empty(), CreateClients()), + new SubgraphConfiguration("B", schemaB, Array.Empty(), CreateClients()), + }); + + // act + var result = await CreateQueryPlanAsync( + fusionGraph, + """ + query Query { + data { + a + b + } + } + """); + + var snapshot = new Snapshot(); + snapshot.Add(result.UserRequest, nameof(result.UserRequest)); + snapshot.Add(result.QueryPlan, nameof(result.QueryPlan)); + await snapshot.MatchAsync(); + } + + [Fact] + public async Task Query_Plan_29_Simple_Root_List_Data() + { + // arrange + var schemaA = + """ + type Query { + data: [Data] + } + + type Data { + a: String + } + + schema { + query: Query + } + """; + + var schemaB = + """ + type Query { + data: [Data] + } + + type Data { + b: String + } + + schema { + query: Query + } + """; + + var fusionGraph = await FusionGraphComposer.ComposeAsync( + new[] + { + new SubgraphConfiguration("A", schemaA, Array.Empty(), CreateClients()), + new SubgraphConfiguration("B", schemaB, Array.Empty(), CreateClients()), + }); + + // act + var result = await CreateQueryPlanAsync( + fusionGraph, + """ + query Query { + data { + a + b + } + } + """); + + var snapshot = new Snapshot(); + snapshot.Add(result.UserRequest, nameof(result.UserRequest)); + snapshot.Add(result.QueryPlan, nameof(result.QueryPlan)); + await snapshot.MatchAsync(); + } + + [Fact] + public async Task Query_Plan_30_Entity_Data() + { + // arrange + var schemaA = + """ + type Query { + entity(id: ID!): Entity + } + + type Entity { + id: ID! + a: String + } + + schema { + query: Query + } + """; + + var schemaB = + """ + type Query { + entity(id: ID!): Entity + } + + type Entity { + id: ID! + b: String + } + + schema { + query: Query + } + """; + + var fusionGraph = await FusionGraphComposer.ComposeAsync( + new[] + { + new SubgraphConfiguration("A", schemaA, Array.Empty(), CreateClients()), + new SubgraphConfiguration("B", schemaB, Array.Empty(), CreateClients()), + }); + + // act + var result = await CreateQueryPlanAsync( + fusionGraph, + """ + query Query { + entity(id: 123) { + a + b + } + } + """); + + var snapshot = new Snapshot(); + snapshot.Add(result.UserRequest, nameof(result.UserRequest)); + snapshot.Add(result.QueryPlan, nameof(result.QueryPlan)); + await snapshot.MatchAsync(); + } private static async Task<(DocumentNode UserRequest, Execution.Nodes.QueryPlan QueryPlan)> CreateQueryPlanAsync( Skimmed.Schema fusionGraph, @@ -1135,4 +1315,10 @@ query Query { return (request, queryPlan); } + + private static IClientConfiguration[] CreateClients() + => new IClientConfiguration[] + { + new HttpClientConfiguration(new Uri("http://nothing")) + }; } diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DataTests.Fetch_Data_Across_Subgraphs.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DataTests.Fetch_Data_Across_Subgraphs.snap index 2191073c030..6bac7dd6537 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DataTests.Fetch_Data_Across_Subgraphs.snap +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DataTests.Fetch_Data_Across_Subgraphs.snap @@ -30,16 +30,15 @@ QueryPlan { "type": "Resolve", "subgraph": "Reviews2", - "document": "query GetUser_2 { reviewsValue }", - "selectionSetId": 2 + "document": "query GetUser_2 { viewer { data { reviewsValue } } }", + "selectionSetId": 0 } ] }, { "type": "Compose", "selectionSetIds": [ - 0, - 2 + 0 ] } ] @@ -49,7 +48,7 @@ QueryPlan QueryPlan Hash --------------- -4870A9047BB973D0DE07AFF7E991C9397E87F8EF +0945927521FCEB9066754DDECB9BC7D43B187B4E --------------- Result @@ -58,7 +57,8 @@ Result "data": { "viewer": { "data": { - "accountValue": "Account" + "accountValue": "Account", + "reviewsValue": "Reviews2" } } } diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Query_Plan_28_Simple_Root_Data.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Query_Plan_28_Simple_Root_Data.snap new file mode 100644 index 00000000000..1746330758a --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Query_Plan_28_Simple_Root_Data.snap @@ -0,0 +1,45 @@ +UserRequest +--------------- +query Query { + data { + a + b + } +} +--------------- + +QueryPlan +--------------- +{ + "document": "query Query { data { a b } }", + "operation": "Query", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "A", + "document": "query Query_1 { data { a } }", + "selectionSetId": 0 + }, + { + "type": "Resolve", + "subgraph": "B", + "document": "query Query_2 { data { b } }", + "selectionSetId": 0 + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + } + ] + } +} +--------------- diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Query_Plan_29_Simple_Root_List_Data.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Query_Plan_29_Simple_Root_List_Data.snap new file mode 100644 index 00000000000..1746330758a --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Query_Plan_29_Simple_Root_List_Data.snap @@ -0,0 +1,45 @@ +UserRequest +--------------- +query Query { + data { + a + b + } +} +--------------- + +QueryPlan +--------------- +{ + "document": "query Query { data { a b } }", + "operation": "Query", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "A", + "document": "query Query_1 { data { a } }", + "selectionSetId": 0 + }, + { + "type": "Resolve", + "subgraph": "B", + "document": "query Query_2 { data { b } }", + "selectionSetId": 0 + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + } + ] + } +} +--------------- diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Query_Plan_30_Entity_Data.snap b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Query_Plan_30_Entity_Data.snap new file mode 100644 index 00000000000..45a2a411d4f --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/RequestPlannerTests.Query_Plan_30_Entity_Data.snap @@ -0,0 +1,45 @@ +UserRequest +--------------- +query Query { + entity(id: 123) { + a + b + } +} +--------------- + +QueryPlan +--------------- +{ + "document": "query Query { entity(id: 123) { a b } }", + "operation": "Query", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "A", + "document": "query Query_1 { entity(id: 123) { a } }", + "selectionSetId": 0 + }, + { + "type": "Resolve", + "subgraph": "B", + "document": "query Query_2 { entity(id: 123) { b } }", + "selectionSetId": 0 + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + } + ] + } +} +---------------