From 777ec9ebd9659a37678b42ca6b02c17762e5b78d Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:40:09 +0200 Subject: [PATCH 01/16] Prevent infinite loops in BuildExecutionPlan --- .../Planning/OperationPlanner.BuildExecutionTree.cs | 10 +++++++--- .../src/Fusion.Execution/Planning/OperationPlanner.cs | 5 +++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs index e4710ba2ece..c016ff9773a 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs @@ -24,7 +24,8 @@ private OperationPlan BuildExecutionPlan( OperationDefinitionNode operationDefinition, ImmutableList planSteps, int searchSpace, - int expandedNodes) + int expandedNodes, + CancellationToken cancellationToken) { if (operation.IsIntrospectionOnly()) { @@ -44,7 +45,7 @@ private OperationPlan BuildExecutionPlan( planSteps = TransformPlanSteps(planSteps, operationDefinition); IndexDependencies(planSteps, ctx); - BuildExecutionNodes(planSteps, ctx, _schema, hasVariables); + BuildExecutionNodes(planSteps, ctx, _schema, hasVariables, cancellationToken); MergeAndBatchOperations(ctx, _options.EnableRequestGrouping, _options.MergePolicy); WireExecutionDependencies(ctx); @@ -244,7 +245,8 @@ private static void BuildExecutionNodes( ImmutableList planSteps, ExecutionPlanBuildContext ctx, ISchemaDefinition schema, - bool hasVariables) + bool hasVariables, + CancellationToken cancellationToken) { var requiresUpload = schema.Types.TryGetType(UploadScalarName, out var uploadType) && uploadType.IsScalarType(); var readySteps = planSteps.Where(t => !ctx.DependenciesByStepId.ContainsKey(t.Id)).ToList(); @@ -252,6 +254,8 @@ private static void BuildExecutionNodes( while (ctx.ProcessedStepIds.Count < planSteps.Count) { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var step in readySteps) { if (!ctx.ProcessedStepIds.Add(step.Id)) diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs index ab4eb45402d..862fbd4af81 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -159,7 +159,8 @@ node with operationDefinition, planSteps, searchSpace, - expandedNodes); + expandedNodes, + cancellationToken); if (eventSourceEnabled) { @@ -212,7 +213,7 @@ node with // that are already worse. If it cannot finish a full plan, it returns null and the planner // continues without that early shortcut. var bestCompletePlan = TryBuildGreedyCompletePlan(possiblePlans, cancellationToken); - var bestCompletePlanCost = bestCompletePlan is null ? double.PositiveInfinity : bestCompletePlan.PathCost; + var bestCompletePlanCost = bestCompletePlan?.PathCost ?? double.PositiveInfinity; while (possiblePlans.TryDequeue(out var current, out _)) { From 2f9d691c1b6a404b0e93710a0b6ed375760288da Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:40:27 +0200 Subject: [PATCH 02/16] Do not determine lookups for introspection types --- .../src/Fusion.Execution.Types/PlannerTopologyCache.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution.Types/PlannerTopologyCache.cs b/src/HotChocolate/Fusion/src/Fusion.Execution.Types/PlannerTopologyCache.cs index 6c93004384d..82074318f42 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution.Types/PlannerTopologyCache.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution.Types/PlannerTopologyCache.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using HotChocolate.Fusion.Types.Metadata; +using HotChocolate.Types; namespace HotChocolate.Fusion.Types; @@ -31,7 +32,10 @@ public static PlannerTopologyCache Build(FusionSchemaDefinition schema) { ArgumentNullException.ThrowIfNull(schema); - var complexTypes = schema.Types.AsEnumerable().OfType().ToArray(); + var complexTypes = schema.Types.AsEnumerable() + .OfType() + .Where(t => !((ITypeDefinition)t).IsIntrospectionType) + .ToArray(); var schemaNames = CollectSchemaNames(complexTypes); var fieldResolutions = BuildFieldResolutions(complexTypes); var orderedLookups = BuildOrderedLookups(schema, complexTypes, schemaNames); From dba87b4f42d30f4f0661c709c392825fe92106fa Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:35:02 +0200 Subject: [PATCH 03/16] Add reproduction --- .../Planning/OperationPlanner.cs | 1 + .../AbstractTypeTests.cs | 112 +++++++ ...ments_All_Fields_On_Dedicated_Schemas.yaml | 298 ++++++++++++++++++ 3 files changed, 411 insertions(+) create mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs index 862fbd4af81..1de5a574eac 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -451,6 +451,7 @@ static long ToGuardrailMilliseconds(TimeSpan value) while (true) { cancellationToken.ThrowIfCancellationRequested(); + var backlog = current.Backlog; if (backlog.IsEmpty) diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs index d42252d5d05..c6920060ff6 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs @@ -602,6 +602,118 @@ type Comment implements Votable { await MatchSnapshotAsync(gateway, request, result); } + [Fact] + public async Task Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas() + { + // arrange + using var serverA = CreateSourceSchema( + "A", + """ + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + """); + + using var serverB = CreateSourceSchema( + "B", + """ + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + """); + + using var serverC = CreateSourceSchema( + "C", + """ + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + """); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", serverA), + ("B", serverB), + ("C", serverC) + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + """ + { + abstractTypes { + upvotes + ... on Discussion { + score + } + ... on Author { + score + } + } + } + """); + + using var result = await client.PostAsync( + request, + new Uri("http://localhost:5000/graphql")); + + // assert + await MatchSnapshotAsync(gateway, request, result); + } + [Fact] public async Task Union_Field_With_Type_Refinements_And_Concrete_Lookups() { diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml new file mode 100644 index 00000000000..4fac70a50dd --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml @@ -0,0 +1,298 @@ +title: Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas +request: + document: | + { + abstractTypes { + upvotes + ... on Discussion { + score + } + ... on Author { + score + } + } + } +response: + body: | + { + "data": { + "abstractTypes": [ + { + "upvotes": 123, + "score": 123 + }, + { + "upvotes": 123, + "score": 123 + }, + { + "upvotes": 123, + "score": 123 + } + ] + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_13354b96_1 { + abstractTypes { + __typename + id + } + } + response: + results: + - | + { + "data": { + "abstractTypes": [ + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjox" + }, + { + "__typename": "Author", + "id": "QXV0aG9yOjI=" + }, + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjoz" + } + ] + } + } + - name: B + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + document: | + query Op_13354b96_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + ... on Discussion { + score + } + } + } + variables: | + [ + { + "__fusion_1_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_1_id": "RGlzY3Vzc2lvbjoz" + } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123, + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123, + "score": 123 + } + } + } + - name: C + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_13354b96_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + ... on Author { + score + } + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjI=" + } + response: + results: + - | + { + "data": { + "authorById": { + "__typename": "Author", + "upvotes": 123, + "score": 123 + } + } + } +operationPlan: + operation: + - document: | + { + abstractTypes { + __typename @fusion__requirement + upvotes + id @fusion__requirement + ... on Discussion { + score + } + ... on Author { + score + } + } + } + hash: 13354b96bf8e9c5bb2d7ab9d87a0cb2b + searchSpace: 1 + expandedNodes: 2 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_13354b96_1 { + abstractTypes { + __typename + id + } + } + - id: 2 + type: Operation + schema: B + operation: | + query Op_13354b96_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + ... on Discussion { + score + } + } + } + source: $.discussionById + target: $.abstractTypes + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: Operation + schema: C + operation: | + query Op_13354b96_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + ... on Author { + score + } + } + } + source: $.authorById + target: $.abstractTypes + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 1 From 2aeab7b3d420bd09cd0d8507c75b8c0d48dbd064 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:39:42 +0200 Subject: [PATCH 04/16] Remvoe existing snapsoht --- ...ments_All_Fields_On_Dedicated_Schemas.yaml | 298 ------------------ 1 file changed, 298 deletions(-) delete mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml deleted file mode 100644 index 4fac70a50dd..00000000000 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml +++ /dev/null @@ -1,298 +0,0 @@ -title: Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas -request: - document: | - { - abstractTypes { - upvotes - ... on Discussion { - score - } - ... on Author { - score - } - } - } -response: - body: | - { - "data": { - "abstractTypes": [ - { - "upvotes": 123, - "score": 123 - }, - { - "upvotes": 123, - "score": 123 - }, - { - "upvotes": 123, - "score": 123 - } - ] - } - } -sourceSchemas: - - name: A - schema: | - schema { - query: Query - } - - interface Node { - id: ID! - } - - interface Votable @key(fields: "id") { - id: ID! - } - - type Author implements Votable & Node @key(fields: "id") { - id: ID! - } - - type Discussion implements Votable & Node @key(fields: "id") { - id: ID! - } - - type Query { - abstractTypes: [Votable] - node(id: ID!): Node @lookup @shareable - } - interactions: - - request: - accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 - document: | - query Op_13354b96_1 { - abstractTypes { - __typename - id - } - } - response: - results: - - | - { - "data": { - "abstractTypes": [ - { - "__typename": "Discussion", - "id": "RGlzY3Vzc2lvbjox" - }, - { - "__typename": "Author", - "id": "QXV0aG9yOjI=" - }, - { - "__typename": "Discussion", - "id": "RGlzY3Vzc2lvbjoz" - } - ] - } - } - - name: B - schema: | - schema { - query: Query - } - - interface Node { - id: ID! - } - - interface Votable @key(fields: "id") { - id: ID! - upvotes: Int! - score: Int! - } - - type Discussion implements Votable & Node @key(fields: "id") { - id: ID! - upvotes: Int! - score: Int! - } - - type Query { - node(id: ID!): Node @lookup @shareable - discussionById(id: ID!): Discussion @lookup @shareable - } - interactions: - - request: - accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 - document: | - query Op_13354b96_2( - $__fusion_1_id: ID! - ) { - discussionById(id: $__fusion_1_id) { - __typename - upvotes - ... on Discussion { - score - } - } - } - variables: | - [ - { - "__fusion_1_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_1_id": "RGlzY3Vzc2lvbjoz" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - - | - { - "data": { - "discussionById": { - "__typename": "Discussion", - "upvotes": 123, - "score": 123 - } - } - } - - | - { - "data": { - "discussionById": { - "__typename": "Discussion", - "upvotes": 123, - "score": 123 - } - } - } - - name: C - schema: | - schema { - query: Query - } - - interface Node { - id: ID! - } - - interface Votable @key(fields: "id") { - id: ID! - upvotes: Int! - score: Int! - } - - type Author implements Votable & Node @key(fields: "id") { - id: ID! - upvotes: Int! - score: Int! - } - - type Query { - node(id: ID!): Node @lookup @shareable - authorById(id: ID!): Author @lookup @shareable - } - interactions: - - request: - accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 - document: | - query Op_13354b96_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - __typename - upvotes - ... on Author { - score - } - } - } - variables: | - { - "__fusion_2_id": "QXV0aG9yOjI=" - } - response: - results: - - | - { - "data": { - "authorById": { - "__typename": "Author", - "upvotes": 123, - "score": 123 - } - } - } -operationPlan: - operation: - - document: | - { - abstractTypes { - __typename @fusion__requirement - upvotes - id @fusion__requirement - ... on Discussion { - score - } - ... on Author { - score - } - } - } - hash: 13354b96bf8e9c5bb2d7ab9d87a0cb2b - searchSpace: 1 - expandedNodes: 2 - nodes: - - id: 1 - type: Operation - schema: A - operation: | - query Op_13354b96_1 { - abstractTypes { - __typename - id - } - } - - id: 2 - type: Operation - schema: B - operation: | - query Op_13354b96_2( - $__fusion_1_id: ID! - ) { - discussionById(id: $__fusion_1_id) { - __typename - upvotes - ... on Discussion { - score - } - } - } - source: $.discussionById - target: $.abstractTypes - requirements: - - name: __fusion_1_id - selectionMap: >- - id - dependencies: - - id: 1 - - id: 3 - type: Operation - schema: C - operation: | - query Op_13354b96_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - __typename - upvotes - ... on Author { - score - } - } - } - source: $.authorById - target: $.abstractTypes - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 From 2a5195963fae67c78103e39c5971ca1399043def Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:49:30 +0200 Subject: [PATCH 05/16] Add another regression test --- .../AbstractTypeTests.cs | 107 +++++ ...ments_All_Fields_On_Dedicated_Schemas.yaml | 385 ++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs index c6920060ff6..3eb91a7a2d9 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs @@ -714,6 +714,113 @@ ... on Author { await MatchSnapshotAsync(gateway, request, result); } + [Fact] + public async Task Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match() + { + // arrange + using var serverA = CreateSourceSchema( + "A", + """ + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + """); + + using var serverB = CreateSourceSchema( + "B", + """ + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + """); + + using var serverC = CreateSourceSchema( + "C", + """ + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + } + """); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", serverA), + ("B", serverB), + ("C", serverC) + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + """ + { + abstractTypes { + upvotes + ... on Discussion { + score + } + } + } + """); + + using var result = await client.PostAsync( + request, + new Uri("http://localhost:5000/graphql")); + + // assert + await MatchSnapshotAsync(gateway, request, result); + } + [Fact] public async Task Union_Field_With_Type_Refinements_And_Concrete_Lookups() { diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml new file mode 100644 index 00000000000..820c14b0075 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml @@ -0,0 +1,385 @@ +title: Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas +request: + document: | + { + abstractTypes { + upvotes + ... on Discussion { + score + } + ... on Author { + score + } + } + } +response: + body: | + { + "data": { + "abstractTypes": [ + { + "upvotes": 123, + "score": 123 + }, + { + "upvotes": 123, + "score": 123 + }, + { + "upvotes": 123, + "score": 123 + } + ] + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_13354b96_1 { + abstractTypes { + __typename + id + } + } + response: + results: + - | + { + "data": { + "abstractTypes": [ + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjox" + }, + { + "__typename": "Author", + "id": "QXV0aG9yOjI=" + }, + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjoz" + } + ] + } + } + - name: B + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + kind: OperationBatch + items: + - document: | + query Op_13354b96_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + } + } + variables: | + [ + { + "__fusion_1_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_1_id": "RGlzY3Vzc2lvbjoz" + } + ] + - document: | + query Op_13354b96_5( + $__fusion_4_id: ID! + ) { + discussionById(id: $__fusion_4_id) { + score + } + } + variables: | + [ + { + "__fusion_4_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_4_id": "RGlzY3Vzc2lvbjoz" + } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - name: C + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + kind: OperationBatch + items: + - document: | + query Op_13354b96_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjI=" + } + - document: | + query Op_13354b96_4( + $__fusion_3_id: ID! + ) { + authorById(id: $__fusion_3_id) { + score + } + } + variables: | + { + "__fusion_3_id": "QXV0aG9yOjI=" + } + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "authorById": { + "__typename": "Author", + "upvotes": 123 + } + } + } + - | + { + "data": { + "authorById": { + "score": 123 + } + } + } +operationPlan: + operation: + - document: | + { + abstractTypes { + __typename @fusion__requirement + upvotes + id @fusion__requirement + ... on Discussion { + score + id @fusion__requirement + } + ... on Author { + score + id @fusion__requirement + } + } + } + hash: 13354b96bf8e9c5bb2d7ab9d87a0cb2b + searchSpace: 1 + expandedNodes: 2 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_13354b96_1 { + abstractTypes { + __typename + id + } + } + - id: 2 + type: Operation + schema: B + operation: | + query Op_13354b96_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 5 + type: Operation + schema: B + operation: | + query Op_13354b96_5( + $__fusion_4_id: ID! + ) { + discussionById(id: $__fusion_4_id) { + score + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_4_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: Operation + schema: C + operation: | + query Op_13354b96_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + } + } + source: $.authorById + target: $.abstractTypes + batchingGroupId: 3 + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 4 + type: Operation + schema: C + operation: | + query Op_13354b96_4( + $__fusion_3_id: ID! + ) { + authorById(id: $__fusion_3_id) { + score + } + } + source: $.authorById + target: $.abstractTypes + batchingGroupId: 3 + requirements: + - name: __fusion_3_id + selectionMap: >- + id + dependencies: + - id: 1 From 0480ca1d9aa8119fba26a09e2a19299280b2fa10 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:51:48 +0200 Subject: [PATCH 06/16] Remove snapshot --- ...ments_All_Fields_On_Dedicated_Schemas.yaml | 385 ------------------ 1 file changed, 385 deletions(-) delete mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml deleted file mode 100644 index 820c14b0075..00000000000 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml +++ /dev/null @@ -1,385 +0,0 @@ -title: Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas -request: - document: | - { - abstractTypes { - upvotes - ... on Discussion { - score - } - ... on Author { - score - } - } - } -response: - body: | - { - "data": { - "abstractTypes": [ - { - "upvotes": 123, - "score": 123 - }, - { - "upvotes": 123, - "score": 123 - }, - { - "upvotes": 123, - "score": 123 - } - ] - } - } -sourceSchemas: - - name: A - schema: | - schema { - query: Query - } - - interface Node { - id: ID! - } - - interface Votable @key(fields: "id") { - id: ID! - } - - type Author implements Votable & Node @key(fields: "id") { - id: ID! - } - - type Discussion implements Votable & Node @key(fields: "id") { - id: ID! - } - - type Query { - abstractTypes: [Votable] - node(id: ID!): Node @lookup @shareable - } - interactions: - - request: - accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 - document: | - query Op_13354b96_1 { - abstractTypes { - __typename - id - } - } - response: - results: - - | - { - "data": { - "abstractTypes": [ - { - "__typename": "Discussion", - "id": "RGlzY3Vzc2lvbjox" - }, - { - "__typename": "Author", - "id": "QXV0aG9yOjI=" - }, - { - "__typename": "Discussion", - "id": "RGlzY3Vzc2lvbjoz" - } - ] - } - } - - name: B - schema: | - schema { - query: Query - } - - interface Node { - id: ID! - } - - interface Votable @key(fields: "id") { - id: ID! - upvotes: Int! - score: Int! - } - - type Discussion implements Votable & Node @key(fields: "id") { - id: ID! - upvotes: Int! - score: Int! - } - - type Query { - node(id: ID!): Node @lookup @shareable - discussionById(id: ID!): Discussion @lookup @shareable - } - interactions: - - request: - accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 - kind: OperationBatch - items: - - document: | - query Op_13354b96_2( - $__fusion_1_id: ID! - ) { - discussionById(id: $__fusion_1_id) { - __typename - upvotes - } - } - variables: | - [ - { - "__fusion_1_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_1_id": "RGlzY3Vzc2lvbjoz" - } - ] - - document: | - query Op_13354b96_5( - $__fusion_4_id: ID! - ) { - discussionById(id: $__fusion_4_id) { - score - } - } - variables: | - [ - { - "__fusion_4_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_4_id": "RGlzY3Vzc2lvbjoz" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - - | - { - "data": { - "discussionById": { - "score": 123 - } - } - } - - | - { - "data": { - "discussionById": { - "score": 123 - } - } - } - - | - { - "data": { - "discussionById": { - "__typename": "Discussion", - "upvotes": 123 - } - } - } - - | - { - "data": { - "discussionById": { - "__typename": "Discussion", - "upvotes": 123 - } - } - } - - name: C - schema: | - schema { - query: Query - } - - interface Node { - id: ID! - } - - interface Votable @key(fields: "id") { - id: ID! - upvotes: Int! - score: Int! - } - - type Author implements Votable & Node @key(fields: "id") { - id: ID! - upvotes: Int! - score: Int! - } - - type Query { - node(id: ID!): Node @lookup @shareable - authorById(id: ID!): Author @lookup @shareable - } - interactions: - - request: - accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 - kind: OperationBatch - items: - - document: | - query Op_13354b96_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - __typename - upvotes - } - } - variables: | - { - "__fusion_2_id": "QXV0aG9yOjI=" - } - - document: | - query Op_13354b96_4( - $__fusion_3_id: ID! - ) { - authorById(id: $__fusion_3_id) { - score - } - } - variables: | - { - "__fusion_3_id": "QXV0aG9yOjI=" - } - response: - contentType: application/jsonl; charset=utf-8 - results: - - | - { - "data": { - "authorById": { - "__typename": "Author", - "upvotes": 123 - } - } - } - - | - { - "data": { - "authorById": { - "score": 123 - } - } - } -operationPlan: - operation: - - document: | - { - abstractTypes { - __typename @fusion__requirement - upvotes - id @fusion__requirement - ... on Discussion { - score - id @fusion__requirement - } - ... on Author { - score - id @fusion__requirement - } - } - } - hash: 13354b96bf8e9c5bb2d7ab9d87a0cb2b - searchSpace: 1 - expandedNodes: 2 - nodes: - - id: 1 - type: Operation - schema: A - operation: | - query Op_13354b96_1 { - abstractTypes { - __typename - id - } - } - - id: 2 - type: Operation - schema: B - operation: | - query Op_13354b96_2( - $__fusion_1_id: ID! - ) { - discussionById(id: $__fusion_1_id) { - __typename - upvotes - } - } - source: $.discussionById - target: $.abstractTypes - batchingGroupId: 2 - requirements: - - name: __fusion_1_id - selectionMap: >- - id - dependencies: - - id: 1 - - id: 5 - type: Operation - schema: B - operation: | - query Op_13354b96_5( - $__fusion_4_id: ID! - ) { - discussionById(id: $__fusion_4_id) { - score - } - } - source: $.discussionById - target: $.abstractTypes - batchingGroupId: 2 - requirements: - - name: __fusion_4_id - selectionMap: >- - id - dependencies: - - id: 1 - - id: 3 - type: Operation - schema: C - operation: | - query Op_13354b96_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - __typename - upvotes - } - } - source: $.authorById - target: $.abstractTypes - batchingGroupId: 3 - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 - - id: 4 - type: Operation - schema: C - operation: | - query Op_13354b96_4( - $__fusion_3_id: ID! - ) { - authorById(id: $__fusion_3_id) { - score - } - } - source: $.authorById - target: $.abstractTypes - batchingGroupId: 3 - requirements: - - name: __fusion_3_id - selectionMap: >- - id - dependencies: - - id: 1 From e4dfdcc8a8a326f6f3de42600dd884b6d6aea24c Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:04:22 +0200 Subject: [PATCH 07/16] fix test setup --- .../Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs index 3eb91a7a2d9..34ab3e99b6e 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs @@ -758,7 +758,6 @@ interface Node { interface Votable @key(fields: "id") { id: ID! upvotes: Int! - score: Int! } type Discussion implements Votable & Node @key(fields: "id") { From c06568896b9c5dd54af1e0430295b6de1193ea29 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:29:32 +0200 Subject: [PATCH 08/16] Add more tests --- .../AbstractTypeTests.cs | 117 ++++++++++++++++++ .../ConditionalTests.cs | 115 +++++++++++++++++ 2 files changed, 232 insertions(+) diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs index 34ab3e99b6e..50efba150c3 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/AbstractTypeTests.cs @@ -714,6 +714,123 @@ ... on Author { await MatchSnapshotAsync(gateway, request, result); } + [Fact] + public async Task Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections() + { + // arrange + using var serverA = CreateSourceSchema( + "A", + """ + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + """); + + using var serverB = CreateSourceSchema( + "B", + """ + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + """); + + using var serverC = CreateSourceSchema( + "C", + """ + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + """); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", serverA), + ("B", serverB), + ("C", serverC) + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + """ + { + abstractTypes { + upvotes + ... on Discussion { + score + } + ... on Author { + someId: id + } + } + b: abstractTypes { + ... on Author { + upvotes + } + } + } + """); + + using var result = await client.PostAsync( + request, + new Uri("http://localhost:5000/graphql")); + + // assert + await MatchSnapshotAsync(gateway, request, result); + } + [Fact] public async Task Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match() { diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs index 6150238d42a..9646e45feaf 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs @@ -2135,4 +2135,119 @@ ... @skip(if: $skip) { } #endregion + + [Fact] + public async Task Interface_Field_With_Type_Refinement_Under_Skip() + { + // arrange + using var serverA = CreateSourceSchema( + "A", + """ + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + """); + + using var serverB = CreateSourceSchema( + "B", + """ + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + """); + + using var serverC = CreateSourceSchema( + "C", + """ + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + """); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", serverA), + ("B", serverB), + ("C", serverC) + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + """ + query($skip: Boolean!) { + abstractTypes { + upvotes + ... on Discussion { + score + } + ... @skip(if: $skip) { + ... on Author { + score + } + } + } + } + """, + variables: new Dictionary { ["skip"] = true }); + + using var result = await client.PostAsync( + request, + new Uri("http://localhost:5000/graphql")); + + // assert + await MatchSnapshotAsync(gateway, request, result); + } } From f52b496877b727e931ad13f5b20321a33defbc33 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:05:32 +0200 Subject: [PATCH 09/16] First attempt at a fix --- .../Planning/OperationPlanner.cs | 354 ++++++++++++++ ...nts_Aliased_With_Different_Selections.yaml | 432 ++++++++++++++++++ ...ments_All_Fields_On_Dedicated_Schemas.yaml | 385 ++++++++++++++++ ...nements_Type_Refinements_Do_Not_Match.yaml | 330 +++++++++++++ ...Field_With_Type_Refinement_Under_Skip.yaml | 390 ++++++++++++++++ 5 files changed, 1891 insertions(+) create mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml create mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml create mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml create mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs index 1de5a574eac..0e97db36f4a 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -882,6 +882,72 @@ private PlanNode InlineLookupRequirements( } } + // Fallback: if no candidate step was found via exact selection set ID match, + // walk the internal operation AST to find the nearest ancestor step and the + // correct wrapping structure (preserving inline fragments with directives). + if (selectionSet is not null + && TryFindAncestorStepForRequirement( + internalOperation, steps, index, workItemSelectionSet.Id, + out var ancestorStep, out var ancestorStepIndex, + out var ancestorSSId, out var ancestorType, out var wrappingNodes)) + { + var schemaName = ancestorStep.SchemaName!; + if (processed.Add(schemaName) && !lookup.SchemaName.Equals(schemaName)) + { + var wrappedSelections = BuildWrappedSelections(wrappingNodes, selectionSet); + + var input = new SelectionSetPartitionerInput + { + SchemaName = schemaName, + SelectionSet = new SelectionSet( + ancestorSSId, + wrappedSelections, + ancestorType, + workItemSelectionSet.Path), + SelectionSetIndex = index + }; + + var (resolvable, unresolvable, _, _) = _partitioner.Partition(input); + + if (resolvable is { Selections.Count: > 0 }) + { + var operation = InlineSelections( + ancestorStep.Definition, + index, + ancestorType, + ancestorSSId, + resolvable); + + EnsureAllSelectionSetsRegistered(operation.SelectionSet, index); + + var updatedStep = ancestorStep with + { + Definition = operation, + SelectionSets = SelectionSetIndexer.CreateIdSet(operation.SelectionSet, index), + Dependents = ancestorStep.Dependents.Add(lookupStepId) + }; + + steps = steps.SetItem(ancestorStepIndex, updatedStep); + selectionSet = null; + + if (!unresolvable.IsEmpty) + { + var top = unresolvable.Peek(); + if (top.SelectionSet.Id == ancestorSSId) + { + unresolvable = unresolvable.Pop(out top); + selectionSet = top.SelectionSet.Node; + } + + backlog = backlog.PushUnresolvable( + unresolvable, + current.SchemaName, + GetOperationStepDepth(current, ancestorStep.Id)); + } + } + } + } + // if we have still selections left we need to add them to the backlog. if (selectionSet is not null) { @@ -1739,6 +1805,85 @@ private static List GetLookupArguments(Lookup lookup, string requi } } + // Fallback: if no candidate step was found via exact selection set ID match, + // walk the internal operation AST to find the nearest ancestor step and the + // correct wrapping structure (preserving inline fragments with directives). + if (requirements is not null + && TryFindAncestorStepForRequirement( + current.InternalOperationDefinition, steps, index, workItem.Selection.SelectionSetId, + out var ancestorStep, out var ancestorStepIndex, + out var ancestorSSId, out var ancestorType, out var wrappingNodes)) + { + if (currentStep.Id != ancestorStep.Id + && !ancestorStep.DependsOn(currentStep, steps)) + { + var wrappedSelections = BuildWrappedSelections(wrappingNodes, requirements); + + var input = new SelectionSetPartitionerInput + { + SchemaName = ancestorStep.SchemaName!, + SelectionSet = new SelectionSet( + ancestorSSId, + wrappedSelections, + ancestorType, + workItem.Selection.Path), + SelectionSetIndex = index + }; + + var (resolvable, unresolvable, _, _) = _partitioner.Partition(input); + + if (resolvable is { Selections.Count: > 0 }) + { + if (resolvable != requirements) + { + index.Register(workItem.Selection.SelectionSetId, resolvable); + } + + var operation = InlineSelections( + ancestorStep.Definition, + index, + ancestorType, + ancestorSSId, + resolvable); + + EnsureAllSelectionSetsRegistered(operation.SelectionSet, index); + + var updatedStep = ancestorStep with + { + Definition = operation, + SelectionSets = SelectionSetIndexer.CreateIdSet(operation.SelectionSet, index), + Dependents = ancestorStep.Dependents.Add(workItem.StepId) + }; + + steps = steps.SetItem(ancestorStepIndex, updatedStep); + requirements = null; + + if (!unresolvable.IsEmpty) + { + var top = unresolvable.Peek(); + if (top.SelectionSet.Id == workItem.Selection.SelectionSetId) + { + unresolvable = unresolvable.Pop(out top); + requirements = top.SelectionSet.Node; + } + + foreach (var entry in unresolvable.Reverse()) + { + backlog = backlog.Push( + new OperationWorkItem( + OperationWorkItemKind.Lookup, + entry.SelectionSet, + FromSchema: current.SchemaName) + { + ParentDepth = GetOperationStepDepth(current, ancestorStep.Id), + Conditions = entry.Conditions + }); + } + } + } + } + } + return requirements; } @@ -1991,6 +2136,215 @@ private static bool IsTypeNameSelection(ISelectionNode selection) return false; } + private bool TryFindAncestorStepForRequirement( + OperationDefinitionNode internalOperation, + ImmutableList steps, + SelectionSetIndexBuilder index, + uint targetSSId, + out OperationPlanStep ancestorStep, + out int ancestorStepIndex, + out uint ancestorSSId, + out ITypeDefinition ancestorType, + out List wrappingNodes) + { + ancestorStep = default!; + ancestorStepIndex = -1; + ancestorSSId = 0; + ancestorType = default!; + wrappingNodes = default!; + + // Walk the internal operation AST depth-first, tracking: + // - A stack of (SelectionSetNode, ISelectionNode?, ITypeDefinition) from root to current + // - The current type at each level + var path = new List<(SelectionSetNode SelectionSet, ISelectionNode? ConnectingNode, ITypeDefinition Type)>(); + var rootType = _schema.GetOperationType(internalOperation.Operation); + + if (!WalkSelectionSet( + internalOperation.SelectionSet, rootType, path, + _schema, steps, index, targetSSId, + out var foundStep, out var foundStepIndex, + out var foundSSId, out var foundType, out var foundWrapping)) + { + return false; + } + + ancestorStep = foundStep; + ancestorStepIndex = foundStepIndex; + ancestorSSId = foundSSId; + ancestorType = foundType; + wrappingNodes = foundWrapping; + return true; + + static bool WalkSelectionSet( + SelectionSetNode selectionSet, + ITypeDefinition currentType, + List<(SelectionSetNode SelectionSet, ISelectionNode? ConnectingNode, ITypeDefinition Type)> path, + FusionSchemaDefinition schema, + ImmutableList steps, + SelectionSetIndexBuilder index, + uint targetSSId, + out OperationPlanStep foundStep, + out int foundStepIndex, + out uint foundSSId, + out ITypeDefinition foundType, + out List foundWrapping) + { + foundStep = default!; + foundStepIndex = -1; + foundSSId = 0; + foundType = default!; + foundWrapping = default!; + + var ssId = index.GetId(selectionSet); + + if (ssId == targetSSId) + { + // Found the target. Trace back to find the deepest ancestor SS ID + // that exists in any step's SelectionSets. + for (var i = path.Count - 1; i >= 0; i--) + { + var entry = path[i]; + + // If a FieldNode is the connecting node, this is a different nesting level + if (entry.ConnectingNode is FieldNode) + { + continue; + } + + var candidateSSId = index.GetId(entry.SelectionSet); + + for (var s = 0; s < steps.Count; s++) + { + if (steps[s] is OperationPlanStep step + && step.SelectionSets.Contains(candidateSSId) + && !string.IsNullOrEmpty(step.SchemaName)) + { + foundStep = step; + foundStepIndex = s; + foundSSId = candidateSSId; + foundType = entry.Type; + + // Collect intermediate InlineFragmentNodes between ancestor and target + foundWrapping = []; + for (var j = i + 1; j < path.Count; j++) + { + if (path[j].ConnectingNode is InlineFragmentNode fragment) + { + foundWrapping.Add(fragment); + } + } + + return true; + } + } + } + + return false; + } + + foreach (var selection in selectionSet.Selections) + { + switch (selection) + { + case FieldNode fieldNode when fieldNode.SelectionSet is not null: + if (currentType is FusionComplexTypeDefinition complexType + && complexType.Fields.TryGetField( + fieldNode.Name.Value, + allowInaccessibleFields: true, + out var field)) + { + var fieldType = field.Type.NamedType(); + path.Add((selectionSet, fieldNode, currentType)); + if (WalkSelectionSet( + fieldNode.SelectionSet, fieldType, path, + schema, steps, index, targetSSId, + out foundStep, out foundStepIndex, + out foundSSId, out foundType, out foundWrapping)) + { + return true; + } + path.RemoveAt(path.Count - 1); + } + break; + + case InlineFragmentNode inlineFragment: + var fragmentType = inlineFragment.TypeCondition is not null + ? schema.Types[inlineFragment.TypeCondition.Name.Value] + : currentType; + + path.Add((selectionSet, inlineFragment, currentType)); + if (WalkSelectionSet( + inlineFragment.SelectionSet, fragmentType, path, + schema, steps, index, targetSSId, + out foundStep, out foundStepIndex, + out foundSSId, out foundType, out foundWrapping)) + { + return true; + } + path.RemoveAt(path.Count - 1); + break; + } + } + + return false; + } + } + + private static SelectionSetNode BuildWrappedSelections( + List wrappingNodes, + SelectionSetNode requirements) + { + var current = requirements; + + for (var i = wrappingNodes.Count - 1; i >= 0; i--) + { + var wrapper = wrappingNodes[i]; + current = new SelectionSetNode( + [ + new InlineFragmentNode( + null, + wrapper.TypeCondition, + wrapper.Directives, + current) + ]); + } + + return current; + } + + private static void EnsureAllSelectionSetsRegistered( + SelectionSetNode selectionSet, + SelectionSetIndexBuilder index) + { + var stack = new List(); + stack.Add(selectionSet); + + while (stack.Count > 0) + { + var current = stack[stack.Count - 1]; + stack.RemoveAt(stack.Count - 1); + + if (!index.IsRegistered(current)) + { + index.Register(current); + } + + foreach (var selection in current.Selections) + { + switch (selection) + { + case FieldNode { SelectionSet: { } fieldSelectionSet }: + stack.Add(fieldSelectionSet); + break; + + case InlineFragmentNode { SelectionSet: { } fragmentSelectionSet }: + stack.Add(fragmentSelectionSet); + break; + } + } + } + } + private readonly record struct PlanResult( OperationDefinitionNode InternalOperationDefinition, ImmutableList Steps, diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml new file mode 100644 index 00000000000..12c3ede2f86 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml @@ -0,0 +1,432 @@ +title: Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections +request: + document: | + { + abstractTypes { + upvotes + ... on Discussion { + score + } + ... on Author { + someId: id + } + } + b: abstractTypes { + ... on Author { + upvotes + } + } + } +response: + body: | + { + "data": { + "abstractTypes": [ + { + "upvotes": 123, + "score": 123 + }, + { + "upvotes": 123, + "someId": "QXV0aG9yOjI=" + }, + { + "upvotes": 123, + "score": 123 + } + ], + "b": [ + {}, + { + "upvotes": 123 + }, + {} + ] + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_2bff8025_1 { + abstractTypes { + __typename + someId: id + id + } + b: abstractTypes { + __typename + ... on Author { + id + } + } + } + response: + results: + - | + { + "data": { + "abstractTypes": [ + { + "__typename": "Discussion", + "someId": "RGlzY3Vzc2lvbjox", + "id": "RGlzY3Vzc2lvbjox" + }, + { + "__typename": "Author", + "someId": "QXV0aG9yOjI=", + "id": "QXV0aG9yOjI=" + }, + { + "__typename": "Discussion", + "someId": "RGlzY3Vzc2lvbjoz", + "id": "RGlzY3Vzc2lvbjoz" + } + ], + "b": [ + { + "__typename": "Discussion" + }, + { + "__typename": "Author", + "id": "QXV0aG9yOjU=" + }, + { + "__typename": "Discussion" + } + ] + } + } + - name: B + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + kind: OperationBatch + items: + - document: | + query Op_2bff8025_3( + $__fusion_2_id: ID! + ) { + discussionById(id: $__fusion_2_id) { + __typename + upvotes + } + } + variables: | + [ + { + "__fusion_2_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_2_id": "RGlzY3Vzc2lvbjoz" + } + ] + - document: | + query Op_2bff8025_5( + $__fusion_4_id: ID! + ) { + discussionById(id: $__fusion_4_id) { + score + } + } + variables: | + [ + { + "__fusion_4_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_4_id": "RGlzY3Vzc2lvbjoz" + } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - name: C + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + kind: OperationBatch + items: + - document: | + query Op_2bff8025_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + upvotes + } + } + variables: | + { + "__fusion_1_id": "QXV0aG9yOjU=" + } + - document: | + query Op_2bff8025_4( + $__fusion_3_id: ID! + ) { + authorById(id: $__fusion_3_id) { + __typename + upvotes + } + } + variables: | + { + "__fusion_3_id": "QXV0aG9yOjI=" + } + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "authorById": { + "upvotes": 123 + } + } + } + - | + { + "data": { + "authorById": { + "__typename": "Author", + "upvotes": 123 + } + } + } +operationPlan: + operation: + - document: | + { + abstractTypes { + __typename @fusion__requirement + upvotes + id @fusion__requirement + ... on Discussion { + score + id @fusion__requirement + } + ... on Author { + someId: id + } + } + b: abstractTypes { + __typename @fusion__requirement + ... on Author { + upvotes + id @fusion__requirement + } + } + } + hash: 2bff80251ec40ed7284da396d31caf09 + searchSpace: 1 + expandedNodes: 2 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_2bff8025_1 { + abstractTypes { + __typename + someId: id + id + } + b: abstractTypes { + __typename + ... on Author { + id + } + } + } + - id: 2 + type: Operation + schema: C + operation: | + query Op_2bff8025_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + upvotes + } + } + source: $.authorById + target: $.b + batchingGroupId: 2 + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 4 + type: Operation + schema: C + operation: | + query Op_2bff8025_4( + $__fusion_3_id: ID! + ) { + authorById(id: $__fusion_3_id) { + __typename + upvotes + } + } + source: $.authorById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_3_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: Operation + schema: B + operation: | + query Op_2bff8025_3( + $__fusion_2_id: ID! + ) { + discussionById(id: $__fusion_2_id) { + __typename + upvotes + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 3 + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 5 + type: Operation + schema: B + operation: | + query Op_2bff8025_5( + $__fusion_4_id: ID! + ) { + discussionById(id: $__fusion_4_id) { + score + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 3 + requirements: + - name: __fusion_4_id + selectionMap: >- + id + dependencies: + - id: 1 diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml new file mode 100644 index 00000000000..820c14b0075 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml @@ -0,0 +1,385 @@ +title: Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas +request: + document: | + { + abstractTypes { + upvotes + ... on Discussion { + score + } + ... on Author { + score + } + } + } +response: + body: | + { + "data": { + "abstractTypes": [ + { + "upvotes": 123, + "score": 123 + }, + { + "upvotes": 123, + "score": 123 + }, + { + "upvotes": 123, + "score": 123 + } + ] + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_13354b96_1 { + abstractTypes { + __typename + id + } + } + response: + results: + - | + { + "data": { + "abstractTypes": [ + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjox" + }, + { + "__typename": "Author", + "id": "QXV0aG9yOjI=" + }, + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjoz" + } + ] + } + } + - name: B + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + kind: OperationBatch + items: + - document: | + query Op_13354b96_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + } + } + variables: | + [ + { + "__fusion_1_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_1_id": "RGlzY3Vzc2lvbjoz" + } + ] + - document: | + query Op_13354b96_5( + $__fusion_4_id: ID! + ) { + discussionById(id: $__fusion_4_id) { + score + } + } + variables: | + [ + { + "__fusion_4_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_4_id": "RGlzY3Vzc2lvbjoz" + } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - name: C + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + kind: OperationBatch + items: + - document: | + query Op_13354b96_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjI=" + } + - document: | + query Op_13354b96_4( + $__fusion_3_id: ID! + ) { + authorById(id: $__fusion_3_id) { + score + } + } + variables: | + { + "__fusion_3_id": "QXV0aG9yOjI=" + } + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "authorById": { + "__typename": "Author", + "upvotes": 123 + } + } + } + - | + { + "data": { + "authorById": { + "score": 123 + } + } + } +operationPlan: + operation: + - document: | + { + abstractTypes { + __typename @fusion__requirement + upvotes + id @fusion__requirement + ... on Discussion { + score + id @fusion__requirement + } + ... on Author { + score + id @fusion__requirement + } + } + } + hash: 13354b96bf8e9c5bb2d7ab9d87a0cb2b + searchSpace: 1 + expandedNodes: 2 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_13354b96_1 { + abstractTypes { + __typename + id + } + } + - id: 2 + type: Operation + schema: B + operation: | + query Op_13354b96_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 5 + type: Operation + schema: B + operation: | + query Op_13354b96_5( + $__fusion_4_id: ID! + ) { + discussionById(id: $__fusion_4_id) { + score + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_4_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: Operation + schema: C + operation: | + query Op_13354b96_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + } + } + source: $.authorById + target: $.abstractTypes + batchingGroupId: 3 + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 4 + type: Operation + schema: C + operation: | + query Op_13354b96_4( + $__fusion_3_id: ID! + ) { + authorById(id: $__fusion_3_id) { + score + } + } + source: $.authorById + target: $.abstractTypes + batchingGroupId: 3 + requirements: + - name: __fusion_3_id + selectionMap: >- + id + dependencies: + - id: 1 diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml new file mode 100644 index 00000000000..bb382caad61 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml @@ -0,0 +1,330 @@ +title: Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match +request: + document: | + { + abstractTypes { + upvotes + ... on Discussion { + score + } + } + } +response: + body: | + { + "data": { + "abstractTypes": [ + { + "upvotes": 123, + "score": 123 + }, + { + "upvotes": 123 + }, + { + "upvotes": 123, + "score": 123 + } + ] + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_3902e3a1_1 { + abstractTypes { + __typename + id + } + } + response: + results: + - | + { + "data": { + "abstractTypes": [ + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjox" + }, + { + "__typename": "Author", + "id": "QXV0aG9yOjI=" + }, + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjoz" + } + ] + } + } + - name: B + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + kind: OperationBatch + items: + - document: | + query Op_3902e3a1_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + } + } + variables: | + [ + { + "__fusion_1_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_1_id": "RGlzY3Vzc2lvbjoz" + } + ] + - document: | + query Op_3902e3a1_4( + $__fusion_3_id: ID! + ) { + discussionById(id: $__fusion_3_id) { + score + } + } + variables: | + [ + { + "__fusion_3_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_3_id": "RGlzY3Vzc2lvbjoz" + } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - name: C + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_3902e3a1_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjI=" + } + response: + results: + - | + { + "data": { + "authorById": { + "__typename": "Author", + "upvotes": 123 + } + } + } +operationPlan: + operation: + - document: | + { + abstractTypes { + __typename @fusion__requirement + upvotes + id @fusion__requirement + ... on Discussion { + score + id @fusion__requirement + } + } + } + hash: 3902e3a186739fe80e8ac3851d657af5 + searchSpace: 1 + expandedNodes: 2 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_3902e3a1_1 { + abstractTypes { + __typename + id + } + } + - id: 2 + type: Operation + schema: B + operation: | + query Op_3902e3a1_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 4 + type: Operation + schema: B + operation: | + query Op_3902e3a1_4( + $__fusion_3_id: ID! + ) { + discussionById(id: $__fusion_3_id) { + score + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_3_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: Operation + schema: C + operation: | + query Op_3902e3a1_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + } + } + source: $.authorById + target: $.abstractTypes + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 1 diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml new file mode 100644 index 00000000000..86ba27f6948 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml @@ -0,0 +1,390 @@ +title: Interface_Field_With_Type_Refinement_Under_Skip +request: + document: | + query( + $skip: Boolean! + ) { + abstractTypes { + upvotes + ... on Discussion { + score + } + ... @skip(if: $skip) { + ... on Author { + score + } + } + } + } + variables: | + { + "skip": true + } +response: + body: | + { + "data": { + "abstractTypes": [ + { + "upvotes": 123, + "score": 123 + }, + { + "upvotes": 123 + }, + { + "upvotes": 123, + "score": 123 + } + ] + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_c37553a4_1( + $skip: Boolean! + ) { + abstractTypes { + __typename + ... on Author @skip(if: $skip) { + id + } + id + } + } + variables: | + { + "skip": true + } + response: + results: + - | + { + "data": { + "abstractTypes": [ + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjox" + }, + { + "__typename": "Author", + "id": "QXV0aG9yOjI=" + }, + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjoz" + } + ] + } + } + - name: B + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + kind: OperationBatch + items: + - document: | + query Op_c37553a4_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + } + } + variables: | + [ + { + "__fusion_1_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_1_id": "RGlzY3Vzc2lvbjoz" + } + ] + - document: | + query Op_c37553a4_5( + $__fusion_4_id: ID! + ) { + discussionById(id: $__fusion_4_id) { + score + } + } + variables: | + [ + { + "__fusion_4_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_4_id": "RGlzY3Vzc2lvbjoz" + } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "__typename": "Discussion", + "upvotes": 123 + } + } + } + - name: C + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + document: | + query Op_c37553a4_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjI=" + } + response: + results: + - | + { + "data": { + "authorById": { + "__typename": "Author", + "upvotes": 123 + } + } + } +operationPlan: + operation: + - document: | + query( + $skip: Boolean! + ) { + abstractTypes { + __typename @fusion__requirement + upvotes + id @fusion__requirement + ... on Discussion { + score + id @fusion__requirement + } + ... on Author @skip(if: $skip) { + score + id @fusion__requirement + } + } + } + hash: c37553a43ab326faf465e4b233a62261 + searchSpace: 1 + expandedNodes: 2 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_c37553a4_1( + $skip: Boolean! + ) { + abstractTypes { + __typename + ... on Author @skip(if: $skip) { + id + } + id + } + } + forwardedVariables: + - skip + - id: 2 + type: Operation + schema: B + operation: | + query Op_c37553a4_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 5 + type: Operation + schema: B + operation: | + query Op_c37553a4_5( + $__fusion_4_id: ID! + ) { + discussionById(id: $__fusion_4_id) { + score + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_4_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: Operation + schema: C + operation: | + query Op_c37553a4_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + } + } + source: $.authorById + target: $.abstractTypes + batchingGroupId: 3 + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 4 + type: Operation + schema: C + operation: | + query Op_c37553a4_4( + $__fusion_3_id: ID! + ) { + authorById(id: $__fusion_3_id) { + score + } + } + source: $.authorById + target: $.abstractTypes + batchingGroupId: 3 + requirements: + - name: __fusion_3_id + selectionMap: >- + id + conditions: + - variable: $skip + passingValue: false + dependencies: + - id: 1 From f414cf9c7da8c630573b362f0b5aa3a3e36ad0ca Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:20:24 +0200 Subject: [PATCH 10/16] wip --- .../Planning/OperationPlanner.cs | 328 ++++++++---------- 1 file changed, 146 insertions(+), 182 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs index 0e97db36f4a..dffa952fc91 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -887,63 +887,37 @@ private PlanNode InlineLookupRequirements( // correct wrapping structure (preserving inline fragments with directives). if (selectionSet is not null && TryFindAncestorStepForRequirement( - internalOperation, steps, index, workItemSelectionSet.Id, - out var ancestorStep, out var ancestorStepIndex, - out var ancestorSSId, out var ancestorType, out var wrappingNodes)) - { - var schemaName = ancestorStep.SchemaName!; - if (processed.Add(schemaName) && !lookup.SchemaName.Equals(schemaName)) + internalOperation, + steps, + index, + workItemSelectionSet.Id) is { } ancestorMatch + && processed.Add(ancestorMatch.Step.SchemaName!) + && !lookup.SchemaName.Equals(ancestorMatch.Step.SchemaName)) + { + if (TryInlineIntoAncestorStep( + ancestorMatch, + selectionSet, + workItemSelectionSet.Path, + lookupStepId, + index, + ref steps, + out var unresolvable)) { - var wrappedSelections = BuildWrappedSelections(wrappingNodes, selectionSet); - - var input = new SelectionSetPartitionerInput - { - SchemaName = schemaName, - SelectionSet = new SelectionSet( - ancestorSSId, - wrappedSelections, - ancestorType, - workItemSelectionSet.Path), - SelectionSetIndex = index - }; - - var (resolvable, unresolvable, _, _) = _partitioner.Partition(input); + selectionSet = null; - if (resolvable is { Selections.Count: > 0 }) + if (!unresolvable.IsEmpty) { - var operation = InlineSelections( - ancestorStep.Definition, - index, - ancestorType, - ancestorSSId, - resolvable); - - EnsureAllSelectionSetsRegistered(operation.SelectionSet, index); - - var updatedStep = ancestorStep with - { - Definition = operation, - SelectionSets = SelectionSetIndexer.CreateIdSet(operation.SelectionSet, index), - Dependents = ancestorStep.Dependents.Add(lookupStepId) - }; - - steps = steps.SetItem(ancestorStepIndex, updatedStep); - selectionSet = null; - - if (!unresolvable.IsEmpty) + var top = unresolvable.Peek(); + if (top.SelectionSet.Id == ancestorMatch.SelectionSetId) { - var top = unresolvable.Peek(); - if (top.SelectionSet.Id == ancestorSSId) - { - unresolvable = unresolvable.Pop(out top); - selectionSet = top.SelectionSet.Node; - } - - backlog = backlog.PushUnresolvable( - unresolvable, - current.SchemaName, - GetOperationStepDepth(current, ancestorStep.Id)); + unresolvable = unresolvable.Pop(out top); + selectionSet = top.SelectionSet.Node; } + + backlog = backlog.PushUnresolvable( + unresolvable, + current.SchemaName, + GetOperationStepDepth(current, ancestorMatch.Step.Id)); } } } @@ -1810,75 +1784,40 @@ private static List GetLookupArguments(Lookup lookup, string requi // correct wrapping structure (preserving inline fragments with directives). if (requirements is not null && TryFindAncestorStepForRequirement( - current.InternalOperationDefinition, steps, index, workItem.Selection.SelectionSetId, - out var ancestorStep, out var ancestorStepIndex, - out var ancestorSSId, out var ancestorType, out var wrappingNodes)) + current.InternalOperationDefinition, + steps, + index, + workItem.Selection.SelectionSetId) is { } ancestorMatch + && currentStep.Id != ancestorMatch.Step.Id + && !ancestorMatch.Step.DependsOn(currentStep, steps)) { - if (currentStep.Id != ancestorStep.Id - && !ancestorStep.DependsOn(currentStep, steps)) + if (TryInlineIntoAncestorStep( + ancestorMatch, requirements, workItem.Selection.Path, + workItem.StepId, index, ref steps, out var unresolvable, + resolvableRegistrationId: workItem.Selection.SelectionSetId)) { - var wrappedSelections = BuildWrappedSelections(wrappingNodes, requirements); - - var input = new SelectionSetPartitionerInput - { - SchemaName = ancestorStep.SchemaName!, - SelectionSet = new SelectionSet( - ancestorSSId, - wrappedSelections, - ancestorType, - workItem.Selection.Path), - SelectionSetIndex = index - }; - - var (resolvable, unresolvable, _, _) = _partitioner.Partition(input); + requirements = null; - if (resolvable is { Selections.Count: > 0 }) + if (!unresolvable.IsEmpty) { - if (resolvable != requirements) + var top = unresolvable.Peek(); + if (top.SelectionSet.Id == workItem.Selection.SelectionSetId) { - index.Register(workItem.Selection.SelectionSetId, resolvable); + unresolvable = unresolvable.Pop(out top); + requirements = top.SelectionSet.Node; } - var operation = InlineSelections( - ancestorStep.Definition, - index, - ancestorType, - ancestorSSId, - resolvable); - - EnsureAllSelectionSetsRegistered(operation.SelectionSet, index); - - var updatedStep = ancestorStep with + foreach (var entry in unresolvable.Reverse()) { - Definition = operation, - SelectionSets = SelectionSetIndexer.CreateIdSet(operation.SelectionSet, index), - Dependents = ancestorStep.Dependents.Add(workItem.StepId) - }; - - steps = steps.SetItem(ancestorStepIndex, updatedStep); - requirements = null; - - if (!unresolvable.IsEmpty) - { - var top = unresolvable.Peek(); - if (top.SelectionSet.Id == workItem.Selection.SelectionSetId) - { - unresolvable = unresolvable.Pop(out top); - requirements = top.SelectionSet.Node; - } - - foreach (var entry in unresolvable.Reverse()) - { - backlog = backlog.Push( - new OperationWorkItem( - OperationWorkItemKind.Lookup, - entry.SelectionSet, - FromSchema: current.SchemaName) - { - ParentDepth = GetOperationStepDepth(current, ancestorStep.Id), - Conditions = entry.Conditions - }); - } + backlog = backlog.Push( + new OperationWorkItem( + OperationWorkItemKind.Lookup, + entry.SelectionSet, + FromSchema: current.SchemaName) + { + ParentDepth = GetOperationStepDepth(current, ancestorMatch.Step.Id), + Conditions = entry.Conditions + }); } } } @@ -2136,68 +2075,34 @@ private static bool IsTypeNameSelection(ISelectionNode selection) return false; } - private bool TryFindAncestorStepForRequirement( + private AncestorStepMatch? TryFindAncestorStepForRequirement( OperationDefinitionNode internalOperation, ImmutableList steps, SelectionSetIndexBuilder index, - uint targetSSId, - out OperationPlanStep ancestorStep, - out int ancestorStepIndex, - out uint ancestorSSId, - out ITypeDefinition ancestorType, - out List wrappingNodes) + uint targetSelectionSetId) { - ancestorStep = default!; - ancestorStepIndex = -1; - ancestorSSId = 0; - ancestorType = default!; - wrappingNodes = default!; - // Walk the internal operation AST depth-first, tracking: // - A stack of (SelectionSetNode, ISelectionNode?, ITypeDefinition) from root to current // - The current type at each level var path = new List<(SelectionSetNode SelectionSet, ISelectionNode? ConnectingNode, ITypeDefinition Type)>(); var rootType = _schema.GetOperationType(internalOperation.Operation); - if (!WalkSelectionSet( + return WalkSelectionSet( internalOperation.SelectionSet, rootType, path, - _schema, steps, index, targetSSId, - out var foundStep, out var foundStepIndex, - out var foundSSId, out var foundType, out var foundWrapping)) - { - return false; - } - - ancestorStep = foundStep; - ancestorStepIndex = foundStepIndex; - ancestorSSId = foundSSId; - ancestorType = foundType; - wrappingNodes = foundWrapping; - return true; + _schema, steps, index, targetSelectionSetId); - static bool WalkSelectionSet( + static AncestorStepMatch? WalkSelectionSet( SelectionSetNode selectionSet, ITypeDefinition currentType, List<(SelectionSetNode SelectionSet, ISelectionNode? ConnectingNode, ITypeDefinition Type)> path, FusionSchemaDefinition schema, ImmutableList steps, SelectionSetIndexBuilder index, - uint targetSSId, - out OperationPlanStep foundStep, - out int foundStepIndex, - out uint foundSSId, - out ITypeDefinition foundType, - out List foundWrapping) + uint targetSelectionSetId) { - foundStep = default!; - foundStepIndex = -1; - foundSSId = 0; - foundType = default!; - foundWrapping = default!; - - var ssId = index.GetId(selectionSet); + var selectionSetId = index.GetId(selectionSet); - if (ssId == targetSSId) + if (selectionSetId == targetSelectionSetId) { // Found the target. Trace back to find the deepest ancestor SS ID // that exists in any step's SelectionSets. @@ -2211,42 +2116,37 @@ static bool WalkSelectionSet( continue; } - var candidateSSId = index.GetId(entry.SelectionSet); + var candidateSelectionSetId = index.GetId(entry.SelectionSet); for (var s = 0; s < steps.Count; s++) { if (steps[s] is OperationPlanStep step - && step.SelectionSets.Contains(candidateSSId) + && step.SelectionSets.Contains(candidateSelectionSetId) && !string.IsNullOrEmpty(step.SchemaName)) { - foundStep = step; - foundStepIndex = s; - foundSSId = candidateSSId; - foundType = entry.Type; - // Collect intermediate InlineFragmentNodes between ancestor and target - foundWrapping = []; + var ancestorFragments = new List(); for (var j = i + 1; j < path.Count; j++) { if (path[j].ConnectingNode is InlineFragmentNode fragment) { - foundWrapping.Add(fragment); + ancestorFragments.Add(fragment); } } - return true; + return new AncestorStepMatch(step, s, candidateSelectionSetId, entry.Type, ancestorFragments); } } } - return false; + return null; } foreach (var selection in selectionSet.Selections) { switch (selection) { - case FieldNode fieldNode when fieldNode.SelectionSet is not null: + case FieldNode { SelectionSet: not null } fieldNode: if (currentType is FusionComplexTypeDefinition complexType && complexType.Fields.TryGetField( fieldNode.Name.Value, @@ -2255,13 +2155,12 @@ static bool WalkSelectionSet( { var fieldType = field.Type.NamedType(); path.Add((selectionSet, fieldNode, currentType)); - if (WalkSelectionSet( + var result = WalkSelectionSet( fieldNode.SelectionSet, fieldType, path, - schema, steps, index, targetSSId, - out foundStep, out foundStepIndex, - out foundSSId, out foundType, out foundWrapping)) + schema, steps, index, targetSelectionSetId); + if (result is not null) { - return true; + return result; } path.RemoveAt(path.Count - 1); } @@ -2273,32 +2172,90 @@ static bool WalkSelectionSet( : currentType; path.Add((selectionSet, inlineFragment, currentType)); - if (WalkSelectionSet( + var fragmentResult = WalkSelectionSet( inlineFragment.SelectionSet, fragmentType, path, - schema, steps, index, targetSSId, - out foundStep, out foundStepIndex, - out foundSSId, out foundType, out foundWrapping)) + schema, steps, index, targetSelectionSetId); + if (fragmentResult is not null) { - return true; + return fragmentResult; } path.RemoveAt(path.Count - 1); break; } } + return null; + } + } + + private bool TryInlineIntoAncestorStep( + AncestorStepMatch match, + SelectionSetNode requirements, + SelectionPath path, + int dependentStepId, + SelectionSetIndexBuilder index, + ref ImmutableList steps, + out ImmutableStack unresolvable, + uint? resolvableRegistrationId = null) + { + unresolvable = ImmutableStack.Empty; + + var wrappedSelections = WrapSelectionsInFragments(match.AncestorFragments, requirements); + + var input = new SelectionSetPartitionerInput + { + SchemaName = match.Step.SchemaName!, + SelectionSet = new SelectionSet( + match.SelectionSetId, + wrappedSelections, + match.Type, + path), + SelectionSetIndex = index + }; + + var (resolvable, partitionUnresolvable, _, _) = _partitioner.Partition(input); + + if (resolvable is not { Selections.Count: > 0 }) + { return false; } + + if (resolvableRegistrationId is { } registrationId && resolvable != requirements) + { + index.Register(registrationId, resolvable); + } + + var operation = InlineSelections( + match.Step.Definition, + index, + match.Type, + match.SelectionSetId, + resolvable); + + EnsureAllSelectionSetsRegistered(operation.SelectionSet, index); + + var updatedStep = match.Step with + { + Definition = operation, + SelectionSets = SelectionSetIndexer.CreateIdSet(operation.SelectionSet, index), + Dependents = match.Step.Dependents.Add(dependentStepId) + }; + + steps = steps.SetItem(match.StepIndex, updatedStep); + unresolvable = partitionUnresolvable; + + return true; } - private static SelectionSetNode BuildWrappedSelections( - List wrappingNodes, + private static SelectionSetNode WrapSelectionsInFragments( + List ancestorFragments, SelectionSetNode requirements) { var current = requirements; - for (var i = wrappingNodes.Count - 1; i >= 0; i--) + for (var i = ancestorFragments.Count - 1; i >= 0; i--) { - var wrapper = wrappingNodes[i]; + var wrapper = ancestorFragments[i]; current = new SelectionSetNode( [ new InlineFragmentNode( @@ -2321,7 +2278,7 @@ private static void EnsureAllSelectionSetsRegistered( while (stack.Count > 0) { - var current = stack[stack.Count - 1]; + var current = stack[^1]; stack.RemoveAt(stack.Count - 1); if (!index.IsRegistered(current)) @@ -2345,6 +2302,13 @@ private static void EnsureAllSelectionSetsRegistered( } } + private readonly record struct AncestorStepMatch( + OperationPlanStep Step, + int StepIndex, + uint SelectionSetId, + ITypeDefinition Type, + List AncestorFragments); + private readonly record struct PlanResult( OperationDefinitionNode InternalOperationDefinition, ImmutableList Steps, From 29f7225002f56f2d362d1aa181b4dc75cf89ba25 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:48:05 +0200 Subject: [PATCH 11/16] add more nesting to conditional test --- .../ConditionalTests.cs | 12 +-- ...Field_With_Type_Refinement_Under_Skip.yaml | 75 ++++++++++++------- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs index 9646e45feaf..adad181a882 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs @@ -2227,21 +2227,23 @@ interface Votable @key(fields: "id") { var request = new OperationRequest( """ - query($skip: Boolean!) { + query($skip1: Boolean!, $skip2: Boolean!) { abstractTypes { upvotes ... on Discussion { score } - ... @skip(if: $skip) { - ... on Author { - score + ... @skip(if: $skip1) { + ... @skip(if: $skip2) { + ... on Author { + score + } } } } } """, - variables: new Dictionary { ["skip"] = true }); + variables: new Dictionary { ["skip1"] = true, ["skip2"] = true }); using var result = await client.PostAsync( request, diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml index 86ba27f6948..6bb3d6bcad1 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml @@ -2,23 +2,27 @@ title: Interface_Field_With_Type_Refinement_Under_Skip request: document: | query( - $skip: Boolean! + $skip1: Boolean! + $skip2: Boolean! ) { abstractTypes { upvotes ... on Discussion { score } - ... @skip(if: $skip) { - ... on Author { - score + ... @skip(if: $skip1) { + ... @skip(if: $skip2) { + ... on Author { + score + } } } } } variables: | { - "skip": true + "skip1": true, + "skip2": true } response: body: | @@ -70,20 +74,25 @@ sourceSchemas: - request: accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 document: | - query Op_c37553a4_1( - $skip: Boolean! + query Op_2c04e967_1( + $skip1: Boolean! + $skip2: Boolean! ) { abstractTypes { __typename - ... on Author @skip(if: $skip) { - id + ... @skip(if: $skip1) { + __typename + ... on Author @skip(if: $skip2) { + id + } } id } } variables: | { - "skip": true + "skip1": true, + "skip2": true } response: results: @@ -138,7 +147,7 @@ sourceSchemas: kind: OperationBatch items: - document: | - query Op_c37553a4_2( + query Op_2c04e967_2( $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { @@ -156,7 +165,7 @@ sourceSchemas: } ] - document: | - query Op_c37553a4_5( + query Op_2c04e967_5( $__fusion_4_id: ID! ) { discussionById(id: $__fusion_4_id) { @@ -239,7 +248,7 @@ sourceSchemas: - request: accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 document: | - query Op_c37553a4_3( + query Op_2c04e967_3( $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { @@ -266,7 +275,8 @@ operationPlan: operation: - document: | query( - $skip: Boolean! + $skip1: Boolean! + $skip2: Boolean! ) { abstractTypes { __typename @fusion__requirement @@ -276,13 +286,15 @@ operationPlan: score id @fusion__requirement } - ... on Author @skip(if: $skip) { - score - id @fusion__requirement + ... @skip(if: $skip1) { + ... on Author @skip(if: $skip2) { + score + id @fusion__requirement + } } } } - hash: c37553a43ab326faf465e4b233a62261 + hash: 2c04e9678504894af47de4bcb5c4f3b1 searchSpace: 1 expandedNodes: 2 nodes: @@ -290,24 +302,29 @@ operationPlan: type: Operation schema: A operation: | - query Op_c37553a4_1( - $skip: Boolean! + query Op_2c04e967_1( + $skip1: Boolean! + $skip2: Boolean! ) { abstractTypes { __typename - ... on Author @skip(if: $skip) { - id + ... @skip(if: $skip1) { + __typename + ... on Author @skip(if: $skip2) { + id + } } id } } forwardedVariables: - - skip + - skip1 + - skip2 - id: 2 type: Operation schema: B operation: | - query Op_c37553a4_2( + query Op_2c04e967_2( $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { @@ -328,7 +345,7 @@ operationPlan: type: Operation schema: B operation: | - query Op_c37553a4_5( + query Op_2c04e967_5( $__fusion_4_id: ID! ) { discussionById(id: $__fusion_4_id) { @@ -348,7 +365,7 @@ operationPlan: type: Operation schema: C operation: | - query Op_c37553a4_3( + query Op_2c04e967_3( $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { @@ -369,7 +386,7 @@ operationPlan: type: Operation schema: C operation: | - query Op_c37553a4_4( + query Op_2c04e967_4( $__fusion_3_id: ID! ) { authorById(id: $__fusion_3_id) { @@ -384,7 +401,9 @@ operationPlan: selectionMap: >- id conditions: - - variable: $skip + - variable: $skip1 + passingValue: false + - variable: $skip2 passingValue: false dependencies: - id: 1 From 84d7d8909d50645d9fe6cc8501588a826dba7764 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:34:49 +0200 Subject: [PATCH 12/16] Reduce number of nodes --- .../Partitioners/SelectionSetPartitioner.cs | 72 +++++++ ...nts_Aliased_With_Different_Selections.yaml | 103 +++------- ...ments_All_Fields_On_Dedicated_Schemas.yaml | 181 +++++------------- ...nements_Type_Refinements_Do_Not_Match.yaml | 103 +++------- ...Field_With_Type_Refinement_Under_Skip.yaml | 103 +++------- 5 files changed, 200 insertions(+), 362 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs index 00610ad8664..55b353a6861 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs @@ -152,6 +152,17 @@ public SelectionSetPartitionerResult Partition( if (unresolvableSelections is not null) { + // When we have unresolvable selections on an abstract type, check if inner + // recursive calls already pushed type-specific entries (from inline fragments) + // to the unresolvable stack. If so, merge them into this entry to avoid + // creating duplicate lookups for the same downstream schema. + if (isAbstractType) + { + var currentPath = context.BuildPath(); + var currentConditions = context.SnapshotConditions(); + MergeChildUnresolvableEntries(context, currentPath, currentConditions, unresolvableSelections); + } + if (isAbstractType && !unresolvableSelections.Any(IsTypeNameSelection)) { unresolvableSelections = [ @@ -258,6 +269,67 @@ static bool IsTypeNameSelection(ISelectionNode selection) } } + /// + /// Merges child unresolvable entries from inline fragments back into the parent's + /// unresolvable selections. This prevents duplicate lookups when both interface-level + /// fields and type-refinement fields target the same downstream schema. + /// + private static void MergeChildUnresolvableEntries( + Context context, + SelectionPath currentPath, + ExecutionNodeCondition[] currentConditions, + List unresolvableSelections) + { + if (context.Unresolvable.IsEmpty) + { + return; + } + + // Collect entries that are NOT children of the current path (to keep), + // and entries that ARE children (to merge). + var keep = ImmutableStack.Empty; + var merge = new List(); + + foreach (var entry in context.Unresolvable) + { + var entryPath = entry.SelectionSet.Path; + + // A child entry has exactly one more segment than the current path, + // and that extra segment is an InlineFragment. + if (entryPath.Length == currentPath.Length + 1 + && entryPath[entryPath.Length - 1].Kind == SelectionPathSegmentKind.InlineFragment + && currentPath.IsParentOfOrSame(entryPath) + && currentConditions.SequenceEqual(entry.Conditions)) + { + merge.Add(entry); + } + else + { + keep = keep.Push(entry); + } + } + + if (merge.Count == 0) + { + return; + } + + // Wrap each child's selections in an inline fragment and add to the parent. + foreach (var entry in merge) + { + var typeName = entry.SelectionSet.Path[entry.SelectionSet.Path.Length - 1].Name; + var inlineFragment = new InlineFragmentNode( + null, + new NamedTypeNode(typeName), + [], + entry.SelectionSet.Node); + unresolvableSelections.Add(inlineFragment); + } + + // Rebuild the stack without the merged entries. + context.Unresolvable = keep; + } + private (FieldNode?, FieldNode?) RewriteFieldNode( Context context, FusionComplexTypeDefinition complexType, diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml index 12c3ede2f86..36a6c469349 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml @@ -153,68 +153,37 @@ sourceSchemas: interactions: - request: accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 - kind: OperationBatch - items: - - document: | - query Op_2bff8025_3( - $__fusion_2_id: ID! - ) { - discussionById(id: $__fusion_2_id) { - __typename - upvotes - } - } - variables: | - [ - { - "__fusion_2_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_2_id": "RGlzY3Vzc2lvbjoz" - } - ] - - document: | - query Op_2bff8025_5( - $__fusion_4_id: ID! - ) { - discussionById(id: $__fusion_4_id) { - score - } - } - variables: | - [ - { - "__fusion_4_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_4_id": "RGlzY3Vzc2lvbjoz" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - - | - { - "data": { - "discussionById": { - "score": 123 - } + document: | + query Op_2bff8025_3( + $__fusion_2_id: ID! + ) { + discussionById(id: $__fusion_2_id) { + __typename + upvotes + ... on Discussion { + score } } - - | + } + variables: | + [ { - "data": { - "discussionById": { - "score": 123 - } - } + "__fusion_2_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_2_id": "RGlzY3Vzc2lvbjoz" } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: - | { "data": { "discussionById": { "__typename": "Discussion", - "upvotes": 123 + "upvotes": 123, + "score": 123 } } } @@ -223,7 +192,8 @@ sourceSchemas: "data": { "discussionById": { "__typename": "Discussion", - "upvotes": 123 + "upvotes": 123, + "score": 123 } } } @@ -313,7 +283,6 @@ operationPlan: id @fusion__requirement ... on Discussion { score - id @fusion__requirement } ... on Author { someId: id @@ -399,34 +368,16 @@ operationPlan: discussionById(id: $__fusion_2_id) { __typename upvotes + ... on Discussion { + score + } } } source: $.discussionById target: $.abstractTypes - batchingGroupId: 3 requirements: - name: __fusion_2_id selectionMap: >- id dependencies: - id: 1 - - id: 5 - type: Operation - schema: B - operation: | - query Op_2bff8025_5( - $__fusion_4_id: ID! - ) { - discussionById(id: $__fusion_4_id) { - score - } - } - source: $.discussionById - target: $.abstractTypes - batchingGroupId: 3 - requirements: - - name: __fusion_4_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml index 820c14b0075..4fac70a50dd 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml @@ -119,68 +119,37 @@ sourceSchemas: interactions: - request: accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 - kind: OperationBatch - items: - - document: | - query Op_13354b96_2( - $__fusion_1_id: ID! - ) { - discussionById(id: $__fusion_1_id) { - __typename - upvotes - } - } - variables: | - [ - { - "__fusion_1_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_1_id": "RGlzY3Vzc2lvbjoz" - } - ] - - document: | - query Op_13354b96_5( - $__fusion_4_id: ID! - ) { - discussionById(id: $__fusion_4_id) { - score - } - } - variables: | - [ - { - "__fusion_4_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_4_id": "RGlzY3Vzc2lvbjoz" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - - | - { - "data": { - "discussionById": { - "score": 123 - } + document: | + query Op_13354b96_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + ... on Discussion { + score } } - - | + } + variables: | + [ { - "data": { - "discussionById": { - "score": 123 - } - } + "__fusion_1_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_1_id": "RGlzY3Vzc2lvbjoz" } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: - | { "data": { "discussionById": { "__typename": "Discussion", - "upvotes": 123 + "upvotes": 123, + "score": 123 } } } @@ -189,7 +158,8 @@ sourceSchemas: "data": { "discussionById": { "__typename": "Discussion", - "upvotes": 123 + "upvotes": 123, + "score": 123 } } } @@ -221,50 +191,31 @@ sourceSchemas: } interactions: - request: - accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 - kind: OperationBatch - items: - - document: | - query Op_13354b96_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - __typename - upvotes - } - } - variables: | - { - "__fusion_2_id": "QXV0aG9yOjI=" - } - - document: | - query Op_13354b96_4( - $__fusion_3_id: ID! - ) { - authorById(id: $__fusion_3_id) { - score - } - } - variables: | - { - "__fusion_3_id": "QXV0aG9yOjI=" + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_13354b96_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + __typename + upvotes + ... on Author { + score } + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjI=" + } response: - contentType: application/jsonl; charset=utf-8 results: - | { "data": { "authorById": { "__typename": "Author", - "upvotes": 123 - } - } - } - - | - { - "data": { - "authorById": { + "upvotes": 123, "score": 123 } } @@ -279,11 +230,9 @@ operationPlan: id @fusion__requirement ... on Discussion { score - id @fusion__requirement } ... on Author { score - id @fusion__requirement } } } @@ -311,37 +260,19 @@ operationPlan: discussionById(id: $__fusion_1_id) { __typename upvotes + ... on Discussion { + score + } } } source: $.discussionById target: $.abstractTypes - batchingGroupId: 2 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 5 - type: Operation - schema: B - operation: | - query Op_13354b96_5( - $__fusion_4_id: ID! - ) { - discussionById(id: $__fusion_4_id) { - score - } - } - source: $.discussionById - target: $.abstractTypes - batchingGroupId: 2 - requirements: - - name: __fusion_4_id - selectionMap: >- - id - dependencies: - - id: 1 - id: 3 type: Operation schema: C @@ -352,34 +283,16 @@ operationPlan: authorById(id: $__fusion_2_id) { __typename upvotes + ... on Author { + score + } } } source: $.authorById target: $.abstractTypes - batchingGroupId: 3 requirements: - name: __fusion_2_id selectionMap: >- id dependencies: - id: 1 - - id: 4 - type: Operation - schema: C - operation: | - query Op_13354b96_4( - $__fusion_3_id: ID! - ) { - authorById(id: $__fusion_3_id) { - score - } - } - source: $.authorById - target: $.abstractTypes - batchingGroupId: 3 - requirements: - - name: __fusion_3_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml index bb382caad61..7d246864db1 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml @@ -114,68 +114,37 @@ sourceSchemas: interactions: - request: accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 - kind: OperationBatch - items: - - document: | - query Op_3902e3a1_2( - $__fusion_1_id: ID! - ) { - discussionById(id: $__fusion_1_id) { - __typename - upvotes - } - } - variables: | - [ - { - "__fusion_1_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_1_id": "RGlzY3Vzc2lvbjoz" - } - ] - - document: | - query Op_3902e3a1_4( - $__fusion_3_id: ID! - ) { - discussionById(id: $__fusion_3_id) { - score - } - } - variables: | - [ - { - "__fusion_3_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_3_id": "RGlzY3Vzc2lvbjoz" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - - | - { - "data": { - "discussionById": { - "score": 123 - } + document: | + query Op_3902e3a1_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + ... on Discussion { + score } } - - | + } + variables: | + [ { - "data": { - "discussionById": { - "score": 123 - } - } + "__fusion_1_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_1_id": "RGlzY3Vzc2lvbjoz" } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: - | { "data": { "discussionById": { "__typename": "Discussion", - "upvotes": 123 + "upvotes": 123, + "score": 123 } } } @@ -184,7 +153,8 @@ sourceSchemas: "data": { "discussionById": { "__typename": "Discussion", - "upvotes": 123 + "upvotes": 123, + "score": 123 } } } @@ -249,7 +219,6 @@ operationPlan: id @fusion__requirement ... on Discussion { score - id @fusion__requirement } } } @@ -277,37 +246,19 @@ operationPlan: discussionById(id: $__fusion_1_id) { __typename upvotes + ... on Discussion { + score + } } } source: $.discussionById target: $.abstractTypes - batchingGroupId: 2 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 4 - type: Operation - schema: B - operation: | - query Op_3902e3a1_4( - $__fusion_3_id: ID! - ) { - discussionById(id: $__fusion_3_id) { - score - } - } - source: $.discussionById - target: $.abstractTypes - batchingGroupId: 2 - requirements: - - name: __fusion_3_id - selectionMap: >- - id - dependencies: - - id: 1 - id: 3 type: Operation schema: C diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml index 6bb3d6bcad1..74f9fd77343 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml @@ -144,68 +144,37 @@ sourceSchemas: interactions: - request: accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 - kind: OperationBatch - items: - - document: | - query Op_2c04e967_2( - $__fusion_1_id: ID! - ) { - discussionById(id: $__fusion_1_id) { - __typename - upvotes - } - } - variables: | - [ - { - "__fusion_1_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_1_id": "RGlzY3Vzc2lvbjoz" - } - ] - - document: | - query Op_2c04e967_5( - $__fusion_4_id: ID! - ) { - discussionById(id: $__fusion_4_id) { - score - } - } - variables: | - [ - { - "__fusion_4_id": "RGlzY3Vzc2lvbjox" - }, - { - "__fusion_4_id": "RGlzY3Vzc2lvbjoz" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - - | - { - "data": { - "discussionById": { - "score": 123 - } + document: | + query Op_2c04e967_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + __typename + upvotes + ... on Discussion { + score } } - - | + } + variables: | + [ { - "data": { - "discussionById": { - "score": 123 - } - } + "__fusion_1_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_1_id": "RGlzY3Vzc2lvbjoz" } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: - | { "data": { "discussionById": { "__typename": "Discussion", - "upvotes": 123 + "upvotes": 123, + "score": 123 } } } @@ -214,7 +183,8 @@ sourceSchemas: "data": { "discussionById": { "__typename": "Discussion", - "upvotes": 123 + "upvotes": 123, + "score": 123 } } } @@ -284,7 +254,6 @@ operationPlan: id @fusion__requirement ... on Discussion { score - id @fusion__requirement } ... @skip(if: $skip1) { ... on Author @skip(if: $skip2) { @@ -330,37 +299,19 @@ operationPlan: discussionById(id: $__fusion_1_id) { __typename upvotes + ... on Discussion { + score + } } } source: $.discussionById target: $.abstractTypes - batchingGroupId: 2 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 5 - type: Operation - schema: B - operation: | - query Op_2c04e967_5( - $__fusion_4_id: ID! - ) { - discussionById(id: $__fusion_4_id) { - score - } - } - source: $.discussionById - target: $.abstractTypes - batchingGroupId: 2 - requirements: - - name: __fusion_4_id - selectionMap: >- - id - dependencies: - - id: 1 - id: 3 type: Operation schema: C From e9d49141688ce5020b59bf36a1f8ac2681f838df Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:42:57 +0200 Subject: [PATCH 13/16] Improvements --- .../Partitioners/SelectionSetPartitioner.cs | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs index 55b353a6861..99fa116ed78 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs @@ -156,7 +156,7 @@ public SelectionSetPartitionerResult Partition( // recursive calls already pushed type-specific entries (from inline fragments) // to the unresolvable stack. If so, merge them into this entry to avoid // creating duplicate lookups for the same downstream schema. - if (isAbstractType) + if (isAbstractType && !context.Unresolvable.IsEmpty) { var currentPath = context.BuildPath(); var currentConditions = context.SnapshotConditions(); @@ -280,15 +280,8 @@ private static void MergeChildUnresolvableEntries( ExecutionNodeCondition[] currentConditions, List unresolvableSelections) { - if (context.Unresolvable.IsEmpty) - { - return; - } - - // Collect entries that are NOT children of the current path (to keep), - // and entries that ARE children (to merge). var keep = ImmutableStack.Empty; - var merge = new List(); + var anyMerged = false; foreach (var entry in context.Unresolvable) { @@ -301,7 +294,13 @@ private static void MergeChildUnresolvableEntries( && currentPath.IsParentOfOrSame(entryPath) && currentConditions.SequenceEqual(entry.Conditions)) { - merge.Add(entry); + var typeName = entryPath[entryPath.Length - 1].Name; + unresolvableSelections.Add(new InlineFragmentNode( + null, + new NamedTypeNode(typeName), + [], + entry.SelectionSet.Node)); + anyMerged = true; } else { @@ -309,25 +308,10 @@ private static void MergeChildUnresolvableEntries( } } - if (merge.Count == 0) + if (anyMerged) { - return; + context.Unresolvable = keep; } - - // Wrap each child's selections in an inline fragment and add to the parent. - foreach (var entry in merge) - { - var typeName = entry.SelectionSet.Path[entry.SelectionSet.Path.Length - 1].Name; - var inlineFragment = new InlineFragmentNode( - null, - new NamedTypeNode(typeName), - [], - entry.SelectionSet.Node); - unresolvableSelections.Add(inlineFragment); - } - - // Rebuild the stack without the merged entries. - context.Unresolvable = keep; } private (FieldNode?, FieldNode?) RewriteFieldNode( From 18aa7bab2f8526e091fe8bafd5776feab8bf886d Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Wed, 8 Apr 2026 07:29:41 +0200 Subject: [PATCH 14/16] Cleanup plan further --- .../Partitioners/SelectionSetPartitioner.cs | 25 +++--- ...nts_Aliased_With_Different_Selections.yaml | 76 +++++-------------- ...ments_All_Fields_On_Dedicated_Schemas.yaml | 7 -- ...nements_Type_Refinements_Do_Not_Match.yaml | 7 -- ...ype_Refinements_With_Concrete_Lookups.yaml | 4 - ...ookups_And_Field_From_Specific_Source.yaml | 4 - ...ype_Refinements_With_Interface_Lookup.yaml | 2 - ...Lookup_And_Field_From_Specific_Source.yaml | 2 - ...Field_With_Type_Refinement_Under_Skip.yaml | 7 -- ...mer_Interface_With_Email_Is_Plannable.yaml | 2 - 10 files changed, 31 insertions(+), 105 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs index 99fa116ed78..6b567a43820 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs @@ -126,16 +126,18 @@ public SelectionSetPartitionerResult Partition( var fragmentConditions = ExtractDirectiveConditions(inlineFragmentNode.Directives); var savedCount = context.PushConditions(fragmentConditions); - var (resolvable, unresolvable) = - RewriteFragmentNode( - context, - type, - inlineFragmentNode, - providedSelectionSetNode); + { + var (resolvable, unresolvable) = + RewriteFragmentNode( + context, + type, + inlineFragmentNode, + providedSelectionSetNode); - context.PopConditions(savedCount); + CompleteSelection(inlineFragmentNode, resolvable, unresolvable, i); + } - CompleteSelection(inlineFragmentNode, resolvable, unresolvable, i); + context.PopConditions(savedCount); break; } } @@ -163,13 +165,6 @@ public SelectionSetPartitionerResult Partition( MergeChildUnresolvableEntries(context, currentPath, currentConditions, unresolvableSelections); } - if (isAbstractType && !unresolvableSelections.Any(IsTypeNameSelection)) - { - unresolvableSelections = [ - new FieldNode(IntrospectionFieldNames.TypeName), - ..unresolvableSelections]; - } - var unresolvableSelectionSet = new SelectionSetNode(unresolvableSelections); context.Register(selectionSetNode, unresolvableSelectionSet); diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml index 36a6c469349..871f8d3ac9c 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Aliased_With_Different_Selections.yaml @@ -158,7 +158,6 @@ sourceSchemas: $__fusion_2_id: ID! ) { discussionById(id: $__fusion_2_id) { - __typename upvotes ... on Discussion { score @@ -181,7 +180,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "upvotes": 123, "score": 123 } @@ -191,7 +189,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "upvotes": 123, "score": 123 } @@ -226,33 +223,23 @@ sourceSchemas: interactions: - request: accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 - kind: OperationBatch - items: - - document: | - query Op_2bff8025_2( - $__fusion_1_id: ID! - ) { - authorById(id: $__fusion_1_id) { - upvotes - } - } - variables: | - { - "__fusion_1_id": "QXV0aG9yOjU=" - } - - document: | - query Op_2bff8025_4( - $__fusion_3_id: ID! - ) { - authorById(id: $__fusion_3_id) { - __typename - upvotes - } - } - variables: | - { - "__fusion_3_id": "QXV0aG9yOjI=" - } + document: | + query Op_2bff8025_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + upvotes + } + } + variables: | + [ + { + "__fusion_1_id": "QXV0aG9yOjU=" + }, + { + "__fusion_1_id": "QXV0aG9yOjI=" + } + ] response: contentType: application/jsonl; charset=utf-8 results: @@ -268,7 +255,6 @@ sourceSchemas: { "data": { "authorById": { - "__typename": "Author", "upvotes": 123 } } @@ -318,7 +304,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: C operation: | query Op_2bff8025_2( @@ -329,7 +315,9 @@ operationPlan: } } source: $.authorById - target: $.b + targets: + - $.b + - $.abstractTypes batchingGroupId: 2 requirements: - name: __fusion_1_id @@ -337,27 +325,6 @@ operationPlan: id dependencies: - id: 1 - - id: 4 - type: Operation - schema: C - operation: | - query Op_2bff8025_4( - $__fusion_3_id: ID! - ) { - authorById(id: $__fusion_3_id) { - __typename - upvotes - } - } - source: $.authorById - target: $.abstractTypes - batchingGroupId: 2 - requirements: - - name: __fusion_3_id - selectionMap: >- - id - dependencies: - - id: 1 - id: 3 type: Operation schema: B @@ -366,7 +333,6 @@ operationPlan: $__fusion_2_id: ID! ) { discussionById(id: $__fusion_2_id) { - __typename upvotes ... on Discussion { score diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml index 4fac70a50dd..e2bbfdc6d8d 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_All_Fields_On_Dedicated_Schemas.yaml @@ -124,7 +124,6 @@ sourceSchemas: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename upvotes ... on Discussion { score @@ -147,7 +146,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "upvotes": 123, "score": 123 } @@ -157,7 +155,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "upvotes": 123, "score": 123 } @@ -197,7 +194,6 @@ sourceSchemas: $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { - __typename upvotes ... on Author { score @@ -214,7 +210,6 @@ sourceSchemas: { "data": { "authorById": { - "__typename": "Author", "upvotes": 123, "score": 123 } @@ -258,7 +253,6 @@ operationPlan: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename upvotes ... on Discussion { score @@ -281,7 +275,6 @@ operationPlan: $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { - __typename upvotes ... on Author { score diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml index 7d246864db1..3f86248f6f6 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_With_And_Without_Type_Refinements_Type_Refinements_Do_Not_Match.yaml @@ -119,7 +119,6 @@ sourceSchemas: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename upvotes ... on Discussion { score @@ -142,7 +141,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "upvotes": 123, "score": 123 } @@ -152,7 +150,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "upvotes": 123, "score": 123 } @@ -190,7 +187,6 @@ sourceSchemas: $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { - __typename upvotes } } @@ -204,7 +200,6 @@ sourceSchemas: { "data": { "authorById": { - "__typename": "Author", "upvotes": 123 } } @@ -244,7 +239,6 @@ operationPlan: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename upvotes ... on Discussion { score @@ -267,7 +261,6 @@ operationPlan: $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { - __typename upvotes } } diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Concrete_Lookups.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Concrete_Lookups.yaml index 76e0e491c98..54eb717e83b 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Concrete_Lookups.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Concrete_Lookups.yaml @@ -93,7 +93,6 @@ sourceSchemas: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename viewerCanVote } } @@ -107,7 +106,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "viewerCanVote": true } } @@ -145,7 +143,6 @@ operationPlan: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename viewerCanVote } } @@ -166,7 +163,6 @@ operationPlan: $__fusion_2_id: ID! ) { commentById(id: $__fusion_2_id) { - __typename viewerCanVote } } diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Concrete_Lookups_And_Field_From_Specific_Source.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Concrete_Lookups_And_Field_From_Specific_Source.yaml index db3ffbb4b6a..5092760954a 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Concrete_Lookups_And_Field_From_Specific_Source.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Concrete_Lookups_And_Field_From_Specific_Source.yaml @@ -118,7 +118,6 @@ sourceSchemas: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename totalVotes } } @@ -132,7 +131,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "totalVotes": 123 } } @@ -170,7 +168,6 @@ operationPlan: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename totalVotes } } @@ -191,7 +188,6 @@ operationPlan: $__fusion_2_id: ID! ) { commentById(id: $__fusion_2_id) { - __typename totalVotes } } diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Interface_Lookup.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Interface_Lookup.yaml index ac461331c97..d4aef315a29 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Interface_Lookup.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Interface_Lookup.yaml @@ -85,7 +85,6 @@ sourceSchemas: votableById(id: $__fusion_1_id) { __typename ... on Discussion { - __typename viewerCanVote } } @@ -140,7 +139,6 @@ operationPlan: votableById(id: $__fusion_1_id) { __typename ... on Discussion { - __typename viewerCanVote } } diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Interface_Lookup_And_Field_From_Specific_Source.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Interface_Lookup_And_Field_From_Specific_Source.yaml index 93339941563..1257aa16799 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Interface_Lookup_And_Field_From_Specific_Source.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/AbstractTypeTests.Interface_Field_Without_Type_Refinements_With_Interface_Lookup_And_Field_From_Specific_Source.yaml @@ -104,7 +104,6 @@ sourceSchemas: votableById(id: $__fusion_1_id) { __typename ... on Discussion { - __typename totalVotes } } @@ -159,7 +158,6 @@ operationPlan: votableById(id: $__fusion_1_id) { __typename ... on Discussion { - __typename totalVotes } } diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml index 74f9fd77343..917b4efcc73 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml @@ -149,7 +149,6 @@ sourceSchemas: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename upvotes ... on Discussion { score @@ -172,7 +171,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "upvotes": 123, "score": 123 } @@ -182,7 +180,6 @@ sourceSchemas: { "data": { "discussionById": { - "__typename": "Discussion", "upvotes": 123, "score": 123 } @@ -222,7 +219,6 @@ sourceSchemas: $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { - __typename upvotes } } @@ -236,7 +232,6 @@ sourceSchemas: { "data": { "authorById": { - "__typename": "Author", "upvotes": 123 } } @@ -297,7 +292,6 @@ operationPlan: $__fusion_1_id: ID! ) { discussionById(id: $__fusion_1_id) { - __typename upvotes ... on Discussion { score @@ -320,7 +314,6 @@ operationPlan: $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { - __typename upvotes } } diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/InterfaceLookupPlanningTests.Abstract_Customer_Interface_With_Email_Is_Plannable.yaml b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/InterfaceLookupPlanningTests.Abstract_Customer_Interface_With_Email_Is_Plannable.yaml index 7d69b1eade0..aa5d4d25439 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/InterfaceLookupPlanningTests.Abstract_Customer_Interface_With_Email_Is_Plannable.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/InterfaceLookupPlanningTests.Abstract_Customer_Interface_With_Email_Is_Plannable.yaml @@ -44,7 +44,6 @@ nodes: $__fusion_1_id: ID! ) { de_customerById(id: $__fusion_1_id) { - __typename email } } @@ -64,7 +63,6 @@ nodes: $__fusion_2_id: ID! ) { ch_customerById(id: $__fusion_2_id) { - __typename email } } From 84c241d9d2b98f168ccfeed8267d770956bd0c92 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:37:37 +0200 Subject: [PATCH 15/16] Address feedback --- .../Planning/OperationPlanner.cs | 8 ++++++++ .../Partitioners/SelectionSetPartitioner.cs | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs index dffa952fc91..19b0b6b22ca 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -2202,6 +2202,14 @@ private bool TryInlineIntoAncestorStep( var wrappedSelections = WrapSelectionsInFragments(match.AncestorFragments, requirements); + // Register the wrapped selection set nodes in the index before partitioning, + // so the partitioner can resolve their IDs via GetId. + if (wrappedSelections != requirements) + { + index.Register(match.SelectionSetId, wrappedSelections); + EnsureAllSelectionSetsRegistered(wrappedSelections, index); + } + var input = new SelectionSetPartitionerInput { SchemaName = match.Step.SchemaName!, diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs index 6b567a43820..3e8e0904138 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs @@ -275,7 +275,7 @@ private static void MergeChildUnresolvableEntries( ExecutionNodeCondition[] currentConditions, List unresolvableSelections) { - var keep = ImmutableStack.Empty; + List? kept = null; var anyMerged = false; foreach (var entry in context.Unresolvable) @@ -299,13 +299,25 @@ private static void MergeChildUnresolvableEntries( } else { - keep = keep.Push(entry); + kept ??= []; + kept.Add(entry); } } if (anyMerged) { - context.Unresolvable = keep; + // Rebuild the stack in reverse so the original ordering is preserved, + // since ImmutableStack enumeration is LIFO. + var stack = ImmutableStack.Empty; + if (kept is not null) + { + for (var i = kept.Count - 1; i >= 0; i--) + { + stack = stack.Push(kept[i]); + } + } + + context.Unresolvable = stack; } } From 187a3a35477489132db57c38169824c53f88a33b Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:12:31 +0200 Subject: [PATCH 16/16] more optimizations --- .../Partitioners/SelectionSetPartitioner.cs | 68 ++- .../ConditionalTests.cs | 115 +++++ ...ith_Type_Refinement_Under_Nested_Skip.yaml | 400 ++++++++++++++++++ ...Field_With_Type_Refinement_Under_Skip.yaml | 60 ++- 4 files changed, 608 insertions(+), 35 deletions(-) create mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Nested_Skip.yaml diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs index 3e8e0904138..d308fc5b7c1 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/Partitioners/SelectionSetPartitioner.cs @@ -283,18 +283,27 @@ private static void MergeChildUnresolvableEntries( var entryPath = entry.SelectionSet.Path; // A child entry has exactly one more segment than the current path, - // and that extra segment is an InlineFragment. + // and that extra segment is an InlineFragment. The child's conditions must + // start with the parent's conditions (prefix check), because the Unresolvable + // stack is shared across sibling fragments and may contain entries from + // unconditional siblings that must not inherit the parent's conditions. if (entryPath.Length == currentPath.Length + 1 && entryPath[entryPath.Length - 1].Kind == SelectionPathSegmentKind.InlineFragment && currentPath.IsParentOfOrSame(entryPath) - && currentConditions.SequenceEqual(entry.Conditions)) + && IsConditionPrefix(currentConditions, entry.Conditions)) { var typeName = entryPath[entryPath.Length - 1].Name; + var selectionSet = WrapInExtraConditions( + context, + entry.SelectionSet.Node, + entry.Conditions, + currentConditions.Length); + unresolvableSelections.Add(new InlineFragmentNode( null, new NamedTypeNode(typeName), [], - entry.SelectionSet.Node)); + selectionSet)); anyMerged = true; } else @@ -321,6 +330,59 @@ private static void MergeChildUnresolvableEntries( } } + /// + /// Checks whether is a prefix of . + /// Conditions accumulate as the partitioner recurses deeper, so a child entry's conditions + /// always start with the parent's conditions followed by any additional ones. However, + /// the Unresolvable stack is shared across sibling fragments, so entries from unconditional + /// siblings may have fewer conditions than the current parent scope. + /// + private static bool IsConditionPrefix( + ExecutionNodeCondition[] prefix, + ExecutionNodeCondition[] conditions) + { + if (conditions.Length < prefix.Length) + { + return false; + } + + for (var i = 0; i < prefix.Length; i++) + { + if (!ReferenceEquals(prefix[i], conditions[i])) + { + return false; + } + } + + return true; + } + + /// + /// Wraps a selection set in inline fragments for each extra condition directive + /// beyond the parent's conditions. This preserves the conditional semantics + /// within the merged selection set rather than as node-level conditions. + /// + private static SelectionSetNode WrapInExtraConditions( + Context context, + SelectionSetNode selectionSet, + ExecutionNodeCondition[] conditions, + int startIndex) + { + for (var i = conditions.Length - 1; i >= startIndex; i--) + { + selectionSet = new SelectionSetNode([ + new InlineFragmentNode( + null, + null, + [conditions[i].Directive!], + selectionSet) + ]); + context.SelectionSetIndexBuilder.Register(selectionSet); + } + + return selectionSet; + } + private (FieldNode?, FieldNode?) RewriteFieldNode( Context context, FusionComplexTypeDefinition complexType, diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs index adad181a882..6dc94f738f6 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ConditionalTests.cs @@ -2252,4 +2252,119 @@ ... on Author { // assert await MatchSnapshotAsync(gateway, request, result); } + + [Fact] + public async Task Interface_Field_With_Type_Refinement_Under_Nested_Skip() + { + // arrange + using var serverA = CreateSourceSchema( + "A", + """ + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + """); + + using var serverB = CreateSourceSchema( + "B", + """ + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + """); + + using var serverC = CreateSourceSchema( + "C", + """ + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + """); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", serverA), + ("B", serverB), + ("C", serverC) + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + """ + query($skip1: Boolean!, $skip2: Boolean!) { + abstractTypes { + ... on Discussion { + score + } + ... @skip(if: $skip1) { + upvotes + ... on Author @skip(if: $skip2) { + score + } + } + } + } + """, + variables: new Dictionary { ["skip1"] = false, ["skip2"] = true }); + + using var result = await client.PostAsync( + request, + new Uri("http://localhost:5000/graphql")); + + // assert + await MatchSnapshotAsync(gateway, request, result); + } } diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Nested_Skip.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Nested_Skip.yaml new file mode 100644 index 00000000000..d71c70cfd3f --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Nested_Skip.yaml @@ -0,0 +1,400 @@ +title: Interface_Field_With_Type_Refinement_Under_Nested_Skip +request: + document: | + query( + $skip1: Boolean! + $skip2: Boolean! + ) { + abstractTypes { + ... on Discussion { + score + } + ... @skip(if: $skip1) { + upvotes + ... on Author @skip(if: $skip2) { + score + } + } + } + } + variables: | + { + "skip1": false, + "skip2": true + } +response: + body: | + { + "data": { + "abstractTypes": [ + { + "score": 123, + "upvotes": 123 + }, + { + "upvotes": 123 + }, + { + "score": 123, + "upvotes": 123 + } + ] + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + } + + type Query { + abstractTypes: [Votable] + node(id: ID!): Node @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_0731cc53_1( + $skip1: Boolean! + $skip2: Boolean! + ) { + abstractTypes { + __typename + ... on Discussion { + id + } + ... @skip(if: $skip1) { + __typename + ... on Author @skip(if: $skip2) { + __typename + } + id + } + } + } + variables: | + { + "skip1": false, + "skip2": true + } + response: + results: + - | + { + "data": { + "abstractTypes": [ + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjox" + }, + { + "__typename": "Author", + "id": "QXV0aG9yOjI=" + }, + { + "__typename": "Discussion", + "id": "RGlzY3Vzc2lvbjoz" + } + ] + } + } + - name: B + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Discussion implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + discussionById(id: ID!): Discussion @lookup @shareable + } + interactions: + - request: + accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + kind: OperationBatch + items: + - document: | + query Op_0731cc53_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + upvotes + } + } + variables: | + [ + { + "__fusion_1_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_1_id": "RGlzY3Vzc2lvbjoz" + } + ] + - document: | + query Op_0731cc53_4( + $__fusion_3_id: ID! + ) { + discussionById(id: $__fusion_3_id) { + score + } + } + variables: | + [ + { + "__fusion_3_id": "RGlzY3Vzc2lvbjox" + }, + { + "__fusion_3_id": "RGlzY3Vzc2lvbjoz" + } + ] + response: + contentType: application/jsonl; charset=utf-8 + results: + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "score": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "upvotes": 123 + } + } + } + - | + { + "data": { + "discussionById": { + "upvotes": 123 + } + } + } + - name: C + schema: | + schema { + query: Query + } + + interface Node { + id: ID! + } + + interface Votable @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Author implements Votable & Node @key(fields: "id") { + id: ID! + upvotes: Int! + score: Int! + } + + type Query { + node(id: ID!): Node @lookup @shareable + authorById(id: ID!): Author @lookup @shareable + } + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + document: | + query Op_0731cc53_3( + $skip2: Boolean! + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + upvotes + ... on Author { + ... @skip(if: $skip2) { + score + } + } + } + } + variables: | + { + "skip2": true, + "__fusion_2_id": "QXV0aG9yOjI=" + } + response: + results: + - | + { + "data": { + "authorById": { + "upvotes": 123 + } + } + } +operationPlan: + operation: + - document: | + query( + $skip1: Boolean! + $skip2: Boolean! + ) { + abstractTypes { + __typename @fusion__requirement + ... on Discussion { + score + id @fusion__requirement + } + ... @skip(if: $skip1) { + upvotes + id @fusion__requirement + ... on Author @skip(if: $skip2) { + score + } + } + } + } + hash: 0731cc535062aece2d84c08e6667a388 + searchSpace: 1 + expandedNodes: 2 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_0731cc53_1( + $skip1: Boolean! + $skip2: Boolean! + ) { + abstractTypes { + __typename + ... on Discussion { + id + } + ... @skip(if: $skip1) { + __typename + ... on Author @skip(if: $skip2) { + __typename + } + id + } + } + } + forwardedVariables: + - skip1 + - skip2 + - id: 2 + type: Operation + schema: B + operation: | + query Op_0731cc53_2( + $__fusion_1_id: ID! + ) { + discussionById(id: $__fusion_1_id) { + upvotes + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_1_id + selectionMap: >- + id + conditions: + - variable: $skip1 + passingValue: false + dependencies: + - id: 1 + - id: 4 + type: Operation + schema: B + operation: | + query Op_0731cc53_4( + $__fusion_3_id: ID! + ) { + discussionById(id: $__fusion_3_id) { + score + } + } + source: $.discussionById + target: $.abstractTypes + batchingGroupId: 2 + requirements: + - name: __fusion_3_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: Operation + schema: C + operation: | + query Op_0731cc53_3( + $skip2: Boolean! + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + upvotes + ... on Author { + ... @skip(if: $skip2) { + score + } + } + } + } + source: $.authorById + target: $.abstractTypes + requirements: + - name: __fusion_2_id + selectionMap: >- + id + conditions: + - variable: $skip1 + passingValue: false + forwardedVariables: + - skip2 + dependencies: + - id: 1 diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml index 917b4efcc73..0b2a326fd75 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Interface_Field_With_Type_Refinement_Under_Skip.yaml @@ -83,7 +83,7 @@ sourceSchemas: ... @skip(if: $skip1) { __typename ... on Author @skip(if: $skip2) { - id + __typename } } id @@ -213,17 +213,28 @@ sourceSchemas: } interactions: - request: - accept: application/jsonl; charset=utf-8, text/event-stream; charset=utf-8, application/graphql-response+json; charset=utf-8, application/json; charset=utf-8 + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 document: | query Op_2c04e967_3( - $__fusion_2_id: ID! + $skip1: Boolean! + $skip2: Boolean! + $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { upvotes + ... on Author { + ... @skip(if: $skip1) { + ... @skip(if: $skip2) { + score + } + } + } } } variables: | { + "skip1": true, + "skip2": true, "__fusion_2_id": "QXV0aG9yOjI=" } response: @@ -253,7 +264,6 @@ operationPlan: ... @skip(if: $skip1) { ... on Author @skip(if: $skip2) { score - id @fusion__requirement } } } @@ -275,7 +285,7 @@ operationPlan: ... @skip(if: $skip1) { __typename ... on Author @skip(if: $skip2) { - id + __typename } } id @@ -311,43 +321,29 @@ operationPlan: schema: C operation: | query Op_2c04e967_3( - $__fusion_2_id: ID! + $skip1: Boolean! + $skip2: Boolean! + $__fusion_2_id: ID! ) { authorById(id: $__fusion_2_id) { upvotes + ... on Author { + ... @skip(if: $skip1) { + ... @skip(if: $skip2) { + score + } + } + } } } source: $.authorById target: $.abstractTypes - batchingGroupId: 3 requirements: - name: __fusion_2_id selectionMap: >- id - dependencies: - - id: 1 - - id: 4 - type: Operation - schema: C - operation: | - query Op_2c04e967_4( - $__fusion_3_id: ID! - ) { - authorById(id: $__fusion_3_id) { - score - } - } - source: $.authorById - target: $.abstractTypes - batchingGroupId: 3 - requirements: - - name: __fusion_3_id - selectionMap: >- - id - conditions: - - variable: $skip1 - passingValue: false - - variable: $skip2 - passingValue: false + forwardedVariables: + - skip1 + - skip2 dependencies: - id: 1