Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,15 @@ parentSelectionPath is null
var current = leftovers ?? selections;
var subgraph = _config.GetBestMatchingSubgraph(
operation,
parentSelectionPath,
current,
selectionSetTypeMetadata);

if (subgraph is null)
{
throw ThrowHelper.NoResolverInContext();
}

var executionStep = new SelectionExecutionStep(
context.NextStepId(),
subgraph,
Expand Down Expand Up @@ -232,14 +239,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.EnsurePathCanBeResolvedFromRoot(
executionStep.SubgraphName,
executionStep.ParentSelectionPath))
{
Expand Down Expand Up @@ -628,10 +635,16 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps(
? availableSubgraphs[0]
: _config.GetBestMatchingSubgraph(
operation,
null,
entityTypeSelectionSet.Selections,
entityTypeMetadata,
availableSubgraphs);

if (subgraph is null)
{
throw ThrowHelper.NoResolverInContext();
}

var field = nodeSelection.Field;
var fieldInfo = queryTypeMetadata.Fields[field.Name];
var executionStep = new SelectionExecutionStep(
Expand Down Expand Up @@ -914,27 +927,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
48 changes: 44 additions & 4 deletions src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@ namespace HotChocolate.Fusion.Planning;

internal static class QueryPlannerHelpers
{
public static string GetBestMatchingSubgraph(
public static string? GetBestMatchingSubgraph(
this FusionGraphConfiguration configuration,
IOperation operation,
SelectionPath? parentSelectionPath,
IReadOnlyList<ISelection> selections,
ObjectTypeMetadata typeMetadataContext,
IReadOnlyList<string>? availableSubgraphs = null)
{
var bestScore = 0;
var bestSubgraph = configuration.SubgraphNames[0];
var bestSubgraph = default(string?);

foreach (var subgraphName in availableSubgraphs ?? configuration.SubgraphNames)
{
var score =
EvaluateSubgraphCompatibilityScore(
configuration,
operation,
parentSelectionPath,
selections,
typeMetadataContext,
subgraphName);
Expand All @@ -38,10 +40,13 @@ public static string GetBestMatchingSubgraph(
private static int EvaluateSubgraphCompatibilityScore(
FusionGraphConfiguration configuration,
IOperation operation,
SelectionPath? parentSelectionPath,
IReadOnlyList<ISelection> selections,
ObjectTypeMetadata typeMetadataContext,
string schemaName)
{
var pathCanBeResolvedFromRoot = configuration.EnsurePathCanBeResolvedFromRoot(schemaName, parentSelectionPath);

var score = 0;
var stack = new Stack<(IReadOnlyList<ISelection> selections, ObjectTypeMetadata typeContext)>();
stack.Push((selections, typeMetadataContext));
Expand All @@ -50,11 +55,24 @@ private static int EvaluateSubgraphCompatibilityScore(
{
var (currentSelections, currentTypeContext) = stack.Pop();

if (currentSelections.Count == 0)
{
score++;
}

foreach (var selection in currentSelections)
{
if (!selection.Field.IsIntrospectionField &&
currentTypeContext.Fields[selection.Field.Name].Bindings
.ContainsSubgraph(schemaName))
currentTypeContext
.Fields[selection.Field.Name]
.Bindings
.ContainsSubgraph(schemaName) &&
(pathCanBeResolvedFromRoot ||
currentTypeContext
.Fields[selection.Field.Name]
.Resolvers
.ContainsResolvers(schemaName) ||
typeMetadataContext.Resolvers.ContainsResolvers(schemaName)))
{
score++;

Expand All @@ -73,4 +91,26 @@ private static int EvaluateSubgraphCompatibilityScore(

return score;
}

public static bool EnsurePathCanBeResolvedFromRoot(
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
Expand Up @@ -29,7 +29,7 @@ query ProductReviews($id: ID!, $first: Int!) {
## QueryPlan Hash

```text
3EB74A019DB95A4FA68CF9569951EA91659E7186
90288E30B42792662254D8D4CF683A745331D191
```

## QueryPlan
Expand All @@ -50,7 +50,7 @@ query ProductReviews($id: ID!, $first: Int!) {
"type": "User",
"node": {
"type": "Resolve",
"subgraph": "Accounts",
"subgraph": "Reviews2",
"document": "query ProductReviews_1($id: ID!) { node(id: $id) { ... on User { __typename } } }",
"selectionSetId": 0,
"forwardedVariables": [
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