From 7d1e1762602da3f7b3eaad7fa7679e81b370c6f8 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:44:51 +0100 Subject: [PATCH 1/4] Add reproduction --- .../test/Core.Tests/DemoIntegrationTests.cs | 147 ++++++++++++++++++ .../DemoIntegrationTests.Test.md | 48 ++++++ 2 files changed, 195 insertions(+) create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index d6a7be5e7e9..0102ddf45b4 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -1916,6 +1916,153 @@ public async Task ResolveByKey_Handles_Null_Item_Correctly() await snapshot.MatchMarkdownAsync(); } + [Fact] + public async Task Test() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + builder + .AddDocumentFromString( + """ + type Query { + node(id: ID!): Node + wrapper: Wrapper + } + + type Wrapper implements Node { + id: ID! + items: [ItemUnion!]! + } + + type Item1 implements Node { + id: ID! + product: Product + } + + type Item2 implements Node { + id: ID! + product: Product + } + + type Item3 implements Node { + id: ID! + product: Product + } + + type Product implements Node { + id: ID! + } + + interface Node { + id: ID! + } + + union ItemUnion = Item1 | Item2 | Item3 + """) + .AddResolverMocking() + .UseDefaultPipeline() + .UseRequest(_ => context => + { + context.Result = OperationResultBuilder.New() + .SetData(new Dictionary + { + ["wrapper"] = new Dictionary + { + ["id"] = "1", + ["items"] = new List + { + new Dictionary + { + ["__typename"] = "Item1", + ["id"] = "2", + ["product"] = new Dictionary + { + ["id"] = "22" + } + }, + new Dictionary + { + ["__typename"] = "Item2", + ["id"] = "3", + ["product"] = new Dictionary + { + ["id"] = "33" + } + }, + new Dictionary + { + ["__typename"] = "Item3", + ["id"] = "4", + ["product"] = new Dictionary + { + ["id"] = "44" + } + }, + } + } + }) + .Build(); + return default; + })); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + nodes(ids: [ID!]!): [Node]! + } + + type Product implements Node { + id: ID! + name: String! + } + + interface Node { + id: ID! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + wrapper { + id + items { + __typename + ... on Item1 { + id + product { + id + name + } + } + ... on Item2 { + id + product { + id + name + } + } + ... on Item3 { + id + product { + id + name + } + } + } + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + MatchMarkdownSnapshot(request, result); + } + public sealed class HotReloadConfiguration : IObservable { private GatewayConfiguration _configuration; diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md new file mode 100644 index 00000000000..14a4c1b379e --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md @@ -0,0 +1,48 @@ +# Test + +## Result + +```json +{ + "errors": [ + { + "message": "Unexpected Execution Error" + } + ] +} +``` + +## Request + +```graphql +{ + wrapper { + id + items { + __typename + ... on Item1 { + id + product { + id + name + } + } + ... on Item2 { + id + product { + id + name + } + } + ... on Item3 { + id + product { + id + name + } + } + } + } +} +``` + From d554ff2ef5a08df1e8a9916763471618a71153a6 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:25:17 +0100 Subject: [PATCH 2/4] Add more tests --- .../test/Core.Tests/DemoIntegrationTests.cs | 147 --------- .../Fusion/test/Core.Tests/UnionTests.cs | 294 +++++++++++++++++- ..._With_Differing_Union_Item_Dependencies.md | 176 +++++++++++ ...ches_With_Differing_Resolve_Nodes_Item1.md | 124 ++++++++ ...ches_With_Differing_Resolve_Nodes_Item2.md | 124 ++++++++ 5 files changed, 711 insertions(+), 154 deletions(-) create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index 0102ddf45b4..d6a7be5e7e9 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -1916,153 +1916,6 @@ public async Task ResolveByKey_Handles_Null_Item_Correctly() await snapshot.MatchMarkdownAsync(); } - [Fact] - public async Task Test() - { - // arrange - var subgraphA = await TestSubgraph.CreateAsync( - configure: builder => - builder - .AddDocumentFromString( - """ - type Query { - node(id: ID!): Node - wrapper: Wrapper - } - - type Wrapper implements Node { - id: ID! - items: [ItemUnion!]! - } - - type Item1 implements Node { - id: ID! - product: Product - } - - type Item2 implements Node { - id: ID! - product: Product - } - - type Item3 implements Node { - id: ID! - product: Product - } - - type Product implements Node { - id: ID! - } - - interface Node { - id: ID! - } - - union ItemUnion = Item1 | Item2 | Item3 - """) - .AddResolverMocking() - .UseDefaultPipeline() - .UseRequest(_ => context => - { - context.Result = OperationResultBuilder.New() - .SetData(new Dictionary - { - ["wrapper"] = new Dictionary - { - ["id"] = "1", - ["items"] = new List - { - new Dictionary - { - ["__typename"] = "Item1", - ["id"] = "2", - ["product"] = new Dictionary - { - ["id"] = "22" - } - }, - new Dictionary - { - ["__typename"] = "Item2", - ["id"] = "3", - ["product"] = new Dictionary - { - ["id"] = "33" - } - }, - new Dictionary - { - ["__typename"] = "Item3", - ["id"] = "4", - ["product"] = new Dictionary - { - ["id"] = "44" - } - }, - } - } - }) - .Build(); - return default; - })); - - var subgraphB = await TestSubgraph.CreateAsync( - """ - type Query { - nodes(ids: [ID!]!): [Node]! - } - - type Product implements Node { - id: ID! - name: String! - } - - interface Node { - id: ID! - } - """); - - using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); - var executor = await subgraphs.GetExecutorAsync(); - var request = """ - query { - wrapper { - id - items { - __typename - ... on Item1 { - id - product { - id - name - } - } - ... on Item2 { - id - product { - id - name - } - } - ... on Item3 { - id - product { - id - name - } - } - } - } - } - """; - - // act - var result = await executor.ExecuteAsync(request); - - // assert - MatchMarkdownSnapshot(request, result); - } - public sealed class HotReloadConfiguration : IObservable { private GatewayConfiguration _configuration; diff --git a/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs index 60392133e65..313d4f162b4 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs @@ -5,6 +5,7 @@ using HotChocolate.Fusion.Shared; using HotChocolate.Skimmed.Serialization; using HotChocolate.Types; +using HotChocolate.Types.Relay; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; using static HotChocolate.Fusion.Shared.DemoProjectSchemaExtensions; @@ -13,14 +14,9 @@ namespace HotChocolate.Fusion; -public class UnionTests +public class UnionTests(ITestOutputHelper output) { - private readonly Func _logFactory; - - public UnionTests(ITestOutputHelper output) - { - _logFactory = () => new TestCompositionLog(output); - } + private readonly Func _logFactory = () => new TestCompositionLog(output); [Fact] public async Task Error_Union_With_Inline_Fragment() @@ -264,6 +260,290 @@ mutation Upload($input: UploadProductPictureInput!) { await snapshot.MatchMarkdownAsync(cts.Token); } + [Fact] + public async Task Union_Two_Branches_With_Differing_Resolve_Nodes_Item1() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + var subgraphC = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + union(item: 1) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + Assert.False(subgraphC.HasReceivedRequest); + } + + [Fact] + public async Task Union_Two_Branches_With_Differing_Resolve_Nodes_Item2() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + var subgraphC = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + union(item: 3) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + Assert.False(subgraphB.HasReceivedRequest); + } + + [Fact] + public async Task Union_List_With_Differing_Union_Item_Dependencies() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + } + + [ObjectType("Query")] + public class SubgraphA_Query + { + public ISomeUnion GetUnion(int item) + { + return item switch + { + 1 => new SubgraphA_Item1("Something", new SubgraphA_Product(2)), + _ => new SubgraphA_Item3(true, new SubgraphA_Review(1)) + }; + } + + public List GetListOfUnion() + { + return + [ + new SubgraphA_Item1("Something", new SubgraphA_Product(2)), + new SubgraphA_Item2(123, new SubgraphA_Product(4)), + new SubgraphA_Item3(true, new SubgraphA_Review(1)) + ]; + } + } + + [UnionType("SomeUnion")] + public interface ISomeUnion + { + } + + [ObjectType("Item1")] + public record SubgraphA_Item1(string Something, SubgraphA_Product Product) : ISomeUnion; + + [ObjectType("Item2")] + public record SubgraphA_Item2(int Other, SubgraphA_Product Product) : ISomeUnion; + + [ObjectType("Item3")] + public record SubgraphA_Item3(bool Another, SubgraphA_Review Review) : ISomeUnion; + + [Node] + [ObjectType("Product")] + public record SubgraphA_Product(int Id); + + [Node] + [ObjectType("Product")] + public record SubgraphB_Product(int Id, string Name) + { + [NodeResolver] + public static SubgraphB_Product Get(int id) + => new SubgraphB_Product(id, "Product_" + id); + } + + [Node] + [ObjectType("Review")] + public record SubgraphA_Review(int Id); + + [Node] + [ObjectType("Review")] + public record SubgraphB_Review(int Id, int Score) + { + [NodeResolver] + public static SubgraphB_Review Get(int id) + => new SubgraphB_Review(id, id % 5); + } + private sealed class NoWebSockets : IWebSocketConnectionFactory { public IWebSocketConnection CreateConnection(string name) diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md new file mode 100644 index 00000000000..cac74e6c808 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md @@ -0,0 +1,176 @@ +# Union_List_With_Differing_Union_Item_Dependencies + +## Result + +```json +{ + "errors": [ + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 1, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 0, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + } + ], + "data": null +} +``` + +## Request + +```graphql +{ + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +51CA519135EDC9C49C71D22D7EF0562D417753EA +``` + +## QueryPlan + +```json +{ + "document": "{ listOfUnion { __typename ... on Item1 { something product { id name } } ... on Item2 { other product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_listOfUnion_1 { listOfUnion { __typename ... on Item3 { __typename another review { id __fusion_exports__1: id } } ... on Item2 { __typename other product { id __fusion_exports__2: id } } ... on Item1 { __typename something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on Review { score __fusion_exports__1: id } } }", + "selectionSetId": 4, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_3($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_4($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md new file mode 100644 index 00000000000..e3c3709ad37 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md @@ -0,0 +1,124 @@ +# Union_Two_Branches_With_Differing_Resolve_Nodes_Item1 + +## Result + +```json +{ + "data": { + "union": { + "something": "Something", + "product": { + "id": "UHJvZHVjdDoy", + "name": "Product_2" + } + } + } +} +``` + +## Request + +```graphql +{ + union(item: 1) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +6162CA1B5815FE006F5BEC9B4A2F51035B1990DC +``` + +## QueryPlan + +```json +{ + "document": "{ union(item: 1) { ... on Item1 { something product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_union_1 { union(item: 1) { __typename ... on Item3 { another review { id __fusion_exports__1: id } } ... on Item2 { } ... on Item1 { something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_3", + "document": "query fetch_union_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Review { score } } }", + "selectionSetId": 4, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_union_3($__fusion_exports__2: ID!) { node(id: $__fusion_exports__2) { ... on Product { name } } }", + "selectionSetId": 5, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md new file mode 100644 index 00000000000..0d4bfca4cb7 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md @@ -0,0 +1,124 @@ +# Union_Two_Branches_With_Differing_Resolve_Nodes_Item2 + +## Result + +```json +{ + "data": { + "union": { + "another": true, + "review": { + "id": "UmV2aWV3OjE=", + "score": 1 + } + } + } +} +``` + +## Request + +```graphql +{ + union(item: 3) { + ... on Item1 { + something + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +18FEEDD69A2D3612AD7DAAA5D221864EE5A17514 +``` + +## QueryPlan + +```json +{ + "document": "{ union(item: 3) { ... on Item1 { something product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_union_1 { union(item: 3) { __typename ... on Item3 { another review { id __fusion_exports__1: id } } ... on Item2 { } ... on Item1 { something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_3", + "document": "query fetch_union_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Review { score } } }", + "selectionSetId": 4, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_2", + "document": "query fetch_union_3($__fusion_exports__2: ID!) { node(id: $__fusion_exports__2) { ... on Product { name } } }", + "selectionSetId": 5, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + From 09bc10df7fe915fc107a0c5b4e9808f2facd7cd8 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Sun, 10 Nov 2024 14:48:34 +0100 Subject: [PATCH 3/4] Isolate problem further --- .../Fusion/test/Core.Tests/UnionTests.cs | 91 +++++++- .../DemoIntegrationTests.Test.md | 48 ---- ..._With_Differing_Union_Item_Dependencies.md | 105 +++++---- ..._Union_Item_Dependencies_SameSelections.md | 212 ++++++++++++++++++ ...ches_With_Differing_Resolve_Nodes_Item1.md | 4 +- ...ches_With_Differing_Resolve_Nodes_Item2.md | 4 +- .../Fusion/test/Shared/TestSubgraph.cs | 8 +- 7 files changed, 366 insertions(+), 106 deletions(-) delete mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md diff --git a/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs index 313d4f162b4..8d03ebabc05 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/UnionTests.cs @@ -408,6 +408,81 @@ ... on Item3 { Assert.False(subgraphB.HasReceivedRequest); } + [Fact] + public async Task Union_List_With_Differing_Union_Item_Dependencies_SameSelections() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .AddType() + .ModifyOptions(o => o.EnsureAllNodesCanBeResolved = false) + .AddGlobalObjectIdentification(); + }); + + var subgraphB = await TestSubgraph.CreateAsync( + configure: builder => + { + builder + .AddQueryType() + .AddType() + .AddType() + .AddGlobalObjectIdentification(); + }); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + // Ideally it would just be one request, but that's for another day... + Assert.Equal(3, subgraphB.NumberOfReceivedRequests); + } + [Fact] public async Task Union_List_With_Differing_Union_Item_Dependencies() { @@ -453,7 +528,6 @@ ... on Item1 { ... on Item2 { other product { - id name } } @@ -479,6 +553,8 @@ ... on Item3 { var snapshot = new Snapshot(); CollectSnapshotData(snapshot, request, result); await snapshot.MatchMarkdownAsync(); + // Ideally it would just be one request, but that's for another day... + Assert.Equal(3, subgraphB.NumberOfReceivedRequests); } [ObjectType("Query")] @@ -488,8 +564,8 @@ public ISomeUnion GetUnion(int item) { return item switch { - 1 => new SubgraphA_Item1("Something", new SubgraphA_Product(2)), - _ => new SubgraphA_Item3(true, new SubgraphA_Review(1)) + 1 => new SubgraphA_Item1("Something", new SubgraphA_Product(1)), + _ => new SubgraphA_Item3(true, new SubgraphA_Review(2)) }; } @@ -497,9 +573,12 @@ public List GetListOfUnion() { return [ - new SubgraphA_Item1("Something", new SubgraphA_Product(2)), - new SubgraphA_Item2(123, new SubgraphA_Product(4)), - new SubgraphA_Item3(true, new SubgraphA_Review(1)) + new SubgraphA_Item1("Something", new SubgraphA_Product(1)), + new SubgraphA_Item2(123, new SubgraphA_Product(2)), + new SubgraphA_Item3(true, new SubgraphA_Review(3)), + new SubgraphA_Item1("Something", new SubgraphA_Product(4)), + new SubgraphA_Item2(123, new SubgraphA_Product(5)), + new SubgraphA_Item3(true, new SubgraphA_Review(6)) ]; } } diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md deleted file mode 100644 index 14a4c1b379e..00000000000 --- a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md +++ /dev/null @@ -1,48 +0,0 @@ -# Test - -## Result - -```json -{ - "errors": [ - { - "message": "Unexpected Execution Error" - } - ] -} -``` - -## Request - -```graphql -{ - wrapper { - id - items { - __typename - ... on Item1 { - id - product { - id - name - } - } - ... on Item2 { - id - product { - id - name - } - } - ... on Item3 { - id - product { - id - name - } - } - } - } -} -``` - diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md index cac74e6c808..182019a3c69 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies.md @@ -4,45 +4,56 @@ ```json { - "errors": [ - { - "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 15, - "column": 9 + "data": { + "listOfUnion": [ + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDox", + "name": "Product_1" } - ], - "path": [ - "listOfUnion", - 1, - "product", - "name" - ], - "extensions": { - "code": "HC0018" - } - }, - { - "message": "Cannot return null for non-nullable field.", - "locations": [ - { - "line": 15, - "column": 9 + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "name": "Product_2" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjM=", + "score": 3 + } + }, + { + "__typename": "Item1", + "something": "Something", + "product": { + "id": "UHJvZHVjdDo0", + "name": "Product_4" + } + }, + { + "__typename": "Item2", + "other": 123, + "product": { + "name": "Product_5" + } + }, + { + "__typename": "Item3", + "another": true, + "review": { + "id": "UmV2aWV3OjY=", + "score": 1 } - ], - "path": [ - "listOfUnion", - 0, - "product", - "name" - ], - "extensions": { - "code": "HC0018" } - } - ], - "data": null + ] + } } ``` @@ -62,7 +73,6 @@ ... on Item2 { other product { - id name } } @@ -80,21 +90,21 @@ ## QueryPlan Hash ```text -51CA519135EDC9C49C71D22D7EF0562D417753EA +6F3D15770F165F5A7166C5598F4B1A7D6910A88D ``` ## QueryPlan ```json { - "document": "{ listOfUnion { __typename ... on Item1 { something product { id name } } ... on Item2 { other product { id name } } ... on Item3 { another review { id score } } } }", + "document": "{ listOfUnion { __typename ... on Item1 { something product { id name } } ... on Item2 { other product { name } } ... on Item3 { another review { id score } } } }", "rootNode": { "type": "Sequence", "nodes": [ { "type": "Resolve", "subgraph": "Subgraph_1", - "document": "query fetch_listOfUnion_1 { listOfUnion { __typename ... on Item3 { __typename another review { id __fusion_exports__1: id } } ... on Item2 { __typename other product { id __fusion_exports__2: id } } ... on Item1 { __typename something product { id __fusion_exports__2: id } } } }", + "document": "query fetch_listOfUnion_1 { listOfUnion { __typename ... on Item3 { __typename another review { id __fusion_exports__1: id } } ... on Item2 { __typename other product { __fusion_exports__2: id } } ... on Item1 { __typename something product { id __fusion_exports__3: id } } } }", "selectionSetId": 0, "provides": [ { @@ -102,6 +112,9 @@ }, { "variable": "__fusion_exports__2" + }, + { + "variable": "__fusion_exports__3" } ] }, @@ -145,14 +158,14 @@ { "type": "ResolveByKeyBatch", "subgraph": "Subgraph_2", - "document": "query fetch_listOfUnion_4($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", - "selectionSetId": 5, + "document": "query fetch_listOfUnion_4($__fusion_exports__3: [ID!]!) { nodes(ids: $__fusion_exports__3) { ... on Product { name __fusion_exports__3: id } } }", + "selectionSetId": 6, "path": [ "nodes" ], "requires": [ { - "variable": "__fusion_exports__2" + "variable": "__fusion_exports__3" } ] } @@ -162,14 +175,16 @@ "type": "Compose", "selectionSetIds": [ 4, - 5 + 5, + 6 ] } ] }, "state": { "__fusion_exports__1": "Review_id", - "__fusion_exports__2": "Product_id" + "__fusion_exports__2": "Product_id", + "__fusion_exports__3": "Product_id" } } ``` diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md new file mode 100644 index 00000000000..a4f8111a225 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_List_With_Differing_Union_Item_Dependencies_SameSelections.md @@ -0,0 +1,212 @@ +# Union_List_With_Differing_Union_Item_Dependencies_SameSelections + +## Result + +```json +{ + "errors": [ + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 4, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 3, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 1, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 15, + "column": 9 + } + ], + "path": [ + "listOfUnion", + 0, + "product", + "name" + ], + "extensions": { + "code": "HC0018" + } + } + ], + "data": null +} +``` + +## Request + +```graphql +{ + listOfUnion { + __typename + ... on Item1 { + something + product { + id + name + } + } + ... on Item2 { + other + product { + id + name + } + } + ... on Item3 { + another + review { + id + score + } + } + } +} +``` + +## QueryPlan Hash + +```text +51CA519135EDC9C49C71D22D7EF0562D417753EA +``` + +## QueryPlan + +```json +{ + "document": "{ listOfUnion { __typename ... on Item1 { something product { id name } } ... on Item2 { other product { id name } } ... on Item3 { another review { id score } } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_listOfUnion_1 { listOfUnion { __typename ... on Item3 { __typename another review { id __fusion_exports__1: id } } ... on Item2 { __typename other product { id __fusion_exports__2: id } } ... on Item1 { __typename something product { id __fusion_exports__2: id } } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on Review { score __fusion_exports__1: id } } }", + "selectionSetId": 4, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_3($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_listOfUnion_4($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { name __fusion_exports__2: id } } }", + "selectionSetId": 5, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 4, + 5 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Review_id", + "__fusion_exports__2": "Product_id" + } +} +``` + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md index e3c3709ad37..3202ff88d96 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item1.md @@ -8,8 +8,8 @@ "union": { "something": "Something", "product": { - "id": "UHJvZHVjdDoy", - "name": "Product_2" + "id": "UHJvZHVjdDox", + "name": "Product_1" } } } diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md index 0d4bfca4cb7..3c7d274db82 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/UnionTests.Union_Two_Branches_With_Differing_Resolve_Nodes_Item2.md @@ -8,8 +8,8 @@ "union": { "another": true, "review": { - "id": "UmV2aWV3OjE=", - "score": 1 + "id": "UmV2aWV3OjI=", + "score": 2 } } } diff --git a/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs b/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs index f23d4b0f40b..5406d830b90 100644 --- a/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs +++ b/src/HotChocolate/Fusion/test/Shared/TestSubgraph.cs @@ -46,7 +46,7 @@ public static async Task CreateAsync( { app.Use(next => context => { - testContext.HasReceivedRequest = true; + testContext.NumberOfReceivedRequests++; return next(context); }); @@ -58,10 +58,12 @@ public static async Task CreateAsync( return new TestSubgraph(testServer, schema, testContext, extensions, isOffline); } - public bool HasReceivedRequest => Context.HasReceivedRequest; + public int NumberOfReceivedRequests => Context.NumberOfReceivedRequests; + + public bool HasReceivedRequest => Context.NumberOfReceivedRequests > 0; } public class SubgraphTestContext { - public bool HasReceivedRequest { get; set; } + public int NumberOfReceivedRequests { get; set; } } From ba91007231886caeae6b103ac877a4e7930e4bbd Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Sun, 17 Nov 2024 14:25:14 +0100 Subject: [PATCH 4/4] Showcase issue without unions --- .../test/Core.Tests/DemoIntegrationTests.cs | 73 +++++++ .../DemoIntegrationTests.Test.md | 191 ++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index d6a7be5e7e9..9413161336d 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -16,6 +16,79 @@ public class DemoIntegrationTests(ITestOutputHelper output) { private readonly Func _logFactory = () => new TestCompositionLog(output); + [Fact] + public async Task Test() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + + type Query { + productsA: [Product] + productsB: [Product] + node(id: ID!): Node + nodes(ids: [ID!]!): [Node]! + } + + type Product implements Node { + id: ID! + name: String! + } + """); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + interface Node { + id: ID! + } + + type Query { + node(id: ID!): Node + nodes(ids: [ID!]!): [Node]! + } + + type Product implements Node { + id: ID! + price: Float! + reviewCount: Int! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB]); + var executor = await subgraphs.GetExecutorAsync(); + var request = Parse(""" + query { + productsA { + id + name + price + reviewCount + } + productsB { + id + name + price + reviewCount + } + } + """); + + // act + var result = await executor.ExecuteAsync( + OperationRequestBuilder + .New() + .SetDocument(request) + .Build()); + + // assert + var snapshot = new Snapshot(); + CollectSnapshotData(snapshot, request, result); + await snapshot.MatchMarkdownAsync(); + } + [Fact] public async Task Authors_And_Reviews_AutoCompose() { diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md new file mode 100644 index 00000000000..a00e2d961fc --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Test.md @@ -0,0 +1,191 @@ +# Test + +## Result + +```json +{ + "errors": [ + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 11, + "column": 5 + } + ], + "path": [ + "productsB", + 2, + "price" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 11, + "column": 5 + } + ], + "path": [ + "productsB", + 1, + "price" + ], + "extensions": { + "code": "HC0018" + } + }, + { + "message": "Cannot return null for non-nullable field.", + "locations": [ + { + "line": 11, + "column": 5 + } + ], + "path": [ + "productsB", + 0, + "price" + ], + "extensions": { + "code": "HC0018" + } + } + ], + "data": { + "productsA": [ + { + "id": "1", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "2", + "name": "string", + "price": 123.456, + "reviewCount": 123 + }, + { + "id": "3", + "name": "string", + "price": 123.456, + "reviewCount": 123 + } + ], + "productsB": [ + null, + null, + null + ] + } +} +``` + +## Request + +```graphql +{ + productsA { + id + name + price + reviewCount + } + productsB { + id + name + price + reviewCount + } +} +``` + +## QueryPlan Hash + +```text +57DE21D1B552226339A985FBFA65E0DA2E33703C +``` + +## QueryPlan + +```json +{ + "document": "{ productsA { id name price reviewCount } productsB { id name price reviewCount } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_productsA_productsB_1 { productsA { id name __fusion_exports__1: id } productsB { id name __fusion_exports__2: id } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + }, + { + "variable": "__fusion_exports__2" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Parallel", + "nodes": [ + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_productsA_productsB_2($__fusion_exports__1: [ID!]!) { nodes(ids: $__fusion_exports__1) { ... on Product { price reviewCount __fusion_exports__1: id } } }", + "selectionSetId": 1, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "ResolveByKeyBatch", + "subgraph": "Subgraph_2", + "document": "query fetch_productsA_productsB_3($__fusion_exports__2: [ID!]!) { nodes(ids: $__fusion_exports__2) { ... on Product { price reviewCount __fusion_exports__2: id } } }", + "selectionSetId": 2, + "path": [ + "nodes" + ], + "requires": [ + { + "variable": "__fusion_exports__2" + } + ] + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 1, + 2 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Product_id", + "__fusion_exports__2": "Product_id" + } +} +``` +