diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 71b9192d7ec..534cac383c9 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -13,6 +13,7 @@ + diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/DocumentRewriterBenchmark.cs b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/DocumentRewriterBenchmark.cs new file mode 100644 index 00000000000..3006f4684f2 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/DocumentRewriterBenchmark.cs @@ -0,0 +1,48 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using HotChocolate.Fusion.Rewriters; +using HotChocolate.Language; + +namespace Fusion.Execution.Benchmarks; + +[MemoryDiagnoser] +[ShortRunJob(RuntimeMoniker.Net10_0)] +[MarkdownExporter] +public class DocumentRewriterBenchmark : FusionBenchmarkBase +{ + private DocumentRewriter _documentRewriter = null!; + + private DocumentNode _simpleQueryWithRequirements = null!; + private DocumentNode _complexQuery = null!; + private DocumentNode _conditionalRedundancyQuery = null!; + + [GlobalSetup] + public void GlobalSetup() + { + _simpleQueryWithRequirements = CreateSimpleQueryWithRequirementsDocument(); + _complexQuery = CreateComplexDocument(); + _conditionalRedundancyQuery = CreateConditionalRedundancyDocument(); + + var schema = CreateFusionSchema(); + + _documentRewriter = new DocumentRewriter(schema); + } + + [Benchmark] + public OperationDefinitionNode Rewrite_Simple_Query_With_Requirements() + { + return _documentRewriter.RewriteOperation(_simpleQueryWithRequirements); + } + + [Benchmark] + public OperationDefinitionNode Rewrite_Complex_Query() + { + return _documentRewriter.RewriteOperation(_complexQuery); + } + + [Benchmark] + public OperationDefinitionNode Rewrite_ConditionalRedundancy_Query() + { + return _documentRewriter.RewriteOperation(_conditionalRedundancyQuery); + } +} diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Fusion.Execution.Benchmarks.csproj b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Fusion.Execution.Benchmarks.csproj index 213b98e0d1e..4f2fd985693 100644 --- a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Fusion.Execution.Benchmarks.csproj +++ b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Fusion.Execution.Benchmarks.csproj @@ -10,6 +10,7 @@ + @@ -19,6 +20,7 @@ + diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/FusionBenchmarkBase.cs b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/FusionBenchmarkBase.cs new file mode 100644 index 00000000000..22713c4cb83 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/FusionBenchmarkBase.cs @@ -0,0 +1,846 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Fusion; +using HotChocolate.Fusion.Logging; +using HotChocolate.Fusion.Options; +using HotChocolate.Fusion.Types; +using HotChocolate.Language; + +namespace Fusion.Execution.Benchmarks; + +public abstract class FusionBenchmarkBase +{ + protected static FusionSchemaDefinition CreateFusionSchema() + { + List sourceSchemas = [ + new SourceSchemaText( + "products", + """ + type Query { + productById(id: ID!): Product @lookup + products(first: Int, after: String, last: Int, before: String): ProductConnection + } + + type Product { + id: ID! + name: String! + description: String @shareable + price: Float! + dimension: ProductDimension! + estimatedDelivery(postCode: String): Int! + } + + type ProductDimension { + height: Int! + width: Int! + } + + type ProductConnection { + pageInfo: PageInfo! + edges: [ProductEdge!] + nodes: [Product!] + } + + type ProductEdge { + cursor: String! + node: Product! + } + + type PageInfo @shareable { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String + } + """), + new SourceSchemaText( + "reviews", + """ + type Query { + reviewById(id: ID!): Review @lookup + productById(id: ID!): Product @lookup @internal + viewer: Viewer + } + + type Viewer { + reviews(first: Int, after: String, last: Int, before: String): ProductReviewConnection + } + + type Product { + id: ID! + averageRating: Int! + reviews(first: Int, after: String, last: Int, before: String): ProductReviewConnection + } + + type Review { + id: ID! + body: String! + stars: Int! + author: User + product: Product + } + + type User { + id: ID! + } + + type ProductReviewConnection { + pageInfo: PageInfo! + edges: [ProductReviewEdge!] + nodes: [Review!] + } + + type ProductReviewEdge { + cursor: String! + node: Review! + } + + type PageInfo @shareable { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String + } + """), + new SourceSchemaText( + "users", + """ + type Query { + userById(id: ID!): User @lookup + viewer: Viewer + } + + type Viewer { + displayName: String! + } + + type User { + id: ID! + displayName: String! + reviews(first: Int, after: String, last: Int, before: String): UserReviewConnection + } + + type UserReviewConnection { + pageInfo: PageInfo! + edges: [UserReviewEdge!] + nodes: [Review!] + } + + type UserReviewEdge { + cursor: String! + node: Review! + } + + type Review { + id: ID! + } + + type PageInfo @shareable { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String + } + """), + new SourceSchemaText( + "search", + """ + type Query { + searchContent(query: String!): [SearchResult!]! + productById(id: ID!): Product @lookup @internal + } + + interface SearchResult { + id: ID! + title: String! + description: String + } + + type Product implements SearchResult { + id: ID! + title: String! + description: String @shareable + } + + type Article implements SearchResult { + id: ID! + title: String! + description: String + content: String! + author: User! + publishedAt: String! + tags: [String!]! + } + + type User { + id: ID! + } + """) + ]; + + var compositionLog = new CompositionLog(); + var composerOptions = new SchemaComposerOptions { EnableGlobalObjectIdentification = true }; + var composer = new SchemaComposer(sourceSchemas, composerOptions, compositionLog); + var result = composer.Compose(); + + if (!result.IsSuccess) + { + throw new InvalidOperationException(result.Errors[0].Message); + } + + var compositeSchemaDoc = result.Value.ToSyntaxNode(); + return FusionSchemaDefinition.Create(compositeSchemaDoc); + } + + protected static DocumentNode CreateSimpleQueryWithRequirementsDocument() + { + return Utf8GraphQLParser.Parse( + """ + query($productId: ID!) { + productById(id: $id) { + name + reviews { + nodes { + id + body + author { + displayName + } + } + } + } + } + """); + } + + protected static DocumentNode CreateComplexDocument() + { + return Utf8GraphQLParser.Parse( + """ + query($level1: Boolean!, $level2: Boolean!, $level3: Boolean!, $level4: Boolean!, $level5: Boolean!, $level6: Boolean!, $includeExpensive: Boolean!, $includeReviews: Boolean!, $includeMetadata: Boolean!) { + # Deep conditional nesting (6+ levels) + productById(id: "1") { + id + name + ... @include(if: $level1) { + description + ... @skip(if: $level2) { + price + ... @include(if: $level3) { + dimension { + height + width + } + ... @skip(if: $level4) { + # Level 4 nesting + ... @include(if: $level5) { + # Level 5 nesting + ... @skip(if: $level6) { + # Level 6 nesting - extreme depth + reviews(first: 10) { + pageInfo { + hasNextPage + ... @include(if: $includeMetadata) { + hasPreviousPage + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + ... @include(if: $includeReviews) { + author { + id + displayName + ... @skip(if: $level1) { + reviews(first: 5) { + nodes { + id + body + ... @include(if: $level2) { + stars + product { + id + name + ... @skip(if: $level3) { + price + ... @include(if: $level4) { + dimension { + height + width + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + # Many fields with same response name but different conditionals + products(first: 3) { + edges { + node { + # Multiple 'name' fields with different conditionals + name + name @include(if: $level1) + name @skip(if: $level2) + name @include(if: $level3) @skip(if: $level4) + name @skip(if: $level5) @include(if: $level6) + # Multiple 'description' fields with complex conditionals + description + description @include(if: $includeExpensive) + description @skip(if: $level1) @include(if: $level2) + description @include(if: $level3) @skip(if: $level4) @include(if: $level5) + # Multiple 'price' fields with nested conditionals + price + price @include(if: $includeExpensive) + price @skip(if: $level1) + price @include(if: $level2) @skip(if: $level3) + price @skip(if: $level4) @include(if: $level5) @skip(if: $level6) + # Complex field merging with fragments + ... ProductBasicFields + ... @include(if: $level1) { + ... ProductBasicFields + } + ... @skip(if: $level2) { + ... ProductBasicFields + } + ... @include(if: $level3) @skip(if: $level4) { + ... ProductBasicFields + } + # Interface type refinements with deep conditionals + ... on Product { + ... @include(if: $includeReviews) { + reviews(first: 5) { + nodes { + id + body + stars + ... @skip(if: $level1) { + author { + id + displayName + ... @include(if: $level2) { + reviews(first: 3) { + nodes { + id + body + ... @skip(if: $level3) { + stars + product { + id + name + ... @include(if: $level4) { + price + ... @skip(if: $level5) { + dimension { + height + width + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + # Complex conditional merging scenarios + searchContent(query: "extreme") { + # Interface field with multiple conditionals + id + id @include(if: $level1) + id @skip(if: $level2) + title + title @include(if: $includeExpensive) + title @skip(if: $level3) + description + description @include(if: $level4) + description @skip(if: $level5) @include(if: $level6) + # Type refinements with extreme conditional complexity + ... on Product { + name + name @include(if: $level1) + name @skip(if: $level2) @include(if: $level3) + price + price @include(if: $includeExpensive) + price @skip(if: $level4) @include(if: $level5) @skip(if: $level6) + ... @include(if: $includeReviews) { + reviews(first: 3) { + nodes { + id + body + stars + ... @skip(if: $level1) { + author { + id + displayName + ... @include(if: $level2) { + reviews(first: 2) { + nodes { + id + body + ... @skip(if: $level3) { + stars + product { + id + name + ... @include(if: $level4) { + price + ... @skip(if: $level5) { + dimension { + height + width + } + ... @include(if: $level6) { + # Extreme nesting level - move reviews to Product level + reviews(first: 1) { + nodes { + id + body + stars + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ... on Article { + content + content @include(if: $includeExpensive) + content @skip(if: $level1) @include(if: $level2) + author { + id + displayName + ... @include(if: $level3) { + reviews(first: 2) { + nodes { + id + body + ... @skip(if: $level4) { + stars + product { + id + name + ... @include(if: $level5) { + price + ... @skip(if: $level6) { + dimension { + height + width + } + } + } + } + } + } + } + } + } + publishedAt + publishedAt @include(if: $includeMetadata) + publishedAt @skip(if: $level1) @include(if: $level2) @skip(if: $level3) + tags + tags @include(if: $level4) + tags @skip(if: $level5) @include(if: $level6) + } + } + # Viewer with extreme conditional complexity + viewer { + displayName + displayName @include(if: $level1) + displayName @skip(if: $level2) @include(if: $level3) + ... @include(if: $includeReviews) { + reviews(first: 5) { + pageInfo { + hasNextPage + hasNextPage @include(if: $level4) + hasNextPage @skip(if: $level5) @include(if: $level6) + hasPreviousPage + hasPreviousPage @include(if: $includeMetadata) + hasPreviousPage @skip(if: $level1) @include(if: $level2) + startCursor + startCursor @include(if: $level3) + startCursor @skip(if: $level4) @include(if: $level5) + endCursor + endCursor @include(if: $level6) + endCursor @skip(if: $level1) @include(if: $level2) @skip(if: $level3) + } + edges { + cursor + cursor @include(if: $level4) + cursor @skip(if: $level5) @include(if: $level6) + node { + id + id @include(if: $level1) + id @skip(if: $level2) @include(if: $level3) + body + body @include(if: $includeExpensive) + body @skip(if: $level4) @include(if: $level5) @skip(if: $level6) + stars + stars @include(if: $level1) + stars @skip(if: $level2) + ... @include(if: $includeReviews) { + author { + id + displayName + ... @skip(if: $level3) { + reviews(first: 3) { + nodes { + id + body + stars + ... @include(if: $level4) { + product { + id + name + ... @skip(if: $level5) { + price + ... @include(if: $level6) { + dimension { + height + width + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + fragment ProductBasicFields on Product { + id + name + description + price + averageRating + } + """); + } + + protected static DocumentNode CreateConditionalRedundancyDocument() + { + return Utf8GraphQLParser.Parse( + """ + query($includeExpensive: Boolean!, $includeReviews: Boolean!, $includeMetadata: Boolean!, $includeDetails: Boolean!) { + # Unconditional selections that are also inside conditionals + productById(id: "1") { + # These fields exist unconditionally + id + name + description + price + averageRating + # Same fields inside conditionals - should be deduplicated + ... @include(if: $includeExpensive) { + id + name + description + price + averageRating + } + # More redundancy with different conditionals + ... @include(if: $includeReviews) { + id + name + description + price + averageRating + } + # Nested redundancy + dimension { + height + width + ... @include(if: $includeDetails) { + height + width + } + } + # Reviews with redundant selections + reviews(first: 5) { + pageInfo { + hasNextPage + hasPreviousPage + ... @include(if: $includeMetadata) { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + # Same fields in conditional + ... @include(if: $includeReviews) { + id + body + stars + author { + id + displayName + # Nested redundancy + ... @include(if: $includeDetails) { + id + displayName + } + } + } + } + } + } + } + # Products with extensive redundancy + products(first: 3) { + edges { + node { + # Unconditional fields + id + name + description + price + averageRating + # Redundant conditional selections + ... @include(if: $includeExpensive) { + id + name + description + price + averageRating + dimension { + height + width + } + } + ... @include(if: $includeReviews) { + id + name + description + price + averageRating + reviews(first: 3) { + nodes { + id + body + stars + # More redundancy + ... @include(if: $includeDetails) { + id + body + stars + } + } + } + } + # Fragment redundancy + ... ProductBasicInfo + ... @include(if: $includeExpensive) { + ... ProductBasicInfo + } + ... @include(if: $includeReviews) { + ... ProductBasicInfo + } + } + } + } + # Search content with interface redundancy + searchContent(query: "redundant") { + # Interface fields unconditionally + id + title + description + # Same interface fields in conditionals + ... @include(if: $includeExpensive) { + id + title + description + } + # Type-specific redundancy + ... on Product { + # Unconditional product fields + id + name + price + averageRating + # Redundant conditional selections + ... @include(if: $includeExpensive) { + id + name + price + averageRating + dimension { + height + width + } + } + ... @include(if: $includeReviews) { + id + name + price + averageRating + reviews(first: 2) { + nodes { + id + body + stars + # Nested redundancy + ... @include(if: $includeDetails) { + id + body + stars + } + } + } + } + } + ... on Article { + # Unconditional article fields + id + title + description + content + publishedAt + # Redundant conditional selections + ... @include(if: $includeExpensive) { + id + title + description + content + publishedAt + tags + } + ... @include(if: $includeDetails) { + id + title + description + content + publishedAt + author { + id + displayName + # More redundancy + ... @include(if: $includeReviews) { + id + displayName + } + } + } + } + } + # Viewer with extensive redundancy + viewer { + # Unconditional fields + displayName + # Redundant conditional selections + ... @include(if: $includeReviews) { + displayName + reviews(first: 5) { + pageInfo { + hasNextPage + hasPreviousPage + # Redundant pageInfo fields + ... @include(if: $includeMetadata) { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + edges { + cursor + node { + # Unconditional review fields + id + body + stars + # Redundant conditional selections + ... @include(if: $includeDetails) { + id + body + stars + author { + id + displayName + # Nested redundancy + ... @include(if: $includeReviews) { + id + displayName + reviews(first: 2) { + nodes { + id + body + stars + # Deep redundancy + ... @include(if: $includeExpensive) { + id + body + stars + } + } + } + } + } + } + } + } + } + } + } + } + + fragment ProductBasicInfo on Product { + id + name + description + price + averageRating + } + """); + } +} diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/OperationCompilerBenchmark.cs b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/OperationCompilerBenchmark.cs new file mode 100644 index 00000000000..9710fa9679b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/OperationCompilerBenchmark.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnostics.dotMemory; +using BenchmarkDotNet.Jobs; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Fusion.Rewriters; +using HotChocolate.Language; +using Microsoft.Extensions.ObjectPool; + +namespace Fusion.Execution.Benchmarks; + +[DotMemoryDiagnoser] +[MemoryDiagnoser] +[ShortRunJob(RuntimeMoniker.Net10_0)] +[MarkdownExporter] +public class OperationCompilerBenchmark : FusionBenchmarkBase +{ + private const string Id = "123456789101112"; + + private OperationCompiler _compiler = null!; + + private OperationDefinitionNode _simpleQueryWithRequirements = null!; + private OperationDefinitionNode _complexQuery = null!; + private OperationDefinitionNode _conditionalRedundancyQuery = null!; + + [GlobalSetup] + public void GlobalSetup() + { + var schema = CreateFusionSchema(); + + var documentRewriter = new DocumentRewriter(schema); + + _simpleQueryWithRequirements = documentRewriter.RewriteOperation(CreateSimpleQueryWithRequirementsDocument()); + _complexQuery = documentRewriter.RewriteOperation(CreateComplexDocument()); + _conditionalRedundancyQuery = documentRewriter.RewriteOperation(CreateConditionalRedundancyDocument()); + + var pool = new NoOpObjectPool>>(); + _compiler = new OperationCompiler(schema, pool); + } + + [Benchmark] + public Operation Compile_Simple_Query_With_Requirements() + { + return _compiler.Compile(Id, Id, _simpleQueryWithRequirements); + } + + [Benchmark] + public Operation Compile_Complex_Query() + { + return _compiler.Compile(Id, Id, _complexQuery); + } + + [Benchmark] + public Operation Compile_ConditionalRedundancy_Query() + { + return _compiler.Compile(Id, Id, _conditionalRedundancyQuery); + } + + private sealed class NoOpObjectPool : ObjectPool where T : class, new() + { + public override T Get() + { + return new T(); + } + + public override void Return(T obj) + { + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/OperationPlannerBenchmark.cs b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/OperationPlannerBenchmark.cs new file mode 100644 index 00000000000..a1ca7e1bc06 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/OperationPlannerBenchmark.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Fusion.Planning; +using HotChocolate.Fusion.Rewriters; +using HotChocolate.Language; +using Microsoft.Extensions.ObjectPool; + +namespace Fusion.Execution.Benchmarks; + +[MemoryDiagnoser] +[ShortRunJob(RuntimeMoniker.Net10_0)] +[MarkdownExporter] +public class OperationPlannerBenchmark : FusionBenchmarkBase +{ + private const string Id = "123456789101112"; + + private OperationPlanner _planner = null!; + + private OperationDefinitionNode _simpleQueryWithRequirements = null!; + private OperationDefinitionNode _complexQuery = null!; + private OperationDefinitionNode _conditionalRedundancyQuery = null!; + + [GlobalSetup] + public void GlobalSetup() + { + var schema = CreateFusionSchema(); + + var documentRewriter = new DocumentRewriter(schema); + + _simpleQueryWithRequirements = documentRewriter.RewriteOperation(CreateSimpleQueryWithRequirementsDocument()); + _complexQuery = documentRewriter.RewriteOperation(CreateComplexDocument()); + _conditionalRedundancyQuery = documentRewriter.RewriteOperation(CreateConditionalRedundancyDocument()); + + var pool = new DefaultObjectPool>>( + new DefaultPooledObjectPolicy>>()); + var operationCompiler = new OperationCompiler(schema, pool); + + _planner = new OperationPlanner(schema, operationCompiler); + } + + [Benchmark] + public uint? Plan_Simple_Query_With_Requirements() + { + return _planner.TryCreatePlan(Id, _simpleQueryWithRequirements)?.SearchSpace; + } + + [Benchmark] + public uint? Plan_Complex_Query() + { + return _planner.TryCreatePlan(Id, _complexQuery)?.SearchSpace; + } + + [Benchmark] + public uint? Plan_ConditionalRedundancy_Query() + { + return _planner.TryCreatePlan(Id, _conditionalRedundancyQuery)?.SearchSpace; + } +} diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Program.cs b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Program.cs index 01aeb56b5e3..f1a5ad0c0ec 100644 --- a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Program.cs +++ b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Program.cs @@ -5,4 +5,4 @@ var config = DefaultConfig.Instance .WithOption(ConfigOptions.DisableOptimizationsValidator, true); -BenchmarkRunner.Run(config); +BenchmarkRunner.Run(config); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs index 70ebc5edaae..0cd9572bb59 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs @@ -1,5 +1,4 @@ using System.Buffers; -using HotChocolate.Fusion.Rewriters; using HotChocolate.Language; using HotChocolate.Language.Visitors; using HotChocolate.Types; @@ -10,7 +9,6 @@ namespace HotChocolate.Fusion.Execution.Nodes; public sealed class OperationCompiler { private readonly ISchemaDefinition _schema; - private readonly DocumentRewriter _documentRewriter; private readonly ObjectPool>> _fieldsPool; private readonly TypeNameField _typeNameField; private static readonly ArrayPool s_objectArrayPool = ArrayPool.Shared; @@ -24,7 +22,6 @@ public OperationCompiler( _schema = schema; _fieldsPool = fieldsPool; - _documentRewriter = new(schema, removeStaticallyExcludedSelections: true); var nonNullStringType = new NonNullType(_schema.Types.GetType(SpecScalarNames.String)); _typeNameField = new TypeNameField(nonNullStringType); } @@ -34,10 +31,6 @@ public Operation Compile(string id, string hash, OperationDefinitionNode operati ArgumentException.ThrowIfNullOrWhiteSpace(id); ArgumentNullException.ThrowIfNull(operationDefinition); - var document = new DocumentNode(new IDefinitionNode[] { operationDefinition }); - document = _documentRewriter.RewriteDocument(document); - operationDefinition = (OperationDefinitionNode)document.Definitions[0]; - var includeConditions = new IncludeConditionCollection(); IncludeConditionVisitor.Instance.Visit(operationDefinition, includeConditions); var fields = _fieldsPool.Get(); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationPlanMiddleware.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationPlanMiddleware.cs index a3dc0d3f4c7..cb8b5cc762b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationPlanMiddleware.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Pipeline/OperationPlanMiddleware.cs @@ -61,8 +61,7 @@ private void PlanOperation( try { // Before we can plan an operation, we must de-fragmentize it and remove static include conditions. - var rewritten = _documentRewriter.RewriteDocument(operationDocument, context.Request.OperationName); - var operation = rewritten.GetOperation(context.Request.OperationName); + var operation = _documentRewriter.RewriteOperation(operationDocument, context.Request.OperationName); // After optimizing the query structure we can begin the planning process. var operationPlan = _planner.CreatePlan(operationId, operationHash, operationShortHash, operation); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/HotChocolate.Fusion.Execution.csproj b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/HotChocolate.Fusion.Execution.csproj index 4373935f26e..691adacf2e0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/HotChocolate.Fusion.Execution.csproj +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/HotChocolate.Fusion.Execution.csproj @@ -9,6 +9,7 @@ + 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 bd583095cdc..1abb4e4561b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.cs @@ -56,7 +56,24 @@ public OperationPlan CreatePlan( ArgumentException.ThrowIfNullOrEmpty(shortHash); ArgumentNullException.ThrowIfNull(operationDefinition); - // We first need to create an index to keep track of the logical selections + var plan = TryCreatePlan(shortHash, operationDefinition); + + var planSteps = plan?.Steps ?? []; + var searchSpace = plan?.SearchSpace ?? 0; + var internalOperationDefinition = plan?.InternalOperationDefinition ?? operationDefinition; + + var operation = _operationCompiler.Compile(id, hash, internalOperationDefinition); + + return BuildExecutionPlan( + operation, + operationDefinition, + planSteps, + searchSpace); + } + + internal PlanResult? TryCreatePlan(string shortHash, OperationDefinitionNode operationDefinition) + { + // We first need to create an index to keep track of the logical selections // sets before we can branch them. This allows us to inline requirements later // into the right place. var index = SelectionSetIndexer.Create(operationDefinition); @@ -69,52 +86,40 @@ public OperationPlan CreatePlan( _ => throw new ArgumentOutOfRangeException() }; - var internalOperationDefinition = operationDefinition; - ImmutableList planSteps = []; - uint searchSpace = 0; - - if (!node.Backlog.IsEmpty) + if (node.Backlog.IsEmpty) { - var possiblePlans = new PriorityQueue(); + return null; + } - foreach (var (schemaName, resolutionCost) in _schema.GetPossibleSchemas(selectionSet)) - { - possiblePlans.Enqueue( - node with - { - SchemaName = schemaName, - ResolutionCost = resolutionCost - }); - } + var possiblePlans = new PriorityQueue(); - if (possiblePlans.Count < 1) - { - possiblePlans.Enqueue(node); - } - - var plan = Plan(possiblePlans); + foreach (var (schemaName, resolutionCost) in _schema.GetPossibleSchemas(selectionSet)) + { + possiblePlans.Enqueue( + node with + { + SchemaName = schemaName, + ResolutionCost = resolutionCost + }); + } - if (!plan.HasValue) - { - throw new InvalidOperationException("No possible plan was found."); - } + if (possiblePlans.Count < 1) + { + possiblePlans.Enqueue(node); + } - internalOperationDefinition = plan.Value.InternalOperationDefinition; - planSteps = plan.Value.Steps; - searchSpace = plan.Value.SearchSpace; + var result = Plan(possiblePlans); - internalOperationDefinition = AddTypeNameToAbstractSelections( - internalOperationDefinition, - _schema.GetOperationType(operationDefinition.Operation)); + if (!result.HasValue) + { + throw new InvalidOperationException("No possible plan was found."); } - var operation = _operationCompiler.Compile(id, hash, internalOperationDefinition); + var rewrittenOperation = AddTypeNameToAbstractSelections( + result.Value.InternalOperationDefinition, + selectionSet.Type); - return BuildExecutionPlan( - operation, - operationDefinition, - planSteps, - searchSpace); + return result.Value with { InternalOperationDefinition = rewrittenOperation }; } private (PlanNode Node, SelectionSet First) CreateQueryPlanBase( @@ -1368,8 +1373,12 @@ private OperationDefinitionNode InlineSelections( if (inlineInternal) { var size = selectionSet.Selections.Count + selectionsToInline.Selections.Count; - var selections = new List(size); - selections.AddRange(originalSelectionSet.Selections); + var newSelections = new List(size); + newSelections.AddRange(originalSelectionSet.Selections); + + var seenSelections = new HashSet( + originalSelectionSet.Selections, + DocumentRewriter.ShallowSyntaxNodeComparer.Instance); foreach (var selection in selectionsToInline.Selections) { @@ -1378,18 +1387,48 @@ private OperationDefinitionNode InlineSelections( switch (selection) { case FieldNode field: - selections.Add(field.WithDirectives(directives)); - IndexInternalSelections(field.SelectionSet, index, ref backlog); + var fieldWithDirective = field.WithDirectives(directives); + + if (seenSelections.Add(fieldWithDirective)) + { + var insertIndex = newSelections.Count; + var responseName = field.Alias?.Value ?? field.Name.Value; + + for (var i = newSelections.Count - 1; i >= 0; i--) + { + if (newSelections[i] is not FieldNode existingField) + { + continue; + } + + var responseNameToCompare = + existingField.Alias?.Value ?? existingField.Name.Value; + + if (responseNameToCompare.Equals(responseName, StringComparison.Ordinal)) + { + insertIndex = i + 1; + break; + } + } + + newSelections.Insert(insertIndex, fieldWithDirective); + IndexInternalSelections(field.SelectionSet, index, ref backlog); + } break; case InlineFragmentNode inlineFragment: - selections.Add(inlineFragment.WithDirectives(directives)); - IndexInternalSelections(inlineFragment.SelectionSet, index, ref backlog); + var inlineFragmentWithDirective = inlineFragment.WithDirectives(directives); + + if (seenSelections.Add(inlineFragmentWithDirective)) + { + newSelections.Add(inlineFragmentWithDirective); + IndexInternalSelections(inlineFragment.SelectionSet, index, ref backlog); + } break; } } - newSelectionSet = new SelectionSetNode(selections); + newSelectionSet = new SelectionSetNode(newSelections); } else { @@ -1552,7 +1591,7 @@ private static bool IsTypeNameSelection(ISelectionNode selection) return false; } - private readonly record struct PlanResult( + internal readonly record struct PlanResult( OperationDefinitionNode InternalOperationDefinition, ImmutableList Steps, uint SearchSpace); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/HotChocolate.Fusion.Utilities.csproj b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/HotChocolate.Fusion.Utilities.csproj index c19e15e7a94..7dc8880bc73 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/HotChocolate.Fusion.Utilities.csproj +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/HotChocolate.Fusion.Utilities.csproj @@ -5,6 +5,10 @@ HotChocolate.Fusion + + + + diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/DocumentRewriter.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/DocumentRewriter.cs index 689620e30dc..6286a5ca199 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/DocumentRewriter.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Utilities/Rewriters/DocumentRewriter.cs @@ -18,7 +18,7 @@ [new DirectiveNode("fusion__empty")], ImmutableArray.Empty, null); - public DocumentNode RewriteDocument(DocumentNode document, string? operationName = null) + public OperationDefinitionNode RewriteOperation(DocumentNode document, string? operationName = null) { var operation = document.GetOperation(operationName); var operationType = schema.GetOperationType(operation.Operation); @@ -38,7 +38,7 @@ public DocumentNode RewriteDocument(DocumentNode document, string? operationName RewriteDirectives(operation.Directives), newSelectionSet); - return new DocumentNode([newOperation]); + return newOperation; } private SelectionSetNode RewriteSelectionSet( @@ -868,7 +868,7 @@ public Context GetOrAddConditionalContext(Conditional conditional) public void RecordReferenceInConditionalContext(ISelectionNode selectionNode, Context conditionalContext) { ReferencesInConditionalContexts ??= - new Dictionary>(SyntaxNodeComparer.Instance); + new Dictionary>(ShallowSyntaxNodeComparer.Instance); if (!ReferencesInConditionalContexts.TryGetValue(selectionNode, out var conditionalContexts)) { @@ -1122,7 +1122,11 @@ private static int GetDirectiveHashCode(DirectiveNode? node) #region Comparers - private sealed class SyntaxNodeComparer : IEqualityComparer + /// + /// Compares fields just by their alias, name and directives, + /// and inline fragments just by their type condition and directives. + /// + internal sealed class ShallowSyntaxNodeComparer : IEqualityComparer { public bool Equals(ISyntaxNode? x, ISyntaxNode? y) { @@ -1151,10 +1155,10 @@ public int GetHashCode(ISyntaxNode obj) return InlineFragmentNodeComparer.Instance.GetHashCode(inlineFragment); } - throw new NotImplementedException(); + throw new NotSupportedException(); } - public static SyntaxNodeComparer Instance { get; } = new(); + public static ShallowSyntaxNodeComparer Instance { get; } = new(); } private sealed class InlineFragmentNodeComparer : IEqualityComparer @@ -1229,10 +1233,7 @@ public bool Equals(FieldNode? x, FieldNode? y) return false; } - return Equals(x.Alias, y.Alias) - && x.Name.Equals(y.Name) - && Equals(x.Directives, y.Directives) - && Equals(x.Arguments, y.Arguments); + return Equals(x.Alias, y.Alias) && x.Name.Equals(y.Name) && Equals(x.Directives, y.Directives); } private bool Equals(IReadOnlyList a, IReadOnlyList b) @@ -1261,11 +1262,6 @@ public int GetHashCode(FieldNode obj) hashCode.Add(SyntaxComparer.BySyntax.GetHashCode(obj.Directives[i])); } - for (var i = 0; i < obj.Arguments.Count; i++) - { - hashCode.Add(SyntaxComparer.BySyntax.GetHashCode(obj.Arguments[i])); - } - return hashCode.ToHashCode(); } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Lookup_Skip_On_Field_Fetched_Through_Lookup.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Lookup_Skip_On_Field_Fetched_Through_Lookup.yaml index 0af4b91a00e..a5d5846b2c5 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Lookup_Skip_On_Field_Fetched_Through_Lookup.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Lookup_Skip_On_Field_Fetched_Through_Lookup.yaml @@ -31,17 +31,17 @@ sourceSchemas: schema { query: Query } - + type Product { id: ID! name: String! review: Review } - + type Query { productBySlug(slug: String!): Product } - + type Review { id: ID! } @@ -74,11 +74,11 @@ sourceSchemas: schema { query: Query } - + type Query { reviewById(id: ID!): Review @lookup } - + type Review { id: ID! title: String! @@ -92,8 +92,8 @@ operationPlan: productBySlug(slug: "product") { name review { - id @fusion__requirement title @skip(if: $skip) + id @fusion__requirement } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Lookup_Skip_Only_On_Some_Fields_Fetched_Through_Lookup.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Lookup_Skip_Only_On_Some_Fields_Fetched_Through_Lookup.yaml index 9c65a6c49f8..11258204d8e 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Lookup_Skip_Only_On_Some_Fields_Fetched_Through_Lookup.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.Lookup_Skip_Only_On_Some_Fields_Fetched_Through_Lookup.yaml @@ -34,17 +34,17 @@ sourceSchemas: schema { query: Query } - + type Product { id: ID! name: String! review: Review } - + type Query { productBySlug(slug: String!): Product } - + type Review { id: ID! } @@ -77,11 +77,11 @@ sourceSchemas: schema { query: Query } - + type Query { reviewById(id: ID!): Review @lookup } - + type Review { id: ID! title: String! @@ -124,8 +124,8 @@ operationPlan: name review { rating - id @fusion__requirement title @skip(if: $skip) + id @fusion__requirement } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Interface_Type_Refinement.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Interface_Type_Refinement.yaml index 9fea58ed864..8014bbfd5da 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Interface_Type_Refinement.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Interface_Type_Refinement.yaml @@ -35,27 +35,27 @@ sourceSchemas: schema { query: Query } - + interface Node { id: ID! } - + interface Votable { viewerCanVote: Boolean! } - + type Author implements Node & Votable { id: ID! username: String viewerCanVote: Boolean! } - + type Discussion implements Node & Votable { id: ID! title: String viewerCanVote: Boolean! } - + type Query { node(id: ID!): Node @lookup } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Multiple_Type_Refinements.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Multiple_Type_Refinements.yaml index e3b3e31a5d0..1b4e26650da 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Multiple_Type_Refinements.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Multiple_Type_Refinements.yaml @@ -32,16 +32,16 @@ sourceSchemas: schema { query: Query } - + interface Node { id: ID! } - + type Discussion implements Node { id: ID! title: String } - + type Query { node(id: ID!): Node @lookup } @@ -80,16 +80,16 @@ sourceSchemas: schema { query: Query } - + interface Node { id: ID! } - + type Author implements Node { id: ID! username: String } - + type Query { node(id: ID!): Node @lookup } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Shared_Selections.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Shared_Selections.yaml index 421bde24e1b..f041a62b371 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Shared_Selections.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Shared_Selections.yaml @@ -32,16 +32,16 @@ sourceSchemas: schema { query: Query } - + interface Node { id: ID! } - + type Discussion implements Node { id: ID! title: String } - + type Query { node(id: ID!): Node @lookup } @@ -80,16 +80,16 @@ sourceSchemas: schema { query: Query } - + interface Node { id: ID! } - + type Author implements Node { id: ID! username: String } - + type Query { node(id: ID!): Node @lookup } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Selection.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Selection.yaml index 6d7285f7c31..c4b8a8f2cfe 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Selection.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Selection.yaml @@ -34,25 +34,25 @@ sourceSchemas: schema { query: Query } - + interface Authorable { author: Author } - + interface Node { id: ID! } - + type Author implements Node { id: ID! } - + type Discussion implements Node & Authorable { id: ID! title: String author: Author } - + type Query { node(id: ID!): Node @lookup } @@ -93,13 +93,13 @@ sourceSchemas: schema { query: Query } - + type Author { id: ID! username: String rating: Int } - + type Query { authorById(id: ID!): Author @lookup } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Type_Refinement.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Type_Refinement.yaml index cc792c8e2fb..0173b6df66f 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Type_Refinement.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Type_Refinement.yaml @@ -27,26 +27,26 @@ sourceSchemas: schema { query: Query } - + interface Node { id: ID! } - + interface Votable { viewerCanVote: Boolean! } - + type Author implements Node & Votable { id: ID! viewerCanVote: Boolean! } - + type Discussion implements Node & Votable { id: ID! title: String viewerCanVote: Boolean! } - + type Query { node(id: ID!): Node @lookup } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Shared_Selection.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Shared_Selection.yaml index 4389eada585..a32521d6716 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Shared_Selection.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Shared_Selection.yaml @@ -30,16 +30,16 @@ sourceSchemas: schema { query: Query } - + interface Node { id: ID! } - + type Discussion implements Node { id: ID! title: String } - + type Query { node(id: ID!): Node @lookup } @@ -78,16 +78,16 @@ sourceSchemas: schema { query: Query } - + interface Node { id: ID! } - + type Author implements Node { id: ID! username: String } - + type Query { node(id: ID!): Node @lookup } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Concrete_Type_Branch_Requested.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Concrete_Type_Branch_Requested.yaml index d65e6b8281b..a8975940401 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Concrete_Type_Branch_Requested.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Concrete_Type_Branch_Requested.yaml @@ -115,11 +115,11 @@ operationPlan: { node(id: "RGlzY3Vzc2lvbjox") { __typename @fusion__requirement - id @fusion__requirement ... on Discussion { title commentCount } + id @fusion__requirement } } hash: e567807ec1ad5128cfb566fad1645abc diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Concrete_Type_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Concrete_Type_Has_Dependency.yaml index 65360d65171..8b702b5d305 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Concrete_Type_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Concrete_Type_Has_Dependency.yaml @@ -125,11 +125,11 @@ operationPlan: ) { node(id: $id) { __typename @fusion__requirement - id @fusion__requirement ... on Discussion { name commentCount } + id @fusion__requirement } } name: testQuery diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Two_Node_Fields_With_Alias.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Two_Node_Fields_With_Alias.yaml index 4342b5dfc34..787000f2fd3 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Two_Node_Fields_With_Alias.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Two_Node_Fields_With_Alias.yaml @@ -143,11 +143,11 @@ operationPlan: } b: node(id: "RGlzY3Vzc2lvbjoy") { __typename @fusion__requirement - id @fusion__requirement ... on Discussion { title commentCount } + id @fusion__requirement } } hash: a361113ff8d3c2a57d63e65e469e73a7 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/FusionTestBase.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/FusionTestBase.cs index 97df04894f6..f05cae1b561 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/FusionTestBase.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/FusionTestBase.cs @@ -488,8 +488,7 @@ protected static OperationPlan PlanOperation( var operationDoc = Utf8GraphQLParser.Parse(operationText); var rewriter = new DocumentRewriter(schema); - var rewritten = rewriter.RewriteDocument(operationDoc, operationName: null); - var operation = rewritten.Definitions.OfType().First(); + var operation = rewriter.RewriteOperation(operationDoc, operationName: null); var compiler = new OperationCompiler(schema, pool); var planner = new OperationPlanner(schema, compiler); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/FusionBenchmarkTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/FusionBenchmarkTests.cs new file mode 100644 index 00000000000..61b4c45f92d --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/FusionBenchmarkTests.cs @@ -0,0 +1,898 @@ +using HotChocolate.Fusion.Logging; +using HotChocolate.Fusion.Options; +using HotChocolate.Fusion.Types; +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Execution; + +// If one of these tests fails, when fixing, you also need to update the +// FusionBenchmarkBase.cs in Fusion.Execution.Benchmarks. +public class FusionBenchmarkTests : FusionTestBase +{ + [Fact] + public void Simple_Query_With_Requirements() + { + // arrange + var schema = CreateSchema(); + + var doc = CreateSimpleQueryWithRequirementsDocument().ToString(); + + // act + var plan = PlanOperation(schema, doc); + + // assert + MatchSnapshot(plan); + } + + [Fact] + public void Complex_Query() + { + // arrange + var schema = CreateSchema(); + + var doc = CreateComplexDocument().ToString(); + + // act + var plan = PlanOperation(schema, doc); + + // assert + MatchSnapshot(plan); + } + + [Fact] + public void Conditional_Redundancy_Query() + { + // arrange + var schema = CreateSchema(); + + var doc = CreateConditionalRedundancyDocument().ToString(); + + // act + var plan = PlanOperation(schema, doc); + + // assert + MatchSnapshot(plan); + } + + private FusionSchemaDefinition CreateSchema() + { + var sourceSchemas = CreateSourceSchemas(); + + var compositionLog = new CompositionLog(); + var composerOptions = new SchemaComposerOptions + { + EnableGlobalObjectIdentification = true + }; + var composer = new SchemaComposer(sourceSchemas, composerOptions, compositionLog); + var result = composer.Compose(); + + if (!result.IsSuccess) + { + throw new InvalidOperationException(result.Errors[0].Message); + } + + var compositeSchemaDoc = result.Value.ToSyntaxNode(); + return FusionSchemaDefinition.Create(compositeSchemaDoc); + } + + private List CreateSourceSchemas() + { + return [ + new SourceSchemaText( + "products", + """ + type Query { + productById(id: ID!): Product @lookup + products(first: Int, after: String, last: Int, before: String): ProductConnection + } + + type Product { + id: ID! + name: String! + description: String @shareable + price: Float! + dimension: ProductDimension! + estimatedDelivery(postCode: String): Int! + } + + type ProductDimension { + height: Int! + width: Int! + } + + type ProductConnection { + pageInfo: PageInfo! + edges: [ProductEdge!] + nodes: [Product!] + } + + type ProductEdge { + cursor: String! + node: Product! + } + + type PageInfo @shareable { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String + } + """), + new SourceSchemaText( + "reviews", + """ + type Query { + reviewById(id: ID!): Review @lookup + productById(id: ID!): Product @lookup @internal + viewer: Viewer + } + + type Viewer { + reviews(first: Int, after: String, last: Int, before: String): ProductReviewConnection + } + + type Product { + id: ID! + averageRating: Int! + reviews(first: Int, after: String, last: Int, before: String): ProductReviewConnection + } + + type Review { + id: ID! + body: String! + stars: Int! + author: User + product: Product + } + + type User { + id: ID! + } + + type ProductReviewConnection { + pageInfo: PageInfo! + edges: [ProductReviewEdge!] + nodes: [Review!] + } + + type ProductReviewEdge { + cursor: String! + node: Review! + } + + type PageInfo @shareable { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String + } + """), + new SourceSchemaText( + "users", + """ + type Query { + userById(id: ID!): User @lookup + viewer: Viewer + } + + type Viewer { + displayName: String! + } + + type User { + id: ID! + displayName: String! + reviews(first: Int, after: String, last: Int, before: String): UserReviewConnection + } + + type UserReviewConnection { + pageInfo: PageInfo! + edges: [UserReviewEdge!] + nodes: [Review!] + } + + type UserReviewEdge { + cursor: String! + node: Review! + } + + type Review { + id: ID! + } + + type PageInfo @shareable { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String + } + """), + new SourceSchemaText( + "search", + """ + type Query { + searchContent(query: String!): [SearchResult!]! + productById(id: ID!): Product @lookup @internal + } + + interface SearchResult { + id: ID! + title: String! + description: String + } + + type Product implements SearchResult { + id: ID! + title: String! + description: String @shareable + } + + type Article implements SearchResult { + id: ID! + title: String! + description: String + content: String! + author: User! + publishedAt: String! + tags: [String!]! + } + + type User { + id: ID! + } + """) + ]; + } + + private DocumentNode CreateSimpleQueryWithRequirementsDocument() + { + return Utf8GraphQLParser.Parse( + """ + query($productId: ID!) { + productById(id: $id) { + name + reviews { + nodes { + id + body + author { + displayName + } + } + } + } + } + """); + } + + private DocumentNode CreateComplexDocument() + { + return Utf8GraphQLParser.Parse( + """ + query($level1: Boolean!, $level2: Boolean!, $level3: Boolean!, $level4: Boolean!, $level5: Boolean!, $level6: Boolean!, $includeExpensive: Boolean!, $includeReviews: Boolean!, $includeMetadata: Boolean!) { + # Deep conditional nesting (6+ levels) + productById(id: "1") { + id + name + ... @include(if: $level1) { + description + ... @skip(if: $level2) { + price + ... @include(if: $level3) { + dimension { + height + width + } + ... @skip(if: $level4) { + # Level 4 nesting + ... @include(if: $level5) { + # Level 5 nesting + ... @skip(if: $level6) { + # Level 6 nesting - extreme depth + reviews(first: 10) { + pageInfo { + hasNextPage + ... @include(if: $includeMetadata) { + hasPreviousPage + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + ... @include(if: $includeReviews) { + author { + id + displayName + ... @skip(if: $level1) { + reviews(first: 5) { + nodes { + id + body + ... @include(if: $level2) { + stars + product { + id + name + ... @skip(if: $level3) { + price + ... @include(if: $level4) { + dimension { + height + width + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + # Many fields with same response name but different conditionals + products(first: 3) { + edges { + node { + # Multiple 'name' fields with different conditionals + name + name @include(if: $level1) + name @skip(if: $level2) + name @include(if: $level3) @skip(if: $level4) + name @skip(if: $level5) @include(if: $level6) + # Multiple 'description' fields with complex conditionals + description + description @include(if: $includeExpensive) + description @skip(if: $level1) @include(if: $level2) + description @include(if: $level3) @skip(if: $level4) @include(if: $level5) + # Multiple 'price' fields with nested conditionals + price + price @include(if: $includeExpensive) + price @skip(if: $level1) + price @include(if: $level2) @skip(if: $level3) + price @skip(if: $level4) @include(if: $level5) @skip(if: $level6) + # Complex field merging with fragments + ... ProductBasicFields + ... @include(if: $level1) { + ... ProductBasicFields + } + ... @skip(if: $level2) { + ... ProductBasicFields + } + ... @include(if: $level3) @skip(if: $level4) { + ... ProductBasicFields + } + # Interface type refinements with deep conditionals + ... on Product { + ... @include(if: $includeReviews) { + reviews(first: 5) { + nodes { + id + body + stars + ... @skip(if: $level1) { + author { + id + displayName + ... @include(if: $level2) { + reviews(first: 3) { + nodes { + id + body + ... @skip(if: $level3) { + stars + product { + id + name + ... @include(if: $level4) { + price + ... @skip(if: $level5) { + dimension { + height + width + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + # Complex conditional merging scenarios + searchContent(query: "extreme") { + # Interface field with multiple conditionals + id + id @include(if: $level1) + id @skip(if: $level2) + title + title @include(if: $includeExpensive) + title @skip(if: $level3) + description + description @include(if: $level4) + description @skip(if: $level5) @include(if: $level6) + # Type refinements with extreme conditional complexity + ... on Product { + name + name @include(if: $level1) + name @skip(if: $level2) @include(if: $level3) + price + price @include(if: $includeExpensive) + price @skip(if: $level4) @include(if: $level5) @skip(if: $level6) + ... @include(if: $includeReviews) { + reviews(first: 3) { + nodes { + id + body + stars + ... @skip(if: $level1) { + author { + id + displayName + ... @include(if: $level2) { + reviews(first: 2) { + nodes { + id + body + ... @skip(if: $level3) { + stars + product { + id + name + ... @include(if: $level4) { + price + ... @skip(if: $level5) { + dimension { + height + width + } + ... @include(if: $level6) { + # Extreme nesting level - move reviews to Product level + reviews(first: 1) { + nodes { + id + body + stars + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + ... on Article { + content + content @include(if: $includeExpensive) + content @skip(if: $level1) @include(if: $level2) + author { + id + displayName + ... @include(if: $level3) { + reviews(first: 2) { + nodes { + id + body + ... @skip(if: $level4) { + stars + product { + id + name + ... @include(if: $level5) { + price + ... @skip(if: $level6) { + dimension { + height + width + } + } + } + } + } + } + } + } + } + publishedAt + publishedAt @include(if: $includeMetadata) + publishedAt @skip(if: $level1) @include(if: $level2) @skip(if: $level3) + tags + tags @include(if: $level4) + tags @skip(if: $level5) @include(if: $level6) + } + } + # Viewer with extreme conditional complexity + viewer { + displayName + displayName @include(if: $level1) + displayName @skip(if: $level2) @include(if: $level3) + ... @include(if: $includeReviews) { + reviews(first: 5) { + pageInfo { + hasNextPage + hasNextPage @include(if: $level4) + hasNextPage @skip(if: $level5) @include(if: $level6) + hasPreviousPage + hasPreviousPage @include(if: $includeMetadata) + hasPreviousPage @skip(if: $level1) @include(if: $level2) + startCursor + startCursor @include(if: $level3) + startCursor @skip(if: $level4) @include(if: $level5) + endCursor + endCursor @include(if: $level6) + endCursor @skip(if: $level1) @include(if: $level2) @skip(if: $level3) + } + edges { + cursor + cursor @include(if: $level4) + cursor @skip(if: $level5) @include(if: $level6) + node { + id + id @include(if: $level1) + id @skip(if: $level2) @include(if: $level3) + body + body @include(if: $includeExpensive) + body @skip(if: $level4) @include(if: $level5) @skip(if: $level6) + stars + stars @include(if: $level1) + stars @skip(if: $level2) + ... @include(if: $includeReviews) { + author { + id + displayName + ... @skip(if: $level3) { + reviews(first: 3) { + nodes { + id + body + stars + ... @include(if: $level4) { + product { + id + name + ... @skip(if: $level5) { + price + ... @include(if: $level6) { + dimension { + height + width + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + + fragment ProductBasicFields on Product { + id + name + description + price + averageRating + } + """); + } + + private DocumentNode CreateConditionalRedundancyDocument() + { + return Utf8GraphQLParser.Parse( + """ + query($includeExpensive: Boolean!, $includeReviews: Boolean!, $includeMetadata: Boolean!, $includeDetails: Boolean!) { + # Unconditional selections that are also inside conditionals + productById(id: "1") { + # These fields exist unconditionally + id + name + description + price + averageRating + # Same fields inside conditionals - should be deduplicated + ... @include(if: $includeExpensive) { + id + name + description + price + averageRating + } + # More redundancy with different conditionals + ... @include(if: $includeReviews) { + id + name + description + price + averageRating + } + # Nested redundancy + dimension { + height + width + ... @include(if: $includeDetails) { + height + width + } + } + # Reviews with redundant selections + reviews(first: 5) { + pageInfo { + hasNextPage + hasPreviousPage + ... @include(if: $includeMetadata) { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + # Same fields in conditional + ... @include(if: $includeReviews) { + id + body + stars + author { + id + displayName + # Nested redundancy + ... @include(if: $includeDetails) { + id + displayName + } + } + } + } + } + } + } + # Products with extensive redundancy + products(first: 3) { + edges { + node { + # Unconditional fields + id + name + description + price + averageRating + # Redundant conditional selections + ... @include(if: $includeExpensive) { + id + name + description + price + averageRating + dimension { + height + width + } + } + ... @include(if: $includeReviews) { + id + name + description + price + averageRating + reviews(first: 3) { + nodes { + id + body + stars + # More redundancy + ... @include(if: $includeDetails) { + id + body + stars + } + } + } + } + # Fragment redundancy + ... ProductBasicInfo + ... @include(if: $includeExpensive) { + ... ProductBasicInfo + } + ... @include(if: $includeReviews) { + ... ProductBasicInfo + } + } + } + } + # Search content with interface redundancy + searchContent(query: "redundant") { + # Interface fields unconditionally + id + title + description + # Same interface fields in conditionals + ... @include(if: $includeExpensive) { + id + title + description + } + # Type-specific redundancy + ... on Product { + # Unconditional product fields + id + name + price + averageRating + # Redundant conditional selections + ... @include(if: $includeExpensive) { + id + name + price + averageRating + dimension { + height + width + } + } + ... @include(if: $includeReviews) { + id + name + price + averageRating + reviews(first: 2) { + nodes { + id + body + stars + # Nested redundancy + ... @include(if: $includeDetails) { + id + body + stars + } + } + } + } + } + ... on Article { + # Unconditional article fields + id + title + description + content + publishedAt + # Redundant conditional selections + ... @include(if: $includeExpensive) { + id + title + description + content + publishedAt + tags + } + ... @include(if: $includeDetails) { + id + title + description + content + publishedAt + author { + id + displayName + # More redundancy + ... @include(if: $includeReviews) { + id + displayName + } + } + } + } + } + # Viewer with extensive redundancy + viewer { + # Unconditional fields + displayName + # Redundant conditional selections + ... @include(if: $includeReviews) { + displayName + reviews(first: 5) { + pageInfo { + hasNextPage + hasPreviousPage + # Redundant pageInfo fields + ... @include(if: $includeMetadata) { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + edges { + cursor + node { + # Unconditional review fields + id + body + stars + # Redundant conditional selections + ... @include(if: $includeDetails) { + id + body + stars + author { + id + displayName + # Nested redundancy + ... @include(if: $includeReviews) { + id + displayName + reviews(first: 2) { + nodes { + id + body + stars + # Deep redundancy + ... @include(if: $includeExpensive) { + id + body + stars + } + } + } + } + } + } + } + } + } + } + } + } + + fragment ProductBasicInfo on Product { + id + name + description + price + averageRating + } + """); + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/SelectionSetByTypePartitionerTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/SelectionSetByTypePartitionerTests.cs index 66362cd6cf4..c3ec07ec726 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/SelectionSetByTypePartitionerTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/SelectionSetByTypePartitionerTests.cs @@ -676,9 +676,7 @@ ... @skip(if: $skip) { private static SelectionSetByTypePartitionerResult Partition(FusionSchemaDefinition schema, DocumentNode document) { var rewriter = new DocumentRewriter(schema); - var operation = rewriter.RewriteDocument(document).Definitions - .OfType() - .Single(); + var operation = rewriter.RewriteOperation(document); var index = SelectionSetIndexer.Create(operation); var nodeField = operation.SelectionSet.Selections diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/SelectionSetPartitionerTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/SelectionSetPartitionerTests.cs index 2288fb71a7c..a099cb581e9 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/SelectionSetPartitionerTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/SelectionSetPartitionerTests.cs @@ -43,7 +43,7 @@ fragment ProductFragment2 on Product { """); var rewriter = new DocumentRewriter(compositeSchema); - var operation = rewriter.RewriteDocument(doc).Definitions.OfType().Single(); + var operation = rewriter.RewriteOperation(doc); var index = SelectionSetIndexer.Create(operation); // act @@ -115,7 +115,7 @@ fragment ProductFragment2 on Product { """); var rewriter = new DocumentRewriter(compositeSchema); - var operation = rewriter.RewriteDocument(doc).Definitions.OfType().Single(); + var operation = rewriter.RewriteOperation(doc); var index = SelectionSetIndexer.Create(operation); // act diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml new file mode 100644 index 00000000000..80e7a4e809f --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml @@ -0,0 +1,1015 @@ +operation: + - document: | + query( + $level1: Boolean! + $level2: Boolean! + $level3: Boolean! + $level4: Boolean! + $level5: Boolean! + $level6: Boolean! + $includeExpensive: Boolean! + $includeReviews: Boolean! + $includeMetadata: Boolean! + ) { + productById(id: "1") { + id + name + ... @include(if: $level1) { + description + ... @skip(if: $level2) { + price + ... @include(if: $level3) { + dimension { + height + width + } + ... @skip(if: $level4) { + ... @include(if: $level5) { + reviews(first: 10) @skip(if: $level6) { + pageInfo { + hasNextPage + ... @include(if: $includeMetadata) { + hasPreviousPage + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + author @include(if: $includeReviews) { + id + id @fusion__requirement + displayName + } + } + } + } + id @fusion__requirement + } + } + } + } + } + } + products(first: 3) { + edges { + node { + name + description + price + id + id @fusion__requirement + averageRating + reviews(first: 5) @include(if: $includeReviews) { + nodes { + id + body + stars + author @skip(if: $level1) { + id + id @fusion__requirement + displayName + reviews(first: 3) @include(if: $level2) { + nodes { + id + id @fusion__requirement + body + ... @skip(if: $level3) { + stars + product { + id + id @fusion__requirement + name + ... @include(if: $level4) { + price + dimension @skip(if: $level5) { + height + width + } + id @fusion__requirement + } + } + id @fusion__requirement + } + } + } + } + } + } + } + } + } + searchContent(query: "extreme") { + __typename @fusion__requirement + id + title + description + ... on Product { + name + price + reviews(first: 3) @include(if: $includeReviews) { + nodes { + id + body + stars + author @skip(if: $level1) { + id + id @fusion__requirement + displayName + reviews(first: 2) @include(if: $level2) { + nodes { + id + id @fusion__requirement + body + ... @skip(if: $level3) { + stars + product { + id + id @fusion__requirement + name + ... @include(if: $level4) { + price + ... @skip(if: $level5) { + dimension { + height + width + } + reviews(first: 1) @include(if: $level6) { + nodes { + id + body + stars + } + } + id @fusion__requirement + } + id @fusion__requirement + } + } + id @fusion__requirement + } + } + } + } + } + } + id @fusion__requirement + } + ... on Article { + content + author { + id + id @fusion__requirement + displayName + reviews(first: 2) @include(if: $level3) { + nodes { + id + id @fusion__requirement + body + ... @skip(if: $level4) { + stars + product { + id + id @fusion__requirement + name + ... @include(if: $level5) { + price + dimension @skip(if: $level6) { + height + width + } + id @fusion__requirement + } + } + id @fusion__requirement + } + } + } + } + publishedAt + tags + } + } + viewer { + displayName + reviews(first: 5) @include(if: $includeReviews) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + cursor + node { + id + body + stars + author { + id + id @fusion__requirement + displayName + reviews(first: 3) @skip(if: $level3) { + nodes { + id + id @fusion__requirement + body + stars + product @include(if: $level4) { + id + id @fusion__requirement + name + ... @skip(if: $level5) { + price + dimension @include(if: $level6) { + height + width + } + id @fusion__requirement + } + } + } + } + } + } + } + } + } + } + hash: 123456789101112 + searchSpace: 23 +nodes: + - id: 1 + type: Operation + schema: products + operation: | + query Op_123456789101112_1( + $level1: Boolean! + $level2: Boolean! + $level3: Boolean! + $level4: Boolean! + $level5: Boolean! + ) { + productById(id: "1") { + id + name + ... @include(if: $level1) { + description + ... @skip(if: $level2) { + price + ... @include(if: $level3) { + dimension { + height + width + } + ... @skip(if: $level4) { + ... @include(if: $level5) { + id + } + } + } + } + } + } + products(first: 3) { + edges { + node { + name + description + price + id + } + } + } + } + forwardedVariables: + - level1 + - level2 + - level3 + - level4 + - level5 + - id: 2 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_2( + $includeReviews: Boolean! + ) { + viewer { + reviews(first: 5) @include(if: $includeReviews) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + cursor + node { + id + body + stars + author { + id + } + } + } + } + } + } + forwardedVariables: + - includeReviews + - id: 3 + type: Operation + schema: search + operation: | + query Op_123456789101112_3 { + searchContent(query: "extreme") { + __typename + id + title + description + ... on Product { + id + } + ... on Article { + content + author { + id + } + publishedAt + tags + } + } + } + - id: 4 + type: Operation + schema: users + operation: | + query Op_123456789101112_4( + $level3: Boolean! + $level4: Boolean! + $__fusion_1_id: ID! + ) { + userById(id: $__fusion_1_id) { + displayName + reviews(first: 2) @include(if: $level3) { + nodes { + id + ... @skip(if: $level4) { + id + } + } + } + } + } + source: $.userById + target: $.searchContent
.author + requirements: + - name: __fusion_1_id + selectionMap: >- + id + forwardedVariables: + - level3 + - level4 + dependencies: + - id: 3 + - id: 5 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_5( + $__fusion_2_id: ID! + ) { + reviewById(id: $__fusion_2_id) { + body + } + } + source: $.reviewById + target: $.searchContent
.author.reviews.nodes + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 4 + - id: 6 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_6( + $level5: Boolean! + $__fusion_3_id: ID! + ) { + reviewById(id: $__fusion_3_id) { + stars + product { + id + ... @include(if: $level5) { + id + } + } + } + } + source: $.reviewById + target: $.searchContent
.author.reviews.nodes + requirements: + - name: __fusion_3_id + selectionMap: >- + id + forwardedVariables: + - level5 + dependencies: + - id: 4 + - id: 7 + type: Operation + schema: products + operation: | + query Op_123456789101112_7( + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + name + } + } + source: $.productById + target: $.searchContent
.author.reviews.nodes.product + requirements: + - name: __fusion_4_id + selectionMap: >- + id + dependencies: + - id: 6 + - id: 8 + type: Operation + schema: products + operation: | + query Op_123456789101112_8( + $level6: Boolean! + $__fusion_5_id: ID! + ) { + productById(id: $__fusion_5_id) { + price + dimension @skip(if: $level6) { + height + width + } + } + } + source: $.productById + target: $.searchContent
.author.reviews.nodes.product + requirements: + - name: __fusion_5_id + selectionMap: >- + id + forwardedVariables: + - level6 + dependencies: + - id: 6 + - id: 9 + type: Operation + schema: products + operation: | + query Op_123456789101112_9( + $__fusion_6_id: ID! + ) { + productById(id: $__fusion_6_id) { + name + price + } + } + source: $.productById + target: $.searchContent + requirements: + - name: __fusion_6_id + selectionMap: >- + id + dependencies: + - id: 3 + - id: 10 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_10( + $level1: Boolean! + $__fusion_7_id: ID! + ) { + productById(id: $__fusion_7_id) { + reviews(first: 3) { + nodes { + id + body + stars + author @skip(if: $level1) { + id + } + } + } + } + } + source: $.productById + target: $.searchContent + requirements: + - name: __fusion_7_id + selectionMap: >- + id + conditions: + - variable: $includeReviews + passingValue: true + forwardedVariables: + - level1 + dependencies: + - id: 3 + - id: 11 + type: Operation + schema: users + operation: | + query Op_123456789101112_11( + $level2: Boolean! + $level3: Boolean! + $__fusion_8_id: ID! + ) { + userById(id: $__fusion_8_id) { + displayName + reviews(first: 2) @include(if: $level2) { + nodes { + id + ... @skip(if: $level3) { + id + } + } + } + } + } + source: $.userById + target: $.searchContent.reviews.nodes.author + requirements: + - name: __fusion_8_id + selectionMap: >- + id + forwardedVariables: + - level2 + - level3 + dependencies: + - id: 10 + - id: 12 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_12( + $__fusion_9_id: ID! + ) { + reviewById(id: $__fusion_9_id) { + body + } + } + source: $.reviewById + target: $.searchContent.reviews.nodes.author.reviews.nodes + requirements: + - name: __fusion_9_id + selectionMap: >- + id + dependencies: + - id: 11 + - id: 13 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_13( + $level4: Boolean! + $level5: Boolean! + $level6: Boolean! + $__fusion_10_id: ID! + ) { + reviewById(id: $__fusion_10_id) { + stars + product { + id + ... @include(if: $level4) { + ... @skip(if: $level5) { + reviews(first: 1) @include(if: $level6) { + nodes { + id + body + stars + } + } + id + } + id + } + } + } + } + source: $.reviewById + target: $.searchContent.reviews.nodes.author.reviews.nodes + requirements: + - name: __fusion_10_id + selectionMap: >- + id + forwardedVariables: + - level4 + - level5 + - level6 + dependencies: + - id: 11 + - id: 14 + type: Operation + schema: products + operation: | + query Op_123456789101112_14( + $__fusion_11_id: ID! + ) { + productById(id: $__fusion_11_id) { + name + } + } + source: $.productById + target: $.searchContent.reviews.nodes.author.reviews.nodes.product + requirements: + - name: __fusion_11_id + selectionMap: >- + id + dependencies: + - id: 13 + - id: 15 + type: Operation + schema: products + operation: | + query Op_123456789101112_15( + $__fusion_12_id: ID! + ) { + productById(id: $__fusion_12_id) { + price + } + } + source: $.productById + target: $.searchContent.reviews.nodes.author.reviews.nodes.product + requirements: + - name: __fusion_12_id + selectionMap: >- + id + dependencies: + - id: 13 + - id: 16 + type: Operation + schema: products + operation: | + query Op_123456789101112_16( + $__fusion_13_id: ID! + ) { + productById(id: $__fusion_13_id) { + dimension { + height + width + } + } + } + source: $.productById + target: $.searchContent.reviews.nodes.author.reviews.nodes.product + requirements: + - name: __fusion_13_id + selectionMap: >- + id + dependencies: + - id: 13 + - id: 17 + type: Operation + schema: users + operation: | + query Op_123456789101112_17 { + viewer { + displayName + } + } + - id: 18 + type: Operation + schema: users + operation: | + query Op_123456789101112_18( + $level3: Boolean! + $__fusion_14_id: ID! + ) { + userById(id: $__fusion_14_id) { + displayName + reviews(first: 3) @skip(if: $level3) { + nodes { + id + } + } + } + } + source: $.userById + target: $.viewer.reviews.edges.node.author + requirements: + - name: __fusion_14_id + selectionMap: >- + id + forwardedVariables: + - level3 + dependencies: + - id: 2 + - id: 19 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_19( + $level4: Boolean! + $level5: Boolean! + $__fusion_15_id: ID! + ) { + reviewById(id: $__fusion_15_id) { + body + stars + product @include(if: $level4) { + id + ... @skip(if: $level5) { + id + } + } + } + } + source: $.reviewById + target: $.viewer.reviews.edges.node.author.reviews.nodes + requirements: + - name: __fusion_15_id + selectionMap: >- + id + forwardedVariables: + - level4 + - level5 + dependencies: + - id: 18 + - id: 20 + type: Operation + schema: products + operation: | + query Op_123456789101112_20( + $__fusion_16_id: ID! + ) { + productById(id: $__fusion_16_id) { + name + } + } + source: $.productById + target: $.viewer.reviews.edges.node.author.reviews.nodes.product + requirements: + - name: __fusion_16_id + selectionMap: >- + id + dependencies: + - id: 19 + - id: 21 + type: Operation + schema: products + operation: | + query Op_123456789101112_21( + $level6: Boolean! + $__fusion_17_id: ID! + ) { + productById(id: $__fusion_17_id) { + price + dimension @include(if: $level6) { + height + width + } + } + } + source: $.productById + target: $.viewer.reviews.edges.node.author.reviews.nodes.product + requirements: + - name: __fusion_17_id + selectionMap: >- + id + forwardedVariables: + - level6 + dependencies: + - id: 19 + - id: 22 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_22( + $level1: Boolean! + $includeReviews: Boolean! + $__fusion_18_id: ID! + ) { + productById(id: $__fusion_18_id) { + averageRating + reviews(first: 5) @include(if: $includeReviews) { + nodes { + id + body + stars + author @skip(if: $level1) { + id + } + } + } + } + } + source: $.productById + target: $.products.edges.node + requirements: + - name: __fusion_18_id + selectionMap: >- + id + forwardedVariables: + - level1 + - includeReviews + dependencies: + - id: 1 + - id: 23 + type: Operation + schema: users + operation: | + query Op_123456789101112_23( + $level2: Boolean! + $level3: Boolean! + $__fusion_19_id: ID! + ) { + userById(id: $__fusion_19_id) { + displayName + reviews(first: 3) @include(if: $level2) { + nodes { + id + ... @skip(if: $level3) { + id + } + } + } + } + } + source: $.userById + target: $.products.edges.node.reviews.nodes.author + requirements: + - name: __fusion_19_id + selectionMap: >- + id + forwardedVariables: + - level2 + - level3 + dependencies: + - id: 22 + - id: 24 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_24( + $__fusion_20_id: ID! + ) { + reviewById(id: $__fusion_20_id) { + body + } + } + source: $.reviewById + target: $.products.edges.node.reviews.nodes.author.reviews.nodes + requirements: + - name: __fusion_20_id + selectionMap: >- + id + dependencies: + - id: 23 + - id: 25 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_25( + $level4: Boolean! + $__fusion_21_id: ID! + ) { + reviewById(id: $__fusion_21_id) { + stars + product { + id + ... @include(if: $level4) { + id + } + } + } + } + source: $.reviewById + target: $.products.edges.node.reviews.nodes.author.reviews.nodes + requirements: + - name: __fusion_21_id + selectionMap: >- + id + forwardedVariables: + - level4 + dependencies: + - id: 23 + - id: 26 + type: Operation + schema: products + operation: | + query Op_123456789101112_26( + $__fusion_22_id: ID! + ) { + productById(id: $__fusion_22_id) { + name + } + } + source: $.productById + target: $.products.edges.node.reviews.nodes.author.reviews.nodes.product + requirements: + - name: __fusion_22_id + selectionMap: >- + id + dependencies: + - id: 25 + - id: 27 + type: Operation + schema: products + operation: | + query Op_123456789101112_27( + $level5: Boolean! + $__fusion_23_id: ID! + ) { + productById(id: $__fusion_23_id) { + price + dimension @skip(if: $level5) { + height + width + } + } + } + source: $.productById + target: $.products.edges.node.reviews.nodes.author.reviews.nodes.product + requirements: + - name: __fusion_23_id + selectionMap: >- + id + forwardedVariables: + - level5 + dependencies: + - id: 25 + - id: 28 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_28( + $includeReviews: Boolean! + $includeMetadata: Boolean! + $__fusion_24_id: ID! + ) { + productById(id: $__fusion_24_id) { + reviews(first: 10) { + pageInfo { + hasNextPage + ... @include(if: $includeMetadata) { + hasPreviousPage + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + author @include(if: $includeReviews) { + id + } + } + } + } + } + } + source: $.productById + target: $.productById + requirements: + - name: __fusion_24_id + selectionMap: >- + id + conditions: + - variable: $level6 + passingValue: false + forwardedVariables: + - includeReviews + - includeMetadata + dependencies: + - id: 1 + - id: 29 + type: Operation + schema: users + operation: | + query Op_123456789101112_29( + $__fusion_25_id: ID! + ) { + userById(id: $__fusion_25_id) { + displayName + } + } + source: $.userById + target: $.productById.reviews.edges.node.author + requirements: + - name: __fusion_25_id + selectionMap: >- + id + dependencies: + - id: 28 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Conditional_Redundancy_Query.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Conditional_Redundancy_Query.yaml new file mode 100644 index 00000000000..6503bbb2190 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Conditional_Redundancy_Query.yaml @@ -0,0 +1,465 @@ +operation: + - document: | + query( + $includeExpensive: Boolean! + $includeReviews: Boolean! + $includeMetadata: Boolean! + $includeDetails: Boolean! + ) { + productById(id: "1") { + id + id @fusion__requirement + name + description + price + averageRating + dimension { + height + width + } + reviews(first: 5) { + pageInfo { + hasNextPage + hasPreviousPage + ... @include(if: $includeMetadata) { + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + author @include(if: $includeReviews) { + id + id @fusion__requirement + displayName + } + } + } + } + } + products(first: 3) { + edges { + node { + id + id @fusion__requirement + name + description + price + averageRating + dimension @include(if: $includeExpensive) { + height + width + } + reviews(first: 3) @include(if: $includeReviews) { + nodes { + id + body + stars + } + } + } + } + } + searchContent(query: "redundant") { + __typename @fusion__requirement + id + title + description + ... on Product { + id + id @fusion__requirement + name + price + averageRating + dimension @include(if: $includeExpensive) { + height + width + } + reviews(first: 2) @include(if: $includeReviews) { + nodes { + id + body + stars + } + } + } + ... on Article { + id + title + description + content + publishedAt + tags @include(if: $includeExpensive) + author @include(if: $includeDetails) { + id + id @fusion__requirement + displayName + } + } + } + viewer { + displayName + reviews(first: 5) @include(if: $includeReviews) { + pageInfo { + hasNextPage + hasPreviousPage + ... @include(if: $includeMetadata) { + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + author @include(if: $includeDetails) { + id + id @fusion__requirement + displayName + reviews(first: 2) { + nodes { + id + id @fusion__requirement + body + stars + } + } + } + } + } + } + } + } + hash: 123456789101112 + searchSpace: 23 +nodes: + - id: 1 + type: Operation + schema: products + operation: | + query Op_123456789101112_1( + $includeExpensive: Boolean! + ) { + productById(id: "1") { + id + name + description + price + dimension { + height + width + } + } + products(first: 3) { + edges { + node { + id + name + description + price + dimension @include(if: $includeExpensive) { + height + width + } + } + } + } + } + forwardedVariables: + - includeExpensive + - id: 2 + type: Operation + schema: search + operation: | + query Op_123456789101112_2( + $includeExpensive: Boolean! + $includeDetails: Boolean! + ) { + searchContent(query: "redundant") { + __typename + id + title + description + ... on Product { + id + } + ... on Article { + id + title + description + content + publishedAt + tags @include(if: $includeExpensive) + author @include(if: $includeDetails) { + id + } + } + } + } + forwardedVariables: + - includeExpensive + - includeDetails + - id: 3 + type: Operation + schema: users + operation: | + query Op_123456789101112_3 { + viewer { + displayName + } + } + - id: 4 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_4( + $includeReviews: Boolean! + $includeMetadata: Boolean! + $includeDetails: Boolean! + ) { + viewer { + reviews(first: 5) @include(if: $includeReviews) { + pageInfo { + hasNextPage + hasPreviousPage + ... @include(if: $includeMetadata) { + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + author @include(if: $includeDetails) { + id + } + } + } + } + } + } + forwardedVariables: + - includeReviews + - includeMetadata + - includeDetails + - id: 5 + type: Operation + schema: users + operation: | + query Op_123456789101112_5( + $__fusion_1_id: ID! + ) { + userById(id: $__fusion_1_id) { + displayName + reviews(first: 2) { + nodes { + id + } + } + } + } + source: $.userById + target: $.viewer.reviews.edges.node.author + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 4 + - id: 6 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_6( + $__fusion_2_id: ID! + ) { + reviewById(id: $__fusion_2_id) { + body + stars + } + } + source: $.reviewById + target: $.viewer.reviews.edges.node.author.reviews.nodes + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 5 + - id: 7 + type: Operation + schema: users + operation: | + query Op_123456789101112_7( + $__fusion_3_id: ID! + ) { + userById(id: $__fusion_3_id) { + displayName + } + } + source: $.userById + target: $.searchContent
.author + requirements: + - name: __fusion_3_id + selectionMap: >- + id + dependencies: + - id: 2 + - id: 8 + type: Operation + schema: products + operation: | + query Op_123456789101112_8( + $includeExpensive: Boolean! + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + name + price + dimension @include(if: $includeExpensive) { + height + width + } + } + } + source: $.productById + target: $.searchContent + requirements: + - name: __fusion_4_id + selectionMap: >- + id + forwardedVariables: + - includeExpensive + dependencies: + - id: 2 + - id: 9 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_9( + $includeReviews: Boolean! + $__fusion_5_id: ID! + ) { + productById(id: $__fusion_5_id) { + averageRating + reviews(first: 2) @include(if: $includeReviews) { + nodes { + id + body + stars + } + } + } + } + source: $.productById + target: $.searchContent + requirements: + - name: __fusion_5_id + selectionMap: >- + id + forwardedVariables: + - includeReviews + dependencies: + - id: 2 + - id: 10 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_10( + $includeReviews: Boolean! + $__fusion_6_id: ID! + ) { + productById(id: $__fusion_6_id) { + averageRating + reviews(first: 3) @include(if: $includeReviews) { + nodes { + id + body + stars + } + } + } + } + source: $.productById + target: $.products.edges.node + requirements: + - name: __fusion_6_id + selectionMap: >- + id + forwardedVariables: + - includeReviews + dependencies: + - id: 1 + - id: 11 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_11( + $includeReviews: Boolean! + $includeMetadata: Boolean! + $__fusion_7_id: ID! + ) { + productById(id: $__fusion_7_id) { + averageRating + reviews(first: 5) { + pageInfo { + hasNextPage + hasPreviousPage + ... @include(if: $includeMetadata) { + startCursor + endCursor + } + } + edges { + cursor + node { + id + body + stars + author @include(if: $includeReviews) { + id + } + } + } + } + } + } + source: $.productById + target: $.productById + requirements: + - name: __fusion_7_id + selectionMap: >- + id + forwardedVariables: + - includeReviews + - includeMetadata + dependencies: + - id: 1 + - id: 12 + type: Operation + schema: users + operation: | + query Op_123456789101112_12( + $__fusion_8_id: ID! + ) { + userById(id: $__fusion_8_id) { + displayName + } + } + source: $.userById + target: $.productById.reviews.edges.node.author + requirements: + - name: __fusion_8_id + selectionMap: >- + id + dependencies: + - id: 11 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Simple_Query_With_Requirements.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Simple_Query_With_Requirements.yaml new file mode 100644 index 00000000000..9f798a90a94 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Simple_Query_With_Requirements.yaml @@ -0,0 +1,79 @@ +operation: + - document: | + query( + $productId: ID! + ) { + productById(id: $id) { + name + reviews { + nodes { + id + body + author { + displayName + id @fusion__requirement + } + } + } + id @fusion__requirement + } + } + hash: 123456789101112 + searchSpace: 1 +nodes: + - id: 1 + type: Operation + schema: products + operation: | + query Op_123456789101112_1 { + productById(id: $id) { + name + id + } + } + - id: 2 + type: Operation + schema: reviews + operation: | + query Op_123456789101112_2( + $__fusion_1_id: ID! + ) { + productById(id: $__fusion_1_id) { + reviews { + nodes { + id + body + author { + id + } + } + } + } + } + source: $.productById + target: $.productById + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: Operation + schema: users + operation: | + query Op_123456789101112_3( + $__fusion_2_id: ID! + ) { + userById(id: $__fusion_2_id) { + displayName + } + } + source: $.userById + target: $.productById.reviews.nodes.author + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 2 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementTests.Requirement_Directive_Leaks_Into_SourceSchema_Request_Shop.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementTests.Requirement_Directive_Leaks_Into_SourceSchema_Request_Shop.yaml index d5752041c1e..87a877f707e 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementTests.Requirement_Directive_Leaks_Into_SourceSchema_Request_Shop.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementTests.Requirement_Directive_Leaks_Into_SourceSchema_Request_Shop.yaml @@ -46,12 +46,12 @@ operation: } } } + quantity @skip(if: $skip) dimension @fusion__requirement { length width height } - quantity @skip(if: $skip) } } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/DocumentRewriterTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/DocumentRewriterTests.cs index f7877ffde7d..2aaffd7a0b3 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/DocumentRewriterTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Utilities.Tests/Rewriters/DocumentRewriterTests.cs @@ -28,7 +28,7 @@ fragment Product on Product { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -69,7 +69,7 @@ fragment Product2 on Product { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -104,7 +104,7 @@ public void Inline_Inline_Fragment_Into_ProductById_SelectionSet_1() // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -147,7 +147,7 @@ fragment Product2 on Product { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -183,7 +183,7 @@ ... @include(if: true) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -219,7 +219,7 @@ ... @include(if: false) { // act var rewriter = new DocumentRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -257,7 +257,7 @@ fragment Product on Product { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -296,7 +296,7 @@ fragment Product on Product { // act var rewriter = new DocumentRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -331,7 +331,7 @@ name @include(if: false) // act var rewriter = new DocumentRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -362,7 +362,7 @@ id @include(if: false) // act var rewriter = new DocumentRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -396,7 +396,7 @@ description @skip(if: false) // act var rewriter = new DocumentRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -438,7 +438,7 @@ name @skip(if: $skip) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -490,7 +490,7 @@ fragment ProductFragment2 on Product { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -537,7 +537,7 @@ public void Merge_Fields_With_Aliases() // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -574,7 +574,7 @@ id @fusion__requirement // act var rewriter = new DocumentRewriter(schemaDefinition, true); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -608,7 +608,7 @@ name @skip(if: $skip) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -644,7 +644,7 @@ name @skip(if: $skip) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -680,7 +680,7 @@ public void Field_With_SelectionSet_With_Conditional_Merged_With_Unconditional_F // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -717,7 +717,7 @@ public void Field_With_SelectionSet_With_Conditional_Merged_With_Unconditional_F // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -758,7 +758,7 @@ dimension @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -801,7 +801,7 @@ dimension @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -844,7 +844,7 @@ public void Parent_Field_With_Conditional_Merged_With_Unconditional_Nested_Field // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -887,7 +887,7 @@ public void Parent_Field_With_Conditional_Merged_With_Unconditional_Nested_Field // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -923,7 +923,7 @@ name @skip(if: $skip) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -959,7 +959,7 @@ name @skip(if: $skip) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -999,7 +999,7 @@ width @skip(if: $skip) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1040,7 +1040,7 @@ name @skip(if: $skip) @include(if: $include) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1081,7 +1081,7 @@ fragment Fragment1 on Query { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1122,7 +1122,7 @@ fragment Fragment1 on Query { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1177,7 +1177,7 @@ ... on Product @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1233,7 +1233,7 @@ ... on Product @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1290,7 +1290,7 @@ ... @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1350,7 +1350,7 @@ voteCount @skip(if: $skip) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1407,7 +1407,7 @@ ... on Votable @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1463,7 +1463,7 @@ dimension @skip(if: $skip2) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1528,7 +1528,7 @@ ... @skip(if: $skip2) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1580,7 +1580,7 @@ name @skip(if: $skip2) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1639,7 +1639,7 @@ ... on Product @skip(if: $skip2) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1706,7 +1706,7 @@ ... on Product @skip(if: $skip2) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1753,7 +1753,7 @@ dimension @include(if: $include) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1797,7 +1797,7 @@ dimension @include(if: $include) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1864,7 +1864,7 @@ ... @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1905,7 +1905,7 @@ ... @include(if: $include) @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -1975,7 +1975,7 @@ ... @skip(if: $skip3) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2045,7 +2045,7 @@ ... @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2092,7 +2092,7 @@ ... @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2132,7 +2132,7 @@ public void Field_With_Conditional_And_Alias_Merged_With_Same_Field_Different_Al // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2171,7 +2171,7 @@ description @skip(if: $skip) // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2215,7 +2215,7 @@ ... @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2255,7 +2255,7 @@ ... @include(if: $include) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2320,7 +2320,7 @@ ... @skip(if: $skip2) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert - should merge dimension fields appropriately rewritten.MatchInlineSnapshot( @@ -2382,7 +2382,7 @@ ... @skip(if: $skip) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2430,7 +2430,7 @@ fragment ProductFields on Product { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2472,7 +2472,7 @@ public void Field_Arguments_With_Different_Values_And_Conditionals_Not_Merged() // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2545,7 +2545,7 @@ ... @skip(if: $skip2) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2607,7 +2607,7 @@ dimension @include(if: $conditional) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot( @@ -2658,7 +2658,7 @@ dimension @skip(if: $conditional) { // act var rewriter = new DocumentRewriter(schemaDefinition); - var rewritten = rewriter.RewriteDocument(doc); + var rewritten = rewriter.RewriteOperation(doc); // assert rewritten.MatchInlineSnapshot(