From 88df6b85d87b8e2c7fc44975eed2864ef7331482 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 3 Aug 2026 18:57:21 +0200 Subject: [PATCH 1/2] Fix Fusion defer patches when delivery path crosses intermediate lists --- .../Execution/OperationPlanExecutor.cs | 344 ++++++++++++---- .../Fusion.AspNetCore.Tests/DeferTests.cs | 121 ++++++ ..._When_Path_Crosses_Intermediate_Lists.yaml | 386 ++++++++++++++++++ 3 files changed, 780 insertions(+), 71 deletions(-) create mode 100644 src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DeferTests.Defer_Should_Return_Patches_When_Path_Crosses_Intermediate_Lists.yaml diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.cs index 0d2eb35c09a..e438718c4a1 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.cs @@ -8,6 +8,7 @@ using HotChocolate.Fusion.Execution.Results; using HotChocolate.Fusion.Text.Json; using HotChocolate.Language; +using HotChocolate.Types; using Microsoft.Extensions.DependencyInjection; namespace HotChocolate.Fusion.Execution; @@ -105,6 +106,7 @@ public static async Task ExecuteWithDeferAsync( // @defer(if:) evaluates to true) and the incremental plans that will actually run. // An incremental plan is active if at least one of its delivery groups is active. var activeDeliveryGroupIds = new HashSet(); + var deliveryPaths = CreateDeliveryPaths(operationPlan); foreach (var deliveryGroup in operationPlan.DeliveryGroups) { if (IsDeliveryGroupActive(deliveryGroup, variables)) @@ -131,7 +133,7 @@ public static async Task ExecuteWithDeferAsync( pendingResults.Add(new PendingResult( deliveryGroup.Id, - BuildPath(deliveryGroup.Path ?? SelectionPath.Root), + deliveryPaths[deliveryGroup.Id].PendingPath, deliveryGroup.Label)); } @@ -159,6 +161,7 @@ public static async Task ExecuteWithDeferAsync( operationPlan, initialResult, activeDeliveryGroupIds, + deliveryPaths, rootContext, cancellationToken), ExecutionResultKind.DeferredResult); @@ -186,6 +189,7 @@ private static async IAsyncEnumerable CreateIncrementalStream( OperationPlan operationPlan, OperationResult initialResult, HashSet activeDeliveryGroupIds, + IReadOnlyDictionary deliveryPaths, OperationPlanContext rootContext, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -349,7 +353,7 @@ private static async IAsyncEnumerable CreateIncrementalStream( childPending.Add(new PendingResult( deliveryGroup.Id, - BuildPath(deliveryGroup.Path ?? SelectionPath.Root), + deliveryPaths[deliveryGroup.Id].PendingPath, deliveryGroup.Label)); } } @@ -387,6 +391,7 @@ private static async IAsyncEnumerable CreateIncrementalStream( && TryCreateIncrementalResults( result.Data.Value, bestDeliveryGroup, + deliveryPaths[bestDeliveryGroup.Id].PendingFieldCount, result.Errors.Count > 0 ? result.Errors : null, out var incrementalResults)) { @@ -731,21 +736,170 @@ private static void CompleteDeliveryGroupsForIncrementalPlan( } } - private static Path BuildPath(SelectionPath selectionPath) + private static Dictionary CreateDeliveryPaths(OperationPlan operationPlan) + { + var deliveryPaths = new Dictionary(); + + foreach (var incrementalPlan in operationPlan.IncrementalPlans) + { + foreach (var deliveryGroup in incrementalPlan.DeliveryGroups) + { + deliveryPaths.TryAdd( + deliveryGroup.Id, + CreateDeliveryPath( + incrementalPlan.Operation, + deliveryGroup.Path ?? SelectionPath.Root)); + } + } + + return deliveryPaths; + } + + private static DeliveryPath CreateDeliveryPath( + Operation operation, + SelectionPath selectionPath) { var path = Path.Root; + var pendingFieldCount = 0; + var currentSelectionSet = operation.Definition.SelectionSet; + IOutputTypeDefinition currentType = operation.RootType; for (var i = 0; i < selectionPath.Length; i++) { var segment = selectionPath[i]; - if (segment.Kind is SelectionPathSegmentKind.Field) + if (segment.Kind is SelectionPathSegmentKind.Root) + { + continue; + } + + if (segment.Kind is SelectionPathSegmentKind.InlineFragment) + { + if (!TryFindInlineFragment(currentSelectionSet, segment.Name, out var inlineFragment) + || !operation.Schema.Types.TryGetType(segment.Name, out var fragmentType)) + { + throw new InvalidOperationException( + $"Could not resolve selection path segment '{segment.Name}'."); + } + + currentSelectionSet = inlineFragment.SelectionSet; + currentType = fragmentType; + continue; + } + + if (!TryFindField( + operation.Schema, + currentSelectionSet, + currentType, + segment.Name, + out var fieldNode, + out var field)) + { + throw new InvalidOperationException( + $"Could not resolve selection path segment '{segment.Name}'."); + } + + path = path.Append(fieldNode.Alias?.Value ?? fieldNode.Name.Value); + pendingFieldCount++; + + if (field.Type.IsListType()) + { + break; + } + + currentSelectionSet = fieldNode.SelectionSet + ?? throw new InvalidOperationException( + $"Selection path segment '{segment.Name}' has no selection set."); + currentType = field.Type.NamedType(); + } + + return new DeliveryPath(path, pendingFieldCount); + } + + private static bool TryFindField( + ISchemaDefinition schema, + SelectionSetNode selectionSet, + IOutputTypeDefinition typeContext, + string responseName, + out FieldNode fieldNode, + out IOutputFieldDefinition field) + { + foreach (var selection in selectionSet.Selections) + { + if (selection is FieldNode candidate + && (candidate.Alias?.Value ?? candidate.Name.Value) == responseName + && typeContext is IComplexTypeDefinition complexType + && complexType.Fields.TryGetField(candidate.Name.Value, out var resolvedField) + && resolvedField is not null) + { + fieldNode = candidate; + field = resolvedField; + return true; + } + + if (selection is InlineFragmentNode inlineFragment) + { + IOutputTypeDefinition fragmentType; + if (inlineFragment.TypeCondition?.Name.Value is not { } typeName) + { + fragmentType = typeContext; + } + else if (schema.Types.TryGetType( + typeName, + out var resolvedType) + && resolvedType is not null) + { + fragmentType = resolvedType; + } + else + { + continue; + } + + if (TryFindField( + schema, + inlineFragment.SelectionSet, + fragmentType, + responseName, + out fieldNode, + out field)) + { + return true; + } + } + } + + fieldNode = null!; + field = null!; + return false; + } + + private static bool TryFindInlineFragment( + SelectionSetNode selectionSet, + string typeName, + out InlineFragmentNode inlineFragment) + { + foreach (var selection in selectionSet.Selections) + { + if (selection is not InlineFragmentNode candidate) + { + continue; + } + + if (candidate.TypeCondition?.Name.Value == typeName) + { + inlineFragment = candidate; + return true; + } + + if (TryFindInlineFragment(candidate.SelectionSet, typeName, out inlineFragment)) { - path = path.Append(segment.Name); + return true; } } - return path; + inlineFragment = null!; + return false; } /// @@ -753,101 +907,147 @@ private static Path BuildPath(SelectionPath selectionPath) /// is the subtree at the best delivery group's path within the deferred /// plan's composite result. The incremental delivery contract requires /// incremental.data to be a map of fields to merge at the pending - /// path, not the fully rooted result. When the pending path points at a list, - /// each list element is emitted as a separate incremental result with a - /// relative index subPath. + /// path, not the fully rooted result. When the delivery path crosses a list, + /// the pending path ends at the first list and each concrete descendant is + /// emitted with a relative field-and-index subPath. /// private static bool TryCreateIncrementalResults( OperationResultData rootData, DeliveryGroup bestDeliveryGroup, + int pendingFieldCount, ImmutableList? errors, out ImmutableList incrementalResults) { if (rootData.Value is not CompositeResultDocument document) { - // Unknown backing value: fall through to the default behavior and - // emit the result as-is. - incrementalResults = - [ - new IncrementalObjectResult( - bestDeliveryGroup.Id, - errors, - data: rootData) - ]; - return true; + throw new InvalidOperationException( + "Deferred result data must be backed by a composite result document."); } - var element = document.Data; var selectionPath = bestDeliveryGroup.Path ?? SelectionPath.Root; + var builder = ImmutableList.CreateBuilder(); - for (var i = 0; i < selectionPath.Length; i++) - { - var segment = selectionPath[i]; + CollectIncrementalResults( + document, + document.Data, + selectionPath, + segmentIndex: 0, + fieldIndex: 0, + pendingFieldCount, + Path.Root, + bestDeliveryGroup.Id, + errors, + builder); + + incrementalResults = builder.ToImmutable(); + return incrementalResults.Count > 0; + } - // Inline fragments/type-conditions do not introduce an extra level - // in the result tree, so we only walk field segments. - if (segment.Kind is not SelectionPathSegmentKind.Field) + private static void CollectIncrementalResults( + CompositeResultDocument document, + CompositeResultElement element, + SelectionPath selectionPath, + int segmentIndex, + int fieldIndex, + int pendingFieldCount, + Path subPath, + int deliveryGroupId, + ImmutableList? errors, + ImmutableList.Builder results) + { + if (element.IsNullMarker) + { + if (HasRemainingFieldSegments(selectionPath, segmentIndex)) { - continue; + return; } + } + else if (element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) + { + return; + } + + if (element.ValueKind is JsonValueKind.Array && !element.IsNullMarker) + { + var length = element.GetArrayLength(); - if (!element.TryGetProperty(segment.Name, out var next) - || next.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) + for (var i = 0; i < length; i++) { - // The path could not be resolved or is null; nothing to merge. - incrementalResults = []; - return false; + CollectIncrementalResults( + document, + element[i], + selectionPath, + segmentIndex, + fieldIndex, + pendingFieldCount, + subPath.Append(i), + deliveryGroupId, + errors, + results); } - element = next; + return; + } - if (element.IsNullMarker) - { - for (var j = i + 1; j < selectionPath.Length; j++) - { - if (selectionPath[j].Kind is SelectionPathSegmentKind.Field) - { - incrementalResults = []; - return false; - } - } - } + if (segmentIndex == selectionPath.Length) + { + results.Add( + new IncrementalObjectResult( + deliveryGroupId, + errors, + subPath.IsRoot ? null : subPath, + CreateIncrementalData(document, element))); + return; } - if (element.ValueKind is JsonValueKind.Array && !element.IsNullMarker) + var segment = selectionPath[segmentIndex]; + + if (segment.Kind is not SelectionPathSegmentKind.Field) { - var builder = ImmutableList.CreateBuilder(); - var length = element.GetArrayLength(); + CollectIncrementalResults( + document, + element, + selectionPath, + segmentIndex + 1, + fieldIndex, + pendingFieldCount, + subPath, + deliveryGroupId, + errors, + results); + return; + } - for (var i = 0; i < length; i++) - { - var item = element[i]; + if (element.ValueKind is not JsonValueKind.Object + || !element.TryGetProperty(segment.Name, out var next)) + { + return; + } - if (item.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) - { - continue; - } + CollectIncrementalResults( + document, + next, + selectionPath, + segmentIndex + 1, + fieldIndex + 1, + pendingFieldCount, + fieldIndex < pendingFieldCount ? subPath : subPath.Append(segment.Name), + deliveryGroupId, + errors, + results); + } - builder.Add( - new IncrementalObjectResult( - bestDeliveryGroup.Id, - errors, - subPath: Path.Root.Append(i), - data: CreateIncrementalData(document, item))); + private static bool HasRemainingFieldSegments(SelectionPath selectionPath, int segmentIndex) + { + for (var i = segmentIndex; i < selectionPath.Length; i++) + { + if (selectionPath[i].Kind is SelectionPathSegmentKind.Field) + { + return true; } - - incrementalResults = builder.ToImmutable(); - return true; } - incrementalResults = - [ - new IncrementalObjectResult( - bestDeliveryGroup.Id, - errors, - data: CreateIncrementalData(document, element)) - ]; - return true; + return false; } private static OperationResultData CreateIncrementalData( @@ -862,6 +1062,8 @@ private static OperationResultData CreateIncrementalData( new DeferredPayloadDataFormatter(element), memoryHolder: null); + private readonly record struct DeliveryPath(Path PendingPath, int PendingFieldCount); + public static async Task SubscribeAsync( RequestContext requestContext, OperationPlan operationPlan, diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/DeferTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/DeferTests.cs index 0b54b607bd2..a8988df87da 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/DeferTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/DeferTests.cs @@ -1,3 +1,5 @@ +using System.Net; +using System.Text; using System.Text.Json; using HotChocolate.Transport; using HotChocolate.Transport.Http; @@ -1157,6 +1159,125 @@ ... @defer(label: "posts") { await MatchSnapshotAsync(gateway, request, result, stableStream: true); } + [Fact] + public async Task Defer_Should_Return_Patches_When_Path_Crosses_Intermediate_Lists() + { + // arrange + using var server = CreateSourceSchema( + "A", + """ + type Query { + users: UserConnection! + } + + interface UserConnection { + nodes: [User!]! + } + + type DefaultUserConnection implements UserConnection { + nodes: [User!]! + } + + type User { + reviews: [Review!]! + } + + type Review { + product: Product! + } + + type Product { + name: String! + dimension: Dimension! + } + + type Dimension { + height: Float! + } + """, + mockHttpResponse: _ => Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + """ + { + "data": { + "users": { + "__typename": "DefaultUserConnection", + "nodes": [ + { + "reviews": [ + { + "product": { + "name": "Table", + "dimension": { + "height": 30.0 + } + } + }, + { + "product": { + "name": "Chair", + "dimension": { + "height": 40.0 + } + } + } + ] + }, + { + "reviews": [ + { + "product": { + "name": "Couch", + "dimension": { + "height": 50.0 + } + } + } + ] + } + ] + } + } + } + """, + Encoding.UTF8, + "application/json") + })); + + using var gateway = await CreateCompositeSchemaAsync([("A", server)]); + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + var request = new OperationRequest( + """ + query { + users { + nodes { + reviews { + product { + name + ... @defer(label: "dimension") { + dimension { + height + } + } + } + } + } + } + } + """); + + // act + using var result = await client.PostAsync( + request, + new Uri("http://localhost:5000/graphql"), + TestContext.Current.CancellationToken); + + // assert + await MatchSnapshotAsync(gateway, request, result, stableStream: true); + } + [Fact] public async Task Defer_Composite_Field_Under_Type_Condition_On_Abstract_Parent_Is_Wrapped() { diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DeferTests.Defer_Should_Return_Patches_When_Path_Crosses_Intermediate_Lists.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DeferTests.Defer_Should_Return_Patches_When_Path_Crosses_Intermediate_Lists.yaml new file mode 100644 index 00000000000..294fffd5d8e --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/DeferTests.Defer_Should_Return_Patches_When_Path_Crosses_Intermediate_Lists.yaml @@ -0,0 +1,386 @@ +title: Defer_Should_Return_Patches_When_Path_Crosses_Intermediate_Lists +request: + document: | + { + users { + nodes { + reviews { + product { + name + ... @defer(label: "dimension") { + dimension { + height + } + } + } + } + } + } + } +stableResponseStream: | + { + "kind": "stable-stream", + "payloadCount": 2, + "initial": { + "data": { + "users": { + "nodes": [ + { + "reviews": [ + { + "product": { + "name": "Table" + } + }, + { + "product": { + "name": "Chair" + } + } + ] + }, + { + "reviews": [ + { + "product": { + "name": "Couch" + } + } + ] + } + ] + } + } + }, + "pending": [ + { + "id": "0", + "path": [ + "users", + "nodes" + ], + "label": "dimension" + } + ], + "incremental": [ + { + "id": "0", + "path": [ + "users", + "nodes" + ], + "subPath": [ + 0, + "reviews", + 0, + "product" + ], + "data": { + "dimension": { + "height": 30.0 + } + } + }, + { + "id": "0", + "path": [ + "users", + "nodes" + ], + "subPath": [ + 0, + "reviews", + 1, + "product" + ], + "data": { + "dimension": { + "height": 40.0 + } + } + }, + { + "id": "0", + "path": [ + "users", + "nodes" + ], + "subPath": [ + 1, + "reviews", + 0, + "product" + ], + "data": { + "dimension": { + "height": 50.0 + } + } + } + ], + "completed": [ + { + "id": "0" + } + ], + "final": { + "data": { + "users": { + "nodes": [ + { + "reviews": [ + { + "product": { + "dimension": { + "height": 30.0 + }, + "name": "Table" + } + }, + { + "product": { + "dimension": { + "height": 40.0 + }, + "name": "Chair" + } + } + ] + }, + { + "reviews": [ + { + "product": { + "dimension": { + "height": 50.0 + }, + "name": "Couch" + } + } + ] + } + ] + } + } + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + type Query { + users: UserConnection! + } + + type DefaultUserConnection implements UserConnection { + nodes: [User!]! + } + + type Dimension { + height: Float! + } + + type Product { + name: String! + dimension: Dimension! + } + + type Review { + product: Product! + } + + type User { + reviews: [Review!]! + } + + interface UserConnection { + nodes: [User!]! + } + 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_63ddfa41_1 { + users { + __typename + nodes { + reviews { + product { + name + } + } + } + } + } + response: + contentType: application/json; charset=utf-8 + results: + - | + { + "data": { + "users": { + "__typename": "DefaultUserConnection", + "nodes": [ + { + "reviews": [ + { + "product": { + "name": "Table", + "dimension": { + "height": 30.0 + } + } + }, + { + "product": { + "name": "Chair", + "dimension": { + "height": 40.0 + } + } + } + ] + }, + { + "reviews": [ + { + "product": { + "name": "Couch", + "dimension": { + "height": 50.0 + } + } + } + ] + } + ] + } + } + } + - 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_defer_1 { + users { + __typename + nodes { + reviews { + product { + dimension { + height + } + } + } + } + } + } + response: + contentType: application/json; charset=utf-8 + results: + - | + { + "data": { + "users": { + "__typename": "DefaultUserConnection", + "nodes": [ + { + "reviews": [ + { + "product": { + "name": "Table", + "dimension": { + "height": 30.0 + } + } + }, + { + "product": { + "name": "Chair", + "dimension": { + "height": 40.0 + } + } + } + ] + }, + { + "reviews": [ + { + "product": { + "name": "Couch", + "dimension": { + "height": 50.0 + } + } + } + ] + } + ] + } + } + } +operationPlan: + operation: + - document: | + { + users { + __typename @fusion__requirement + nodes { + reviews { + product { + name + } + } + } + } + } + hash: 63ddfa416fc211d1615a0850fed56b9e + searchSpace: 1 + expandedNodes: 1 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_63ddfa41_1 { + users { + __typename + nodes { + reviews { + product { + name + } + } + } + } + } + deliveryGroups: + - id: 0 + path: $.users.nodes.reviews.product + label: dimension + incrementalPlans: + - deliveryGroupIds: + - 0 + parentNodeId: 1 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_defer_1 { + users { + __typename + nodes { + reviews { + product { + dimension { + height + } + } + } + } + } + } From b8393f5584661d741f7912a9c9e096d84826e7ac Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 3 Aug 2026 20:43:45 +0200 Subject: [PATCH 2/2] edits --- .../Execution/OperationPlanExecutor.DeliveryPath.cs | 6 ++++++ .../OperationPlanExecutor.IncrementalPlanResult.cs | 13 +++++++++++++ .../Execution/OperationPlanExecutor.cs | 10 +--------- 3 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.DeliveryPath.cs create mode 100644 src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.IncrementalPlanResult.cs diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.DeliveryPath.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.DeliveryPath.cs new file mode 100644 index 00000000000..1672a551852 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.DeliveryPath.cs @@ -0,0 +1,6 @@ +namespace HotChocolate.Fusion.Execution; + +internal static partial class OperationPlanExecutor +{ + private readonly record struct DeliveryPath(Path PendingPath, int PendingFieldCount); +} diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.IncrementalPlanResult.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.IncrementalPlanResult.cs new file mode 100644 index 00000000000..368b0f0bf07 --- /dev/null +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.IncrementalPlanResult.cs @@ -0,0 +1,13 @@ +using HotChocolate.Execution; +using HotChocolate.Fusion.Execution.Nodes; + +namespace HotChocolate.Fusion.Execution; + +internal static partial class OperationPlanExecutor +{ + private readonly record struct IncrementalPlanResult( + IncrementalPlan IncrementalPlan, + OperationPlanContext? Context, + OperationResult? Result, + Exception? Error); +} diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.cs index e438718c4a1..175bcf54cfe 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.cs @@ -13,7 +13,7 @@ namespace HotChocolate.Fusion.Execution; -internal static class OperationPlanExecutor +internal static partial class OperationPlanExecutor { public static async Task ExecuteAsync( RequestContext requestContext, @@ -1062,8 +1062,6 @@ private static OperationResultData CreateIncrementalData( new DeferredPayloadDataFormatter(element), memoryHolder: null); - private readonly record struct DeliveryPath(Path PendingPath, int PendingFieldCount); - public static async Task SubscribeAsync( RequestContext requestContext, OperationPlan operationPlan, @@ -1508,9 +1506,3 @@ private static string GetSubscriptionSchemaName( => subscriptionNode.SchemaName ?? (subscriptionNode is EventStreamExecutionNode ? "event-stream" : context.GetDynamicSchemaName(subscriptionNode)); } - -internal readonly record struct IncrementalPlanResult( - IncrementalPlan IncrementalPlan, - OperationPlanContext? Context, - OperationResult? Result, - Exception? Error);