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
54 changes: 51 additions & 3 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,23 +40,37 @@ public static string GetBestMatchingSubgraph(
private static int EvaluateSubgraphCompatibilityScore(
FusionGraphConfiguration configuration,
IOperation operation,
SelectionPath? parentSelectionPath,
IReadOnlyList<ISelection> selections,
ObjectTypeMetadata typeMetadataContext,
string schemaName)
{
var score = 0;

var pathOrTypeCanBeResolvedFromRoot = EnsurePathOrTypeCanBeResolvedFromRoot(configuration, parentSelectionPath, typeMetadataContext, schemaName);

var stack = new Stack<(IReadOnlyList<ISelection> selections, ObjectTypeMetadata typeContext)>();
stack.Push((selections, typeMetadataContext));

while (stack.Count > 0)
{
var (currentSelections, currentTypeContext) = stack.Pop();

// If there are no selections at the current node, it means the subgraph
// can resolve the path up to this point without requiring any further fields, so we increase the score.
if (currentSelections.Count == 0)
{
score++;
}

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

Expand All @@ -73,4 +89,36 @@ private static int EvaluateSubgraphCompatibilityScore(

return score;
}

private static bool EnsurePathOrTypeCanBeResolvedFromRoot(
FusionGraphConfiguration configuration,
SelectionPath? parentSelectionPath,
ObjectTypeMetadata typeMetadataContext,
string schemaName)
{
return typeMetadataContext.Resolvers.ContainsResolvers(schemaName) ||
configuration.EnsurePathCanBeResolvedFromRoot(schemaName, parentSelectionPath);
}

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;
}
}
144 changes: 144 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,150 @@ 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);
}

[Fact]
public async Task Subgraph_Containing_More_Selections_Is_Chosen()
{
// arrange
var subgraphA = await TestSubgraph.CreateAsync(
"""
type Query {
productBySlug: Product
}

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

var subgraphB = await TestSubgraph.CreateAsync(
"""
type Query {
productById(id: ID!): Product
}

type Product {
id: ID!
author: Author
}

type Author {
name: String!
}
""");

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

type Product {
id: ID!
author: Author
}

type Author {
name: String!
rating: Int!
}
""");

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

// 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
Loading
Loading