Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ parentSelectionPath is null
var current = leftovers ?? selections;
var subgraph = _config.GetBestMatchingSubgraph(
operation,
parentSelectionPath,
current,
selectionSetTypeMetadata);
var executionStep = new SelectionExecutionStep(
Expand Down Expand Up @@ -232,14 +233,14 @@ parentSelectionPath is null
path.RemoveAt(pathIndex);
}

// if the current execution step has now way to resolve the data
// if the current execution step has no way to resolve the data
// we will try to resolve it from the root.
if (executionStep.ParentSelection is not null
&& executionStep.ParentSelectionPath is not null
&& executionStep.Resolver is null
&& executionStep.SelectionResolvers.Count == 0)
{
if (!EnsureStepCanBeResolvedFromRoot(
if (!_config.EnsureStepCanBeResolvedFromRoot(
executionStep.SubgraphName,
executionStep.ParentSelectionPath))
{
Expand Down Expand Up @@ -628,6 +629,7 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps(
? availableSubgraphs[0]
: _config.GetBestMatchingSubgraph(
operation,
null,
entityTypeSelectionSet.Selections,
entityTypeMetadata,
availableSubgraphs);
Expand Down Expand Up @@ -914,27 +916,6 @@ private static bool IsNodeField(IObjectField field, IOperation operation)
&& field.DeclaringType.Equals(operation.RootType)
&& (field.Name.EqualsOrdinal("node") || field.Name.EqualsOrdinal("nodes"));

private bool EnsureStepCanBeResolvedFromRoot(
string subgraphName,
SelectionPath path)
{
var current = path;

while (current is not null)
{
var typeMetadata = _config.GetType<ObjectTypeMetadata>(current.Selection.DeclaringType.Name);

if (!typeMetadata.Fields[current.Selection.Field.Name].Bindings.ContainsSubgraph(subgraphName))
{
return false;
}

current = current.Parent;
}

return true;
}

private readonly struct BacklogItem(
ISelection parentSelection,
SelectionPath? selectionPath,
Expand Down
32 changes: 32 additions & 0 deletions src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ internal static class QueryPlannerHelpers
public static string GetBestMatchingSubgraph(
this FusionGraphConfiguration configuration,
IOperation operation,
SelectionPath? parentSelectionPath,
IReadOnlyList<ISelection> selections,
ObjectTypeMetadata typeMetadataContext,
IReadOnlyList<string>? availableSubgraphs = null)
Expand All @@ -21,6 +22,7 @@ public static string GetBestMatchingSubgraph(
EvaluateSubgraphCompatibilityScore(
configuration,
operation,
parentSelectionPath,
selections,
typeMetadataContext,
subgraphName);
Expand All @@ -38,6 +40,7 @@ public static string GetBestMatchingSubgraph(
private static int EvaluateSubgraphCompatibilityScore(
FusionGraphConfiguration configuration,
IOperation operation,
SelectionPath? parentSelectionPath,
IReadOnlyList<ISelection> selections,
ObjectTypeMetadata typeMetadataContext,
string schemaName)
Expand All @@ -58,6 +61,13 @@ private static int EvaluateSubgraphCompatibilityScore(
{
score++;

if (parentSelectionPath is null ||
typeMetadataContext.Resolvers.ContainsResolvers(schemaName) ||
configuration.EnsureStepCanBeResolvedFromRoot(schemaName, parentSelectionPath))
{
score++;
}

if (selection.SelectionSet is not null)
{
foreach (var possibleType in operation.GetPossibleTypes(selection))
Expand All @@ -73,4 +83,26 @@ private static int EvaluateSubgraphCompatibilityScore(

return score;
}

public static bool EnsureStepCanBeResolvedFromRoot(
this FusionGraphConfiguration configuration,
string subgraphName,
SelectionPath path)
{
var current = path;

while (current is not null)
{
var typeMetadata = configuration.GetType<ObjectTypeMetadata>(current.Selection.DeclaringType.Name);

if (!typeMetadata.Fields[current.Selection.Field.Name].Bindings.ContainsSubgraph(subgraphName))
{
return false;
}

current = current.Parent;
}

return true;
}
}
75 changes: 75 additions & 0 deletions src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2895,6 +2895,81 @@ query Test($number: Int!) {
Assert.Null(result.ExpectOperationResult().Errors);
}

[Fact]
public async Task Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph()
{
// arrange
var subgraphA = await TestSubgraph.CreateAsync(
"""
type Query {
viewer: Viewer
}

type Viewer {
product: Product!
}

type Product implements Node {
id: ID!
}

interface Node {
id: ID!
}
""");

var subgraphB = await TestSubgraph.CreateAsync(
"""
type Query {
test: Test!
}

type Test {
id: ID!
}

type Product {
id: ID!
name: String!
}
""");

var subgraphC = await TestSubgraph.CreateAsync(
"""
type Query {
node(id: ID!): Node
}

type Product implements Node {
id: ID!
name: String!
}

interface Node {
id: ID!
}
""");

using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]);
var executor = await subgraphs.GetExecutorAsync();
var request = """
query {
viewer {
product {
id
name
}
}
}
""";

// act
var result = await executor.ExecuteAsync(request);

// assert
MatchMarkdownSnapshot(request, result);
}

public sealed class HotReloadConfiguration : IObservable<GatewayConfiguration>
{
private GatewayConfiguration _configuration;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph

## Result

```json
{
"data": {
"viewer": {
"product": {
"id": "1",
"name": "string"
}
}
}
}
```

## Request

```graphql
{
viewer {
product {
id
name
}
}
}
```

## QueryPlan Hash

```text
68F7ACC73F0431E50C4145FBB512E15D04D79198
```

## QueryPlan

```json
{
"document": "{ viewer { product { id name } } }",
"rootNode": {
"type": "Sequence",
"nodes": [
{
"type": "Resolve",
"subgraph": "Subgraph_1",
"document": "query fetch_viewer_1 { viewer { product { id __fusion_exports__1: id } } }",
"selectionSetId": 0,
"provides": [
{
"variable": "__fusion_exports__1"
}
]
},
{
"type": "Compose",
"selectionSetIds": [
0
]
},
{
"type": "Resolve",
"subgraph": "Subgraph_3",
"document": "query fetch_viewer_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Product { name } } }",
"selectionSetId": 2,
"path": [
"node"
],
"requires": [
{
"variable": "__fusion_exports__1"
}
]
},
{
"type": "Compose",
"selectionSetIds": [
2
]
}
]
},
"state": {
"__fusion_exports__1": "Product_id"
}
}
```

Loading