From ff40bd3ea2cdf5884d6c47ddb14436664eae8ab5 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 19 Mar 2026 14:01:53 +0100 Subject: [PATCH 1/2] [Fusion] Support abstract type resolution across multiple source schemas --- .../Fusion.Execution/Planning/PlanQueue.cs | 401 ++++++++++++------ .../Planning/InterfaceLookupPlanningTests.cs | 144 +++++++ ...mer_Interface_With_Email_Is_Plannable.yaml | 78 ++++ 3 files changed, 485 insertions(+), 138 deletions(-) create mode 100644 src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/InterfaceLookupPlanningTests.cs create mode 100644 src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/InterfaceLookupPlanningTests.Abstract_Customer_Interface_With_Email_Is_Plannable.yaml diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/PlanQueue.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/PlanQueue.cs index 02080b4fdd9..b71f97a265f 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/PlanQueue.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Planning/PlanQueue.cs @@ -2,6 +2,7 @@ using HotChocolate.Fusion.Language; using HotChocolate.Fusion.Types; using HotChocolate.Types; +using Lookup = HotChocolate.Fusion.Types.Metadata.Lookup; namespace HotChocolate.Fusion.Planning; @@ -36,6 +37,12 @@ public bool TryPeek(out PlanNode node, out double priority) /// public void Clear() => _queue.Clear(); + /// + /// Enqueues and scores a single plan node to this queue. + /// + public void Enqueue(PlanNode node) + => _queue.Enqueue(node, PlannerCostEstimator.ScoreNode(node, schema)); + /// /// Expands a plan node's next work item into all possible branches /// (one per candidate schema or lookup) and enqueues each branch. @@ -80,24 +87,17 @@ public void EnqueueBranches(PlanNode planNodeTemplate) } } - /// - /// Enqueues and scores a single plan node to this queue. - /// - public void Enqueue(PlanNode node) - => _queue.Enqueue(node, PlannerCostEstimator.ScoreNode(node, schema)); - private void EnqueueRootPlanNodes( PlanNode planNodeTemplate, OperationWorkItem workItem) { foreach (var (schemaName, resolutionCost) in schema.GetPossibleSchemas(workItem.SelectionSet)) { - Enqueue( - planNodeTemplate with - { - SchemaName = schemaName, - ResolutionCost = resolutionCost - }); + Enqueue(planNodeTemplate with + { + SchemaName = schemaName, + ResolutionCost = resolutionCost + }); } } @@ -109,28 +109,36 @@ private void EnqueueLookupPlanNodes( var allCandidateSchemas = planNodeTemplate.GetCandidateSchemas(workItem.SelectionSet.Id); var type = (FusionComplexTypeDefinition)workItem.SelectionSet.Type; - double EstimateBranchLowerBound(Backlog branchBacklog) - => PlannerCostEstimator.EstimateRemainingCost( - planNodeTemplate.Options, - planNodeTemplate.MaxDepth, - planNodeTemplate.OpsPerLevel, - branchBacklog.Cost); - - // Each branch starts from the same popped template and - // mutates a local copy of backlog state. - // This avoids recomputing backlog shape - // from collections for every candidate. - foreach (var (toSchema, resolutionCost) in schema.GetPossibleSchemas(workItem.SelectionSet)) + // If this work item already carries a chosen lookup, keep it and + // only align the node schema to the lookup schema. + if (workItem.Lookup is not null) { - if (toSchema.Equals(workItem.FromSchema, StringComparison.Ordinal)) + var branchBacklog = backlog.Push(workItem); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + + Enqueue(planNodeTemplate with { - continue; - } + SchemaName = workItem.Lookup.SchemaName, + ResolutionCost = GetResolutionCost(workItem.SelectionSet, workItem.Lookup.SchemaName), + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); + return; + } + + // For abstract types, try to resolve through per-concrete-type lookups + // before falling through to the standard abstract-type lookup path. + if (type.Kind is TypeKind.Interface or TypeKind.Union) + { + TryEnqueueConcreteTypeLookupPlanNodes(planNodeTemplate, workItem, backlog, allCandidateSchemas, type); + } - // if the target schema has no lookup for the abstract type itself, - // try resolving through per-concrete-type lookups instead. - if (type.Kind is TypeKind.Interface or TypeKind.Union - && TryEnqueueConcreteTypeLookupPlanNode(toSchema, resolutionCost)) + // Each branch starts from the same popped template and mutates a local copy + // of backlog state. This avoids recomputing backlog shape from collections + // for every candidate. + foreach (var (toSchema, resolutionCost) in schema.GetPossibleSchemas(workItem.SelectionSet)) + { + if (toSchema.Equals(workItem.FromSchema, StringComparison.Ordinal)) { continue; } @@ -143,15 +151,14 @@ double EstimateBranchLowerBound(Backlog branchBacklog) { var lookupWorkItem = workItem with { Lookup = bestLookup }; var branchBacklog = backlog.Push(lookupWorkItem); - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); - Enqueue( - planNodeTemplate with - { - SchemaName = toSchema, - ResolutionCost = resolutionCost, - Backlog = branchBacklog, - RemainingCost = branchRemainingCost - }); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = toSchema, + ResolutionCost = resolutionCost, + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); continue; } @@ -160,15 +167,14 @@ planNodeTemplate with { var lookupWorkItem = workItem with { Lookup = lookup }; var branchBacklog = backlog.Push(lookupWorkItem); - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); - Enqueue( - planNodeTemplate with - { - SchemaName = toSchema, - ResolutionCost = resolutionCost, - Backlog = branchBacklog, - RemainingCost = branchRemainingCost - }); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = toSchema, + ResolutionCost = resolutionCost, + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); hasEnqueuedDirectLookup = true; } @@ -187,24 +193,43 @@ planNodeTemplate with StringComparer.Ordinal)) { var branchBacklog = backlog.Push(lookupThroughPathWorkItem); - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); - Enqueue( - planNodeTemplate with - { - SchemaName = toSchema, - SelectionSetIndex = index, - ResolutionCost = resolutionCost + cost, - Backlog = branchBacklog, - RemainingCost = branchRemainingCost - }); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = toSchema, + SelectionSetIndex = index, + ResolutionCost = resolutionCost + cost, + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); } } } + } + + /// + /// Resolves an abstract type (interface/union) by finding per-concrete-type lookups. + /// First tries to keep all concrete types on a single schema (fewer network hops), + /// then falls back to spreading each concrete type to its best available schema. + /// + private bool TryEnqueueConcreteTypeLookupPlanNodes( + PlanNode planNodeTemplate, + OperationWorkItem workItem, + Backlog backlog, + ImmutableHashSet allCandidateSchemas, + FusionComplexTypeDefinition type) + { + var enqueued = false; - // When the target schema has no lookup that returns the abstract type directly, - // we try to resolve through per-concrete-type lookups instead. - bool TryEnqueueConcreteTypeLookupPlanNode(string toSchema, double resolutionCost) + // Phase 1: we try to find a single schema that can resolve all concrete types as + // this would allow us to batch all requests to these into a single GraphQL batch request. + foreach (var (toSchema, resolutionCost) in schema.GetPossibleSchemas(workItem.SelectionSet)) { + if (toSchema.Equals(workItem.FromSchema, StringComparison.Ordinal)) + { + continue; + } + // if the target schema already has a lookup returning the abstract type, // let the normal lookup path handle it. var hasAbstractLookups = schema @@ -213,11 +238,12 @@ bool TryEnqueueConcreteTypeLookupPlanNode(string toSchema, double resolutionCost if (hasAbstractLookups) { - return false; + continue; } var branchBacklog = backlog; var fromSchemas = allCandidateSchemas.Remove(toSchema); + var allFound = true; // for each concrete type that implements the abstract type, // find a lookup in the target schema. @@ -232,11 +258,11 @@ bool TryEnqueueConcreteTypeLookupPlanNode(string toSchema, double resolutionCost && t.FieldType.Name.Equals(possibleType.Name, StringComparison.Ordinal)); } - // If any concrete type lacks a lookup we bail out so the normal path can try instead. - // otherwise, we could end up with silent failures at runtime. + // If any concrete type lacks a lookup we skip this schema. if (concreteLookup is null) { - return false; + allFound = false; + break; } // rewrite the selection set to target the concrete type with a @@ -251,9 +277,14 @@ bool TryEnqueueConcreteTypeLookupPlanNode(string toSchema, double resolutionCost branchBacklog = branchBacklog.Push(lookupWorkItem); } - // all concrete types have lookups, enqueue a single plan node + if (!allFound) + { + continue; + } + + // all concrete types have lookups in this schema, enqueue a single plan node // that fans out to each concrete type at execution time. - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); Enqueue(planNodeTemplate with { SchemaName = toSchema, @@ -262,8 +293,123 @@ bool TryEnqueueConcreteTypeLookupPlanNode(string toSchema, double resolutionCost RemainingCost = branchRemainingCost }); + enqueued = true; + } + + if (enqueued) + { return true; } + + // Phase 2: if we do not find a single schema that can can resolve all concrete types we + // try to distribute each concrete type to the best available schema. + var crossBacklog = backlog; + string? topSchema = null; + double topCost = 0; + + foreach (var possibleType in schema.GetPossibleTypes(type)) + { + // rewrite the selection set to target the concrete type with a + // fragment path so the executor can match the runtime type. + var selectionSet = new SelectionSet( + workItem.SelectionSet.Id, + workItem.SelectionSet.Node, + possibleType, + workItem.SelectionSet.Path.AppendFragment(possibleType.Name)); + + Lookup? concreteLookup = null; + string? lookupSchema = null; + double lookupCost = 0; + + // scan candidate schemas for the best lookup for this concrete type. + foreach (var (candidateSchema, candidateCost) in schema.GetPossibleSchemas(selectionSet)) + { + if (candidateSchema.Equals(workItem.FromSchema, StringComparison.Ordinal)) + { + continue; + } + + if (schema.TryGetBestDirectLookup( + possibleType, + allCandidateSchemas.Remove(candidateSchema), + candidateSchema, + out var directLookup)) + { + concreteLookup = directLookup; + lookupSchema = candidateSchema; + lookupCost = candidateCost; + break; + } + + var fallbackLookup = schema + .GetPossibleLookupsOrdered(possibleType, candidateSchema) + .FirstOrDefault( + t => !t.IsInternal + && t.FieldType.Name.Equals(possibleType.Name, StringComparison.Ordinal)); + + if (fallbackLookup is not null) + { + concreteLookup = fallbackLookup; + lookupSchema = candidateSchema; + lookupCost = candidateCost; + break; + } + } + + // If any concrete type lacks a lookup we bail out; + // otherwise, we could end up with silent failures at runtime. + if (concreteLookup is null) + { + return false; + } + + // The backlog is LIFO, so the last pushed item is processed first. + // Track its schema so the plan node's SchemaName matches. + topSchema = lookupSchema; + topCost = lookupCost; + + var lookupWorkItem = workItem with { SelectionSet = selectionSet, Lookup = concreteLookup }; + crossBacklog = crossBacklog.Push(lookupWorkItem); + } + + if (topSchema is null) + { + return false; + } + + // all concrete types have lookups, enqueue a single plan node + // that fans out to each concrete type at execution time. + var crossRemainingCost = EstimateRemainingCost(planNodeTemplate, crossBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = topSchema, + ResolutionCost = topCost, + Backlog = crossBacklog, + RemainingCost = crossRemainingCost + }); + + return true; + } + + private static double EstimateRemainingCost(PlanNode planNodeTemplate, Backlog branchBacklog) + => PlannerCostEstimator.EstimateRemainingCost( + planNodeTemplate.Options, + planNodeTemplate.MaxDepth, + planNodeTemplate.OpsPerLevel, + branchBacklog.Cost); + + private double GetResolutionCost(SelectionSet selectionSet, string schemaName) + { + foreach (var (candidateSchema, candidateCost) in schema.GetPossibleSchemas(selectionSet)) + { + if (candidateSchema.Equals(schemaName, StringComparison.Ordinal)) + { + return candidateCost; + } + } + + throw new InvalidOperationException( + $"Schema '{schemaName}' is not a valid candidate for selection set '{selectionSet.Type.Name}'."); } private void EnqueueNodeLookupPlanNodes( @@ -274,13 +420,6 @@ private void EnqueueNodeLookupPlanNodes( var type = workItem.SelectionSet.Type; var hasEnqueuedLookup = false; - double EstimateBranchLowerBound(Backlog branchBacklog) - => PlannerCostEstimator.EstimateRemainingCost( - planNodeTemplate.Options, - planNodeTemplate.MaxDepth, - planNodeTemplate.OpsPerLevel, - branchBacklog.Cost); - // Same branching rule as lookup work items: // copy backlog state per branch, then // materialize a new node with the @@ -301,15 +440,14 @@ double EstimateBranchLowerBound(Backlog branchBacklog) var lookupWorkItem = workItem with { Lookup = byIdLookup }; var branchBacklog = backlog.Push(lookupWorkItem); - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); - Enqueue( - planNodeTemplate with - { - SchemaName = schemaName, - ResolutionCost = resolutionCost, - Backlog = branchBacklog, - RemainingCost = branchRemainingCost - }); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = schemaName, + ResolutionCost = resolutionCost, + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); hasEnqueuedLookup = true; } @@ -327,14 +465,13 @@ planNodeTemplate with var lookupWorkItem = workItem with { Lookup = byIdLookup }; var branchBacklog = backlog.Push(lookupWorkItem); - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); - Enqueue( - planNodeTemplate with - { - SchemaName = byIdLookup.SchemaName, - Backlog = branchBacklog, - RemainingCost = branchRemainingCost - }); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = byIdLookup.SchemaName, + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); } } @@ -346,13 +483,6 @@ private void EnqueueRequirePlanNodes( var allCandidateSchemas = planNodeTemplate.GetCandidateSchemas(workItem.Selection.SelectionSetId); var selectionSetType = workItem.Selection.Field.DeclaringType; - double EstimateBranchLowerBound(Backlog branchBacklog) - => PlannerCostEstimator.EstimateRemainingCost( - planNodeTemplate.Options, - planNodeTemplate.MaxDepth, - planNodeTemplate.OpsPerLevel, - branchBacklog.Cost); - // Requirement planning can fork into inline and lookup paths. // Both are scored from the same popped template by cloning and // mutating backlog state per candidate. @@ -368,13 +498,12 @@ double EstimateBranchLowerBound(Backlog branchBacklog) if (schemaName == planNodeTemplate.SchemaName) { var inlineBacklog = backlog.Push(workItem); - var inlineRemainingCost = EstimateBranchLowerBound(inlineBacklog); - Enqueue( - planNodeTemplate with - { - Backlog = inlineBacklog, - RemainingCost = inlineRemainingCost, - }); + var inlineRemainingCost = EstimateRemainingCost(planNodeTemplate, inlineBacklog); + Enqueue(planNodeTemplate with + { + Backlog = inlineBacklog, + RemainingCost = inlineRemainingCost, + }); if (schema.TryGetBestDirectLookup( selectionSetType, @@ -384,14 +513,13 @@ planNodeTemplate with { var lookupWorkItem = workItem with { Lookup = bestLookup }; var branchBacklog = backlog.Push(lookupWorkItem); - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); - Enqueue( - planNodeTemplate with - { - SchemaName = schemaName, - Backlog = branchBacklog, - RemainingCost = branchRemainingCost - }); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = schemaName, + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); continue; } @@ -399,14 +527,13 @@ planNodeTemplate with { var lookupWorkItem = workItem with { Lookup = lookup }; var branchBacklog = backlog.Push(lookupWorkItem); - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); - Enqueue( - planNodeTemplate with - { - SchemaName = schemaName, - Backlog = branchBacklog, - RemainingCost = branchRemainingCost - }); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = schemaName, + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); } } else @@ -419,14 +546,13 @@ planNodeTemplate with { var lookupWorkItem = workItem with { Lookup = bestLookup }; var branchBacklog = backlog.Push(lookupWorkItem); - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); - Enqueue( - planNodeTemplate with - { - SchemaName = schemaName, - Backlog = branchBacklog, - RemainingCost = branchRemainingCost - }); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = schemaName, + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); continue; } @@ -434,14 +560,13 @@ planNodeTemplate with { var lookupWorkItem = workItem with { Lookup = lookup }; var branchBacklog = backlog.Push(lookupWorkItem); - var branchRemainingCost = EstimateBranchLowerBound(branchBacklog); - Enqueue( - planNodeTemplate with - { - SchemaName = schemaName, - Backlog = branchBacklog, - RemainingCost = branchRemainingCost - }); + var branchRemainingCost = EstimateRemainingCost(planNodeTemplate, branchBacklog); + Enqueue(planNodeTemplate with + { + SchemaName = schemaName, + Backlog = branchBacklog, + RemainingCost = branchRemainingCost + }); } } } diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/InterfaceLookupPlanningTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/InterfaceLookupPlanningTests.cs new file mode 100644 index 00000000000..34bba39bd86 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/InterfaceLookupPlanningTests.cs @@ -0,0 +1,144 @@ +using HotChocolate.Fusion.Types; + +namespace HotChocolate.Fusion.Planning; + +public sealed class InterfaceLookupPlanningTests : FusionTestBase +{ + [Fact] + public void Abstract_Customer_Interface_With_Id_Only_Is_Plannable() + { + // arrange + var schema = CreateSplitCustomerSchema(); + + // act + var error = Record.Exception( + () => PlanOperation( + schema, + """ + query { + ordersByAgent { + edges { + node { + id + customer { + __typename + id + } + } + } + } + } + """)); + + // assert + Assert.Null(error); + } + + [Fact] + public void Abstract_Customer_Interface_With_Email_Is_Plannable() + { + // arrange + var schema = CreateSplitCustomerSchema(); + + // act + var plan = PlanOperation( + schema, + """ + query { + ordersByAgent { + edges { + node { + id + customer { + __typename + id + email + } + } + } + } + } + """); + + // assert + MatchSnapshot(plan); + } + + private static FusionSchemaDefinition CreateSplitCustomerSchema() + => ComposeSchema( + """ + # name: common + schema { + query: Query + } + + type Query { + ordersByAgent: OrderConnection + } + + type OrderConnection { + edges: [OrderEdge!] + } + + type OrderEdge { + node: Order! + } + + type Order @key(fields: "id") { + id: ID! + customer: Customer! + } + + interface Customer { + id: ID! + } + + type ch_Customer implements Customer @key(fields: "id") { + id: ID! + } + + type de_Customer implements Customer @key(fields: "id") { + id: ID! + } + """, + """ + # name: ch + schema { + query: Query + } + + type Query { + ch_customerById(id: ID! @is(field: "id")): ch_Customer @lookup @internal + } + + interface Customer { + id: ID! + email: String! + } + + type ch_Customer implements Customer @key(fields: "id") { + id: ID! + email: String! + } + """, + """ + # name: de + schema { + query: Query + } + + type Query { + de_customerById(id: ID! @is(field: "id")): de_Customer @lookup @internal + } + + interface Customer { + id: ID! + email: String! + } + + type de_Customer implements Customer @key(fields: "id") { + id: ID! + email: String! + } + """); +} 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 new file mode 100644 index 00000000000..7d69b1eade0 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/InterfaceLookupPlanningTests.Abstract_Customer_Interface_With_Email_Is_Plannable.yaml @@ -0,0 +1,78 @@ +operation: + - document: | + { + ordersByAgent { + edges { + node { + id + customer { + __typename + id + id @fusion__requirement + email + } + } + } + } + } + hash: 123456789101112 + searchSpace: 1 + expandedNodes: 2 +nodes: + - id: 1 + type: Operation + schema: common + operation: | + query Op_123456789101112_1 { + ordersByAgent { + edges { + node { + id + customer { + __typename + id + } + } + } + } + } + - id: 2 + type: Operation + schema: de + operation: | + query Op_123456789101112_2( + $__fusion_1_id: ID! + ) { + de_customerById(id: $__fusion_1_id) { + __typename + email + } + } + source: $.de_customerById + target: $.ordersByAgent.edges.node.customer + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: Operation + schema: ch + operation: | + query Op_123456789101112_3( + $__fusion_2_id: ID! + ) { + ch_customerById(id: $__fusion_2_id) { + __typename + email + } + } + source: $.ch_customerById + target: $.ordersByAgent.edges.node.customer + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 1 From 23e1d6a53675e5dbff3c6f999eea07c19e115cc8 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 19 Mar 2026 14:30:54 +0100 Subject: [PATCH 2/2] Update snapshots --- ...Type_Refinements_With_Interface_Lookup.yaml | 18 ++++++++++++------ ..._Lookup_And_Field_From_Specific_Source.yaml | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) 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 0893713efba..b8e0cb08705 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 @@ -82,7 +82,10 @@ sourceSchemas: ) { votableById(id: $__fusion_1_id) { __typename - viewerCanVote + ... on Discussion { + __typename + viewerCanVote + } } } variables: | @@ -112,8 +115,8 @@ operationPlan: } } hash: fb4e28df20f489a10c0f71a560445d04 - searchSpace: 1 - expandedNodes: 2 + searchSpace: 2 + expandedNodes: 3 nodes: - id: 1 type: Operation @@ -134,11 +137,14 @@ operationPlan: ) { votableById(id: $__fusion_1_id) { __typename - viewerCanVote + ... on Discussion { + __typename + viewerCanVote + } } } - source: $.votableById - target: $.votable + source: $.votableById + target: $.votable requirements: - name: __fusion_1_id selectionMap: >- 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 06a4602b6b2..285a8291783 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 @@ -101,7 +101,10 @@ sourceSchemas: ) { votableById(id: $__fusion_1_id) { __typename - totalVotes + ... on Discussion { + __typename + totalVotes + } } } variables: | @@ -131,8 +134,8 @@ operationPlan: } } hash: 1bcbe788569dbb79a305829bc99385cf - searchSpace: 1 - expandedNodes: 2 + searchSpace: 2 + expandedNodes: 3 nodes: - id: 1 type: Operation @@ -153,11 +156,14 @@ operationPlan: ) { votableById(id: $__fusion_1_id) { __typename - totalVotes + ... on Discussion { + __typename + totalVotes + } } } - source: $.votableById - target: $.votable + source: $.votableById + target: $.votable requirements: - name: __fusion_1_id selectionMap: >-