From 1b5b9681269bbb3874e200f7565411249797dd8d Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 27 Nov 2024 13:26:03 +0100 Subject: [PATCH 1/7] Added more test cases --- .../OperationPlannerTests.cs | 63 ++++++ .../__resources__/fusion1.graphql | 208 ++++++++++++++---- 2 files changed, 232 insertions(+), 39 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs index 9e0dced1b9f..a358eaea9e0 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs @@ -94,4 +94,67 @@ await Assert } """); } + + [Test] + public async Task Plan_Simple_Operation_3_Source_Schema() + { + var compositeSchemaDoc = Utf8GraphQLParser.Parse(FileResource.Open("fusion1.graphql")); + var compositeSchema = CompositeSchemaBuilder.Create(compositeSchemaDoc); + + var doc = Utf8GraphQLParser.Parse( + """ + { + productById(id: 1) { + ... ProductCard + } + } + + fragment ProductCard on Product { + name + reviews(first: 10) { + nodes { + ... ReviewCard + } + } + } + + fragment ReviewCard on Review { + body + stars + author { + ... AuthorCard + } + } + + fragment AuthorCard on UserProfile { + displayName + } + """); + + var rewriter = new InlineFragmentOperationRewriter(compositeSchema); + var rewritten = rewriter.RewriteDocument(doc, null); + + // act + var planner = new OperationPlanner(compositeSchema); + var plan = planner.CreatePlan(rewritten, null); + + // assert + await Assert + .That(plan.ToSyntaxNode().ToString(indented: true)) + .IsEqualTo( + """ + { + productById { + id + name + } + } + + { + productById { + estimatedDelivery + } + } + """); + } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/fusion1.graphql b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/fusion1.graphql index 0f530f27e0a..46b412e9e08 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/fusion1.graphql +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/fusion1.graphql @@ -1,54 +1,184 @@ -type Query { - productById(id: ID!): Product - @fusion__field(schema: PRODUCTS) +type Query + @fusion__type(schema: ACCOUNTS) + @fusion__type(schema: PRODUCTS) { + viewer: UserProfile + @fusion__field(schema: ACCOUNTS) + productById(id: ID!): Product + @fusion__field(schema: PRODUCTS) + products(first: Int, after: String, last: Int, before: String): ProductConnection + @fusion__field(schema: PRODUCTS) } type Product - @fusion__type(schema: PRODUCTS) - @fusion__type(schema: SHIPPING) - @fusion__lookup( - schema: PRODUCTS - key: "{ id }" - field: "productById(id: ID!): Product" - map: ["id"] - ) - @fusion__lookup( + @fusion__type(schema: PRODUCTS) + @fusion__type(schema: SHIPPING) + @fusion__type(schema: REVIEWS) + @fusion__lookup( + schema: PRODUCTS + key: "{ id }" + field: "productById(id: ID!): Product" + map: ["id"] + ) + @fusion__lookup( + schema: SHIPPING + key: "{ id }" + field: "productById(id: ID!): Product" + map: ["id"] + ) + @fusion__lookup( + schema: REVIEWS + key: "{ id }" + field: "productById(id: ID!): Product" + map: ["id"] + ) { + id: ID! + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: SHIPPING) + name: String! + @fusion__field(schema: PRODUCTS) + description: String + @fusion__field(schema: PRODUCTS) + price: Float! + @fusion__field(schema: PRODUCTS) + dimension: ProductDimension! + @fusion__field(schema: PRODUCTS) + reviews(first: Int, after: String, last: Int, before: String): ProductReviewConnection + @fusion__field(schema: REVIEWS) + estimatedDelivery(postCode: String): Int! + @fusion__field(schema: SHIPPING) + @fusion__requires( schema: SHIPPING - key: "{ id }" - field: "productById(id: ID!): Product" - map: ["id"] - ) { - id: ID! - @fusion__field(schema: PRODUCTS) - @fusion__field(schema: SHIPPING) - name: String! - @fusion__field(schema: PRODUCTS) - description: String - @fusion__field(schema: PRODUCTS) - price: Float! - @fusion__field(schema: PRODUCTS) - dimension: ProductDimension! - @fusion__field(schema: PRODUCTS) - estimatedDelivery(postCode: String): Int! - @fusion__field(schema: SHIPPING) - @fusion__requires( - schema: SHIPPING - field: "estimatedDelivery(postCode: String, height: Int!, width: Int!): Int!" - map: ["dimension.height", "dimension.width"] - ) + field: "estimatedDelivery(postCode: String, height: Int!, width: Int!): Int!" + map: ["dimension.height", "dimension.width"] + ) } type ProductDimension - @fusion__type(schema: PRODUCTS) { - height: Int! - @fusion__field(schema: PRODUCTS) - width: Int! - @fusion__field(schema: PRODUCTS) + @fusion__type(schema: PRODUCTS) { + height: Int! + @fusion__field(schema: PRODUCTS) + width: Int! + @fusion__field(schema: PRODUCTS) +} + +type Review + @fusion__type(schema: REVIEWS) + @fusion__lookup( + schema: REVIEWS + key: "{ id }" + field: "reviewById(id: ID!): Review" + map: ["id"] + ) { + id: ID! + @fusion__field(schema: REVIEWS) + body: String! + @fusion__field(schema: REVIEWS) + stars: Int! + @fusion__field(schema: REVIEWS) + author: UserProfile + @fusion__field(schema: REVIEWS) +} + +type UserProfile + @fusion__type(schema: REVIEWS) + @fusion__type(schema: ACCOUNTS) + @fusion__lookup( + schema: REVIEWS + key: "{ id }" + field: "authorById(id: ID!): UserProfile" + map: ["id"] + ) + @fusion__lookup( + schema: ACCOUNTS + key: "{ id }" + field: "userById(id: ID!): UserProfile" + map: ["id"] + ) { + id: ID! + @fusion__field(schema: ACCOUNTS) + @fusion__field(schema: REVIEWS) + displayName: String! + @fusion__field(schema: ACCOUNTS) + reviews(first: Int, after: String, last: Int, before: String): UserProfileReviewConnection + @fusion__field(schema: REVIEWS) +} + +type ProductReviewConnection + @fusion__type(schema: REVIEWS) { + pageInfo: PageInfo! + @fusion__field(schema: REVIEWS) + edges: [ProductReviewEdge!] + @fusion__field(schema: REVIEWS) + nodes: [Review!] + @fusion__field(schema: REVIEWS) +} + +type ProductReviewEdge + @fusion__type(schema: REVIEWS) { + cursor: String! + @fusion__field(schema: REVIEWS) + node: Review! + @fusion__field(schema: REVIEWS) +} + +type UserProfileReviewConnection + @fusion__type(schema: REVIEWS) { + pageInfo: PageInfo! + @fusion__field(schema: REVIEWS) + edges: [UserProfileReviewEdge!] + @fusion__field(schema: REVIEWS) + nodes: [Review!] + @fusion__field(schema: REVIEWS) +} + +type UserProfileReviewEdge + @fusion__type(schema: REVIEWS) { + cursor: String! + @fusion__field(schema: REVIEWS) + node: Review! + @fusion__field(schema: REVIEWS) +} + +type ProductConnection + @fusion__type(schema: PRODUCTS) { + pageInfo: PageInfo! + @fusion__field(schema: PRODUCTS) + edges: [ProductEdge!] + @fusion__field(schema: PRODUCTS) + nodes: [Product!] + @fusion__field(schema: PRODUCTS) +} + +type ProductEdge + @fusion__type(schema: PRODUCTS) { + cursor: String! + @fusion__field(schema: PRODUCTS) + node: Product! + @fusion__field(schema: PRODUCTS) +} + +type PageInfo + @fusion__type(schema: PRODUCTS) + @fusion__type(schema: REVIEWS) { + hasNextPage: Boolean! + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: REVIEWS) + hasPreviousPage: Boolean! + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: REVIEWS) + startCursor: String + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: REVIEWS) + endCursor: String + @fusion__field(schema: PRODUCTS) + @fusion__field(schema: REVIEWS) } enum fusion__Schema { PRODUCTS SHIPPING + REVIEWS + ACCOUNTS } scalar fusion__FieldDefinition From 66cdf6262f64ed1994a7a9c8e8fb7b42f11c866c Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 27 Nov 2024 14:14:42 +0100 Subject: [PATCH 2/7] Reworked planning algorithm --- .../Planning/Nodes/FieldPlanNode.cs | 27 +++++++- .../Planning/OperationPlanner.cs | 63 ++++++++++++------- .../Types/SourceObjectField.cs | 7 ++- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs index 70997e1665c..073c211c58b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Diagnostics; using HotChocolate.Fusion.Types; +using HotChocolate.Fusion.Types.Collections; using HotChocolate.Language; namespace HotChocolate.Fusion.Planning.Nodes; @@ -11,7 +12,7 @@ public sealed class FieldPlanNode : SelectionPlanNode public FieldPlanNode( FieldNode fieldNode, - CompositeOutputField field) + OutputFieldInfo field) : base(field.Type.NamedType(), fieldNode.SelectionSet?.Selections) { FieldNode = fieldNode; @@ -19,11 +20,18 @@ public FieldPlanNode( ResponseName = FieldNode.Alias?.Value ?? field.Name; } + public FieldPlanNode( + FieldNode fieldNode, + CompositeOutputField field) + : this(fieldNode, new OutputFieldInfo(field)) + { + } + public string ResponseName { get; } public FieldNode FieldNode { get; } - public CompositeOutputField Field { get; } + public OutputFieldInfo Field { get; } public IReadOnlyList Arguments => _arguments ?? (IReadOnlyList)Array.Empty(); @@ -45,3 +53,18 @@ public FieldNode ToSyntaxNode() Selections.Count == 0 ? null : Selections.ToSyntaxNode()); } } + +public class OutputFieldInfo(string name, ICompositeType type, ImmutableArray sources) +{ + public OutputFieldInfo(CompositeOutputField field) + : this(field.Name, field.Type, field.Sources.Schemas) + { + + } + + public string Name => name; + + public ICompositeType Type => type; + + public ImmutableArray Sources => sources; +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs index 62877a759f7..8bed02969d0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using HotChocolate.Fusion.Planning.Nodes; using HotChocolate.Fusion.Types; using HotChocolate.Language; @@ -11,7 +12,7 @@ public RootPlanNode CreatePlan(DocumentNode document, string? operationName) { ArgumentNullException.ThrowIfNull(document); - var operationDefinition = document.GetOperation(operationName); + var operationDefinition = document.GetOperation(operationName); var schemasWeighted = GetSchemasWeighted(schema.QueryType, operationDefinition.SelectionSet); var rootPlanNode = new RootPlanNode(); @@ -23,7 +24,7 @@ public RootPlanNode CreatePlan(DocumentNode document, string? operationName) schema.QueryType, operationDefinition.SelectionSet); - if (TryResolveSelectionSet(operation, operation, new Stack())) + if (TryPlanSelectionSet(operation, operation, new Stack())) { rootPlanNode.AddOperation(operation); } @@ -32,7 +33,7 @@ public RootPlanNode CreatePlan(DocumentNode document, string? operationName) return rootPlanNode; } - private bool TryResolveSelectionSet( + private bool TryPlanSelectionSet( OperationPlanNode operation, SelectionPlanNode parent, Stack path) @@ -99,7 +100,7 @@ private bool TryResolveSelectionSet( path.Push(pathSegment); - if (TryResolveSelectionSet(operation, fieldPlanNode, path)) + if (TryPlanSelectionSet(operation, fieldPlanNode, path)) { parent.AddSelection(fieldPlanNode); areAnySelectionsResolvable = true; @@ -161,8 +162,7 @@ private bool TryResolveSelectionSet( foreach (var pathSegment in unresolvedPath.Skip(1)) { if (pathSegment is FieldPlanNode selection - && selection.Field is not null - && selection.Field.Sources.ContainsSchema(schemaName)) + && selection.Field.Sources.Contains(schemaName)) { continue; } @@ -193,16 +193,14 @@ private bool TryResolveSelectionSet( } } - var newOperation = new OperationPlanNode( - schemaName, - schema.QueryType, - CreateLookupSelections(lookup, parent, fields), - parent); + type ??= (CompositeComplexType)parent.DeclaringType; + var lookupOperation = CreateLookupOperation(schemaName, lookup, type, parent, fields); + var lookupField = lookupOperation.Selections[0]; // what do we do of its not successful - if (TryResolveSelectionSet(newOperation, newOperation, path)) + if (TryPlanSelectionSet(lookupOperation, lookupField, path)) { - operation.AddOperation(newOperation); + operation.AddOperation(lookupOperation); } } } @@ -237,23 +235,40 @@ private bool TryGetLookup(SelectionPlanNode selection, HashSet schemas, throw new NotImplementedException(); } - private IReadOnlyList CreateLookupSelections( + private OperationPlanNode CreateLookupOperation( + string schemaName, Lookup lookup, + CompositeComplexType entityType, SelectionPlanNode parent, IReadOnlyList selections) { - return - [ - new FieldNode( - new NameNode(lookup.Name), - null, - Array.Empty(), - Array.Empty(), - new SelectionSetNode(selections)) - ]; + var lookupFieldNode = new FieldNode( + new NameNode(lookup.Name), + null, + Array.Empty(), + Array.Empty(), + new SelectionSetNode(selections)); + + var selectionNodes = new ISelectionNode[] { lookupFieldNode }; + + var lookupFieldPlan = new FieldPlanNode( + lookupFieldNode, + new OutputFieldInfo( + lookup.Name, + entityType, + ImmutableArray.Empty.Add(schemaName))); + + var lookupOperation = new OperationPlanNode( + schemaName, + schema.QueryType, + selectionNodes, + parent); + + lookupOperation.AddSelection(lookupFieldPlan); + + return lookupOperation; } - private static Dictionary GetSchemasWeighted( IEnumerable unresolvedFields, IEnumerable skipSchemaNames) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs index 3761c06d6b8..ca2b4c8a325 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs @@ -5,7 +5,7 @@ public sealed class SourceObjectField( string schemaName, FieldRequirements? requirements, ICompositeType type) - : ISourceMember + : ISourceOutputField { public string Name { get; } = name; @@ -17,3 +17,8 @@ public sealed class SourceObjectField( public int BaseCost => 1; } + +public interface ISourceOutputField : ISourceMember +{ + ICompositeType Type { get; } +} From 3293aa7d37a3f28d11d21a923202949469ea7b71 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 27 Nov 2024 15:44:57 +0100 Subject: [PATCH 3/7] Pulled out method to resolve selection node which is an entity --- .../Planning/OperationPlanner.cs | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs index 8bed02969d0..6e4563f2ce8 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using HotChocolate.Fusion.Planning.Nodes; using HotChocolate.Fusion.Types; using HotChocolate.Language; @@ -124,23 +125,8 @@ private bool TryPlanSelectionSet( if (unresolved?.Count > 0) { - var current = parent; - var unresolvedPath = new Stack(); - unresolvedPath.Push(parent); - - // first we try to find an entity from which we can branch. - // We go up until we find the first entity. - while (!current.IsEntity - && current.Parent is SelectionPlanNode parentSelection) + if(!TryResolveEntityType(parent, out var entityPath)) { - current = parentSelection; - unresolvedPath.Push(current); - } - - // If we could not find an entity we cannot resolve the unresolved selections. - if (!current.IsEntity) - { - // TODO: there is a case where we do root selections on data, we will ignore it for now. return false; } @@ -159,7 +145,7 @@ private bool TryPlanSelectionSet( var isPathResolvable = true; // a possible schema must be able to resolve the path to the lookup. - foreach (var pathSegment in unresolvedPath.Skip(1)) + foreach (var pathSegment in entityPath.Skip(1)) { if (pathSegment is FieldPlanNode selection && selection.Field.Sources.Contains(schemaName)) @@ -178,7 +164,7 @@ private bool TryPlanSelectionSet( } // next we try to find a lookup - if (TryGetLookup(current, processed, out var lookup)) + if (TryGetLookup((SelectionPlanNode)entityPath.Peek(), processed, out var lookup)) { // note : this can lead to a operation explosions as fields could be unresolvable // and would be spread out in the lower level call. We do that for now to test out the @@ -210,6 +196,33 @@ private bool TryPlanSelectionSet( return areAnySelectionsResolvable; } + /// + /// Tries to find an entity type in the current selection path. + /// + private static bool TryResolveEntityType( + SelectionPlanNode parent, + [NotNullWhen(true)] out Stack? entityPath) + { + var current = parent; + entityPath = new Stack(); + entityPath.Push(parent); + + // if the current SelectionPlanNode is not an entity we will move up the selection path. + while (current is { IsEntity: false, Parent: SelectionPlanNode parentSelection }) + { + current = parentSelection; + entityPath.Push(current); + } + + if (!current.IsEntity) + { + entityPath = null; + return false; + } + + return true; + } + // this needs more meat private bool IsResolvable( FieldNode fieldNode, From f8f432248ec246b8e4d92330c43480ee0081e437 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Wed, 27 Nov 2024 19:20:51 +0100 Subject: [PATCH 4/7] Reworked Type System for easier access to source types. --- .../Planning/OperationPlanner.cs | 35 ++++++++++--------- .../SourceInterfaceMemberCollection.cs | 4 --- .../Collections/SourceMemberCollection.cs | 2 +- .../SourceObjectFieldCollection.cs | 4 +-- .../Collections/SourceObjectTypeCollection.cs | 35 ++++++++++++++++++- .../Completion/CompositeSchemaBuilder.cs | 4 +-- .../Types/CompositeComplexType.cs | 22 ++++++++++++ .../Types/CompositeObjectType.cs | 5 +-- .../Fusion.Execution/Types/Contracts/Is.cs | 28 +++++++++++++++ .../Types/SourceInterfaceField.cs | 14 -------- ...rceObjectField.cs => SourceOutputField.cs} | 9 ++--- 11 files changed, 112 insertions(+), 50 deletions(-) delete mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceInterfaceMemberCollection.cs create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Contracts/Is.cs delete mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceInterfaceField.cs rename src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/{SourceObjectField.cs => SourceOutputField.cs} (71%) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs index 6e4563f2ce8..bdebe95c567 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -142,23 +142,8 @@ private bool TryPlanSelectionSet( { if (processed.Add(schemaName)) { - var isPathResolvable = true; - - // a possible schema must be able to resolve the path to the lookup. - foreach (var pathSegment in entityPath.Skip(1)) - { - if (pathSegment is FieldPlanNode selection - && selection.Field.Sources.Contains(schemaName)) - { - continue; - } - - isPathResolvable = true; - break; - } - // if the path is not resolvable we will skip it and move to the next. - if (!isPathResolvable) + if (!IsEntityPathResolvable(entityPath, schemaName)) { continue; } @@ -223,6 +208,22 @@ private static bool TryResolveEntityType( return true; } + private static bool IsEntityPathResolvable(Stack entityPath, string schemaName) + { + foreach (var planNode in entityPath) + { + if (planNode is FieldPlanNode fieldPlanNode) + { + if (!fieldPlanNode.Field.Sources.Contains(schemaName)) + { + return false; + } + } + } + + return true; + } + // this needs more meat private bool IsResolvable( FieldNode fieldNode, @@ -237,7 +238,7 @@ private bool TryGetLookup(SelectionPlanNode selection, HashSet schemas, // is available for free. foreach (var schemaName in schemas) { - if (((CompositeObjectType)selection.DeclaringType).Sources.TryGetMember(schemaName, out var source) + if (((CompositeComplexType)selection.DeclaringType).Sources.TryGetType(schemaName, out var source) && source.Lookups.Length > 0) { lookup = source.Lookups[0]; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceInterfaceMemberCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceInterfaceMemberCollection.cs deleted file mode 100644 index 2c5cd1e48a0..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceInterfaceMemberCollection.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace HotChocolate.Fusion.Types.Collections; - -public class SourceInterfaceMemberCollection(IEnumerable members) - : SourceMemberCollection(members); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceMemberCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceMemberCollection.cs index 89d4e7d6409..84b36dc1ea0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceMemberCollection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceMemberCollection.cs @@ -5,7 +5,7 @@ namespace HotChocolate.Fusion.Types.Collections; -public class SourceMemberCollection : IEnumerable where TMember : ISourceMember +public class SourceMemberCollection : ISourceMemberCollection where TMember : ISourceMember { private readonly FrozenDictionary _members; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectFieldCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectFieldCollection.cs index 05aa3d97d23..ba2d5f84933 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectFieldCollection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectFieldCollection.cs @@ -1,4 +1,4 @@ namespace HotChocolate.Fusion.Types.Collections; -public class SourceObjectFieldCollection(IEnumerable members) - : SourceMemberCollection(members); +public class SourceObjectFieldCollection(IEnumerable members) + : SourceMemberCollection(members); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectTypeCollection.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectTypeCollection.cs index ad3713a435f..7ceaf0376c8 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectTypeCollection.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Collections/SourceObjectTypeCollection.cs @@ -1,4 +1,37 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + namespace HotChocolate.Fusion.Types.Collections; public class SourceObjectTypeCollection(IEnumerable members) - : SourceMemberCollection(members); + : SourceMemberCollection(members) + , ISourceComplexTypeCollection + , ISourceComplexTypeCollection +{ + ISourceComplexType ISourceMemberCollection.this[string schemaName] + => this[schemaName]; + + public ImmutableArray Types + => Members; + + ImmutableArray ISourceComplexTypeCollection.Types + => [..Members]; + + public bool TryGetType(string schemaName, [NotNullWhen(true)] out SourceObjectType? type) + => TryGetMember(schemaName, out type); + + public bool TryGetType(string schemaName, [NotNullWhen(true)] out ISourceComplexType? type) + { + if(TryGetMember(schemaName, out var member)) + { + type = member; + return true; + } + + type = null; + return false; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Completion/CompositeSchemaBuilder.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Completion/CompositeSchemaBuilder.cs index ae4c8612e45..efd6663c341 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Completion/CompositeSchemaBuilder.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Completion/CompositeSchemaBuilder.cs @@ -219,12 +219,12 @@ private static SourceObjectFieldCollection BuildSourceObjectFieldCollection( { var fieldDirectives = FieldDirectiveParser.Parse(fieldDef.Directives); var requireDirectives = RequiredDirectiveParser.Parse(fieldDef.Directives); - var temp = ImmutableArray.CreateBuilder(); + var temp = ImmutableArray.CreateBuilder(); foreach (var fieldDirective in fieldDirectives) { temp.Add( - new SourceObjectField( + new SourceOutputField( fieldDirective.SourceName ?? field.Name, fieldDirective.SchemaName, ParseRequirements(requireDirectives, fieldDirective.SchemaName), diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeComplexType.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeComplexType.cs index 68c24e4736d..f9e4248dff8 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeComplexType.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeComplexType.cs @@ -10,6 +10,7 @@ public abstract class CompositeComplexType : ICompositeNamedType { private DirectiveCollection _directives = default!; private CompositeInterfaceTypeCollection _implements = default!; + private ISourceComplexTypeCollection _sources = default!; private bool _completed; protected CompositeComplexType( @@ -69,6 +70,27 @@ private protected set /// public CompositeOutputFieldCollection Fields { get; } + /// + /// Gets the source type definition of this type. + /// + /// + /// The source type definition of this type. + /// + public ISourceComplexTypeCollection Sources + { + get => _sources; + private protected set + { + if (_completed) + { + throw new NotSupportedException( + "The type definition is sealed and cannot be modified."); + } + + _sources = value; + } + } + private protected void Complete() { if (_completed) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeObjectType.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeObjectType.cs index 6ac611f2c3a..bb2e7cddb5c 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeObjectType.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/CompositeObjectType.cs @@ -12,7 +12,7 @@ public sealed class CompositeObjectType( { public override TypeKind Kind => TypeKind.Object; - public SourceObjectTypeCollection Sources { get; private set; } = default!; + public new ISourceComplexTypeCollection Sources { get; private set; } = default!; public bool IsEntity { get; private set; } @@ -21,7 +21,8 @@ internal void Complete(CompositeObjectTypeCompletionContext context) Directives = context.Directives; Implements = context.Interfaces; Sources = context.Sources; - IsEntity = context.Sources.Any(t => t.Lookups.Length > 0); + base.Sources = context.Sources; + IsEntity = Sources.Any(t => t.Lookups.Length > 0); base.Complete(); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Contracts/Is.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Contracts/Is.cs new file mode 100644 index 00000000000..ba180f41d71 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/Contracts/Is.cs @@ -0,0 +1,28 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace HotChocolate.Fusion.Types; + +public interface ISourceMemberCollection : IEnumerable where TMember : ISourceMember +{ + int Count { get; } + + TMember this[string schemaName] { get; } + + bool ContainsSchema(string schemaName); + + ImmutableArray Schemas { get; } +} + +public interface ISourceComplexTypeCollection : ISourceMemberCollection where TType : ISourceComplexType +{ + bool TryGetType(string schemaName, [NotNullWhen(true)] out TType? type); + + ImmutableArray Types { get; } +} + + + + + + diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceInterfaceField.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceInterfaceField.cs deleted file mode 100644 index 67d4a3c2217..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceInterfaceField.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace HotChocolate.Fusion.Types; - -public class SourceInterfaceField( - string name, - string schemaName, - ICompositeType type) - : ISourceMember -{ - public string Name { get; } = name; - - public string SchemaName { get; } = schemaName; - - public ICompositeType Type { get; } = type; -} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceOutputField.cs similarity index 71% rename from src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs rename to src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceOutputField.cs index ca2b4c8a325..bf629016687 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceObjectField.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Types/SourceOutputField.cs @@ -1,11 +1,11 @@ namespace HotChocolate.Fusion.Types; -public sealed class SourceObjectField( +public sealed class SourceOutputField( string name, string schemaName, FieldRequirements? requirements, ICompositeType type) - : ISourceOutputField + : ISourceMember { public string Name { get; } = name; @@ -17,8 +17,3 @@ public sealed class SourceObjectField( public int BaseCost => 1; } - -public interface ISourceOutputField : ISourceMember -{ - ICompositeType Type { get; } -} From 59103edee650654a1e5bb809a84eab71921d071a Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 28 Nov 2024 13:57:19 +0100 Subject: [PATCH 5/7] Pull up handling of unresolved fields --- .../Planning/OperationPlanner.cs | 123 +++++++++++------- .../OperationPlannerTests.cs | 15 ++- 2 files changed, 88 insertions(+), 50 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs index bdebe95c567..11d5c03edb8 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -37,7 +37,8 @@ public RootPlanNode CreatePlan(DocumentNode document, string? operationName) private bool TryPlanSelectionSet( OperationPlanNode operation, SelectionPlanNode parent, - Stack path) + Stack path, + bool skipUnresolved = false) { if (parent.SelectionNodes is null) { @@ -46,15 +47,12 @@ private bool TryPlanSelectionSet( } List? unresolved = null; - CompositeComplexType? type = null; - var areAnySelectionsResolvable = false; + var type = (CompositeComplexType)parent.DeclaringType; foreach (var selection in parent.SelectionNodes) { if (selection is FieldNode fieldNode) { - type ??= (CompositeComplexType)parent.DeclaringType; - if (!type.Fields.TryGetField(fieldNode.Name.Value, out var field)) { throw new InvalidOperationException( @@ -81,7 +79,6 @@ private bool TryPlanSelectionSet( } parent.AddSelection(new FieldPlanNode(fieldNode, field)); - areAnySelectionsResolvable = true; continue; } @@ -104,7 +101,6 @@ private bool TryPlanSelectionSet( if (TryPlanSelectionSet(operation, fieldPlanNode, path)) { parent.AddSelection(fieldPlanNode); - areAnySelectionsResolvable = true; } else { @@ -123,62 +119,93 @@ private bool TryPlanSelectionSet( } } - if (unresolved?.Count > 0) + return skipUnresolved + || unresolved is null + || unresolved.Count == 0 + || TryHandleUnresolvedSelections(operation, parent, type, unresolved, path); + } + + private bool TryHandleUnresolvedSelections( + OperationPlanNode operation, + SelectionPlanNode parent, + CompositeComplexType type, + List unresolved, + Stack path) + { + if (!TryResolveEntityType(parent, out var entityPath)) + { + return false; + } + + // if we have found an entity to branch of from we will check + // if any of the unresolved selections can be resolved through one of the entity lookups. + var processedSchemas = new HashSet(); + var processedFields = new HashSet(); + var fields = new List(); + + // we first try to weight the schemas that the fields can be resolved by. + // The schema is weighted by the fields it potentially can resolve. + var schemasWeighted = GetSchemasWeighted(unresolved, processedSchemas); + + foreach (var schemaName in schemasWeighted.OrderByDescending(t => t.Value).Select(t => t.Key)) { - if(!TryResolveEntityType(parent, out var entityPath)) + if (!processedSchemas.Add(schemaName)) + { + continue; + } + + // if the path is not resolvable we will skip it and move to the next. + if (!IsEntityPathResolvable(entityPath, schemaName)) { - return false; + continue; } - // if we have found an entity to branch of from we will check - // if any of the unresolved selections can be resolved through one of the entity lookups. - var processed = new HashSet(); + // next we try to find a lookup + if (!TryGetLookup((SelectionPlanNode)entityPath.Peek(), processedSchemas, out var lookup)) + { + continue; + } - // we first try to weight the schemas that the fields can be resolved by. - // The schema is weighted by the fields it potentially can resolve. - var schemasWeighted = GetSchemasWeighted(unresolved, processed); + // note : this can lead to a operation explosions as fields could be unresolvable + // and would be spread out in the lower level call. We do that for now to test out the + // overall concept and will backtrack later to the upper call. + fields.Clear(); - foreach (var schemaName in schemasWeighted.OrderByDescending(t => t.Value).Select(t => t.Key)) + foreach (var unresolvedField in unresolved) { - if (processed.Add(schemaName)) + if (unresolvedField.Field.Sources.ContainsSchema(schemaName) + && !processedFields.Contains(unresolvedField.Field.Name)) { - // if the path is not resolvable we will skip it and move to the next. - if (!IsEntityPathResolvable(entityPath, schemaName)) - { - continue; - } + fields.Add(unresolvedField.FieldNode); + } + } - // next we try to find a lookup - if (TryGetLookup((SelectionPlanNode)entityPath.Peek(), processed, out var lookup)) - { - // note : this can lead to a operation explosions as fields could be unresolvable - // and would be spread out in the lower level call. We do that for now to test out the - // overall concept and will backtrack later to the upper call. - var fields = new List(); + var lookupOperation = CreateLookupOperation(schemaName, lookup, type, parent, fields); + var lookupField = lookupOperation.Selections[0]; - foreach (var unresolvedField in unresolved) - { - if (unresolvedField.Field.Sources.ContainsSchema(schemaName)) - { - fields.Add(unresolvedField.FieldNode); - } - } + // what do we do of its not successful + if (!TryPlanSelectionSet(lookupOperation, lookupField, path)) + { + continue; + } - type ??= (CompositeComplexType)parent.DeclaringType; - var lookupOperation = CreateLookupOperation(schemaName, lookup, type, parent, fields); - var lookupField = lookupOperation.Selections[0]; + operation.AddOperation(lookupOperation); - // what do we do of its not successful - if (TryPlanSelectionSet(lookupOperation, lookupField, path)) - { - operation.AddOperation(lookupOperation); - } - } + foreach (var selection in lookupField.Selections) + { + switch (selection) + { + case FieldPlanNode field: + processedFields.Add(field.Field.Name); + break; + + default: + throw new NotSupportedException(); } } } - return areAnySelectionsResolvable; + return unresolved.Count == processedFields.Count; } /// @@ -210,7 +237,7 @@ private static bool TryResolveEntityType( private static bool IsEntityPathResolvable(Stack entityPath, string schemaName) { - foreach (var planNode in entityPath) + foreach (var planNode in entityPath.Skip(1)) { if (planNode is FieldPlanNode fieldPlanNode) { diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs index a358eaea9e0..9258cb2f42b 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs @@ -145,14 +145,25 @@ await Assert """ { productById { - id name } } { productById { - estimatedDelivery + reviews { + nodes { + body + stars + author + } + } + } + } + + { + userById { + displayName } } """); From f31341f7b6a419cf63e88f31727ef4fb8848c6c6 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 28 Nov 2024 18:51:58 +0100 Subject: [PATCH 6/7] Added arguments --- .../Planning/Nodes/FieldPlanNode.cs | 21 +++++-------------- .../Planning/Nodes/OutputFieldInfo.cs | 19 +++++++++++++++++ .../OperationPlannerTests.cs | 10 ++++----- 3 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OutputFieldInfo.cs diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs index 073c211c58b..4db8fe3ba21 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/FieldPlanNode.cs @@ -1,4 +1,3 @@ -using System.Collections.Immutable; using System.Diagnostics; using HotChocolate.Fusion.Types; using HotChocolate.Fusion.Types.Collections; @@ -18,6 +17,11 @@ public FieldPlanNode( FieldNode = fieldNode; Field = field; ResponseName = FieldNode.Alias?.Value ?? field.Name; + + foreach (var argument in fieldNode.Arguments) + { + AddArgument(new ArgumentAssignment(argument.Name.Value, argument.Value)); + } } public FieldPlanNode( @@ -53,18 +57,3 @@ public FieldNode ToSyntaxNode() Selections.Count == 0 ? null : Selections.ToSyntaxNode()); } } - -public class OutputFieldInfo(string name, ICompositeType type, ImmutableArray sources) -{ - public OutputFieldInfo(CompositeOutputField field) - : this(field.Name, field.Type, field.Sources.Schemas) - { - - } - - public string Name => name; - - public ICompositeType Type => type; - - public ImmutableArray Sources => sources; -} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OutputFieldInfo.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OutputFieldInfo.cs new file mode 100644 index 00000000000..70c4a012a3f --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OutputFieldInfo.cs @@ -0,0 +1,19 @@ +using System.Collections.Immutable; +using HotChocolate.Fusion.Types; + +namespace HotChocolate.Fusion.Planning.Nodes; + +public class OutputFieldInfo(string name, ICompositeType type, ImmutableArray sources) +{ + public OutputFieldInfo(CompositeOutputField field) + : this(field.Name, field.Type, field.Sources.Schemas) + { + + } + + public string Name => name; + + public ICompositeType Type => type; + + public ImmutableArray Sources => sources; +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs index 9258cb2f42b..634e97503ea 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs @@ -39,7 +39,7 @@ await Assert .IsEqualTo( """ { - productById { + productById(id: 1) { id name } @@ -81,7 +81,7 @@ await Assert .IsEqualTo( """ { - productById { + productById(id: 1) { id name } @@ -89,7 +89,7 @@ await Assert { productById { - estimatedDelivery + estimatedDelivery(postCode: "12345") } } """); @@ -144,14 +144,14 @@ await Assert .IsEqualTo( """ { - productById { + productById(id: 1) { name } } { productById { - reviews { + reviews(first: 10) { nodes { body stars From cfbe0b96b8b5f8f3503f1e8faee6cb82690d5799 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Thu, 28 Nov 2024 19:25:03 +0100 Subject: [PATCH 7/7] Added Variables --- .../Planning/Nodes/OperationPlanNode.cs | 14 +++- .../Planning/OperationPlanner.cs | 8 +- .../Planning/OperationVariableBinder.cs | 78 +++++++++++++++++++ .../OperationPlannerTests.cs | 74 ++++++++++++++++++ 4 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationVariableBinder.cs diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OperationPlanNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OperationPlanNode.cs index 43e252fcb17..3854a31c536 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OperationPlanNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/Nodes/OperationPlanNode.cs @@ -8,7 +8,10 @@ namespace HotChocolate.Fusion.Planning.Nodes; /// public sealed class OperationPlanNode : SelectionPlanNode, IOperationPlanNodeProvider { + private static readonly IReadOnlyDictionary _emptyVariableMap = + new Dictionary(); private List? _operations; + private Dictionary? _variables; public OperationPlanNode( string schemaName, @@ -37,9 +40,18 @@ public OperationPlanNode( // todo: variable representations are missing. // todo: how to we represent state? + public IReadOnlyDictionary VariableDefinitions + => _variables ?? _emptyVariableMap; + public IReadOnlyList Operations => _operations ?? (IReadOnlyList)Array.Empty(); + public void AddVariableDefinition(VariableDefinitionNode variable) + { + ArgumentNullException.ThrowIfNull(variable); + (_variables ??= new Dictionary()).Add(variable.Variable.Name.Value, variable); + } + public void AddOperation(OperationPlanNode operation) { ArgumentNullException.ThrowIfNull(operation); @@ -53,7 +65,7 @@ public OperationDefinitionNode ToSyntaxNode() null, null, OperationType.Query, - Array.Empty(), + _variables?.Values.OrderBy(t => t.Variable.Name.Value).ToArray() ?? [], Directives.ToSyntaxNode(), Selections.ToSyntaxNode()); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs index 11d5c03edb8..0da6037e917 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -15,7 +15,7 @@ public RootPlanNode CreatePlan(DocumentNode document, string? operationName) var operationDefinition = document.GetOperation(operationName); var schemasWeighted = GetSchemasWeighted(schema.QueryType, operationDefinition.SelectionSet); - var rootPlanNode = new RootPlanNode(); + var operationPlan = new RootPlanNode(); // this need to be rewritten to check if everything is planned for. foreach (var schemaName in schemasWeighted.OrderByDescending(t => t.Value).Select(t => t.Key)) @@ -27,11 +27,13 @@ public RootPlanNode CreatePlan(DocumentNode document, string? operationName) if (TryPlanSelectionSet(operation, operation, new Stack())) { - rootPlanNode.AddOperation(operation); + operationPlan.AddOperation(operation); } } - return rootPlanNode; + OperationVariableBinder.BindOperationVariables(operationDefinition, operationPlan); + + return operationPlan; } private bool TryPlanSelectionSet( diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationVariableBinder.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationVariableBinder.cs new file mode 100644 index 00000000000..503f8cd7924 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationVariableBinder.cs @@ -0,0 +1,78 @@ +using HotChocolate.Fusion.Planning.Nodes; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Planning; + +internal static class OperationVariableBinder +{ + public static void BindOperationVariables( + OperationDefinitionNode operationDefinition, + RootPlanNode operationPlan) + { + var operationBacklog = new Stack(); + var selectionBacklog = new Stack(); + var variableDefinitions = operationDefinition.VariableDefinitions.ToDictionary(t => t.Variable.Name.Value); + var usedVariables = new HashSet(); + + foreach (var operation in operationPlan.Operations) + { + operationBacklog.Push(operation); + } + + while (operationBacklog.TryPop(out var operation)) + { + CollectAndBindUsedVariables(operation, variableDefinitions, usedVariables, selectionBacklog); + + foreach (var child in operation.Operations) + { + operationBacklog.Push(child); + } + } + } + + private static void CollectAndBindUsedVariables( + OperationPlanNode operation, + Dictionary variableDefinitions, + HashSet usedVariables, + Stack backlog) + { + usedVariables.Clear(); + backlog.Clear(); + backlog.Push(operation); + + while (backlog.TryPop(out var node)) + { + if (node is FieldPlanNode field) + { + foreach (var argument in field.Arguments) + { + if (argument.Value is VariableNode variable) + { + usedVariables.Add(variable.Name.Value); + } + } + + foreach (var directive in field.Directives) + { + foreach (var argument in directive.Arguments) + { + if (argument.Value is VariableNode variable) + { + usedVariables.Add(variable.Name.Value); + } + } + } + } + + foreach (var selection in node.Selections) + { + backlog.Push(selection); + } + } + + foreach (var variable in usedVariables) + { + operation.AddVariableDefinition(variableDefinitions[variable]); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs index 634e97503ea..329089967ca 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/OperationPlannerTests.cs @@ -168,4 +168,78 @@ await Assert } """); } + + [Test] + public async Task Plan_Simple_Operation_3_Source_Schema_And_Single_Variable() + { + var compositeSchemaDoc = Utf8GraphQLParser.Parse(FileResource.Open("fusion1.graphql")); + var compositeSchema = CompositeSchemaBuilder.Create(compositeSchemaDoc); + + var doc = Utf8GraphQLParser.Parse( + """ + query GetProduct($id: ID!, $first: Int! = 10) { + productById(id: $id) { + ... ProductCard + } + } + + fragment ProductCard on Product { + name + reviews(first: $first) { + nodes { + ... ReviewCard + } + } + } + + fragment ReviewCard on Review { + body + stars + author { + ... AuthorCard + } + } + + fragment AuthorCard on UserProfile { + displayName + } + """); + + var rewriter = new InlineFragmentOperationRewriter(compositeSchema); + var rewritten = rewriter.RewriteDocument(doc, null); + + // act + var planner = new OperationPlanner(compositeSchema); + var plan = planner.CreatePlan(rewritten, null); + + // assert + await Assert + .That(plan.ToSyntaxNode().ToString(indented: true)) + .IsEqualTo( + """ + query($id: ID!) { + productById(id: $id) { + name + } + } + + query($first: Int! = 10) { + productById { + reviews(first: $first) { + nodes { + body + stars + author + } + } + } + } + + { + userById { + displayName + } + } + """); + } }