From 50733bdc47962d7e694f661ddc888b5173209871 Mon Sep 17 00:00:00 2001
From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com>
Date: Wed, 13 May 2026 15:01:06 +0200
Subject: [PATCH 1/2] Add DocumentRewriter benchmarks
---
src/All.slnx | 2 +-
src/Directory.Packages.props | 3 +-
.../Fusion/HotChocolate.Fusion.slnx | 2 +-
.../DocumentRewriterBenchmark.cs | 49 +
.../FusionBenchmarkBase.cs | 849 ++++++++++++++
...colate.Fusion.Execution.Benchmarks.csproj} | 2 +
...nlineFragmentOperationRewriterBenchmark.cs | 48 +
.../OperationCompilerBenchmark.cs | 71 ++
.../OperationPlannerBenchmark.cs | 60 +
.../Planning/FusionBenchmarkTests.cs | 898 +++++++++++++++
.../FusionBenchmarkTests.Complex_Query.yaml | 1023 +++++++++++++++++
...arkTests.Conditional_Redundancy_Query.yaml | 463 ++++++++
...kTests.Simple_Query_With_Requirements.yaml | 74 ++
13 files changed, 3541 insertions(+), 3 deletions(-)
create mode 100644 src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/DocumentRewriterBenchmark.cs
create mode 100644 src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/FusionBenchmarkBase.cs
rename src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/{Fusion.Execution.Benchmarks.csproj => HotChocolate.Fusion.Execution.Benchmarks.csproj} (86%)
create mode 100644 src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/InlineFragmentOperationRewriterBenchmark.cs
create mode 100644 src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/OperationCompilerBenchmark.cs
create mode 100644 src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/OperationPlannerBenchmark.cs
create mode 100644 src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/FusionBenchmarkTests.cs
create mode 100644 src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml
create mode 100644 src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Conditional_Redundancy_Query.yaml
create mode 100644 src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Simple_Query_With_Requirements.yaml
diff --git a/src/All.slnx b/src/All.slnx
index f90a2fe29d1..d1ed667624e 100644
--- a/src/All.slnx
+++ b/src/All.slnx
@@ -199,7 +199,7 @@
-
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 0c979487c80..10e0126fd42 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -10,7 +10,8 @@
-
+
+
diff --git a/src/HotChocolate/Fusion/HotChocolate.Fusion.slnx b/src/HotChocolate/Fusion/HotChocolate.Fusion.slnx
index 89e79ece137..823c27423ae 100644
--- a/src/HotChocolate/Fusion/HotChocolate.Fusion.slnx
+++ b/src/HotChocolate/Fusion/HotChocolate.Fusion.slnx
@@ -1,6 +1,6 @@
-
+
diff --git a/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/DocumentRewriterBenchmark.cs b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/DocumentRewriterBenchmark.cs
new file mode 100644
index 00000000000..2eb7d3fda76
--- /dev/null
+++ b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/DocumentRewriterBenchmark.cs
@@ -0,0 +1,49 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using HotChocolate.Fusion.Planning;
+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.RewriteDocument(_simpleQueryWithRequirements).GetOperation(operationName: null);
+ }
+
+ [Benchmark]
+ public OperationDefinitionNode Rewrite_Complex_Query()
+ {
+ return _documentRewriter.RewriteDocument(_complexQuery).GetOperation(operationName: null);
+ }
+
+ [Benchmark]
+ public OperationDefinitionNode Rewrite_ConditionalRedundancy_Query()
+ {
+ return _documentRewriter.RewriteDocument(_conditionalRedundancyQuery).GetOperation(operationName: null);
+ }
+}
diff --git a/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/FusionBenchmarkBase.cs b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/FusionBenchmarkBase.cs
new file mode 100644
index 00000000000..e27f08a52ab
--- /dev/null
+++ b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/FusionBenchmarkBase.cs
@@ -0,0 +1,849 @@
+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 @shareable
+ }
+
+ 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! @shareable
+ }
+
+ 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 @shareable
+ }
+
+ 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! @shareable
+ }
+
+ 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! @shareable
+ }
+ """)
+ ];
+
+ var compositionLog = new CompositionLog();
+ var composerOptions = new SchemaComposerOptions
+ {
+ Merger = new SourceSchemaMergerOptions { 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/benchmarks/Fusion.Execution.Benchmarks/Fusion.Execution.Benchmarks.csproj b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/HotChocolate.Fusion.Execution.Benchmarks.csproj
similarity index 86%
rename from src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/Fusion.Execution.Benchmarks.csproj
rename to src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/HotChocolate.Fusion.Execution.Benchmarks.csproj
index 293f667f6ee..28fd26ca919 100644
--- a/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/Fusion.Execution.Benchmarks.csproj
+++ b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/HotChocolate.Fusion.Execution.Benchmarks.csproj
@@ -11,6 +11,7 @@
+
@@ -19,6 +20,7 @@
+
diff --git a/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/InlineFragmentOperationRewriterBenchmark.cs b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/InlineFragmentOperationRewriterBenchmark.cs
new file mode 100644
index 00000000000..0d05fc2bb49
--- /dev/null
+++ b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/InlineFragmentOperationRewriterBenchmark.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 InlineFragmentOperationRewriterBenchmark : FusionBenchmarkBase
+{
+ private InlineFragmentOperationRewriter _rewriter = 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();
+
+ _rewriter = new InlineFragmentOperationRewriter(schema);
+ }
+
+ [Benchmark]
+ public InlineFragmentOperationRewriterResult Rewrite_Simple_Query_With_Requirements()
+ {
+ return _rewriter.RewriteDocument(_simpleQueryWithRequirements);
+ }
+
+ [Benchmark]
+ public InlineFragmentOperationRewriterResult Rewrite_Complex_Query()
+ {
+ return _rewriter.RewriteDocument(_complexQuery);
+ }
+
+ [Benchmark]
+ public InlineFragmentOperationRewriterResult Rewrite_ConditionalRedundancy_Query()
+ {
+ return _rewriter.RewriteDocument(_conditionalRedundancyQuery);
+ }
+}
diff --git a/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/OperationCompilerBenchmark.cs b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/OperationCompilerBenchmark.cs
new file mode 100644
index 00000000000..e3ea7694bac
--- /dev/null
+++ b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/OperationCompilerBenchmark.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnostics.dotMemory;
+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;
+
+[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.RewriteDocument(CreateSimpleQueryWithRequirementsDocument()).GetOperation(operationName: null);
+ _complexQuery = documentRewriter.RewriteDocument(CreateComplexDocument()).GetOperation(operationName: null);
+ _conditionalRedundancyQuery = documentRewriter.RewriteDocument(CreateConditionalRedundancyDocument()).GetOperation(operationName: null);
+
+ 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/benchmarks/Fusion.Execution.Benchmarks/OperationPlannerBenchmark.cs b/src/HotChocolate/Fusion/benchmarks/Fusion.Execution.Benchmarks/OperationPlannerBenchmark.cs
new file mode 100644
index 00000000000..8291f8fe825
--- /dev/null
+++ b/src/HotChocolate/Fusion/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.RewriteDocument(CreateSimpleQueryWithRequirementsDocument()).GetOperation(operationName: null);
+ _complexQuery = documentRewriter.RewriteDocument(CreateComplexDocument()).GetOperation(operationName: null);
+ _conditionalRedundancyQuery = documentRewriter.RewriteDocument(CreateConditionalRedundancyDocument()).GetOperation(operationName: null);
+
+ var pool = new DefaultObjectPool>>(
+ new DefaultPooledObjectPolicy>>());
+ var operationCompiler = new OperationCompiler(schema, pool);
+
+ _planner = new OperationPlanner(schema, operationCompiler);
+ }
+
+ [Benchmark]
+ public int Plan_Simple_Query_With_Requirements()
+ {
+ return _planner.CreatePlan(Id, Id, Id, _simpleQueryWithRequirements).SearchSpace;
+ }
+
+ [Benchmark]
+ public int Plan_Complex_Query()
+ {
+ return _planner.CreatePlan(Id, Id, Id, _complexQuery).SearchSpace;
+ }
+
+ [Benchmark]
+ public int Plan_ConditionalRedundancy_Query()
+ {
+ return _planner.CreatePlan(Id, Id, Id, _conditionalRedundancyQuery).SearchSpace;
+ }
+}
diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/FusionBenchmarkTests.cs b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/FusionBenchmarkTests.cs
new file mode 100644
index 00000000000..a284fcfd864
--- /dev/null
+++ b/src/HotChocolate/Fusion/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
+ {
+ Merger = new SourceSchemaMergerOptions { 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 @shareable
+ }
+
+ 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! @shareable
+ }
+
+ 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 @shareable
+ }
+
+ 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! @shareable
+ }
+
+ 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! @shareable
+ }
+ """)
+ ];
+ }
+
+ 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/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml
new file mode 100644
index 00000000000..6d12f5f078a
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml
@@ -0,0 +1,1023 @@
+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: 12
+ expandedNodes: 66
+nodes:
+ - id: 1
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_1($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: 2
+ type: Operation
+ schema: products
+ operation: |
+ query Op_123456789101112_2(
+ $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: 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
+ batchingGroupId: 4
+ requirements:
+ - name: __fusion_1_id
+ selectionMap: >-
+ id
+ forwardedVariables:
+ - level3
+ - level4
+ dependencies:
+ - id: 3
+ - id: 26
+ type: Operation
+ schema: users
+ operation: |
+ query Op_123456789101112_26($level3: Boolean!, $__fusion_22_id: ID!) {
+ userById(id: $__fusion_22_id) {
+ displayName
+ reviews(first: 3) @skip(if: $level3) {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ source: $.userById
+ target: $.viewer.reviews.edges.node.author
+ batchingGroupId: 4
+ requirements:
+ - name: __fusion_22_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $includeReviews
+ passingValue: true
+ forwardedVariables:
+ - level3
+ dependencies:
+ - id: 1
+ - 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
+ batchingGroupId: 5
+ requirements:
+ - name: __fusion_2_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level3
+ passingValue: true
+ 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
+ batchingGroupId: 5
+ requirements:
+ - name: __fusion_3_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level3
+ passingValue: true
+ - variable: $level4
+ passingValue: false
+ forwardedVariables:
+ - level5
+ dependencies:
+ - id: 4
+ - id: 27
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_27(
+ $level4: Boolean!
+ $level5: Boolean!
+ $__fusion_23_id: ID!
+ ) {
+ reviewById(id: $__fusion_23_id) {
+ body
+ stars
+ product @include(if: $level4) {
+ id
+ ... @skip(if: $level5) {
+ id
+ }
+ }
+ }
+ }
+ source: $.reviewById
+ target: $.viewer.reviews.edges.node.author.reviews.nodes
+ batchingGroupId: 5
+ requirements:
+ - name: __fusion_23_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level3
+ passingValue: false
+ forwardedVariables:
+ - level4
+ - level5
+ dependencies:
+ - id: 26
+ - id: 7
+ type: OperationBatch
+ schema: products
+ operation: |
+ query Op_123456789101112_7($__fusion_4_id: ID!) {
+ productById(id: $__fusion_4_id) {
+ name
+ }
+ }
+ source: $.productById
+ targets:
+ - $.searchContent.author.reviews.nodes.product
+ - $.searchContent.reviews.nodes.author.reviews.nodes.product
+ - $.products.edges.node.reviews.nodes.author.reviews.nodes.product
+ batchingGroupId: 7
+ requirements:
+ - name: __fusion_4_id
+ selectionMap: >-
+ id
+ dependencies:
+ - id: 6
+ - id: 13
+ - id: 20
+ - 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
+ batchingGroupId: 7
+ requirements:
+ - name: __fusion_12_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level4
+ passingValue: true
+ 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
+ batchingGroupId: 7
+ requirements:
+ - name: __fusion_13_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level4
+ passingValue: true
+ - variable: $level5
+ passingValue: false
+ dependencies:
+ - id: 13
+ - id: 22
+ type: Operation
+ schema: products
+ operation: |
+ query Op_123456789101112_22($level5: Boolean!, $__fusion_19_id: ID!) {
+ productById(id: $__fusion_19_id) {
+ price
+ dimension @skip(if: $level5) {
+ height
+ width
+ }
+ }
+ }
+ source: $.productById
+ target: $.products.edges.node.reviews.nodes.author.reviews.nodes.product
+ batchingGroupId: 7
+ requirements:
+ - name: __fusion_19_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level4
+ passingValue: true
+ forwardedVariables:
+ - level5
+ dependencies:
+ - id: 20
+ - 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
+ batchingGroupId: 8
+ requirements:
+ - name: __fusion_5_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level5
+ passingValue: true
+ forwardedVariables:
+ - level6
+ dependencies:
+ - id: 6
+ - id: 28
+ type: Operation
+ schema: products
+ operation: |
+ query Op_123456789101112_28($__fusion_24_id: ID!) {
+ productById(id: $__fusion_24_id) {
+ name
+ }
+ }
+ source: $.productById
+ target: $.viewer.reviews.edges.node.author.reviews.nodes.product
+ batchingGroupId: 8
+ requirements:
+ - name: __fusion_24_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level4
+ passingValue: true
+ dependencies:
+ - id: 27
+ - id: 29
+ type: Operation
+ schema: products
+ operation: |
+ query Op_123456789101112_29($level6: Boolean!, $__fusion_25_id: ID!) {
+ productById(id: $__fusion_25_id) {
+ price
+ dimension @include(if: $level6) {
+ height
+ width
+ }
+ }
+ }
+ source: $.productById
+ target: $.viewer.reviews.edges.node.author.reviews.nodes.product
+ batchingGroupId: 8
+ requirements:
+ - name: __fusion_25_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level4
+ passingValue: true
+ - variable: $level5
+ passingValue: false
+ forwardedVariables:
+ - level6
+ dependencies:
+ - id: 27
+ - id: 9
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_9($level1: Boolean!, $__fusion_6_id: ID!) {
+ productById(id: $__fusion_6_id) {
+ reviews(first: 3) {
+ nodes {
+ id
+ body
+ stars
+ author @skip(if: $level1) {
+ id
+ }
+ }
+ }
+ }
+ }
+ source: $.productById
+ target: $.searchContent
+ batchingGroupId: 9
+ requirements:
+ - name: __fusion_6_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $includeReviews
+ passingValue: true
+ forwardedVariables:
+ - level1
+ dependencies:
+ - id: 3
+ - id: 17
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_17(
+ $level1: Boolean!
+ $includeReviews: Boolean!
+ $__fusion_14_id: ID!
+ ) {
+ productById(id: $__fusion_14_id) {
+ averageRating
+ reviews(first: 5) @include(if: $includeReviews) {
+ nodes {
+ id
+ body
+ stars
+ author @skip(if: $level1) {
+ id
+ }
+ }
+ }
+ }
+ }
+ source: $.productById
+ target: $.products.edges.node
+ batchingGroupId: 9
+ requirements:
+ - name: __fusion_14_id
+ selectionMap: >-
+ id
+ forwardedVariables:
+ - level1
+ - includeReviews
+ dependencies:
+ - id: 2
+ - id: 23
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_23(
+ $includeReviews: Boolean!
+ $includeMetadata: Boolean!
+ $__fusion_20_id: ID!
+ ) {
+ productById(id: $__fusion_20_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
+ batchingGroupId: 9
+ requirements:
+ - name: __fusion_20_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level1
+ passingValue: true
+ - variable: $level2
+ passingValue: false
+ - variable: $level3
+ passingValue: true
+ - variable: $level4
+ passingValue: false
+ - variable: $level5
+ passingValue: true
+ - variable: $level6
+ passingValue: false
+ forwardedVariables:
+ - includeReviews
+ - includeMetadata
+ dependencies:
+ - id: 2
+ - id: 10
+ type: Operation
+ schema: products
+ operation: |
+ query Op_123456789101112_10($__fusion_7_id: ID!) {
+ productById(id: $__fusion_7_id) {
+ name
+ price
+ }
+ }
+ source: $.productById
+ target: $.searchContent
+ requirements:
+ - name: __fusion_7_id
+ selectionMap: >-
+ id
+ 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
+ batchingGroupId: 11
+ requirements:
+ - name: __fusion_8_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $includeReviews
+ passingValue: true
+ - variable: $level1
+ passingValue: false
+ forwardedVariables:
+ - level2
+ - level3
+ dependencies:
+ - id: 9
+ - id: 18
+ type: Operation
+ schema: users
+ operation: |
+ query Op_123456789101112_18(
+ $level2: Boolean!
+ $level3: Boolean!
+ $__fusion_15_id: ID!
+ ) {
+ userById(id: $__fusion_15_id) {
+ displayName
+ reviews(first: 3) @include(if: $level2) {
+ nodes {
+ id
+ ... @skip(if: $level3) {
+ id
+ }
+ }
+ }
+ }
+ }
+ source: $.userById
+ target: $.products.edges.node.reviews.nodes.author
+ batchingGroupId: 11
+ requirements:
+ - name: __fusion_15_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $includeReviews
+ passingValue: true
+ - variable: $level1
+ passingValue: false
+ forwardedVariables:
+ - level2
+ - level3
+ dependencies:
+ - id: 17
+ - id: 24
+ type: Operation
+ schema: users
+ operation: |
+ query Op_123456789101112_24($__fusion_21_id: ID!) {
+ userById(id: $__fusion_21_id) {
+ displayName
+ }
+ }
+ source: $.userById
+ target: $.productById.reviews.edges.node.author
+ batchingGroupId: 11
+ requirements:
+ - name: __fusion_21_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level6
+ passingValue: false
+ - variable: $includeReviews
+ passingValue: true
+ dependencies:
+ - id: 23
+ - id: 12
+ type: OperationBatch
+ schema: reviews
+ operation: |
+ query Op_123456789101112_12($__fusion_9_id: ID!) {
+ reviewById(id: $__fusion_9_id) {
+ body
+ }
+ }
+ source: $.reviewById
+ targets:
+ - $.searchContent.reviews.nodes.author.reviews.nodes
+ - $.products.edges.node.reviews.nodes.author.reviews.nodes
+ batchingGroupId: 12
+ requirements:
+ - name: __fusion_9_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level2
+ passingValue: true
+ dependencies:
+ - id: 11
+ - id: 18
+ - 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
+ batchingGroupId: 12
+ requirements:
+ - name: __fusion_10_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level2
+ passingValue: true
+ - variable: $level3
+ passingValue: false
+ forwardedVariables:
+ - level4
+ - level5
+ - level6
+ dependencies:
+ - id: 11
+ - id: 20
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_20($level4: Boolean!, $__fusion_17_id: ID!) {
+ reviewById(id: $__fusion_17_id) {
+ stars
+ product {
+ id
+ ... @include(if: $level4) {
+ id
+ }
+ }
+ }
+ }
+ source: $.reviewById
+ target: $.products.edges.node.reviews.nodes.author.reviews.nodes
+ batchingGroupId: 12
+ requirements:
+ - name: __fusion_17_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $level2
+ passingValue: true
+ - variable: $level3
+ passingValue: false
+ forwardedVariables:
+ - level4
+ dependencies:
+ - id: 18
+ - id: 25
+ type: Operation
+ schema: users
+ operation: |
+ query Op_123456789101112_25 {
+ viewer {
+ displayName
+ }
+ }
diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Conditional_Redundancy_Query.yaml b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Conditional_Redundancy_Query.yaml
new file mode 100644
index 00000000000..73c144a9d32
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Conditional_Redundancy_Query.yaml
@@ -0,0 +1,463 @@
+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: 11
+ expandedNodes: 26
+nodes:
+ - id: 1
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_1(
+ $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: 2
+ type: Operation
+ schema: products
+ operation: |
+ query Op_123456789101112_2($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: 3
+ type: Operation
+ schema: search
+ operation: |
+ query Op_123456789101112_3(
+ $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: 4
+ type: Operation
+ schema: users
+ operation: |
+ query Op_123456789101112_4($__fusion_1_id: ID!) {
+ userById(id: $__fusion_1_id) {
+ displayName
+ }
+ }
+ source: $.userById
+ target: $.searchContent.author
+ batchingGroupId: 4
+ requirements:
+ - name: __fusion_1_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $includeDetails
+ passingValue: true
+ dependencies:
+ - id: 3
+ - id: 11
+ type: Operation
+ schema: users
+ operation: |
+ query Op_123456789101112_11($__fusion_7_id: ID!) {
+ userById(id: $__fusion_7_id) {
+ displayName
+ reviews(first: 2) {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ source: $.userById
+ target: $.viewer.reviews.edges.node.author
+ batchingGroupId: 4
+ requirements:
+ - name: __fusion_7_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $includeReviews
+ passingValue: true
+ - variable: $includeDetails
+ passingValue: true
+ dependencies:
+ - id: 1
+ - id: 5
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_5($includeReviews: Boolean!, $__fusion_2_id: ID!) {
+ productById(id: $__fusion_2_id) {
+ averageRating
+ reviews(first: 2) @include(if: $includeReviews) {
+ nodes {
+ id
+ body
+ stars
+ }
+ }
+ }
+ }
+ source: $.productById
+ target: $.searchContent
+ batchingGroupId: 5
+ requirements:
+ - name: __fusion_2_id
+ selectionMap: >-
+ id
+ forwardedVariables:
+ - includeReviews
+ dependencies:
+ - id: 3
+ - id: 7
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_7($includeReviews: Boolean!, $__fusion_4_id: ID!) {
+ productById(id: $__fusion_4_id) {
+ averageRating
+ reviews(first: 3) @include(if: $includeReviews) {
+ nodes {
+ id
+ body
+ stars
+ }
+ }
+ }
+ }
+ source: $.productById
+ target: $.products.edges.node
+ batchingGroupId: 5
+ requirements:
+ - name: __fusion_4_id
+ selectionMap: >-
+ id
+ forwardedVariables:
+ - includeReviews
+ dependencies:
+ - id: 2
+ - id: 8
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_8(
+ $includeReviews: Boolean!
+ $includeMetadata: Boolean!
+ $__fusion_5_id: ID!
+ ) {
+ productById(id: $__fusion_5_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
+ batchingGroupId: 5
+ requirements:
+ - name: __fusion_5_id
+ selectionMap: >-
+ id
+ forwardedVariables:
+ - includeReviews
+ - includeMetadata
+ dependencies:
+ - id: 2
+ - id: 6
+ type: Operation
+ schema: products
+ operation: |
+ query Op_123456789101112_6($includeExpensive: Boolean!, $__fusion_3_id: ID!) {
+ productById(id: $__fusion_3_id) {
+ name
+ price
+ dimension @include(if: $includeExpensive) {
+ height
+ width
+ }
+ }
+ }
+ source: $.productById
+ target: $.searchContent
+ requirements:
+ - name: __fusion_3_id
+ selectionMap: >-
+ id
+ forwardedVariables:
+ - includeExpensive
+ dependencies:
+ - id: 3
+ - id: 9
+ type: Operation
+ schema: users
+ operation: |
+ query Op_123456789101112_9($__fusion_6_id: ID!) {
+ userById(id: $__fusion_6_id) {
+ displayName
+ }
+ }
+ source: $.userById
+ target: $.productById.reviews.edges.node.author
+ requirements:
+ - name: __fusion_6_id
+ selectionMap: >-
+ id
+ conditions:
+ - variable: $includeReviews
+ passingValue: true
+ dependencies:
+ - id: 5
+ - id: 10
+ type: Operation
+ schema: users
+ operation: |
+ query Op_123456789101112_10 {
+ viewer {
+ displayName
+ }
+ }
+ - id: 12
+ type: Operation
+ schema: reviews
+ operation: |
+ query Op_123456789101112_12($__fusion_8_id: ID!) {
+ reviewById(id: $__fusion_8_id) {
+ body
+ stars
+ }
+ }
+ source: $.reviewById
+ target: $.viewer.reviews.edges.node.author.reviews.nodes
+ requirements:
+ - name: __fusion_8_id
+ selectionMap: >-
+ id
+ dependencies:
+ - id: 4
diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Simple_Query_With_Requirements.yaml b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Simple_Query_With_Requirements.yaml
new file mode 100644
index 00000000000..a499083ea76
--- /dev/null
+++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Simple_Query_With_Requirements.yaml
@@ -0,0 +1,74 @@
+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
+ expandedNodes: 3
+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
From 772a1d89738e3c77162f0ff54f99ba2a84cde1aa Mon Sep 17 00:00:00 2001
From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com>
Date: Wed, 13 May 2026 15:52:24 +0200
Subject: [PATCH 2/2] Properly handle @defer in DocumentRewriter
---
.../Rewriters/DocumentRewriter.cs | 458 +++++++-
.../FusionBenchmarkTests.Complex_Query.yaml | 12 +-
.../Rewriters/DocumentRewriterTests.cs | 1016 +++++++++++++++++
3 files changed, 1442 insertions(+), 44 deletions(-)
diff --git a/src/HotChocolate/Fusion/src/Fusion.Utilities/Rewriters/DocumentRewriter.cs b/src/HotChocolate/Fusion/src/Fusion.Utilities/Rewriters/DocumentRewriter.cs
index f5052b2bdbb..5dece8db673 100644
--- a/src/HotChocolate/Fusion/src/Fusion.Utilities/Rewriters/DocumentRewriter.cs
+++ b/src/HotChocolate/Fusion/src/Fusion.Utilities/Rewriters/DocumentRewriter.cs
@@ -10,7 +10,7 @@ namespace HotChocolate.Fusion.Rewriters;
public sealed class DocumentRewriter(ISchemaDefinition schema, bool removeStaticallyExcludedSelections = false)
{
private static readonly FieldNode s_typeNameField =
- new FieldNode(
+ new(
null,
new NameNode(IntrospectionFieldNames.TypeName),
null,
@@ -46,15 +46,13 @@ private SelectionSetNode RewriteSelectionSet(
ITypeDefinition type,
Dictionary? fragmentLookup)
{
- var context = new Context(null, null, type, null, fragmentLookup ?? []);
+ var context = new Context(null, null, null, type, null, null, fragmentLookup ?? []);
CollectSelections(selectionSetNode, context);
var newSelections = RewriteSelections(context) ?? [s_typeNameField];
- var newSelectionSetNode = new SelectionSetNode(newSelections);
-
- return newSelectionSetNode;
+ return new SelectionSetNode(newSelections);
}
#region Collecting
@@ -87,7 +85,7 @@ private void CollectSelections(SelectionSetNode selectionSet, Context context)
private void CollectField(FieldNode fieldNode, Context context)
{
- var (conditional, directives) = DivideDirectives(
+ var (conditional, _, directives) = DivideDirectives(
fieldNode,
Types.DirectiveLocation.Field);
@@ -135,7 +133,7 @@ private void CollectInlineFragment(InlineFragmentNode inlineFragment, Context co
? schema.Types[inlineFragment.TypeCondition.Name.Value]
: context.Type;
- var (conditional, directives) = DivideDirectives(
+ var (conditional, defer, directives) = DivideDirectives(
inlineFragment,
Types.DirectiveLocation.InlineFragment);
@@ -143,6 +141,7 @@ private void CollectInlineFragment(InlineFragmentNode inlineFragment, Context co
inlineFragment.SelectionSet,
typeCondition,
conditional,
+ defer,
directives,
context);
}
@@ -152,7 +151,7 @@ private void CollectFragmentSpread(FragmentSpreadNode fragmentSpread, Context co
var fragmentDefinition = context.GetFragmentDefinition(fragmentSpread.Name.Value);
var typeCondition = schema.Types[fragmentDefinition.TypeCondition.Name.Value];
- var (conditional, directives) = DivideDirectives(
+ var (conditional, defer, directives) = DivideDirectives(
fragmentSpread,
Types.DirectiveLocation.InlineFragment);
@@ -160,6 +159,7 @@ private void CollectFragmentSpread(FragmentSpreadNode fragmentSpread, Context co
fragmentDefinition.SelectionSet,
typeCondition,
conditional,
+ defer,
directives,
context);
}
@@ -168,6 +168,7 @@ private void CollectFragment(
SelectionSetNode selectionSet,
ITypeDefinition typeCondition,
Conditional? conditional,
+ Defer? defer,
IReadOnlyList? otherDirectives,
Context context)
{
@@ -184,6 +185,11 @@ private void CollectFragment(
}
}
+ if (defer is not null)
+ {
+ context = context.AddDeferContext(defer);
+ }
+
var isTypeRefinement = !typeCondition.IsAssignableFrom(context.Type);
var fragmentContext = context;
@@ -205,6 +211,51 @@ private void CollectFragment(
private static Context? GetOrAddContextForField(Context context, FieldNode fieldNode, ITypeDefinition? fieldType)
{
+ if (context.IsDeferContext)
+ {
+ // Walk the full ancestor chain (through defer/conditional scopes, up to and
+ // including the nearest non-scope ancestor) and check whether any of them
+ // already contains the field.
+ var ancestor = context.Parent;
+ while (ancestor is not null)
+ {
+ if (ancestor.HasField(fieldNode, out var ancestorFieldContext))
+ {
+ if (fieldNode.SelectionSet is null)
+ {
+ return null;
+ }
+
+ if (ancestorFieldContext is null)
+ {
+ throw new InvalidOperationException("Expected to have a field context");
+ }
+
+ var deferredContextBelowAncestorFieldContext =
+ RecreateScopeHierarchy(ancestorFieldContext, context, ancestor);
+
+ return deferredContextBelowAncestorFieldContext;
+ }
+
+ if (!ancestor.IsConditionalContext && !ancestor.IsDeferContext)
+ {
+ // The non-scope boundary has been checked. Stop walking.
+ break;
+ }
+
+ ancestor = ancestor.Parent;
+ }
+
+ if (!context.HasField(fieldNode, out var fieldContext))
+ {
+ fieldContext = context.AddField(fieldNode, fieldType);
+
+ context.NonDeferredContext.RecordReferenceInDeferContext(fieldNode, context);
+ }
+
+ return fieldContext;
+ }
+
if (context.IsConditionalContext)
{
var unconditionalContext = context.UnconditionalContext;
@@ -222,7 +273,7 @@ private void CollectFragment(
}
var conditionalContextBelowUnconditionalFieldContext =
- RecreateConditionalContextHierarchy(unconditionalFieldContext, context);
+ RecreateScopeHierarchy(unconditionalFieldContext, context, unconditionalContext);
return conditionalContextBelowUnconditionalFieldContext;
}
@@ -234,6 +285,28 @@ private void CollectFragment(
unconditionalContext.RecordReferenceInConditionalContext(fieldNode, context);
}
+ // Even though this is a conditional context, defer back-refs may have been
+ // registered here when a nested defer's NonDeferredContext was this conditional.
+ if (context.TryGetDeferContextsWithReferences(fieldNode, out var deferContextsInConditional))
+ {
+ foreach (var deferContext in deferContextsInConditional)
+ {
+ if (fieldContext is not null
+ && deferContext.HasField(fieldNode, out var deferFieldContext)
+ && deferFieldContext is not null)
+ {
+ var deferContextBelowField =
+ RecreateScopeHierarchy(fieldContext, deferContext, context);
+
+ MergeContexts(deferFieldContext, deferContextBelowField);
+ }
+
+ deferContext.RemoveField(fieldNode);
+ }
+
+ context.RemoveReferenceToDeferContext(fieldNode);
+ }
+
return fieldContext;
}
else
@@ -252,7 +325,7 @@ private void CollectFragment(
&& conditionalFieldContext is not null)
{
var conditionalContextBelowUnconditionalField =
- RecreateConditionalContextHierarchy(fieldContext, conditionalContext);
+ RecreateScopeHierarchy(fieldContext, conditionalContext, context);
MergeContexts(conditionalFieldContext, conditionalContextBelowUnconditionalField);
}
@@ -263,6 +336,26 @@ private void CollectFragment(
context.RemoveReferenceToConditionalContext(fieldNode);
}
+ if (context.TryGetDeferContextsWithReferences(fieldNode, out var deferContexts))
+ {
+ foreach (var deferContext in deferContexts)
+ {
+ if (fieldContext is not null
+ && deferContext.HasField(fieldNode, out var deferFieldContext)
+ && deferFieldContext is not null)
+ {
+ var deferContextBelowField =
+ RecreateScopeHierarchy(fieldContext, deferContext, context);
+
+ MergeContexts(deferFieldContext, deferContextBelowField);
+ }
+
+ deferContext.RemoveField(fieldNode);
+ }
+
+ context.RemoveReferenceToDeferContext(fieldNode);
+ }
+
return fieldContext;
}
}
@@ -272,6 +365,37 @@ private static Context GetOrAddContextForFragment(
InlineFragmentNode inlineFragmentNode,
ITypeDefinition typeCondition)
{
+ if (context.IsDeferContext)
+ {
+ var ancestor = context.Parent;
+ while (ancestor is not null)
+ {
+ if (ancestor.HasFragment(inlineFragmentNode, out var ancestorFragmentContext))
+ {
+ var deferredContextBelowAncestorFragmentContext =
+ RecreateScopeHierarchy(ancestorFragmentContext, context, ancestor);
+
+ return deferredContextBelowAncestorFragmentContext;
+ }
+
+ if (!ancestor.IsConditionalContext && !ancestor.IsDeferContext)
+ {
+ break;
+ }
+
+ ancestor = ancestor.Parent;
+ }
+
+ if (!context.HasFragment(inlineFragmentNode, out var fragmentContext))
+ {
+ fragmentContext = context.AddFragment(inlineFragmentNode, typeCondition);
+
+ context.NonDeferredContext.RecordReferenceInDeferContext(inlineFragmentNode, context);
+ }
+
+ return fragmentContext;
+ }
+
if (context.IsConditionalContext)
{
var unconditionalContext = context.UnconditionalContext;
@@ -279,7 +403,7 @@ private static Context GetOrAddContextForFragment(
if (unconditionalContext.HasFragment(inlineFragmentNode, out var unconditionalFragmentContext))
{
var conditionalContextBelowUnconditionalFragmentContext =
- RecreateConditionalContextHierarchy(unconditionalFragmentContext, context);
+ RecreateScopeHierarchy(unconditionalFragmentContext, context, unconditionalContext);
return conditionalContextBelowUnconditionalFragmentContext;
}
@@ -291,6 +415,27 @@ private static Context GetOrAddContextForFragment(
unconditionalContext.RecordReferenceInConditionalContext(inlineFragmentNode, context);
}
+ // Even though this is a conditional context, defer back-refs may have been
+ // registered here when a nested defer's NonDeferredContext was this conditional.
+ if (context.TryGetDeferContextsWithReferences(inlineFragmentNode, out var deferContextsInConditional))
+ {
+ foreach (var deferContext in deferContextsInConditional)
+ {
+ if (deferContext.HasFragment(inlineFragmentNode,
+ out var deferFragmentContext))
+ {
+ var deferContextBelowFragment =
+ RecreateScopeHierarchy(fragmentContext, deferContext, context);
+
+ MergeContexts(deferFragmentContext, deferContextBelowFragment);
+ }
+
+ deferContext.RemoveFragment(inlineFragmentNode);
+ }
+
+ context.RemoveReferenceToDeferContext(inlineFragmentNode);
+ }
+
return fragmentContext;
}
else
@@ -308,7 +453,7 @@ private static Context GetOrAddContextForFragment(
out var conditionalFragmentContext))
{
var conditionalContextBelowUnconditionalFragment =
- RecreateConditionalContextHierarchy(fragmentContext, conditionalContext);
+ RecreateScopeHierarchy(fragmentContext, conditionalContext, context);
MergeContexts(conditionalFragmentContext, conditionalContextBelowUnconditionalFragment);
}
@@ -319,28 +464,59 @@ private static Context GetOrAddContextForFragment(
context.RemoveReferenceToConditionalContext(inlineFragmentNode);
}
+ if (context.TryGetDeferContextsWithReferences(inlineFragmentNode, out var deferContexts))
+ {
+ foreach (var deferContext in deferContexts)
+ {
+ if (deferContext.HasFragment(inlineFragmentNode,
+ out var deferFragmentContext))
+ {
+ var deferContextBelowFragment =
+ RecreateScopeHierarchy(fragmentContext, deferContext, context);
+
+ MergeContexts(deferFragmentContext, deferContextBelowFragment);
+ }
+
+ deferContext.RemoveFragment(inlineFragmentNode);
+ }
+
+ context.RemoveReferenceToDeferContext(inlineFragmentNode);
+ }
+
return fragmentContext;
}
}
///
- /// Rebuilds the conditional directive hierarchy of into
- /// , returning the innermost, rebuilt conditional context.
+ /// Rebuilds the conditional and defer scope hierarchy of
+ /// up to (but not including) into ,
+ /// returning the innermost rebuilt scope context. Each defer layer becomes a fresh defer
+ /// context (defers do not merge); each conditional layer is reused or created via the
+ /// existing conditional dictionary on the target.
///
- private static Context RecreateConditionalContextHierarchy(Context targetContext, Context sourceContext)
+ private static Context RecreateScopeHierarchy(Context targetContext, Context sourceContext, Context boundary)
{
- var conditionalStack = new Stack();
+ var stack = new Stack();
var current = sourceContext;
- while (current?.IsConditionalContext == true)
+ while (current is not null
+ && !ReferenceEquals(current, boundary)
+ && (current.IsConditionalContext || current.IsDeferContext))
{
- conditionalStack.Push(current);
+ stack.Push(current);
current = current.Parent;
}
- while (conditionalStack.TryPop(out var conditionalContext))
+ while (stack.TryPop(out var scopeContext))
{
- targetContext = targetContext.GetOrAddConditionalContext(conditionalContext.Conditional!);
+ if (scopeContext.IsConditionalContext)
+ {
+ targetContext = targetContext.GetOrAddConditionalContext(scopeContext.Conditional);
+ }
+ else if (scopeContext.IsDeferContext)
+ {
+ targetContext = targetContext.AddDeferContext(scopeContext.Defer);
+ }
}
return targetContext;
@@ -358,6 +534,16 @@ private static void MergeContexts(Context source, Context target)
}
}
+ if (source.Defers is not null)
+ {
+ foreach (var deferContext in source.Defers)
+ {
+ var targetDeferContext = target.AddDeferContext(deferContext.Defer!);
+
+ MergeContexts(deferContext, targetDeferContext);
+ }
+ }
+
if (source.Fields is not null)
{
foreach (var (_, fieldContextLookup) in source.Fields)
@@ -397,16 +583,17 @@ private static void MergeContexts(Context source, Context target)
}
}
- private (Conditional? Conditional, IReadOnlyList? Directives) DivideDirectives(
+ private (Conditional? Conditional, Defer? Defer, IReadOnlyList? Directives) DivideDirectives(
IHasDirectives directiveProvider,
Types.DirectiveLocation targetLocation)
{
if (directiveProvider.Directives.Count == 0)
{
- return (null, null);
+ return (null, null, null);
}
Conditional? conditional = null;
+ Defer? defer = null;
List? directives = null;
foreach (var directive in directiveProvider.Directives)
@@ -447,20 +634,51 @@ private static void MergeContexts(Context source, Context target)
if (directive.Name.Value.Equals(DirectiveNames.Defer.Name, StringComparison.Ordinal))
{
- var ifArgument = directive.Arguments
- .FirstOrDefault(a => a.Name.Value.Equals("if", StringComparison.Ordinal));
+ StringValueNode? label = null;
+ IValueNode? ifValue = null;
+ var dropDirective = false;
- if (ifArgument?.Value is BooleanValueNode { Value: false })
+ foreach (var argument in directive.Arguments)
+ {
+ if (argument.Name.Value.Equals(DirectiveNames.Defer.Arguments.If, StringComparison.Ordinal))
+ {
+ if (argument.Value is BooleanValueNode { Value: false })
+ {
+ dropDirective = true;
+ break;
+ }
+
+ if (argument.Value is BooleanValueNode { Value: true })
+ {
+ // Canonicalize: @defer(if: true) keeps its identity but the
+ // redundant `if` argument is dropped from the emitted directive.
+ continue;
+ }
+
+ ifValue = argument.Value;
+ }
+ else if (argument.Name.Value.Equals(DirectiveNames.Defer.Arguments.Label, StringComparison.Ordinal)
+ && argument.Value is StringValueNode labelValue)
+ {
+ label = labelValue;
+ }
+ }
+
+ if (dropDirective)
{
continue;
}
+
+ defer = new Defer { Label = label, If = ifValue };
+
+ continue;
}
directives ??= [];
directives.Add(rewrittenDirective);
}
- return (conditional, directives);
+ return (conditional, defer, directives);
}
///
@@ -651,6 +869,22 @@ private static Dictionary CreateFragmentLookup(D
}
}
+ if (context.Defers is not null)
+ {
+ foreach (var deferContext in context.Defers)
+ {
+ var deferSelection = RewriteDeferred(deferContext);
+
+ if (deferSelection is null)
+ {
+ continue;
+ }
+
+ selections ??= [];
+ selections.Add(deferSelection);
+ }
+ }
+
return selections;
}
@@ -684,6 +918,32 @@ private static Dictionary CreateFragmentLookup(D
};
}
+ private InlineFragmentNode? RewriteDeferred(Context deferContext)
+ {
+ var deferSelections = RewriteSelections(deferContext);
+
+ if (deferSelections is null)
+ {
+ return null;
+ }
+
+ var deferDirective = deferContext.Defer!.ToDirective();
+
+ // If we only have a single type-refining inline fragment without other directives,
+ // we can push the @defer directive down onto it. @defer is only valid on fragments,
+ // so we never push it onto a FieldNode.
+ if (deferSelections is [InlineFragmentNode { Directives.Count: 0 } inlineFragmentNode])
+ {
+ return inlineFragmentNode.WithDirectives([deferDirective]);
+ }
+
+ return new InlineFragmentNode(
+ null,
+ null,
+ [deferDirective],
+ new SelectionSetNode(deferSelections));
+ }
+
private FieldNode? RewriteField(
FieldNode fieldNode,
Context? fieldContext)
@@ -782,12 +1042,15 @@ private static IReadOnlyList RewriteArguments(IReadOnlyList fragmentLookup)
{
///
@@ -826,6 +1089,32 @@ private sealed class Context(
///
private Dictionary>? ReferencesInConditionalContexts { get; set; }
+ [MemberNotNullWhen(true, nameof(Defer))]
+ [MemberNotNullWhen(true, nameof(NonDeferredContext))]
+ public bool IsDeferContext { get; } = defer is not null;
+
+ ///
+ /// Contains the defer, if this context is a defer context.
+ ///
+ public Defer? Defer { get; } = defer;
+
+ ///
+ /// If this context is a defer context, this points to the nearest enclosing
+ /// context that is not itself a defer (the transitive non-defer ancestor).
+ ///
+ public Context? NonDeferredContext { get; } = nonDeferredContext;
+
+ ///
+ /// The contexts for each occurrence.
+ /// Each occurrence produces a distinct entry; defers are never merged by identity.
+ ///
+ public List? Defers { get; private set; }
+
+ ///
+ /// Provides a way to find all defer contexts a given selection node is referenced in.
+ ///
+ private Dictionary>? ReferencesInDeferContexts { get; set; }
+
///
/// Provides a fast way to get all FieldNodes for the same response name.
/// The key is the response name.
@@ -851,8 +1140,10 @@ public Context GetOrAddConditionalContext(Conditional conditional)
conditionalContext = new Context(
this,
GetUnconditionalContext(),
+ GetNonDeferredContext(),
Type,
conditional,
+ null,
fragmentLookup);
Conditionals[conditional] = conditionalContext;
@@ -861,14 +1152,30 @@ public Context GetOrAddConditionalContext(Conditional conditional)
return conditionalContext;
}
+ public Context AddDeferContext(Defer defer)
+ {
+ var deferContext = new Context(
+ this,
+ GetUnconditionalContext(),
+ GetNonDeferredContext(),
+ Type,
+ null,
+ defer,
+ fragmentLookup);
+
+ Defers ??= [];
+ Defers.Add(deferContext);
+
+ return deferContext;
+ }
+
///
/// Records that is referenced in ,
/// so we can later quickly jump there.
///
public void RecordReferenceInConditionalContext(ISelectionNode selectionNode, Context conditionalContext)
{
- ReferencesInConditionalContexts ??=
- new Dictionary>(SyntaxNodeComparer.Instance);
+ ReferencesInConditionalContexts ??= new(SyntaxNodeComparer.Instance);
if (!ReferencesInConditionalContexts.TryGetValue(selectionNode, out var conditionalContexts))
{
@@ -898,6 +1205,42 @@ public void RemoveReferenceToConditionalContext(ISelectionNode selectionNode)
ReferencesInConditionalContexts?.Remove(selectionNode);
}
+ ///
+ /// Records that is referenced in ,
+ /// so we can later quickly jump there.
+ ///
+ public void RecordReferenceInDeferContext(ISelectionNode selectionNode, Context deferContext)
+ {
+ ReferencesInDeferContexts ??= new(SyntaxNodeComparer.Instance);
+
+ if (!ReferencesInDeferContexts.TryGetValue(selectionNode, out var deferContexts))
+ {
+ deferContexts = [];
+ ReferencesInDeferContexts[selectionNode] = deferContexts;
+ }
+
+ deferContexts.Add(deferContext);
+ }
+
+ public bool TryGetDeferContextsWithReferences(
+ ISelectionNode selectionNode,
+ [NotNullWhen(true)] out List? deferContexts)
+ {
+ deferContexts = null;
+
+ if (ReferencesInDeferContexts is null)
+ {
+ return false;
+ }
+
+ return ReferencesInDeferContexts.TryGetValue(selectionNode, out deferContexts);
+ }
+
+ public void RemoveReferenceToDeferContext(ISelectionNode selectionNode)
+ {
+ ReferencesInDeferContexts?.Remove(selectionNode);
+ }
+
public bool HasField(FieldNode fieldNode, out Context? fieldContext)
{
fieldContext = null;
@@ -926,8 +1269,10 @@ public bool HasField(FieldNode fieldNode, out Context? fieldContext)
fieldContext = new Context(
this,
GetUnconditionalContext(),
+ GetNonDeferredContext(),
fieldType,
null,
+ null,
fragmentLookup);
}
@@ -944,7 +1289,7 @@ public void AddField(FieldNode fieldNode, Context? fieldContext)
if (!Fields.TryGetValue(responseName, out var existingFieldContextLookup))
{
- existingFieldContextLookup = new Dictionary(FieldNodeComparer.Instance);
+ existingFieldContextLookup = new(FieldNodeComparer.Instance);
Fields[responseName] = existingFieldContextLookup;
}
@@ -994,8 +1339,10 @@ public Context AddFragment(InlineFragmentNode inlineFragmentNode, ITypeDefinitio
var fragmentContext = new Context(
this,
GetUnconditionalContext(),
+ GetNonDeferredContext(),
typeCondition,
null,
+ null,
fragmentLookup);
AddFragment(inlineFragmentNode, fragmentContext);
@@ -1011,8 +1358,7 @@ public void AddFragment(InlineFragmentNode inlineFragmentNode, Context fragmentC
if (!Fragments.TryGetValue(typeName, out var existingFragmentContextLookup))
{
- existingFragmentContextLookup =
- new Dictionary(InlineFragmentNodeComparer.Instance);
+ existingFragmentContextLookup = new(InlineFragmentNodeComparer.Instance);
Fragments[typeName] = existingFragmentContextLookup;
}
@@ -1045,6 +1391,16 @@ private Context GetUnconditionalContext()
return this;
}
+
+ private Context GetNonDeferredContext()
+ {
+ if (IsDeferContext)
+ {
+ return NonDeferredContext;
+ }
+
+ return this;
+ }
}
///
@@ -1115,6 +1471,40 @@ private static int GetDirectiveHashCode(DirectiveNode? node)
}
}
+ ///
+ /// Represents a single occurrence of @defer. Defer instances are identity-bearing
+ /// data carriers without structural equality; sibling defers are never merged.
+ ///
+ private sealed class Defer
+ {
+ public StringValueNode? Label { get; init; }
+
+ ///
+ /// The if argument of the directive, or null when the defer is
+ /// unconditional. @defer(if: true) is canonicalized to null.
+ ///
+ public IValueNode? If { get; init; }
+
+ public DirectiveNode ToDirective()
+ {
+ List? arguments = null;
+
+ if (If is not null)
+ {
+ arguments ??= [];
+ arguments.Add(new ArgumentNode(DirectiveNames.Defer.Arguments.If, If));
+ }
+
+ if (Label is not null)
+ {
+ arguments ??= [];
+ arguments.Add(new ArgumentNode(DirectiveNames.Defer.Arguments.Label, Label));
+ }
+
+ return new DirectiveNode(DirectiveNames.Defer.Name, arguments?.ToArray() ?? []);
+ }
+ }
+
#region Comparers
private sealed class SyntaxNodeComparer : IEqualityComparer
@@ -1175,7 +1565,7 @@ public bool Equals(InlineFragmentNode? x, InlineFragmentNode? y)
&& Equals(x.Directives, y.Directives);
}
- private bool Equals(IReadOnlyList a, IReadOnlyList b)
+ private static bool Equals(IReadOnlyList a, IReadOnlyList b)
{
if (a.Count == 0 && b.Count == 0)
{
@@ -1230,7 +1620,7 @@ public bool Equals(FieldNode? x, FieldNode? y)
&& Equals(x.Arguments, y.Arguments);
}
- private bool Equals(IReadOnlyList a, IReadOnlyList b)
+ private static bool Equals(IReadOnlyList a, IReadOnlyList b)
{
if (a.Count == 0 && b.Count == 0)
{
diff --git a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml
index 6d12f5f078a..d496d4f6d55 100644
--- a/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml
+++ b/src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/FusionBenchmarkTests.Complex_Query.yaml
@@ -25,6 +25,7 @@ operation:
}
... @skip(if: $level4) {
... @include(if: $level5) {
+ id @fusion__requirement
reviews(first: 10) @skip(if: $level6) {
pageInfo {
hasNextPage
@@ -48,7 +49,6 @@ operation:
}
}
}
- id @fusion__requirement
}
}
}
@@ -90,10 +90,8 @@ operation:
height
width
}
- id @fusion__requirement
}
}
- id @fusion__requirement
}
}
}
@@ -111,6 +109,7 @@ operation:
... on Product {
name
price
+ id @fusion__requirement
reviews(first: 3) @include(if: $includeReviews) {
nodes {
id
@@ -145,19 +144,15 @@ operation:
stars
}
}
- id @fusion__requirement
}
- id @fusion__requirement
}
}
- id @fusion__requirement
}
}
}
}
}
}
- id @fusion__requirement
}
... on Article {
content
@@ -182,10 +177,8 @@ operation:
height
width
}
- id @fusion__requirement
}
}
- id @fusion__requirement
}
}
}
@@ -229,7 +222,6 @@ operation:
height
width
}
- id @fusion__requirement
}
}
}
diff --git a/src/HotChocolate/Fusion/test/Fusion.Utilities.Tests/Rewriters/DocumentRewriterTests.cs b/src/HotChocolate/Fusion/test/Fusion.Utilities.Tests/Rewriters/DocumentRewriterTests.cs
index 47de51660b1..887a5f84c1c 100644
--- a/src/HotChocolate/Fusion/test/Fusion.Utilities.Tests/Rewriters/DocumentRewriterTests.cs
+++ b/src/HotChocolate/Fusion/test/Fusion.Utilities.Tests/Rewriters/DocumentRewriterTests.cs
@@ -2553,4 +2553,1020 @@ dimension @skip(if: $conditional) {
}
""");
}
+
+ [Fact]
+ public void Defer_Stripped_When_Field_Also_Selected_Outside_Defer()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ name
+ }
+ name
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ name
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_Moves_Down_To_Children_When_Parent_Field_Selected_Outside_Defer()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ dimension {
+ height
+ }
+ }
+ dimension {
+ width
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ dimension {
+ width
+ ... @defer {
+ height
+ }
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_Preserved_When_Field_Only_In_Defer()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ description
+ ... @defer {
+ name
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ description
+ ... @defer {
+ name
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_With_If_False_Is_Statically_Removed()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(if: false) {
+ name
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ name
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_With_If_True_Is_Canonicalized_To_No_If_But_Not_Merged()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ name
+ }
+ ... @defer(if: true) {
+ description
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ name
+ }
+ ... @defer {
+ description
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_With_Different_Labels_Not_Merged()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(label: "a") {
+ name
+ }
+ ... @defer(label: "b") {
+ description
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(label: "a") {
+ name
+ }
+ ... @defer(label: "b") {
+ description
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_With_Same_Label_Not_Merged()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(label: "a") {
+ name
+ }
+ ... @defer(label: "a") {
+ description
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(label: "a") {
+ name
+ }
+ ... @defer(label: "a") {
+ description
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_With_Different_If_Variables_Not_Merged()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ query($a: Boolean!, $b: Boolean!) {
+ productBySlug(slug: "a") {
+ ... @defer(if: $a) {
+ name
+ }
+ ... @defer(if: $b) {
+ description
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ query($a: Boolean!, $b: Boolean!) {
+ productBySlug(slug: "a") {
+ ... @defer(if: $a) {
+ name
+ }
+ ... @defer(if: $b) {
+ description
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_With_Same_If_Variable_Not_Merged()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ query($a: Boolean!) {
+ productBySlug(slug: "a") {
+ ... @defer(if: $a) {
+ name
+ }
+ ... @defer(if: $a) {
+ description
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ query($a: Boolean!) {
+ productBySlug(slug: "a") {
+ ... @defer(if: $a) {
+ name
+ }
+ ... @defer(if: $a) {
+ description
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Nested_Defer_Field_Pulled_Out_When_Selected_Outside()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ dimension {
+ height
+ width
+ }
+ }
+ dimension {
+ height
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ dimension {
+ height
+ ... @defer {
+ width
+ }
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_Inside_Conditional_Pulled_To_Conditional_When_Same_Field_Outside_Defer()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ query($skip: Boolean!) {
+ productBySlug(slug: "a") {
+ ... @skip(if: $skip) {
+ ... @defer {
+ name
+ }
+ }
+ ... @skip(if: $skip) {
+ name
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ query($skip: Boolean!) {
+ productBySlug(slug: "a") {
+ name @skip(if: $skip)
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_Inside_Conditional_Kept_When_Field_Not_Selected_Outside_Defer()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ query($skip: Boolean!) {
+ productBySlug(slug: "a") {
+ ... @skip(if: $skip) {
+ ... @defer {
+ name
+ }
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ query($skip: Boolean!) {
+ productBySlug(slug: "a") {
+ ... @skip(if: $skip) {
+ ... @defer {
+ name
+ }
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_With_Multiple_Children_Only_Overlapping_Pulled_Out()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ name
+ description
+ price
+ }
+ name
+ price
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ name
+ price
+ ... @defer {
+ description
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_FragmentSpread_Stripped_When_Field_Also_Selected_Outside()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ...ProductFields @defer
+ name
+ }
+ }
+
+ fragment ProductFields on Product {
+ name
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ name
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_On_TypedInlineFragment_Pushed_Down_When_Field_Selected_Outside()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... on Product @defer {
+ dimension {
+ height
+ }
+ }
+ dimension {
+ width
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ dimension {
+ width
+ ... @defer {
+ height
+ }
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_On_TypedInlineFragment_With_Refinement_Preserved_When_No_Overlap()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ reviewById(id: 1) {
+ id
+ ... on Review @defer {
+ body
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ reviewById(id: 1) {
+ id
+ ... @defer {
+ body
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_On_TypedInlineFragment_With_Refinement_Stays_On_InlineFragment()
+ {
+ // arrange
+ var schemaDefinition = SchemaParser.Parse(
+ """
+ type Query {
+ votables: [Votable!]!
+ }
+
+ interface Votable {
+ viewerCanVote: Boolean!
+ voteCount: Int
+ }
+
+ type Product implements Votable {
+ id: ID!
+ viewerCanVote: Boolean!
+ voteCount: Int
+ }
+ """);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ votables {
+ ... on Product @defer {
+ voteCount
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ votables {
+ ... on Product @defer {
+ voteCount
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_On_TypedInlineFragment_With_Refinement_Moves_Down_When_Sibling_Without_Defer()
+ {
+ // arrange
+ var schemaDefinition = SchemaParser.Parse(
+ """
+ type Query {
+ votables: [Votable!]!
+ }
+
+ interface Votable {
+ viewerCanVote: Boolean!
+ voteCount: Int
+ }
+
+ type Product implements Votable {
+ id: ID!
+ viewerCanVote: Boolean!
+ voteCount: Int
+ }
+ """);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ votables {
+ ... on Product @defer {
+ voteCount
+ }
+ ... on Product {
+ viewerCanVote
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ votables {
+ ... on Product {
+ viewerCanVote
+ ... @defer {
+ voteCount
+ }
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Defer_Nested_Inside_Defer_Field_Pulled_Out_When_Selected_Outside_Outer_Defer()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(label: "outer") {
+ ... @defer(label: "inner") {
+ name
+ }
+ }
+ name
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ name
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Nested_Defer_With_Same_Identity_Kept_Intact()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ ... @defer {
+ name
+ }
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ ... @defer {
+ name
+ }
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Nested_Defer_With_Same_Label_Kept_Intact()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(label: "a") {
+ ... @defer(label: "a") {
+ name
+ }
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(label: "a") {
+ ... @defer(label: "a") {
+ name
+ }
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Nested_Defer_With_Different_Labels_Kept_Intact()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(label: "outer") {
+ ... @defer(label: "inner") {
+ name
+ }
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer(label: "outer") {
+ ... @defer(label: "inner") {
+ name
+ }
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Nested_Defer_With_Field_Already_In_Parent_Defer_Is_Stripped()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ name
+ ... @defer {
+ name
+ }
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ name
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Nested_Defer_With_Some_Fields_Already_In_Parent_Defer_Moves_Down()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ name
+ ... @defer {
+ name
+ description
+ }
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ name
+ ... @defer {
+ description
+ }
+ }
+ }
+ }
+ """);
+ }
+
+ [Fact]
+ public void Nested_Defer_With_Parent_Composite_Pushed_Down()
+ {
+ // arrange
+ var sourceText = FileResource.Open("schema1.graphql");
+ var schemaDefinition = SchemaParser.Parse(sourceText);
+
+ var doc = Utf8GraphQLParser.Parse(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ dimension {
+ height
+ }
+ ... @defer {
+ dimension {
+ width
+ }
+ }
+ }
+ }
+ }
+ """);
+
+ // act
+ var rewriter = new DocumentRewriter(schemaDefinition);
+ var rewritten = rewriter.RewriteDocument(doc);
+
+ // assert
+ rewritten.MatchInlineSnapshot(
+ """
+ {
+ productBySlug(slug: "a") {
+ ... @defer {
+ dimension {
+ height
+ ... @defer {
+ width
+ }
+ }
+ }
+ }
+ }
+ """);
+ }
}