From b6428c50488a9cafb1929766fc46655feef6e5fb Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 29 May 2026 10:46:27 +0200 Subject: [PATCH 1/2] [Fusion] Allow `@require` intermediates from the requiring schema --- .../Satisfiability/RequirementsValidator.cs | 8 +++++++- .../SatisfiabilityValidatorTests.cs | 6 +----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Satisfiability/RequirementsValidator.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/Satisfiability/RequirementsValidator.cs index 2465a1bd7a6..865f6ff604a 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/Satisfiability/RequirementsValidator.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Satisfiability/RequirementsValidator.cs @@ -138,7 +138,13 @@ private ImmutableArray Visit( return []; } - var schemaNames = field.GetSchemaNames().Remove(context.ExcludeSchemaName); + // Only leaf fields in the requirement need to be sourced from outside + // the excluded schema. Intermediate fields (with a sub-selection) are + // navigation steps the gateway can resolve locally in the requiring + // schema, so we keep them in scope. + var schemaNames = fieldNode.SelectionSet is null + ? field.GetSchemaNames().Remove(context.ExcludeSchemaName) + : field.GetSchemaNames(); var fieldType = field.Type.AsTypeDefinition(); foreach (var schemaName in schemaNames) diff --git a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs index 346407f2434..000787844b8 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SatisfiabilityValidatorTests.cs @@ -1790,11 +1790,7 @@ type Movie @key(fields: "id") { Assert.True(result.IsSuccess); } - [Fact(Skip = "Circular @require whose intermediate field is owned by the " - + "requiring schema is not yet satisfiable. The validator evaluates the " - + "requirement from the entity's origin path and does not hop into the " - + "requiring schema to gather its locally-owned field first. Enable once " - + "multi-hop requirement gathering is supported.")] + [Fact] // https://github.com/graphql-hive/federation-gateway-audit/tree/main/src/test-suites/requires-circular public void RequiresCircular() { From 981d1a6d446cd9ba2a721b69c062752d41eafc4b Mon Sep 17 00:00:00 2001 From: Glen Date: Fri, 29 May 2026 12:55:22 +0200 Subject: [PATCH 2/2] [Fusion] Limit relaxation to field `@require`, not lookup keys --- .../Satisfiability/RequirementsValidator.cs | 31 +++++++++++++------ .../SatisfiabilityValidator.cs | 3 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Satisfiability/RequirementsValidator.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/Satisfiability/RequirementsValidator.cs index 865f6ff604a..4d6bbae67e9 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/Satisfiability/RequirementsValidator.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Satisfiability/RequirementsValidator.cs @@ -17,12 +17,14 @@ public ImmutableArray Validate( MutableObjectTypeDefinition contextType, SatisfiabilityPathItem? parentPathItem, string excludeSchemaName, + bool allowIntermediatesFromExcludedSchema = false, SatisfiabilityPath? cycleDetectionPath = null) { var context = new RequirementsValidatorContext( contextType, parentPathItem, excludeSchemaName, + allowIntermediatesFromExcludedSchema, cycleDetectionPath); var errors = new List(); @@ -138,13 +140,19 @@ private ImmutableArray Visit( return []; } - // Only leaf fields in the requirement need to be sourced from outside - // the excluded schema. Intermediate fields (with a sub-selection) are - // navigation steps the gateway can resolve locally in the requiring - // schema, so we keep them in scope. - var schemaNames = fieldNode.SelectionSet is null - ? field.GetSchemaNames().Remove(context.ExcludeSchemaName) - : field.GetSchemaNames(); + // Leaf fields in the requirement must be sourced from outside the + // excluded schema. Intermediate fields (with a sub-selection) may also + // be sourced from the excluded schema when validating a field-level + // @require: those intermediates are navigation steps the gateway can + // resolve locally in the requiring schema as part of executing the + // requiring field. For lookup-key validation the excluded schema has + // not been entered yet, so intermediates must also come from outside + // it (default behavior). + var schemaNames = field.GetSchemaNames(); + if (fieldNode.SelectionSet is null || !context.AllowIntermediatesFromExcludedSchema) + { + schemaNames = schemaNames.Remove(context.ExcludeSchemaName); + } var fieldType = field.Type.AsTypeDefinition(); foreach (var schemaName in schemaNames) @@ -208,7 +216,8 @@ private ImmutableArray Visit( type, context.Path.Peek(), excludeSchemaName: schemaName, - context.CycleDetectionPath); + allowIntermediatesFromExcludedSchema: true, + cycleDetectionPath: context.CycleDetectionPath); if (requirementErrors.Length != 0) { @@ -305,7 +314,7 @@ private ImmutableArray ValidateSourceSchemaTransition( contextType, parentPathItem, excludeSchemaName: transitionToSchemaName, - context.CycleDetectionPath), + cycleDetectionPath: context.CycleDetectionPath), RequirementsValidator_NoLookupsFoundForType, RequirementsValidator_UnableToSatisfyRequirementForLookup); } @@ -317,6 +326,7 @@ public RequirementsValidatorContext( MutableObjectTypeDefinition contextType, SatisfiabilityPathItem? parentPathItem, string excludeSchemaName, + bool allowIntermediatesFromExcludedSchema = false, SatisfiabilityPath? cycleDetectionPath = null) { TypeContext.Push(contextType); @@ -327,6 +337,7 @@ public RequirementsValidatorContext( } ExcludeSchemaName = excludeSchemaName; + AllowIntermediatesFromExcludedSchema = allowIntermediatesFromExcludedSchema; CycleDetectionPath = cycleDetectionPath ?? []; } @@ -336,6 +347,8 @@ public RequirementsValidatorContext( public string ExcludeSchemaName { get; } + public bool AllowIntermediatesFromExcludedSchema { get; } + public SatisfiabilityPath CycleDetectionPath { get; } public HashSet FieldAccessCache { get; } = []; diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs index 1f9dfeb6002..908592e89e9 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SatisfiabilityValidator.cs @@ -168,7 +168,8 @@ private void VisitOutputField( requirements, type, path?.Item, - excludeSchemaName: schemaName); + excludeSchemaName: schemaName, + allowIntermediatesFromExcludedSchema: true); if (requirementErrors.Length != 0) {